mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-22 01:25:38 -05:00
Add SQL DB offline migration wizard experience (#20403)
* sql db wizard with target selection * add database table selection * add sqldb to service and IR page * Code complete * navigation bug fixes * fix target db selection * improve sqldb error and status reporting * fix error count bug * remove table status inference * address review feedback * update resource strings and content * fix migraton status string, use localized value * fix ux navigation issues * fix back/fwd w/o changes from changing data
This commit is contained in:
@@ -8,12 +8,12 @@ import * as azdata from 'azdata';
|
||||
import * as azurecore from 'azurecore';
|
||||
import * as constants from '../constants/strings';
|
||||
import { getSessionIdHeader } from './utils';
|
||||
import { ProvisioningState } from '../models/migrationLocalStorage';
|
||||
import { URL } from 'url';
|
||||
|
||||
const ARM_MGMT_API_VERSION = '2021-04-01';
|
||||
const SQL_VM_API_VERSION = '2021-11-01-preview';
|
||||
const SQL_MI_API_VERSION = '2021-11-01-preview';
|
||||
const SQL_SQLDB_API_VERSION = '2021-11-01-preview';
|
||||
const DMSV2_API_VERSION = '2022-03-30-preview';
|
||||
|
||||
async function getAzureCoreAPI(): Promise<azurecore.IExtension> {
|
||||
@@ -93,6 +93,100 @@ export async function getAvailableSqlServers(account: azdata.Account, subscripti
|
||||
return result.resources;
|
||||
}
|
||||
|
||||
export interface SKU {
|
||||
name: string,
|
||||
tier: 'GeneralPurpose' | 'BusinessCritical',
|
||||
family: string,
|
||||
capacity: number,
|
||||
}
|
||||
|
||||
export interface AzureSqlDatabase {
|
||||
id: string,
|
||||
name: string,
|
||||
location: string,
|
||||
tags: any,
|
||||
type: string,
|
||||
sku: SKU,
|
||||
kind: string,
|
||||
properties: {
|
||||
collation: string,
|
||||
maxSizeBytes: number,
|
||||
status: string,
|
||||
databaseId: string,
|
||||
creationDate: string,
|
||||
currentServiceObjectiveName: string,
|
||||
requestedServiceObjectiveName: string,
|
||||
defaultSecondaryLocation: string,
|
||||
catalogCollation: string,
|
||||
zoneRedundant: boolean,
|
||||
earliestRestoreDate: string,
|
||||
readScale: string,
|
||||
currentSku: SKU,
|
||||
currentBackupStorageRedundancy: string,
|
||||
requestedBackupStorageRedundancy: string,
|
||||
maintenanceConfigurationId: string,
|
||||
isLedgerOn: boolean
|
||||
isInfraEncryptionEnabled: boolean,
|
||||
licenseType: string,
|
||||
maxLogSizeBytes: number,
|
||||
},
|
||||
}
|
||||
|
||||
export interface ServerAdministrators {
|
||||
administratorType: string,
|
||||
azureADOnlyAuthentication: boolean,
|
||||
login: string,
|
||||
principalType: string,
|
||||
sid: string,
|
||||
tenantId: string,
|
||||
}
|
||||
|
||||
export interface PrivateEndpointProperty {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface PrivateLinkServiceConnectionStateProperty {
|
||||
status: string;
|
||||
description: string;
|
||||
readonly actionsRequired?: string;
|
||||
}
|
||||
|
||||
export interface PrivateEndpointConnectionProperties {
|
||||
groupIds: string[];
|
||||
privateEndpoint?: PrivateEndpointProperty;
|
||||
privateLinkServiceConnectionState?: PrivateLinkServiceConnectionStateProperty;
|
||||
readonly provisioningState?: string;
|
||||
}
|
||||
|
||||
export interface ServerPrivateEndpointConnection {
|
||||
readonly id?: string;
|
||||
readonly properties?: PrivateEndpointConnectionProperties;
|
||||
}
|
||||
|
||||
export interface AzureSqlDatabaseServer {
|
||||
id: string,
|
||||
name: string,
|
||||
kind: string,
|
||||
location: string,
|
||||
tags?: { [propertyName: string]: string; };
|
||||
type: string,
|
||||
// sku: SKU,
|
||||
// subscriptionId: string,
|
||||
// tenantId: string,
|
||||
// fullName: string,
|
||||
properties: {
|
||||
administratorLogin: string,
|
||||
administrators: ServerAdministrators,
|
||||
fullyQualifiedDomainName: string,
|
||||
minimalTlsVersion: string,
|
||||
privateEndpointConnections: ServerPrivateEndpointConnection[],
|
||||
publicNetworkAccess: string,
|
||||
restrictOutboundNetworkAccess: string,
|
||||
state: string,
|
||||
version: string,
|
||||
},
|
||||
}
|
||||
|
||||
export type SqlVMServer = {
|
||||
properties: {
|
||||
virtualMachineResourceId: string,
|
||||
@@ -108,6 +202,31 @@ export type SqlVMServer = {
|
||||
tenantId: string,
|
||||
subscriptionId: string
|
||||
};
|
||||
|
||||
export async function getAvailableSqlDatabaseServers(account: azdata.Account, subscription: Subscription): Promise<AzureSqlDatabaseServer[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const path = encodeURI(`/subscriptions/${subscription.id}/providers/Microsoft.Sql/servers?api-version=${SQL_SQLDB_API_VERSION}`);
|
||||
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
|
||||
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
|
||||
if (response.errors.length > 0) {
|
||||
throw new Error(response.errors.toString());
|
||||
}
|
||||
sortResourceArrayByName(response.response.data.value);
|
||||
return response.response.data.value;
|
||||
}
|
||||
|
||||
export async function getAvailableSqlDatabases(account: azdata.Account, subscription: Subscription, resourceGroupName: string, serverName: string): Promise<AzureSqlDatabase[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.Sql/servers/${serverName}/databases?api-version=${SQL_SQLDB_API_VERSION}`);
|
||||
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
|
||||
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
|
||||
if (response.errors.length > 0) {
|
||||
throw new Error(response.errors.toString());
|
||||
}
|
||||
sortResourceArrayByName(response.response.data.value);
|
||||
return response.response.data.value;
|
||||
}
|
||||
|
||||
export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription): Promise<SqlVMServer[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const path = encodeURI(`/subscriptions/${subscription.id}/providers/Microsoft.SqlVirtualMachine/sqlVirtualMachines?api-version=${SQL_VM_API_VERSION}`);
|
||||
@@ -203,10 +322,10 @@ export async function getSqlMigrationServices(account: azdata.Account, subscript
|
||||
export async function createSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise<SqlMigrationService> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=${DMSV2_API_VERSION}`);
|
||||
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
|
||||
const requestBody = {
|
||||
'location': regionName
|
||||
};
|
||||
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
|
||||
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, host, getSessionIdHeader(sessionId));
|
||||
if (response.errors.length > 0) {
|
||||
throw new Error(response.errors.toString());
|
||||
@@ -219,9 +338,9 @@ export async function createSqlMigrationService(account: azdata.Account, subscri
|
||||
for (i = 0; i < maxRetry; i++) {
|
||||
const asyncResponse = await api.makeAzureRestRequest(account, subscription, asyncPath, azurecore.HttpRequestMethod.GET, undefined, true, host);
|
||||
const creationStatus = asyncResponse.response.data.status;
|
||||
if (creationStatus === ProvisioningState.Succeeded) {
|
||||
if (creationStatus === constants.ProvisioningState.Succeeded) {
|
||||
break;
|
||||
} else if (creationStatus === ProvisioningState.Failed) {
|
||||
} else if (creationStatus === constants.ProvisioningState.Failed) {
|
||||
throw new Error(asyncResponse.errors.toString());
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 5000)); //adding 5 sec delay before getting creation status
|
||||
@@ -287,7 +406,15 @@ export async function getSqlMigrationServiceMonitoringData(account: azdata.Accou
|
||||
return response.response.data;
|
||||
}
|
||||
|
||||
export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, regionName: string, targetServer: SqlManagedInstance | SqlVMServer, targetDatabaseName: string, requestBody: StartDatabaseMigrationRequest, sessionId: string): Promise<StartDatabaseMigrationResponse> {
|
||||
export async function startDatabaseMigration(
|
||||
account: azdata.Account,
|
||||
subscription: Subscription,
|
||||
regionName: string,
|
||||
targetServer: SqlManagedInstance | SqlVMServer | AzureSqlDatabaseServer,
|
||||
targetDatabaseName: string,
|
||||
requestBody: StartDatabaseMigrationRequest,
|
||||
sessionId: string): Promise<StartDatabaseMigrationResponse> {
|
||||
|
||||
const api = await getAzureCoreAPI();
|
||||
const path = encodeURI(`${targetServer.id}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=${DMSV2_API_VERSION}`);
|
||||
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
|
||||
@@ -438,10 +565,10 @@ export interface SqlMigrationServiceProperties {
|
||||
}
|
||||
|
||||
export interface SqlMigrationService {
|
||||
properties: SqlMigrationServiceProperties;
|
||||
location: string;
|
||||
id: string;
|
||||
name: string;
|
||||
location: string;
|
||||
properties: SqlMigrationServiceProperties;
|
||||
error: {
|
||||
code: string,
|
||||
message: string
|
||||
@@ -485,14 +612,29 @@ export interface StartDatabaseMigrationRequest {
|
||||
},
|
||||
sourceLocation?: SourceLocation
|
||||
},
|
||||
sourceSqlConnection: {
|
||||
authentication: string,
|
||||
targetSqlConnection?: {
|
||||
dataSource: string,
|
||||
username: string,
|
||||
password: string
|
||||
authentication: string,
|
||||
userName: string,
|
||||
password: string,
|
||||
encryptConnection?: boolean,
|
||||
trustServerCertificate?: boolean,
|
||||
},
|
||||
sourceSqlConnection: {
|
||||
dataSource: string,
|
||||
authentication: string,
|
||||
userName: string,
|
||||
password: string,
|
||||
encryptConnection?: boolean,
|
||||
trustServerCertificate?: boolean,
|
||||
},
|
||||
sqlDataCopyThresholds?: {
|
||||
cidxrowthreshold: number,
|
||||
cidxkbsthreshold: number,
|
||||
},
|
||||
tableList?: string[],
|
||||
scope: string,
|
||||
offlineConfiguration: OfflineConfiguration,
|
||||
offlineConfiguration?: OfflineConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,10 +645,10 @@ export interface StartDatabaseMigrationResponse {
|
||||
}
|
||||
|
||||
export interface DatabaseMigration {
|
||||
properties: DatabaseMigrationProperties;
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
properties: DatabaseMigrationProperties;
|
||||
}
|
||||
|
||||
export interface DatabaseMigrationProperties {
|
||||
@@ -525,6 +667,7 @@ export interface DatabaseMigrationProperties {
|
||||
backupConfiguration: BackupConfiguration;
|
||||
offlineConfiguration: OfflineConfiguration;
|
||||
migrationFailureError: ErrorInfo;
|
||||
tableList: string[];
|
||||
}
|
||||
|
||||
export interface MigrationStatusDetails {
|
||||
@@ -543,6 +686,7 @@ export interface MigrationStatusDetails {
|
||||
pendingLogBackupsCount: number;
|
||||
invalidFiles: string[];
|
||||
listOfCopyProgressDetails: CopyProgressDetail[];
|
||||
sqlDataCopyErrors: string[];
|
||||
}
|
||||
|
||||
export interface CopyProgressDetail {
|
||||
|
||||
282
extensions/sql-migration/src/api/sqlUtils.ts
Normal file
282
extensions/sql-migration/src/api/sqlUtils.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { azureResource } from 'azurecore';
|
||||
import { AzureSqlDatabase, AzureSqlDatabaseServer } from './azure';
|
||||
import { generateGuid } from './utils';
|
||||
import * as utils from '../api/utils';
|
||||
|
||||
const query_database_tables_sql = `
|
||||
SELECT
|
||||
DB_NAME() as database_name,
|
||||
QUOTENAME(SCHEMA_NAME(o.schema_id)) + '.' + QUOTENAME(o.name) AS table_name,
|
||||
SUM(p.Rows) AS row_count
|
||||
FROM
|
||||
sys.objects AS o
|
||||
INNER JOIN sys.partitions AS p
|
||||
ON o.object_id = p.object_id
|
||||
WHERE
|
||||
o.type = 'U'
|
||||
AND o.is_ms_shipped = 0x0
|
||||
AND index_id < 2
|
||||
GROUP BY
|
||||
o.schema_id,
|
||||
o.name
|
||||
ORDER BY table_name;`;
|
||||
|
||||
const query_target_databases_sql = `
|
||||
SELECT
|
||||
('servername') as server_name,
|
||||
SERVERPROPERTY ('collation') as server_collation,
|
||||
db.database_id as database_id,
|
||||
db.name as database_name,
|
||||
db.collation_name as database_collation,
|
||||
CASE WHEN 'A' = 'a' THEN 0 ELSE 1 END as is_server_case_sensitive,
|
||||
db.state as database_state,
|
||||
db.is_read_only as is_read_only
|
||||
FROM sys.databases db
|
||||
WHERE
|
||||
db.name not in ('master', 'tempdb', 'model', 'msdb')
|
||||
AND is_distributor <> 1
|
||||
ORDER BY db.name;`;
|
||||
|
||||
export const excludeDatabses: string[] = [
|
||||
'master',
|
||||
'tempdb',
|
||||
'msdb',
|
||||
'model'
|
||||
];
|
||||
|
||||
export enum AuthenticationType {
|
||||
Integrated = 'Integrated',
|
||||
SqlLogin = 'SqlLogin'
|
||||
}
|
||||
|
||||
export interface TableInfo {
|
||||
databaseName: string;
|
||||
tableName: string;
|
||||
rowCount: number;
|
||||
selectedForMigration: boolean;
|
||||
}
|
||||
|
||||
export interface TargetDatabaseInfo {
|
||||
serverName: string;
|
||||
serverCollation: string;
|
||||
databaseId: string;
|
||||
databaseName: string;
|
||||
databaseCollation: string;
|
||||
isServerCaseSensitive: boolean;
|
||||
databaseState: number;
|
||||
isReadOnly: boolean;
|
||||
sourceTables: Map<string, TableInfo>;
|
||||
targetTables: Map<string, TableInfo>;
|
||||
}
|
||||
|
||||
function getSqlDbConnectionProfile(
|
||||
serverName: string,
|
||||
tenantId: string,
|
||||
databaseName: string,
|
||||
userName: string,
|
||||
password: string): azdata.IConnectionProfile {
|
||||
return {
|
||||
id: generateGuid(),
|
||||
providerName: 'MSSQL',
|
||||
connectionName: '',
|
||||
serverName: serverName,
|
||||
databaseName: databaseName,
|
||||
userName: userName,
|
||||
password: password,
|
||||
authenticationType: AuthenticationType.SqlLogin,
|
||||
savePassword: false,
|
||||
saveProfile: false,
|
||||
options: {
|
||||
conectionName: '',
|
||||
server: serverName,
|
||||
database: databaseName,
|
||||
authenticationType: AuthenticationType.SqlLogin,
|
||||
user: userName,
|
||||
password: password,
|
||||
connectionTimeout: 60,
|
||||
columnEncryptionSetting: 'Enabled',
|
||||
encrypt: true,
|
||||
trustServerCertificate: false,
|
||||
connectRetryCount: '1',
|
||||
connectRetryInterval: '10',
|
||||
applicationName: 'azdata',
|
||||
azureTenantId: tenantId,
|
||||
originalDatabase: databaseName,
|
||||
databaseDisplayName: databaseName,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getConnectionProfile(
|
||||
serverName: string,
|
||||
azureResourceId: string,
|
||||
userName: string,
|
||||
password: string): azdata.IConnectionProfile {
|
||||
return {
|
||||
serverName: serverName,
|
||||
id: generateGuid(),
|
||||
connectionName: undefined,
|
||||
azureResourceId: azureResourceId,
|
||||
userName: userName,
|
||||
password: password,
|
||||
authenticationType: AuthenticationType.SqlLogin,
|
||||
savePassword: false,
|
||||
groupFullName: '',
|
||||
groupId: '',
|
||||
providerName: 'MSSQL',
|
||||
saveProfile: false,
|
||||
options: {
|
||||
conectionName: '',
|
||||
server: serverName,
|
||||
authenticationType: AuthenticationType.SqlLogin,
|
||||
user: userName,
|
||||
password: password,
|
||||
connectionTimeout: 60,
|
||||
columnEncryptionSetting: 'Enabled',
|
||||
encrypt: true,
|
||||
trustServerCertificate: false,
|
||||
connectRetryCount: '1',
|
||||
connectRetryInterval: '10',
|
||||
applicationName: 'azdata',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function collectSourceDatabaseTableInfo(sourceConnectionId: string, sourceDatabase: string): Promise<TableInfo[]> {
|
||||
const ownerUri = await azdata.connection.getUriForConnection(sourceConnectionId);
|
||||
const connectionProvider = azdata.dataprotocol.getProvider<azdata.ConnectionProvider>(
|
||||
'MSSQL',
|
||||
azdata.DataProviderType.ConnectionProvider);
|
||||
await connectionProvider.changeDatabase(ownerUri, sourceDatabase);
|
||||
const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(
|
||||
'MSSQL',
|
||||
azdata.DataProviderType.QueryProvider);
|
||||
|
||||
const results = await queryProvider.runQueryAndReturn(
|
||||
ownerUri,
|
||||
query_database_tables_sql);
|
||||
|
||||
return results.rows.map(row => {
|
||||
return {
|
||||
databaseName: getSqlString(row[0]),
|
||||
tableName: getSqlString(row[1]),
|
||||
rowCount: getSqlNumber(row[2]),
|
||||
selectedForMigration: false,
|
||||
};
|
||||
}) ?? [];
|
||||
}
|
||||
|
||||
export async function collectTargetDatabaseTableInfo(
|
||||
targetServer: AzureSqlDatabaseServer,
|
||||
targetDatabaseName: string,
|
||||
tenantId: string,
|
||||
userName: string,
|
||||
password: string): Promise<TableInfo[]> {
|
||||
const connectionProfile = getSqlDbConnectionProfile(
|
||||
targetServer.properties.fullyQualifiedDomainName,
|
||||
tenantId,
|
||||
targetDatabaseName,
|
||||
userName,
|
||||
password);
|
||||
|
||||
const result = await azdata.connection.connect(connectionProfile, false, false);
|
||||
if (result.connected && result.connectionId) {
|
||||
const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(
|
||||
'MSSQL',
|
||||
azdata.DataProviderType.QueryProvider);
|
||||
|
||||
const ownerUri = await azdata.connection.getUriForConnection(result.connectionId);
|
||||
const results = await queryProvider.runQueryAndReturn(
|
||||
ownerUri,
|
||||
query_database_tables_sql);
|
||||
|
||||
return results.rows.map(row => {
|
||||
return {
|
||||
databaseName: getSqlString(row[0]),
|
||||
tableName: getSqlString(row[1]),
|
||||
rowCount: getSqlNumber(row[2]),
|
||||
selectedForMigration: false,
|
||||
};
|
||||
}) ?? [];
|
||||
}
|
||||
|
||||
throw new Error(result.errorMessage);
|
||||
}
|
||||
|
||||
export async function collectTargetDatabaseInfo(
|
||||
targetServer: AzureSqlDatabaseServer,
|
||||
userName: string,
|
||||
password: string): Promise<TargetDatabaseInfo[]> {
|
||||
|
||||
const connectionProfile = getConnectionProfile(
|
||||
targetServer.properties.fullyQualifiedDomainName,
|
||||
targetServer.id,
|
||||
userName,
|
||||
password);
|
||||
|
||||
const result = await azdata.connection.connect(connectionProfile, false, false);
|
||||
if (result.connected && result.connectionId) {
|
||||
const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(
|
||||
'MSSQL',
|
||||
azdata.DataProviderType.QueryProvider);
|
||||
|
||||
const ownerUri = await azdata.connection.getUriForConnection(result.connectionId);
|
||||
const results = await queryProvider.runQueryAndReturn(
|
||||
ownerUri,
|
||||
query_target_databases_sql);
|
||||
|
||||
return results.rows.map(row => {
|
||||
return {
|
||||
serverName: getSqlString(row[0]),
|
||||
serverCollation: getSqlString(row[1]),
|
||||
databaseId: getSqlString(row[2]),
|
||||
databaseName: getSqlString(row[3]),
|
||||
databaseCollation: getSqlString(row[4]),
|
||||
isServerCaseSensitive: getSqlBoolean(row[5]),
|
||||
databaseState: getSqlNumber(row[6]),
|
||||
isReadOnly: getSqlBoolean(row[7]),
|
||||
sourceTables: new Map(),
|
||||
targetTables: new Map(),
|
||||
};
|
||||
}) ?? [];
|
||||
}
|
||||
|
||||
throw new Error(result.errorMessage);
|
||||
}
|
||||
|
||||
export async function collectAzureTargetDatabases(
|
||||
account: azdata.Account,
|
||||
subscription: azureResource.AzureResourceSubscription,
|
||||
resourceGroup: string,
|
||||
targetServerName: string,
|
||||
): Promise<AzureSqlDatabase[]> {
|
||||
const databaseList: AzureSqlDatabase[] = [];
|
||||
if (resourceGroup && targetServerName) {
|
||||
databaseList.push(...
|
||||
await utils.getAzureSqlDatabases(
|
||||
account,
|
||||
subscription,
|
||||
resourceGroup,
|
||||
targetServerName));
|
||||
}
|
||||
return databaseList.filter(
|
||||
database => !excludeDatabses.includes(database.name)) ?? [];
|
||||
}
|
||||
|
||||
export function getSqlString(value: azdata.DbCellValue): string {
|
||||
return value.isNull ? '' : value.displayValue;
|
||||
}
|
||||
|
||||
export function getSqlNumber(value: azdata.DbCellValue): number {
|
||||
return value.isNull ? 0 : parseInt(value.displayValue);
|
||||
}
|
||||
|
||||
export function getSqlBoolean(value: azdata.DbCellValue): boolean {
|
||||
return value.isNull ? false : value.displayValue === '1';
|
||||
}
|
||||
@@ -3,10 +3,9 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { window, Account, accounts, CategoryValue, DropDownComponent, IconPath } from 'azdata';
|
||||
import { window, Account, accounts, CategoryValue, DropDownComponent, IconPath, DisplayType, Component } from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { MigrationStatus, ProvisioningState } from '../models/migrationLocalStorage';
|
||||
import * as crypto from 'crypto';
|
||||
import * as azure from './azure';
|
||||
import { azureResource, Tenant } from 'azurecore';
|
||||
@@ -15,8 +14,26 @@ import { logError, TelemetryViews } from '../telemtery';
|
||||
import { AdsMigrationStatus } from '../dashboard/tabBase';
|
||||
import { getMigrationMode, getMigrationStatus, getMigrationTargetType, PipelineStatusCodes } from '../constants/helper';
|
||||
|
||||
export type TargetServerType = azure.SqlVMServer | azureResource.AzureSqlManagedInstance | azure.AzureSqlDatabaseServer;
|
||||
|
||||
export const SqlMigrationExtensionId = 'microsoft.sql-migration';
|
||||
export const DefaultSettingValue = '---';
|
||||
|
||||
export const MenuCommands = {
|
||||
Cutover: 'sqlmigration.cutover',
|
||||
ViewDatabase: 'sqlmigration.view.database',
|
||||
ViewTarget: 'sqlmigration.view.target',
|
||||
ViewService: 'sqlmigration.view.service',
|
||||
CopyMigration: 'sqlmigration.copy.migration',
|
||||
CancelMigration: 'sqlmigration.cancel.migration',
|
||||
RetryMigration: 'sqlmigration.retry.migration',
|
||||
StartMigration: 'sqlmigration.start',
|
||||
IssueReporter: 'workbench.action.openIssueReporter',
|
||||
OpenNotebooks: 'sqlmigration.openNotebooks',
|
||||
NewSupportRequest: 'sqlmigration.newsupportrequest',
|
||||
SendFeedback: 'sqlmigration.sendfeedback',
|
||||
};
|
||||
|
||||
export function deepClone<T>(obj: T): T {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return obj;
|
||||
@@ -145,19 +162,19 @@ export function filterMigrations(databaseMigrations: azure.DatabaseMigration[],
|
||||
return filteredMigration.filter(
|
||||
value => {
|
||||
const status = getMigrationStatus(value);
|
||||
return status === MigrationStatus.InProgress
|
||||
|| status === MigrationStatus.Retriable
|
||||
|| status === MigrationStatus.Creating;
|
||||
return status === constants.MigrationStatus.InProgress
|
||||
|| status === constants.MigrationStatus.Retriable
|
||||
|| status === constants.MigrationStatus.Creating;
|
||||
});
|
||||
case AdsMigrationStatus.SUCCEEDED:
|
||||
return filteredMigration.filter(
|
||||
value => getMigrationStatus(value) === MigrationStatus.Succeeded);
|
||||
value => getMigrationStatus(value) === constants.MigrationStatus.Succeeded);
|
||||
case AdsMigrationStatus.FAILED:
|
||||
return filteredMigration.filter(
|
||||
value => getMigrationStatus(value) === MigrationStatus.Failed);
|
||||
value => getMigrationStatus(value) === constants.MigrationStatus.Failed);
|
||||
case AdsMigrationStatus.COMPLETING:
|
||||
return filteredMigration.filter(
|
||||
value => getMigrationStatus(value) === MigrationStatus.Completing);
|
||||
value => getMigrationStatus(value) === constants.MigrationStatus.Completing);
|
||||
}
|
||||
return filteredMigration;
|
||||
}
|
||||
@@ -192,6 +209,8 @@ export function selectDefaultDropdownValue(dropDown: DropDownComponent, value?:
|
||||
selectedIndex = -1;
|
||||
}
|
||||
selectDropDownIndex(dropDown, selectedIndex > -1 ? selectedIndex : 0);
|
||||
} else {
|
||||
dropDown.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,19 +270,22 @@ export function getSessionIdHeader(sessionId: string): { [key: string]: string }
|
||||
|
||||
export function getMigrationStatusWithErrors(migration: azure.DatabaseMigration): string {
|
||||
const properties = migration.properties;
|
||||
const migrationStatus = properties.migrationStatus ?? properties.provisioningState;
|
||||
let warningCount = 0;
|
||||
const migrationStatus = getMigrationStatus(migration) ?? '';
|
||||
|
||||
if (properties.migrationFailureError?.message) {
|
||||
warningCount++;
|
||||
}
|
||||
if (properties.migrationStatusDetails?.fileUploadBlockingErrors) {
|
||||
const blockingErrors = properties.migrationStatusDetails?.fileUploadBlockingErrors.length ?? 0;
|
||||
warningCount += blockingErrors;
|
||||
}
|
||||
if (properties.migrationStatusDetails?.restoreBlockingReason) {
|
||||
warningCount++;
|
||||
}
|
||||
// provisioning error
|
||||
let warningCount = properties.provisioningError?.length > 0 ? 1 : 0;
|
||||
|
||||
// migration failure error
|
||||
warningCount += properties.migrationFailureError?.message?.length > 0 ? 1 : 0;
|
||||
|
||||
// file upload blocking errors
|
||||
warningCount += properties.migrationStatusDetails?.fileUploadBlockingErrors?.length ?? 0;
|
||||
|
||||
// restore blocking reason
|
||||
warningCount += properties.migrationStatusDetails?.restoreBlockingReason ? 1 : 0;
|
||||
|
||||
// sql data copy errors
|
||||
warningCount += properties.migrationStatusDetails?.sqlDataCopyErrors?.length ?? 0;
|
||||
|
||||
return constants.STATUS_VALUE(migrationStatus, warningCount)
|
||||
+ (constants.STATUS_WARNING_COUNT(migrationStatus, warningCount) ?? '');
|
||||
@@ -302,20 +324,20 @@ export function getPipelineStatusImage(status: string | undefined): IconPath {
|
||||
export function getMigrationStatusImage(migration: azure.DatabaseMigration): IconPath {
|
||||
const status = getMigrationStatus(migration);
|
||||
switch (status) {
|
||||
case MigrationStatus.InProgress:
|
||||
case constants.MigrationStatus.InProgress:
|
||||
return IconPathHelper.inProgressMigration;
|
||||
case MigrationStatus.Succeeded:
|
||||
case constants.MigrationStatus.Succeeded:
|
||||
return IconPathHelper.completedMigration;
|
||||
case MigrationStatus.Creating:
|
||||
case constants.MigrationStatus.Creating:
|
||||
return IconPathHelper.notStartedMigration;
|
||||
case MigrationStatus.Completing:
|
||||
case constants.MigrationStatus.Completing:
|
||||
return IconPathHelper.completingCutover;
|
||||
case MigrationStatus.Retriable:
|
||||
case constants.MigrationStatus.Retriable:
|
||||
return IconPathHelper.retry;
|
||||
case MigrationStatus.Canceling:
|
||||
case MigrationStatus.Canceled:
|
||||
case constants.MigrationStatus.Canceling:
|
||||
case constants.MigrationStatus.Canceled:
|
||||
return IconPathHelper.cancel;
|
||||
case MigrationStatus.Failed:
|
||||
case constants.MigrationStatus.Failed:
|
||||
default:
|
||||
return IconPathHelper.error;
|
||||
}
|
||||
@@ -379,34 +401,7 @@ export async function getAzureAccountsDropdownValues(accounts: Account[]): Promi
|
||||
}
|
||||
|
||||
export function getAzureTenants(account?: Account): Tenant[] {
|
||||
let tenants: Tenant[] = [];
|
||||
try {
|
||||
if (account) {
|
||||
tenants = account.properties.tenants;
|
||||
}
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.Utils, 'utils.getAzureTenants', e);
|
||||
}
|
||||
return tenants;
|
||||
}
|
||||
|
||||
export async function getAzureTenantsDropdownValues(tenants: Tenant[]): Promise<CategoryValue[]> {
|
||||
let tenantsValues: CategoryValue[] = [];
|
||||
tenants.forEach((tenant) => {
|
||||
tenantsValues.push({
|
||||
name: tenant.id,
|
||||
displayName: tenant.displayName
|
||||
});
|
||||
});
|
||||
if (tenantsValues.length === 0) {
|
||||
tenantsValues = [
|
||||
{
|
||||
displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR,
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
}
|
||||
return tenantsValues;
|
||||
return account?.properties.tenants || [];
|
||||
}
|
||||
|
||||
export async function getAzureSubscriptions(account?: Account): Promise<azureResource.AzureResourceSubscription[]> {
|
||||
@@ -441,172 +436,58 @@ export async function getAzureSubscriptionsDropdownValues(subscriptions: azureRe
|
||||
return subscriptionsValues;
|
||||
}
|
||||
|
||||
export async function getSqlManagedInstanceLocations(account?: Account, subscription?: azureResource.AzureResourceSubscription, managedInstances?: azureResource.AzureSqlManagedInstance[]): Promise<azureResource.AzureLocation[]> {
|
||||
let locations: azureResource.AzureLocation[] = [];
|
||||
export async function getResourceLocations(
|
||||
account?: Account,
|
||||
subscription?: azureResource.AzureResourceSubscription,
|
||||
resources?: { location: string }[]): Promise<azureResource.AzureLocation[]> {
|
||||
|
||||
try {
|
||||
if (account && subscription && managedInstances) {
|
||||
locations = await azure.getLocations(account, subscription);
|
||||
locations = locations.filter((loc, i) => managedInstances.some(mi => mi.location.toLowerCase() === loc.name.toLowerCase()));
|
||||
if (account && subscription && resources) {
|
||||
const locations = await azure.getLocations(account, subscription);
|
||||
return locations
|
||||
.filter((loc, i) => resources.some(resource => resource.location.toLowerCase() === loc.name.toLowerCase()))
|
||||
.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
}
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.Utils, 'utils.getSqlManagedInstanceLocations', e);
|
||||
logError(TelemetryViews.Utils, 'utils.getResourceLocations', e);
|
||||
}
|
||||
locations.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
return locations;
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function getSqlVirtualMachineLocations(account?: Account, subscription?: azureResource.AzureResourceSubscription, virtualMachines?: azure.SqlVMServer[]): Promise<azureResource.AzureLocation[]> {
|
||||
let locations: azureResource.AzureLocation[] = [];
|
||||
try {
|
||||
if (account && subscription && virtualMachines) {
|
||||
locations = await azure.getLocations(account, subscription);
|
||||
locations = locations.filter((loc, i) => virtualMachines.some(vm => vm.location.toLowerCase() === loc.name.toLowerCase()));
|
||||
}
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.Utils, 'utils.getSqlVirtualMachineLocations', e);
|
||||
}
|
||||
locations.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
return locations;
|
||||
}
|
||||
export function getServiceResourceGroupsByLocation(
|
||||
resources: { location: string, id: string, tenantId?: string }[],
|
||||
location: azureResource.AzureLocation): azureResource.AzureResourceResourceGroup[] {
|
||||
|
||||
export async function getSqlMigrationServiceLocations(account?: Account, subscription?: azureResource.AzureResourceSubscription, migrationServices?: azure.SqlMigrationService[]): Promise<azureResource.AzureLocation[]> {
|
||||
let locations: azureResource.AzureLocation[] = [];
|
||||
try {
|
||||
if (account && subscription && migrationServices) {
|
||||
locations = await azure.getLocations(account, subscription);
|
||||
locations = locations.filter((loc, i) => migrationServices.some(dms => dms.location.toLowerCase() === loc.name.toLowerCase()));
|
||||
}
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.Utils, 'utils.getSqlMigrationServiceLocations', e);
|
||||
}
|
||||
locations.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
return locations;
|
||||
}
|
||||
|
||||
export async function getAzureLocationsDropdownValues(locations: azureResource.AzureLocation[]): Promise<CategoryValue[]> {
|
||||
let locationValues: CategoryValue[] = [];
|
||||
locations.forEach((loc) => {
|
||||
locationValues.push({
|
||||
name: loc.name,
|
||||
displayName: loc.displayName
|
||||
});
|
||||
});
|
||||
if (locationValues.length === 0) {
|
||||
locationValues = [
|
||||
{
|
||||
displayName: constants.NO_LOCATION_FOUND,
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
}
|
||||
return locationValues;
|
||||
}
|
||||
|
||||
export async function getSqlManagedInstanceResourceGroups(managedInstances?: azureResource.AzureSqlManagedInstance[], location?: azureResource.AzureLocation): Promise<azureResource.AzureResourceResourceGroup[]> {
|
||||
let resourceGroups: azureResource.AzureResourceResourceGroup[] = [];
|
||||
try {
|
||||
if (managedInstances && location) {
|
||||
resourceGroups = managedInstances
|
||||
.filter((mi) => mi.location.toLowerCase() === location.name.toLowerCase())
|
||||
.map((mi) => {
|
||||
return <azureResource.AzureResourceResourceGroup>{
|
||||
id: azure.getFullResourceGroupFromId(mi.id),
|
||||
name: azure.getResourceGroupFromId(mi.id),
|
||||
subscription: {
|
||||
id: mi.subscriptionId
|
||||
},
|
||||
tenant: mi.tenantId
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.Utils, 'utils.getSqlManagedInstanceResourceGroups', e);
|
||||
if (resources && location) {
|
||||
const locationName = location.name.toLowerCase();
|
||||
resourceGroups = resources
|
||||
.filter(resource => resource.location.toLowerCase() === locationName)
|
||||
.map(resource => {
|
||||
return <azureResource.AzureResourceResourceGroup>{
|
||||
id: azure.getFullResourceGroupFromId(resource.id),
|
||||
name: azure.getResourceGroupFromId(resource.id),
|
||||
subscription: { id: getSubscriptionIdFromResourceId(resource.id) },
|
||||
tenant: resource.tenantId
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// remove duplicates
|
||||
resourceGroups = resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i);
|
||||
resourceGroups.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return resourceGroups;
|
||||
return resourceGroups
|
||||
.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
export async function getSqlVirtualMachineResourceGroups(virtualMachines?: azure.SqlVMServer[], location?: azureResource.AzureLocation): Promise<azureResource.AzureResourceResourceGroup[]> {
|
||||
let resourceGroups: azureResource.AzureResourceResourceGroup[] = [];
|
||||
try {
|
||||
if (virtualMachines && location) {
|
||||
resourceGroups = virtualMachines
|
||||
.filter((vm) => vm.location.toLowerCase() === location.name.toLowerCase())
|
||||
.map((vm) => {
|
||||
return <azureResource.AzureResourceResourceGroup>{
|
||||
id: azure.getFullResourceGroupFromId(vm.id),
|
||||
name: azure.getResourceGroupFromId(vm.id),
|
||||
subscription: {
|
||||
id: vm.subscriptionId
|
||||
},
|
||||
tenant: vm.tenantId
|
||||
};
|
||||
});
|
||||
export function getSubscriptionIdFromResourceId(resourceId: string): string | undefined {
|
||||
let parts = resourceId?.split('/subscriptions/');
|
||||
if (parts?.length > 1) {
|
||||
parts = parts[1]?.split('/resourcegroups/');
|
||||
if (parts?.length > 0) {
|
||||
return parts[0];
|
||||
}
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.Utils, 'utils.getSqlVirtualMachineResourceGroups', e);
|
||||
}
|
||||
|
||||
// remove duplicates
|
||||
resourceGroups = resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i);
|
||||
resourceGroups.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return resourceGroups;
|
||||
}
|
||||
|
||||
export async function getStorageAccountResourceGroups(storageAccounts?: azure.StorageAccount[], location?: azureResource.AzureLocation): Promise<azureResource.AzureResourceResourceGroup[]> {
|
||||
let resourceGroups: azureResource.AzureResourceResourceGroup[] = [];
|
||||
try {
|
||||
if (storageAccounts && location) {
|
||||
resourceGroups = storageAccounts
|
||||
.filter((sa) => sa.location.toLowerCase() === location.name.toLowerCase())
|
||||
.map((sa) => {
|
||||
return <azureResource.AzureResourceResourceGroup>{
|
||||
id: azure.getFullResourceGroupFromId(sa.id),
|
||||
name: azure.getResourceGroupFromId(sa.id),
|
||||
subscription: {
|
||||
id: sa.subscriptionId
|
||||
},
|
||||
tenant: sa.tenantId
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.Utils, 'utils.getStorageAccountResourceGroups', e);
|
||||
}
|
||||
|
||||
// remove duplicates
|
||||
resourceGroups = resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i);
|
||||
resourceGroups.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return resourceGroups;
|
||||
}
|
||||
|
||||
export async function getSqlMigrationServiceResourceGroups(migrationServices?: azure.SqlMigrationService[], location?: azureResource.AzureLocation): Promise<azureResource.AzureResourceResourceGroup[]> {
|
||||
let resourceGroups: azureResource.AzureResourceResourceGroup[] = [];
|
||||
try {
|
||||
if (migrationServices && location) {
|
||||
resourceGroups = migrationServices
|
||||
.filter((dms) => dms.properties.provisioningState === ProvisioningState.Succeeded && dms.location.toLowerCase() === location.name.toLowerCase())
|
||||
.map((dms) => {
|
||||
return <azureResource.AzureResourceResourceGroup>{
|
||||
id: azure.getFullResourceGroupFromId(dms.id),
|
||||
name: azure.getResourceGroupFromId(dms.id),
|
||||
subscription: {
|
||||
id: dms.properties.subscriptionId
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.Utils, 'utils.getSqlMigrationServiceResourceGroups', e);
|
||||
}
|
||||
|
||||
// remove duplicates
|
||||
resourceGroups = resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i);
|
||||
resourceGroups.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return resourceGroups;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function getAllResourceGroups(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azureResource.AzureResourceResourceGroup[]> {
|
||||
@@ -622,25 +503,6 @@ export async function getAllResourceGroups(account?: Account, subscription?: azu
|
||||
return resourceGroups;
|
||||
}
|
||||
|
||||
export async function getAzureResourceGroupsDropdownValues(resourceGroups: azureResource.AzureResourceResourceGroup[]): Promise<CategoryValue[]> {
|
||||
let resourceGroupValues: CategoryValue[] = [];
|
||||
resourceGroups.forEach((rg) => {
|
||||
resourceGroupValues.push({
|
||||
name: rg.id,
|
||||
displayName: rg.name
|
||||
});
|
||||
});
|
||||
if (resourceGroupValues.length === 0) {
|
||||
resourceGroupValues = [
|
||||
{
|
||||
displayName: constants.RESOURCE_GROUP_NOT_FOUND,
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
}
|
||||
return resourceGroupValues;
|
||||
}
|
||||
|
||||
export async function getManagedInstances(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azureResource.AzureSqlManagedInstance[]> {
|
||||
let managedInstances: azureResource.AzureSqlManagedInstance[] = [];
|
||||
try {
|
||||
@@ -687,6 +549,31 @@ export async function getManagedInstancesDropdownValues(managedInstances: azureR
|
||||
return managedInstancesValues;
|
||||
}
|
||||
|
||||
export async function getAzureSqlDatabaseServers(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azure.AzureSqlDatabaseServer[]> {
|
||||
let sqlDatabaseServers: azure.AzureSqlDatabaseServer[] = [];
|
||||
try {
|
||||
if (account && subscription) {
|
||||
sqlDatabaseServers = await azure.getAvailableSqlDatabaseServers(account, subscription);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.Utils, 'utils.getAzureSqlDatabaseServers', e);
|
||||
}
|
||||
sqlDatabaseServers.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return sqlDatabaseServers;
|
||||
}
|
||||
|
||||
export async function getAzureSqlDatabases(account?: Account, subscription?: azureResource.AzureResourceSubscription, resourceGroupName?: string, serverName?: string): Promise<azure.AzureSqlDatabase[]> {
|
||||
if (account && subscription && resourceGroupName && serverName) {
|
||||
try {
|
||||
const databases = await azure.getAvailableSqlDatabases(account, subscription, resourceGroupName, serverName);
|
||||
return databases.sort((a, b) => a.name.localeCompare(b.name));
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.Utils, 'utils.getAzureSqlDatabases', e);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function getVirtualMachines(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azure.SqlVMServer[]> {
|
||||
let virtualMachines: azure.SqlVMServer[] = [];
|
||||
try {
|
||||
@@ -705,30 +592,6 @@ export async function getVirtualMachines(account?: Account, subscription?: azure
|
||||
return virtualMachines;
|
||||
}
|
||||
|
||||
export async function getVirtualMachinesDropdownValues(virtualMachines: azure.SqlVMServer[], location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise<CategoryValue[]> {
|
||||
let virtualMachineValues: CategoryValue[] = [];
|
||||
if (location && resourceGroup) {
|
||||
virtualMachines.forEach((virtualMachine) => {
|
||||
if (virtualMachine.location.toLowerCase() === location.name.toLowerCase() && azure.getResourceGroupFromId(virtualMachine.id).toLowerCase() === resourceGroup.name.toLowerCase()) {
|
||||
virtualMachineValues.push({
|
||||
name: virtualMachine.id,
|
||||
displayName: virtualMachine.name
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (virtualMachineValues.length === 0) {
|
||||
virtualMachineValues = [
|
||||
{
|
||||
displayName: constants.NO_VIRTUAL_MACHINE_FOUND,
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
}
|
||||
return virtualMachineValues;
|
||||
}
|
||||
|
||||
export async function getStorageAccounts(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azure.StorageAccount[]> {
|
||||
let storageAccounts: azure.StorageAccount[] = [];
|
||||
try {
|
||||
@@ -742,65 +605,18 @@ export async function getStorageAccounts(account?: Account, subscription?: azure
|
||||
return storageAccounts;
|
||||
}
|
||||
|
||||
export async function getStorageAccountsDropdownValues(storageAccounts: azure.StorageAccount[], location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise<CategoryValue[]> {
|
||||
let storageAccountValues: CategoryValue[] = [];
|
||||
storageAccounts.forEach((storageAccount) => {
|
||||
if (storageAccount.location.toLowerCase() === location.name.toLowerCase() && storageAccount.resourceGroup?.toLowerCase() === resourceGroup.name.toLowerCase()) {
|
||||
storageAccountValues.push({
|
||||
name: storageAccount.id,
|
||||
displayName: storageAccount.name
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (storageAccountValues.length === 0) {
|
||||
storageAccountValues = [
|
||||
{
|
||||
displayName: constants.NO_STORAGE_ACCOUNT_FOUND,
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
}
|
||||
return storageAccountValues;
|
||||
}
|
||||
|
||||
export async function getAzureSqlMigrationServices(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azure.SqlMigrationService[]> {
|
||||
let sqlMigrationServices: azure.SqlMigrationService[] = [];
|
||||
try {
|
||||
if (account && subscription) {
|
||||
sqlMigrationServices = (await azure.getSqlMigrationServices(account, subscription)).filter(dms => {
|
||||
return dms.properties.provisioningState === ProvisioningState.Succeeded;
|
||||
});
|
||||
const services = await azure.getSqlMigrationServices(account, subscription);
|
||||
return services
|
||||
.filter(dms => dms.properties.provisioningState === constants.ProvisioningState.Succeeded)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.Utils, 'utils.getAzureSqlMigrationServices', e);
|
||||
}
|
||||
sqlMigrationServices.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return sqlMigrationServices;
|
||||
}
|
||||
|
||||
export async function getAzureSqlMigrationServicesDropdownValues(sqlMigrationServices: azure.SqlMigrationService[], location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise<CategoryValue[]> {
|
||||
let SqlMigrationServicesValues: CategoryValue[] = [];
|
||||
if (location && resourceGroup) {
|
||||
sqlMigrationServices.forEach((sqlMigrationService) => {
|
||||
if (sqlMigrationService.location.toLowerCase() === location.name.toLowerCase() && sqlMigrationService.properties.resourceGroup.toLowerCase() === resourceGroup.name.toLowerCase()) {
|
||||
SqlMigrationServicesValues.push({
|
||||
name: sqlMigrationService.id,
|
||||
displayName: sqlMigrationService.name
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (SqlMigrationServicesValues.length === 0) {
|
||||
SqlMigrationServicesValues = [
|
||||
{
|
||||
displayName: constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR,
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
}
|
||||
return SqlMigrationServicesValues;
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function getBlobContainer(account?: Account, subscription?: azureResource.AzureResourceSubscription, storageAccount?: azure.StorageAccount): Promise<azureResource.BlobContainer[]> {
|
||||
@@ -816,25 +632,6 @@ export async function getBlobContainer(account?: Account, subscription?: azureRe
|
||||
return blobContainers;
|
||||
}
|
||||
|
||||
export async function getBlobContainersValues(blobContainers: azureResource.BlobContainer[]): Promise<CategoryValue[]> {
|
||||
let blobContainersValues: CategoryValue[] = [];
|
||||
blobContainers.forEach((blobContainer) => {
|
||||
blobContainersValues.push({
|
||||
name: blobContainer.id,
|
||||
displayName: blobContainer.name
|
||||
});
|
||||
});
|
||||
if (blobContainersValues.length === 0) {
|
||||
blobContainersValues = [
|
||||
{
|
||||
displayName: constants.NO_BLOBCONTAINERS_FOUND,
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
}
|
||||
return blobContainersValues;
|
||||
}
|
||||
|
||||
export async function getBlobLastBackupFileNames(account?: Account, subscription?: azureResource.AzureResourceSubscription, storageAccount?: azure.StorageAccount, blobContainer?: azureResource.BlobContainer): Promise<azureResource.Blob[]> {
|
||||
let lastFileNames: azureResource.Blob[] = [];
|
||||
try {
|
||||
@@ -848,39 +645,91 @@ export async function getBlobLastBackupFileNames(account?: Account, subscription
|
||||
return lastFileNames;
|
||||
}
|
||||
|
||||
export async function getBlobLastBackupFileNamesValues(lastFileNames: azureResource.Blob[]): Promise<CategoryValue[]> {
|
||||
let lastFileNamesValues: CategoryValue[] = [];
|
||||
lastFileNames.forEach((lastFileName) => {
|
||||
lastFileNamesValues.push({
|
||||
name: lastFileName.name,
|
||||
displayName: lastFileName.name
|
||||
});
|
||||
});
|
||||
if (lastFileNamesValues.length === 0) {
|
||||
lastFileNamesValues = [
|
||||
{
|
||||
displayName: constants.NO_BLOBFILES_FOUND,
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
export function getAzureResourceDropdownValues(
|
||||
azureResources: { location: string, id: string, name: string }[],
|
||||
location: azureResource.AzureLocation | undefined,
|
||||
resourceGroup: string | undefined,
|
||||
resourceNotFoundMessage: string): CategoryValue[] {
|
||||
|
||||
if (location?.name && resourceGroup && azureResources?.length > 0) {
|
||||
const locationName = location.name.toLowerCase();
|
||||
const resourceGroupName = resourceGroup.toLowerCase();
|
||||
|
||||
return azureResources
|
||||
.filter(resource =>
|
||||
resource.location?.toLowerCase() === locationName &&
|
||||
azure.getResourceGroupFromId(resource.id)?.toLowerCase() === resourceGroupName)
|
||||
.map(resource => {
|
||||
return { name: resource.id, displayName: resource.name };
|
||||
});
|
||||
}
|
||||
return lastFileNamesValues;
|
||||
|
||||
return [{ name: '', displayName: resourceNotFoundMessage }];
|
||||
}
|
||||
|
||||
export function getResourceDropdownValues(resources: { id: string, name: string }[], resourceNotFoundMessage: string): CategoryValue[] {
|
||||
return resources?.map(resource => { return { name: resource.id, displayName: resource.name }; })
|
||||
|| [{ name: '', displayName: resourceNotFoundMessage }];
|
||||
}
|
||||
|
||||
export async function getAzureTenantsDropdownValues(tenants: Tenant[]): Promise<CategoryValue[]> {
|
||||
return tenants?.map(tenant => { return { name: tenant.id, displayName: tenant.displayName }; })
|
||||
|| [{ name: '', displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR }];
|
||||
}
|
||||
|
||||
export async function getAzureLocationsDropdownValues(locations: azureResource.AzureLocation[]): Promise<CategoryValue[]> {
|
||||
return locations?.map(location => { return { name: location.name, displayName: location.displayName }; })
|
||||
|| [{ name: '', displayName: constants.NO_LOCATION_FOUND }];
|
||||
}
|
||||
|
||||
export async function getBlobLastBackupFileNamesValues(blobs: azureResource.Blob[]): Promise<CategoryValue[]> {
|
||||
return blobs?.map(blob => { return { name: blob.name, displayName: blob.name }; })
|
||||
|| [{ name: '', displayName: constants.NO_BLOBFILES_FOUND }];
|
||||
}
|
||||
|
||||
export async function updateControlDisplay(control: Component, visible: boolean, displayStyle: DisplayType = 'inline'): Promise<void> {
|
||||
const display = visible ? displayStyle : 'none';
|
||||
control.display = display;
|
||||
await control.updateCssStyles({ 'display': display });
|
||||
await control.updateProperties({ 'display': display });
|
||||
}
|
||||
|
||||
export function generateGuid(): string {
|
||||
const hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
|
||||
// c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
|
||||
let oct: string = '';
|
||||
let tmp: number;
|
||||
/* tslint:disable:no-bitwise */
|
||||
for (let a: number = 0; a < 4; a++) {
|
||||
tmp = (4294967296 * Math.random()) | 0;
|
||||
oct += hexValues[tmp & 0xF] +
|
||||
hexValues[tmp >> 4 & 0xF] +
|
||||
hexValues[tmp >> 8 & 0xF] +
|
||||
hexValues[tmp >> 12 & 0xF] +
|
||||
hexValues[tmp >> 16 & 0xF] +
|
||||
hexValues[tmp >> 20 & 0xF] +
|
||||
hexValues[tmp >> 24 & 0xF] +
|
||||
hexValues[tmp >> 28 & 0xF];
|
||||
}
|
||||
|
||||
// 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively'
|
||||
const clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0];
|
||||
return `${oct.substr(0, 8)}-${oct.substr(9, 4)}-4${oct.substr(13, 3)}-${clockSequenceHi}${oct.substr(16, 3)}-${oct.substr(19, 12)}`;
|
||||
/* tslint:enable:no-bitwise */
|
||||
}
|
||||
|
||||
export async function promptUserForFolder(): Promise<string> {
|
||||
let path = '';
|
||||
|
||||
let options: vscode.OpenDialogOptions = {
|
||||
const options: vscode.OpenDialogOptions = {
|
||||
defaultUri: vscode.Uri.file(getUserHome()!),
|
||||
canSelectFiles: false,
|
||||
canSelectFolders: true,
|
||||
canSelectMany: false,
|
||||
};
|
||||
|
||||
let fileUris = await vscode.window.showOpenDialog(options);
|
||||
if (fileUris && fileUris?.length > 0 && fileUris[0]) {
|
||||
path = fileUris[0].fsPath;
|
||||
const fileUris = await vscode.window.showOpenDialog(options);
|
||||
if (fileUris && fileUris.length > 0 && fileUris[0]) {
|
||||
return fileUris[0].fsPath;
|
||||
}
|
||||
|
||||
return path;
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { MigrationStatus } from '../models/migrationLocalStorage';
|
||||
import { DefaultSettingValue } from '../api/utils';
|
||||
import { FileStorageType, MigrationMode, MigrationTargetType } from '../models/stateMachine';
|
||||
import * as loc from './strings';
|
||||
|
||||
@@ -143,6 +143,11 @@ export function getMigrationStatus(migration: DatabaseMigration | undefined): st
|
||||
?? migration?.properties.provisioningState;
|
||||
}
|
||||
|
||||
export function getMigrationStatusString(migration: DatabaseMigration | undefined): string {
|
||||
const migrationStatus = getMigrationStatus(migration) ?? DefaultSettingValue;
|
||||
return loc.StatusLookup[migrationStatus] ?? migrationStatus;
|
||||
}
|
||||
|
||||
export function hasMigrationOperationId(migration: DatabaseMigration | undefined): boolean {
|
||||
const migrationId = migration?.id ?? '';
|
||||
const migationOperationId = migration?.properties?.migrationOperationId ?? '';
|
||||
@@ -153,41 +158,41 @@ export function hasMigrationOperationId(migration: DatabaseMigration | undefined
|
||||
export function canCancelMigration(migration: DatabaseMigration | undefined): boolean {
|
||||
const status = getMigrationStatus(migration);
|
||||
return hasMigrationOperationId(migration)
|
||||
&& (status === MigrationStatus.InProgress ||
|
||||
status === MigrationStatus.Retriable ||
|
||||
status === MigrationStatus.Creating);
|
||||
&& (status === loc.MigrationStatus.InProgress ||
|
||||
status === loc.MigrationStatus.Retriable ||
|
||||
status === loc.MigrationStatus.Creating);
|
||||
}
|
||||
|
||||
export function canDeleteMigration(migration: DatabaseMigration | undefined): boolean {
|
||||
const status = getMigrationStatus(migration);
|
||||
return status === MigrationStatus.Canceled
|
||||
|| status === MigrationStatus.Failed
|
||||
|| status === MigrationStatus.Retriable
|
||||
|| status === MigrationStatus.Succeeded;
|
||||
return status === loc.MigrationStatus.Canceled
|
||||
|| status === loc.MigrationStatus.Failed
|
||||
|| status === loc.MigrationStatus.Retriable
|
||||
|| status === loc.MigrationStatus.Succeeded;
|
||||
}
|
||||
|
||||
export function canRetryMigration(migration: DatabaseMigration | undefined): boolean {
|
||||
const status = getMigrationStatus(migration);
|
||||
return status === MigrationStatus.Canceled
|
||||
|| status === MigrationStatus.Retriable
|
||||
|| status === MigrationStatus.Failed
|
||||
|| status === MigrationStatus.Succeeded;
|
||||
return status === loc.MigrationStatus.Canceled
|
||||
|| status === loc.MigrationStatus.Retriable
|
||||
|| status === loc.MigrationStatus.Failed
|
||||
|| status === loc.MigrationStatus.Succeeded;
|
||||
}
|
||||
|
||||
export function canCutoverMigration(migration: DatabaseMigration | undefined): boolean {
|
||||
const status = getMigrationStatus(migration);
|
||||
return hasMigrationOperationId(migration)
|
||||
&& status === MigrationStatus.InProgress
|
||||
&& status === loc.MigrationStatus.InProgress
|
||||
&& isOnlineMigration(migration)
|
||||
&& isFullBackupRestored(migration);
|
||||
}
|
||||
|
||||
export function isActiveMigration(migration: DatabaseMigration | undefined): boolean {
|
||||
const status = getMigrationStatus(migration);
|
||||
return status === MigrationStatus.Completing
|
||||
|| status === MigrationStatus.Retriable
|
||||
|| status === MigrationStatus.Creating
|
||||
|| status === MigrationStatus.InProgress;
|
||||
return status === loc.MigrationStatus.Completing
|
||||
|| status === loc.MigrationStatus.Retriable
|
||||
|| status === loc.MigrationStatus.Creating
|
||||
|| status === loc.MigrationStatus.InProgress;
|
||||
}
|
||||
|
||||
export function isFullBackupRestored(migration: DatabaseMigration | undefined): boolean {
|
||||
|
||||
@@ -6,11 +6,36 @@
|
||||
import { AzureAccount } from 'azurecore';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { EOL } from 'os';
|
||||
import { MigrationStatus } from '../models/migrationLocalStorage';
|
||||
import { MigrationSourceAuthenticationType } from '../models/stateMachine';
|
||||
import { ParallelCopyTypeCodes, PipelineStatusCodes } from './helper';
|
||||
import { formatNumber, ParallelCopyTypeCodes, PipelineStatusCodes } from './helper';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export enum MigrationStatus {
|
||||
Failed = 'Failed',
|
||||
Succeeded = 'Succeeded',
|
||||
InProgress = 'InProgress',
|
||||
Canceled = 'Canceled',
|
||||
Completing = 'Completing',
|
||||
Creating = 'Creating',
|
||||
Canceling = 'Canceling',
|
||||
Retriable = 'Retriable',
|
||||
}
|
||||
|
||||
export enum ProvisioningState {
|
||||
Failed = 'Failed',
|
||||
Succeeded = 'Succeeded',
|
||||
Creating = 'Creating'
|
||||
}
|
||||
|
||||
export enum BackupFileInfoStatus {
|
||||
Arrived = 'Arrived',
|
||||
Uploading = 'Uploading',
|
||||
Uploaded = 'Uploaded',
|
||||
Restoring = 'Restoring',
|
||||
Restored = 'Restored',
|
||||
Cancelled = 'Cancelled',
|
||||
Ignored = 'Ignored'
|
||||
}
|
||||
|
||||
// #region wizard
|
||||
export function WIZARD_TITLE(instanceName: string): string {
|
||||
@@ -67,12 +92,13 @@ export const REFRESH_ASSESSMENT_BUTTON_LABEL = localize('sql.migration.refresh.a
|
||||
export const SKU_RECOMMENDATION_CHOOSE_A_TARGET = localize('sql.migration.wizard.sku.choose_a_target', "Choose your Azure SQL target");
|
||||
|
||||
export const SKU_RECOMMENDATION_MI_CARD_TEXT = localize('sql.migration.sku.mi.card.title', "Azure SQL Managed Instance");
|
||||
export const SKU_RECOMMENDATION_DB_CARD_TEXT = localize('sql.migration.sku.db.card.title', "Azure SQL Database");
|
||||
export const SKU_RECOMMENDATION_SQLDB_CARD_TEXT = localize('sql.migration.sku.sqldb.card.title', "Azure SQL Database");
|
||||
export const SKU_RECOMMENDATION_VM_CARD_TEXT = localize('sql.migration.sku.vm.card.title', "SQL Server on Azure Virtual Machine");
|
||||
export const SELECT_AZURE_MI = localize('sql.migration.select.azure.mi', "Select your target Azure subscription and your target Azure SQL Managed Instance.");
|
||||
export const SELECT_AZURE_VM = localize('sql.migration.select.azure.vm', "Select your target Azure Subscription and your target SQL Server on Azure Virtual Machine for your target.");
|
||||
export const SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI = localize('sql.migration.sku.recommendation.view.assessment.mi', "To migrate to Azure SQL Managed Instance, view assessment results and select one or more databases.");
|
||||
export const SKU_RECOMMENDATION_VIEW_ASSESSMENT_VM = localize('sql.migration.sku.recommendation.view.assessment.vm', "To migrate to SQL Server on Azure Virtual Machine, view assessment results and select one or more databases.");
|
||||
export const SKU_RECOMMENDATION_VIEW_ASSESSMENT_SQLDB = localize('sql.migration.sku.recommendation.view.assessment.sqldb', "To migrate to Azure SQL Database, view assessment results and select one or more databases.");
|
||||
export const VIEW_SELECT_BUTTON_LABEL = localize('sql.migration.view.select.button.label', "View/Select");
|
||||
export function TOTAL_DATABASES_SELECTED(selectedDbCount: number, totalDbCount: number): string {
|
||||
return localize('total.databases.selected', "{0} of {1} databases selected", selectedDbCount, totalDbCount);
|
||||
@@ -148,7 +174,11 @@ export function ASSESSED_DBS(totalDbs: number): string {
|
||||
return localize('sql.migration.assessed.databases', "(for {0} assessed databases)", totalDbs);
|
||||
}
|
||||
export function RECOMMENDATIONS_AVAILABLE(totalDbs: number): string {
|
||||
return localize('sql.migration.sku.available.recommendations', "{0} recommendations available", totalDbs);
|
||||
if (totalDbs === 1) {
|
||||
return localize('sql.migration.sku.available.recommendations.one', "{0} recommendation available", totalDbs);
|
||||
} else {
|
||||
return localize('sql.migration.sku.available.recommendations.many', "{0} recommendations available", totalDbs);
|
||||
}
|
||||
}
|
||||
export const RECOMMENDATIONS = localize('sql.migration.sku.recommendations', "Recommendations");
|
||||
export const LOADING_RECOMMENDATIONS = localize('sql.migration.sku.recommendations.loading', "Loading...");
|
||||
@@ -160,8 +190,11 @@ export function VM_CONFIGURATION(vmSize: string, vCPU: number): string {
|
||||
export function VM_CONFIGURATION_PREVIEW(dataDisk: string, logDisk: string, temp: string): string {
|
||||
return localize('sql.migration.sku.azureConfiguration.vmPreview', "Data: {0}, Log: {1}, tempdb: {2}", dataDisk, logDisk, temp);
|
||||
}
|
||||
export function DB_CONFIGURATION(computeTier: string, vCore: number): string {
|
||||
return localize('sql.migration.sku.azureConfiguration.db', "{0} - {1} vCore", computeTier, vCore);
|
||||
export function SQLDB_CONFIGURATION(computeTier: string, vCore: number): string {
|
||||
return localize('sql.migration.sku.azureConfiguration.sqldb', "{0} - {1} vCore", computeTier, vCore);
|
||||
}
|
||||
export function SQLDB_CONFIGURATION_PREVIEW(hardwareType: string, computeTier: string, vCore: number, storage: number): string {
|
||||
return localize('sql.migration.sku.azureConfiguration.sqldbPreview', "{0} - {1} - {2} vCore - {3} GB", hardwareType, computeTier, vCore, storage);
|
||||
}
|
||||
export function MI_CONFIGURATION(hardwareType: string, computeTier: string, vCore: number): string {
|
||||
return localize('sql.migration.sku.azureConfiguration.mi', "{0} - {1} - {2} vCore", hardwareType, computeTier, vCore);
|
||||
@@ -253,6 +286,40 @@ export function AZURE_SQL_TARGET_PAGE_DESCRIPTION(targetInstance: string = 'inst
|
||||
return localize('sql.migration.wizard.target.description', "Select an Azure account and your target {0}.", targetInstance);
|
||||
}
|
||||
|
||||
export const AZURE_SQL_TARGET_CONNECTION_ERROR_TITLE = localize('sql.migration.wizard.connection.error.title', "An error occurred while conneting to the target server.");
|
||||
export function SQL_TARGET_CONNECTION_ERROR(message: string): string {
|
||||
return localize('sql.migration.wizard.target.connection.error', "Connection error: {0}", message);
|
||||
}
|
||||
export function SQL_TARGET_CONNECTION_SUCCESS(databaseCount: string): string {
|
||||
return localize('sql.migration.wizard.target.connection.success', "Connection was successful. Target databases found: {0}", databaseCount);
|
||||
}
|
||||
|
||||
export const SQL_TARGET_MISSING_SOURCE_DATABASES = localize('sql.migration.wizard.source.missing', 'Connection was successful but did not find any target databases.');
|
||||
export const SQL_TARGET_MAPPING_ERROR_MISSING_TARGET = localize(
|
||||
'sql.migration.wizard.target.missing',
|
||||
'Database mapping error. Missing target databases to migrate. Please configure the target server connection and click connect to collect the list of available database migration targets.');
|
||||
|
||||
export function SQL_TARGET_CONNECTION_DUPLICATE_TARGET_MAPPING(
|
||||
targetDatabaseName: string,
|
||||
sourceDatabaseName: string,
|
||||
mappedSourceDatabaseName: string,
|
||||
): string {
|
||||
return localize(
|
||||
'sql.migration.wizard.target.mapping.error.duplicate',
|
||||
"Database mapping error. Target database '{0}' cannot be selected to as a migration target for database '{1}'. Target database '${targetDatabaseName}' is already selected as a migration target for database '{2}'. Please select a different target database.",
|
||||
targetDatabaseName,
|
||||
sourceDatabaseName,
|
||||
mappedSourceDatabaseName);
|
||||
}
|
||||
|
||||
//`Database mapping error. Source database '${sourceDatabaseName}' is not mapped to a target database. Please select a target database to migrate to.`
|
||||
export function SQL_TARGET_CONNECTION_SOURCE_NOT_MAPPED(sourceDatabaseName: string): string {
|
||||
return localize(
|
||||
'sql.migration.wizard.target.source.mapping.error',
|
||||
"Database mapping error. Source database '{0}' is not mapped to a target database. Please select a target database to migrate to.",
|
||||
sourceDatabaseName);
|
||||
}
|
||||
|
||||
// Managed Instance
|
||||
export const AZURE_SQL_DATABASE_MANAGED_INSTANCE = localize('sql.migration.azure.sql.database.managed.instance', "Azure SQL Managed Instance");
|
||||
export const NO_MANAGED_INSTANCE_FOUND = localize('sql.migration.no.managedInstance.found', "No managed instances found.");
|
||||
@@ -261,7 +328,6 @@ export function UNAVAILABLE_TARGET_PREFIX(targetName: string): string {
|
||||
return localize('sql.migration.unavailable.target', "(Unavailable) {0}", targetName);
|
||||
}
|
||||
|
||||
|
||||
// Virtual Machine
|
||||
export const AZURE_SQL_DATABASE_VIRTUAL_MACHINE = localize('sql.migration.azure.sql.database.virtual.machine', "SQL Server on Azure Virtual Machines");
|
||||
export const AZURE_SQL_DATABASE_VIRTUAL_MACHINE_SHORT = localize('sql.migration.azure.sql.database.virtual.machine.short', "SQL Server on Azure VM");
|
||||
@@ -269,7 +335,10 @@ export const NO_VIRTUAL_MACHINE_FOUND = localize('sql.migration.no.virtualMachin
|
||||
export const INVALID_VIRTUAL_MACHINE_ERROR = localize('sql.migration.invalid.virtualMachine.error', "To continue, select a valid virtual machine.");
|
||||
|
||||
// Azure SQL Database
|
||||
export const AZURE_SQL_DATABASE = localize('sql.migration.azure.sql.database', "Azure SQL Database");
|
||||
export const AZURE_SQL_DATABASE = localize('sql.migration.azure.sql.database', "Azure SQL Database Server");
|
||||
export const NO_SQL_DATABASE_SERVER_FOUND = localize('sql.migration.no.sqldatabaseserver.found', "No Azure SQL database servers found.");
|
||||
export const NO_SQL_DATABASE_FOUND = localize('sql.migration.no.sqldatabase.found', "No Azure SQL databases found.");
|
||||
export const INVALID_SQL_DATABASE_ERROR = localize('sql.migration.invalid.sqldatabase.error', "To continue, select a valid Azure SQL Database server.");
|
||||
|
||||
// Target info tooltip
|
||||
export const TARGET_SUBSCRIPTION_INFO = localize('sql.migration.sku.subscription', "Subscription name for your Azure SQL target");
|
||||
@@ -313,6 +382,10 @@ export function MI_NOT_READY_ERROR(miName: string, state: string): string {
|
||||
return localize('sql.migration.mi.not.ready', "The managed instance '{0}' is unavailable for migration because it is currently in the '{1}' state. To continue, select an available managed instance.", miName, state);
|
||||
}
|
||||
|
||||
export function SQLDB_NOT_READY_ERROR(sqldbName: string, state: string): string {
|
||||
return localize('sql.migration.sqldb.not.ready', "The SQL database server '{0}' is unavailable for migration because it is currently in the '{1}' state. To continue, select an available SQL database server.", sqldbName, state);
|
||||
}
|
||||
|
||||
export const SELECT_AN_ACCOUNT = localize('sql.migration.select.service.select.a.', "Sign into Azure and select an account");
|
||||
export const SELECT_A_TENANT = localize('sql.migration.select.service.select.a.tenant', "Select a tenant");
|
||||
export const SELECT_A_SUBSCRIPTION = localize('sql.migration.select.service.select.a.subscription', "Select a subscription");
|
||||
@@ -331,6 +404,7 @@ export function ACCOUNT_CREDENTIALS_REFRESH(accountName: string): string {
|
||||
"{0} (requires credentials refresh)",
|
||||
accountName);
|
||||
}
|
||||
export const SELECT_SERVICE_PLACEHOLDER = localize('sql.migration.select.service.select.migration.target', "Select a target server.");
|
||||
|
||||
// database backup page
|
||||
export const DATABASE_BACKUP_PAGE_TITLE = localize('sql.migration.database.page.title', "Database backup");
|
||||
@@ -361,6 +435,18 @@ export const DATABASE_BACKUP_BLOB_STORAGE_TABLE_HELP_TEXT = localize('sql.migrat
|
||||
export const DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL = localize('sql.migration.blob.storage.subscription.label', "Subscription");
|
||||
export const DATABASE_BACKUP_MIGRATION_MODE_LABEL = localize('sql.migration.database.migration.mode.label', "Migration mode");
|
||||
export const DATABASE_BACKUP_MIGRATION_MODE_DESCRIPTION = localize('sql.migration.database.migration.mode.description', "To migrate to the Azure SQL target, choose a migration mode based on your downtime requirements.");
|
||||
export const DATABASE_TABLE_SELECTION_LABEL = localize('sql.migration.database.table.selection.label', "Migration table selection");
|
||||
export const DATABASE_TABLE_SELECTION_DESCRIPTION = localize('sql.migration.database.table.selection.description', "To migrate to the Azure SQL target, select tables in each database for migration.");
|
||||
export const DATABASE_TABLE_REFRESH_LABEL = localize('sql.migration.database.table.refresh.label', "Refresh");
|
||||
export const DATABASE_TABLE_SOURCE_DATABASE_COLUMN_LABEL = localize('sql.migration.database.table.source.column.label', "Source database");
|
||||
export const DATABASE_TABLE_TARGET_DATABASE_COLUMN_LABEL = localize('sql.migration.database.table.target.column.label', "Target database");
|
||||
export const DATABASE_TABLE_SELECTED_TABLES_COLUMN_LABEL = localize('sql.migration.database.table.tables.column.label', "Select tables");
|
||||
export const DATABASE_TABLE_CONNECTION_ERROR = localize('sql.migration.database.connection.error', "An error occurred while connecting to target migration database.");
|
||||
export function DATABASE_TABLE_CONNECTION_ERROR_MESSAGE(message: string): string {
|
||||
return localize('sql.migration.database.connection.error.message', "Connection error:{0} {1}", EOL, message);
|
||||
}
|
||||
export const DATABASE_TABLE_DATA_LOADING = localize('sql.migration.database.loading', "Loading database table list..");
|
||||
export const DATABASE_TABLE_VALIDATE_SELECTION_MESSAGE = localize('sql.migration.database.validate.selection', "Please select target database tables to migrate to. At least one database with one table is required.");
|
||||
export const DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL = localize('sql.migration.database.migration.mode.online.label', "Online migration");
|
||||
export const DATABASE_BACKUP_MIGRATION_MODE_ONLINE_DESCRIPTION = localize('sql.migration.database.migration.mode.online.description', "Application downtime is limited to cutover at the end of migration.");
|
||||
export const DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL = localize('sql.migration.database.migration.mode.offline.label', "Offline migration");
|
||||
@@ -378,6 +464,22 @@ export const INVALID_SUBSCRIPTION_ERROR = localize('sql.migration.invalid.subscr
|
||||
export const INVALID_LOCATION_ERROR = localize('sql.migration.invalid.location.error', "To continue, select a valid location.");
|
||||
export const INVALID_RESOURCE_GROUP_ERROR = localize('sql.migration.invalid.resourceGroup.error', "To continue, select a valid resource group.");
|
||||
export const INVALID_STORAGE_ACCOUNT_ERROR = localize('sql.migration.invalid.storageAccount.error', "To continue, select a valid storage account.");
|
||||
export const MISSING_TARGET_USERNAME_ERROR = localize('sql.migration.missing.targetUserName.error', "To continue, enter a valid target user name.");
|
||||
export const MISSING_TARGET_PASSWORD_ERROR = localize('sql.migration.missing.targetPassword.error', "To continue, enter a valid target password.");
|
||||
|
||||
export const TARGET_TABLE_NOT_EMPTY = localize('sql.migration.target.table.not.empty', "Target table is not empty.");
|
||||
export const TARGET_TABLE_MISSING = localize('sql.migration.target.table.missing', "Target table does not exist");
|
||||
export const TARGET_USERNAME_LAbEL = localize('sql.migration.username.label', "Target user name");
|
||||
export const TARGET_USERNAME_PLACEHOLDER = localize('sql.migration.username.placeholder', "Enter the target user name");
|
||||
export const TARGET_PASSWORD_LAbEL = localize('sql.migration.password.label', "Target password");
|
||||
export const TARGET_PASSWORD_PLACEHOLDER = localize('sql.migration.password.placeholder', "Enter the target password");
|
||||
export const TARGET_CONNECTION_LABEL = localize('sql.migration.connection.label', "Connect");
|
||||
export const MAP_SOURCE_TARGET_HEADING = localize('sql.migration.map.target.heading', "Map selected source databases to target databases for migration");
|
||||
export const MAP_SOURCE_TARGET_DESCRIPTION = localize('sql.migration.map.target.description', "Select the target database where you would like to migrate your source database to. You can choose a target database for only one source database.");
|
||||
export const MAP_SOURCE_COLUMN = localize('sql.migration.map.source.column', "Source database");
|
||||
export const MAP_TARGET_COLUMN = localize('sql.migration.map.target.column', "Target database");
|
||||
export const MAP_TARGET_PLACEHOLDER = localize('sql.migration.map.target.placeholder', "Select a target database");
|
||||
|
||||
export function INVALID_BLOB_RESOURCE_GROUP_ERROR(sourceDb: string): string {
|
||||
return localize('sql.migration.invalid.blob.resourceGroup.error', "To continue, select a valid resource group for source database '{0}'.", sourceDb);
|
||||
}
|
||||
@@ -406,6 +508,26 @@ export const SELECT_RESOURCE_GROUP_PROMPT = localize('sql.migration.blob.resourc
|
||||
export const SELECT_STORAGE_ACCOUNT = localize('sql.migration.blob.storageAccount.select', "Select a storage account value first.");
|
||||
export const SELECT_BLOB_CONTAINER = localize('sql.migration.blob.container.select', "Select a blob container value first.");
|
||||
|
||||
export function SELECT_DATABASE_TABLES_TITLE(targetDatabaseName: string): string {
|
||||
return localize('sql.migration.table.select.label', "Select tables for {0}", targetDatabaseName);
|
||||
}
|
||||
export const TABLE_SELECTION_EDIT = localize('sql.migration.table.selection.edit', "Edit");
|
||||
|
||||
export function TABLE_SELECTION_COUNT(selectedCount: number, rowCount: number): string {
|
||||
return localize('sql.migration.table.selection.count', "{0} of {1}", selectedCount, rowCount);
|
||||
}
|
||||
export function TABLE_SELECTED_COUNT(selectedCount: number, rowCount: number): string {
|
||||
return localize('sql.migration.table.selected.count', "{0} of {1} tables selected", selectedCount, rowCount);
|
||||
}
|
||||
export const DATABASE_MISSING_TABLES = localize('sql.migratino.database.missing.tables', "0 tables found.");
|
||||
export const DATABASE_LOADING_TABLES = localize('sql.migratino.database.loading.tables', "Loading tables list...");
|
||||
export const TABLE_SELECTION_FILTER = localize('sql.migratino.table.selection.filter', "Filter tables");
|
||||
export const TABLE_SELECTION_UPDATE_BUTTON = localize('sql.migratino.table.selection.update.button', "Update");
|
||||
export const TABLE_SELECTION_CANCEL_BUTTON = localize('sql.migratino.table.selection.update.cancel', "Cancel");
|
||||
|
||||
export const TABLE_SELECTION_TABLENAME_COLUMN = localize('sql.migratino.table.selection.tablename.column', "Table name");
|
||||
export const TABLE_SELECTION_HASROWS_COLUMN = localize('sql.migratino.table.selection.status.column', "Has rows");
|
||||
|
||||
// integration runtime page
|
||||
export const SELECT_RESOURCE_GROUP = localize('sql.migration.blob.resourceGroup.select', "Select a resource group.");
|
||||
export const IR_PAGE_TITLE = localize('sql.migration.ir.page.title', "Azure Database Migration Service");
|
||||
@@ -503,6 +625,7 @@ export const START_MIGRATION_TEXT = localize('sql.migration.start.migration.butt
|
||||
export const SUMMARY_PAGE_TITLE = localize('sql.migration.summary.page.title', "Summary");
|
||||
export const SUMMARY_MI_TYPE = localize('sql.migration.summary.mi.type', "Azure SQL Managed Instance");
|
||||
export const SUMMARY_VM_TYPE = localize('sql.migration.summary.vm.type', "SQL Server on Azure Virtual Machine");
|
||||
export const SUMMARY_SQLDB_TYPE = localize('sql.migration.summary.sqldb.type', "Azure SQL Database");
|
||||
export const SUMMARY_DATABASE_COUNT_LABEL = localize('sql.migration.summary.database.count', "Databases for migration");
|
||||
export const SUMMARY_AZURE_STORAGE_SUBSCRIPTION = localize('sql.migration.summary.azure.storage.subscription', "Azure storage subscription");
|
||||
export const SUMMARY_AZURE_STORAGE = localize('sql.migration.summary.azure.storage', "Azure storage");
|
||||
@@ -521,6 +644,9 @@ export const DATABASE_TO_BE_MIGRATED = localize('sql.migration.database.to.be.mi
|
||||
export function COUNT_DATABASES(count: number): string {
|
||||
return (count === 1) ? localize('sql.migration.count.database.single', "{0} database", count) : localize('sql.migration.count.database.multiple', "{0} databases", count);
|
||||
}
|
||||
export function TOTAL_TABLES_SELECTED(selected: number, total: number): string {
|
||||
return localize('total.tables.selected.of.total', "{0} of {1}", formatNumber(selected), formatNumber(total));
|
||||
}
|
||||
|
||||
// Open notebook quick pick string
|
||||
export const NOTEBOOK_QUICK_PICK_PLACEHOLDER = localize('sql.migration.quick.pick.placeholder', "Select the operation you'd like to perform.");
|
||||
@@ -565,6 +691,7 @@ export const SOURCE_DATABASE = localize('sql.migration.source.database', "Source
|
||||
export const SOURCE_SERVER = localize('sql.migration.source.server', "Source server");
|
||||
export const SOURCE_VERSION = localize('sql.migration.source.version', "Source version");
|
||||
export const TARGET_DATABASE_NAME = localize('sql.migration.target.database.name', "Target database name");
|
||||
export const TARGET_TABLE_COUNT_NAME = localize('sql.migration.target.table.count.name', "Tables selected");
|
||||
export const TARGET_SERVER = localize('sql.migration.target.server', "Target server");
|
||||
export const TARGET_VERSION = localize('sql.migration.target.version', "Target version");
|
||||
export const MIGRATION_STATUS = localize('sql.migration.migration.status', "Migration status");
|
||||
@@ -683,12 +810,12 @@ export const OPEN_MIGRATION_TARGET_ERROR = localize('sql.migration.open.migratio
|
||||
export const OPEN_MIGRATION_SERVICE_ERROR = localize('sql.migration.open.migration.service.error', "Error opening migration service dialog");
|
||||
export const LOAD_MIGRATION_LIST_ERROR = localize('sql.migration.load.migration.list.error', "Error loading migrations list");
|
||||
export const ERROR_DIALOG_CLEAR_BUTTON_LABEL = localize('sql.migration.error.dialog.clear.button.label', "Clear");
|
||||
export const ERROR_DIALOG_ARIA_CLICK_VIEW_ERROR_DETAILS = localize('sql.migration.error.aria.view.details', 'Click to view error details');
|
||||
|
||||
export interface LookupTable<T> {
|
||||
[key: string]: T;
|
||||
}
|
||||
|
||||
|
||||
export const StatusLookup: LookupTable<string | undefined> = {
|
||||
[MigrationStatus.InProgress]: localize('sql.migration.status.inprogress', 'In progress'),
|
||||
[MigrationStatus.Succeeded]: localize('sql.migration.status.succeeded', 'Succeeded'),
|
||||
@@ -794,7 +921,7 @@ export const SOURCE_CONFIGURATION = localize('sql.migration.source.configuration
|
||||
export const SOURCE_CREDENTIALS = localize('sql.migration.source.credentials', "Source credentials");
|
||||
export const ENTER_YOUR_SQL_CREDS = localize('sql.migration.enter.your.sql.cred', "Enter the credentials for the source SQL Server instance. These credentials will be used while migrating databases to Azure SQL.");
|
||||
export const SERVER = localize('sql.migration.server', "Server");
|
||||
export const USERNAME = localize('sql.migration.username', "Username");
|
||||
export const USERNAME = localize('sql.migration.username', "User name");
|
||||
export const SIZE = localize('sql.migration.size', "Size (MB)");
|
||||
export const LAST_BACKUP = localize('sql.migration.last.backup', "Last backup");
|
||||
export const DATABASE_MIGRATE_TEXT = localize('sql.migrate.text', "Select the databases that you want to migrate to Azure SQL.");
|
||||
@@ -819,7 +946,8 @@ export const WARNINGS_DETAILS = localize('sql.migration.warnings.details', "Warn
|
||||
export const ISSUES_DETAILS = localize('sql.migration.issues.details', "Issue details");
|
||||
export const SELECT_DB_PROMPT = localize('sql.migration.select.prompt', "Click on SQL Server instance or any of the databases on the left to view its details.");
|
||||
export const NO_ISSUES_FOUND_VM = localize('sql.migration.no.issues.vm', "No issues found for migrating to SQL Server on Azure Virtual Machine.");
|
||||
export const NO_ISSUES_FOUND_MI = localize('sql.migration.no.issues.mi', "No issues found for migrating to SQL Server on Azure SQL Managed Instance.");
|
||||
export const NO_ISSUES_FOUND_MI = localize('sql.migration.no.issues.mi', "No issues found for migrating to Azure SQL Managed Instance.");
|
||||
export const NO_ISSUES_FOUND_SQLDB = localize('sql.migration.no.issues.sqldb', "No issues found for migrating to Azure SQL Database.");
|
||||
export const NO_RESULTS_AVAILABLE = localize('sql.migration.no.results', 'Assessment results are unavailable.');
|
||||
|
||||
export function IMPACT_OBJECT_TYPE(objectType?: string): string {
|
||||
|
||||
125
extensions/sql-migration/src/dashboard/DashboardStatusBar.ts
Normal file
125
extensions/sql-migration/src/dashboard/DashboardStatusBar.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as loc from '../constants/strings';
|
||||
|
||||
export interface ErrorEvent {
|
||||
connectionId: string;
|
||||
title: string;
|
||||
label: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class DashboardStatusBar implements vscode.Disposable {
|
||||
private _errorTitle: string = '';
|
||||
private _errorLabel: string = '';
|
||||
private _errorDescription: string = '';
|
||||
private _errorDialogIsOpen: boolean = false;
|
||||
private _statusInfoBox: azdata.InfoBoxComponent;
|
||||
private _context: vscode.ExtensionContext;
|
||||
private _errorEvent: vscode.EventEmitter<ErrorEvent> = new vscode.EventEmitter<ErrorEvent>();
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(context: vscode.ExtensionContext, connectionId: string, statusInfoBox: azdata.InfoBoxComponent, errorEvent: vscode.EventEmitter<ErrorEvent>) {
|
||||
this._context = context;
|
||||
this._statusInfoBox = statusInfoBox;
|
||||
this._errorEvent = errorEvent;
|
||||
|
||||
this._disposables.push(
|
||||
this._errorEvent.event(
|
||||
async (e) => {
|
||||
if (e.connectionId === connectionId) {
|
||||
return (e.title.length > 0 && e.label.length > 0)
|
||||
? await this.showError(e.title, e.label, e.message)
|
||||
: await this.clearError();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}
|
||||
|
||||
public async showError(errorTitle: string, errorLabel: string, errorDescription: string): Promise<void> {
|
||||
this._errorTitle = errorTitle;
|
||||
this._errorLabel = errorLabel;
|
||||
this._errorDescription = errorDescription;
|
||||
this._statusInfoBox.style = 'error';
|
||||
this._statusInfoBox.text = errorTitle;
|
||||
this._statusInfoBox.ariaLabel = errorTitle;
|
||||
|
||||
await this._updateStatusDisplay(this._statusInfoBox, true);
|
||||
}
|
||||
|
||||
public async clearError(): Promise<void> {
|
||||
await this._updateStatusDisplay(this._statusInfoBox, false);
|
||||
this._errorTitle = '';
|
||||
this._errorLabel = '';
|
||||
this._errorDescription = '';
|
||||
this._statusInfoBox.style = 'success';
|
||||
this._statusInfoBox.text = '';
|
||||
}
|
||||
|
||||
public async openErrorDialog(): Promise<void> {
|
||||
if (this._errorDialogIsOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const tab = azdata.window.createTab(this._errorTitle);
|
||||
tab.registerContent(async (view) => {
|
||||
const flex = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
view.modelBuilder.text()
|
||||
.withProps({ value: this._errorLabel, CSSStyles: { 'margin': '0px 0px 5px 5px' } })
|
||||
.component(),
|
||||
view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
value: this._errorDescription,
|
||||
readOnly: true,
|
||||
multiline: true,
|
||||
height: 400,
|
||||
inputType: 'text',
|
||||
display: 'inline-block',
|
||||
CSSStyles: { 'overflow': 'hidden auto', 'margin': '0px 0px 0px 5px' },
|
||||
})
|
||||
.component()])
|
||||
.withLayout({ flexFlow: 'column', width: 420, })
|
||||
.withProps({ CSSStyles: { 'margin': '0 10px 0 10px' } })
|
||||
.component();
|
||||
|
||||
await view.initializeModel(flex);
|
||||
});
|
||||
|
||||
const dialog = azdata.window.createModelViewDialog(
|
||||
this._errorTitle,
|
||||
'errorDialog',
|
||||
450,
|
||||
'flyout');
|
||||
dialog.content = [tab];
|
||||
dialog.okButton.label = loc.ERROR_DIALOG_CLEAR_BUTTON_LABEL;
|
||||
dialog.okButton.focused = true;
|
||||
dialog.cancelButton.label = loc.CLOSE;
|
||||
this._context.subscriptions.push(
|
||||
dialog.onClosed(async e => {
|
||||
if (e === 'ok') {
|
||||
await this.clearError();
|
||||
}
|
||||
this._errorDialogIsOpen = false;
|
||||
}));
|
||||
|
||||
azdata.window.openDialog(dialog);
|
||||
} catch (error) {
|
||||
this._errorDialogIsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateStatusDisplay(control: azdata.Component, visible: boolean): Promise<void> {
|
||||
await control.updateCssStyles({ 'display': visible ? 'inline' : 'none' });
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,13 @@ import * as vscode from 'vscode';
|
||||
import { IconPath, IconPathHelper } from '../constants/iconPathHelper';
|
||||
import * as styles from '../constants/styles';
|
||||
import * as loc from '../constants/strings';
|
||||
import { filterMigrations } from '../api/utils';
|
||||
import { filterMigrations, MenuCommands } from '../api/utils';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { getCurrentMigrations, getSelectedServiceStatus, isServiceContextValid, MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import { AdsMigrationStatus, MenuCommands, TabBase } from './tabBase';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
import { AdsMigrationStatus, ServiceContextChangeEvent, TabBase } from './tabBase';
|
||||
import { DashboardStatusBar } from './DashboardStatusBar';
|
||||
|
||||
interface IActionMetadata {
|
||||
title?: string,
|
||||
@@ -62,16 +62,15 @@ export class DashboardTab extends TabBase<DashboardTab> {
|
||||
this.icon = IconPathHelper.sqlMigrationLogo;
|
||||
}
|
||||
|
||||
public onDialogClosed = async (): Promise<void> =>
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
|
||||
public async create(
|
||||
view: azdata.ModelView,
|
||||
openMigrationsFcn: (status: AdsMigrationStatus) => Promise<void>,
|
||||
serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>,
|
||||
statusBar: DashboardStatusBar): Promise<DashboardTab> {
|
||||
|
||||
this.view = view;
|
||||
this.openMigrationFcn = openMigrationsFcn;
|
||||
this.openMigrationsFcn = openMigrationsFcn;
|
||||
this.serviceContextChangedEvent = serviceContextChangedEvent;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize(this.view);
|
||||
@@ -80,53 +79,55 @@ export class DashboardTab extends TabBase<DashboardTab> {
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
if (this.isRefreshing || this._migrationStatusCardLoadingContainer === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this._migrationStatusCardLoadingContainer.loading = true;
|
||||
let migrations: DatabaseMigration[] = [];
|
||||
try {
|
||||
this.isRefreshing = true;
|
||||
this._refreshButton.enabled = false;
|
||||
this._migrationStatusCardLoadingContainer.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
migrations = await getCurrentMigrations();
|
||||
const migrations = await getCurrentMigrations();
|
||||
|
||||
const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
|
||||
let warningCount = 0;
|
||||
for (let i = 0; i < inProgressMigrations.length; i++) {
|
||||
if (inProgressMigrations[i].properties.migrationFailureError?.message ||
|
||||
inProgressMigrations[i].properties.migrationStatusDetails?.fileUploadBlockingErrors ||
|
||||
inProgressMigrations[i].properties.migrationStatusDetails?.restoreBlockingReason) {
|
||||
warningCount += 1;
|
||||
}
|
||||
}
|
||||
if (warningCount > 0) {
|
||||
this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
|
||||
this._inProgressMigrationButton.container.display = 'none';
|
||||
this._inProgressWarningMigrationButton.container.display = '';
|
||||
} else {
|
||||
this._inProgressMigrationButton.container.display = '';
|
||||
this._inProgressWarningMigrationButton.container.display = 'none';
|
||||
}
|
||||
|
||||
this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
|
||||
this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
|
||||
|
||||
this._updateStatusCard(migrations, this._successfulMigrationButton, AdsMigrationStatus.SUCCEEDED, true);
|
||||
this._updateStatusCard(migrations, this._failedMigrationButton, AdsMigrationStatus.FAILED);
|
||||
this._updateStatusCard(migrations, this._completingMigrationButton, AdsMigrationStatus.COMPLETING);
|
||||
this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true);
|
||||
|
||||
await this._updateSummaryStatus();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.DASHBOARD_REFRESH_MIGRATIONS_TITLE,
|
||||
loc.DASHBOARD_REFRESH_MIGRATIONS_LABEL,
|
||||
e.message);
|
||||
logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e);
|
||||
} finally {
|
||||
this._migrationStatusCardLoadingContainer.loading = false;
|
||||
this._refreshButton.enabled = true;
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
|
||||
const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING);
|
||||
let warningCount = 0;
|
||||
for (let i = 0; i < inProgressMigrations.length; i++) {
|
||||
if (inProgressMigrations[i].properties.migrationFailureError?.message ||
|
||||
inProgressMigrations[i].properties.migrationStatusDetails?.fileUploadBlockingErrors ||
|
||||
inProgressMigrations[i].properties.migrationStatusDetails?.restoreBlockingReason) {
|
||||
warningCount += 1;
|
||||
}
|
||||
}
|
||||
if (warningCount > 0) {
|
||||
this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
|
||||
this._inProgressMigrationButton.container.display = 'none';
|
||||
this._inProgressWarningMigrationButton.container.display = '';
|
||||
} else {
|
||||
this._inProgressMigrationButton.container.display = '';
|
||||
this._inProgressWarningMigrationButton.container.display = 'none';
|
||||
}
|
||||
|
||||
this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
|
||||
this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
|
||||
|
||||
this._updateStatusCard(migrations, this._successfulMigrationButton, AdsMigrationStatus.SUCCEEDED, true);
|
||||
this._updateStatusCard(migrations, this._failedMigrationButton, AdsMigrationStatus.FAILED);
|
||||
this._updateStatusCard(migrations, this._completingMigrationButton, AdsMigrationStatus.COMPLETING);
|
||||
this._updateStatusCard(migrations, this._allMigrationButton, AdsMigrationStatus.ALL, true);
|
||||
|
||||
await this._updateSummaryStatus();
|
||||
this.isRefreshing = false;
|
||||
this._migrationStatusCardLoadingContainer.loading = false;
|
||||
}
|
||||
|
||||
protected async initialize(view: azdata.ModelView): Promise<void> {
|
||||
@@ -616,11 +617,8 @@ export class DashboardTab extends TabBase<DashboardTab> {
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this._refreshButton.onDidClick(async (e) => {
|
||||
this._refreshButton.enabled = false;
|
||||
await this.refresh();
|
||||
this._refreshButton.enabled = true;
|
||||
}));
|
||||
this._refreshButton.onDidClick(
|
||||
async (e) => await this.refresh()));
|
||||
|
||||
const buttonContainer = view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
@@ -668,7 +666,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
|
||||
loc.MIGRATION_IN_PROGRESS);
|
||||
this.disposables.push(
|
||||
this._inProgressMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ONGOING)));
|
||||
async (e) => await this.openMigrationsFcn(AdsMigrationStatus.ONGOING)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._inProgressMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
@@ -681,7 +679,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
|
||||
true);
|
||||
this.disposables.push(
|
||||
this._inProgressWarningMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ONGOING)));
|
||||
async (e) => await this.openMigrationsFcn(AdsMigrationStatus.ONGOING)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._inProgressWarningMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
@@ -693,7 +691,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
|
||||
loc.MIGRATION_COMPLETED);
|
||||
this.disposables.push(
|
||||
this._successfulMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.SUCCEEDED)));
|
||||
async (e) => await this.openMigrationsFcn(AdsMigrationStatus.SUCCEEDED)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._successfulMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
@@ -705,7 +703,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
|
||||
loc.MIGRATION_CUTOVER_CARD);
|
||||
this.disposables.push(
|
||||
this._completingMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.COMPLETING)));
|
||||
async (e) => await this.openMigrationsFcn(AdsMigrationStatus.COMPLETING)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._completingMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
@@ -717,7 +715,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
|
||||
loc.MIGRATION_FAILED);
|
||||
this.disposables.push(
|
||||
this._failedMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.FAILED)));
|
||||
async (e) => await this.openMigrationsFcn(AdsMigrationStatus.FAILED)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._failedMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
@@ -729,7 +727,7 @@ export class DashboardTab extends TabBase<DashboardTab> {
|
||||
loc.VIEW_ALL);
|
||||
this.disposables.push(
|
||||
this._allMigrationButton.container.onDidClick(
|
||||
async (e) => await this.openMigrationFcn(AdsMigrationStatus.ALL)));
|
||||
async (e) => await this.openMigrationsFcn(AdsMigrationStatus.ALL)));
|
||||
this._migrationStatusCardsContainer.addItem(
|
||||
this._allMigrationButton.container,
|
||||
{ flex: '0 0 auto' });
|
||||
@@ -759,9 +757,21 @@ export class DashboardTab extends TabBase<DashboardTab> {
|
||||
})
|
||||
.component();
|
||||
|
||||
const connectionProfile = await azdata.connection.getCurrentConnection();
|
||||
this.disposables.push(
|
||||
this.serviceContextChangedEvent.event(
|
||||
async (e) => {
|
||||
if (e.connectionId === connectionProfile.connectionId) {
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
));
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
|
||||
this.disposables.push(
|
||||
this._serviceContextButton.onDidClick(async () => {
|
||||
const dialog = new SelectMigrationServiceDialog(async () => await this.onDialogClosed());
|
||||
const dialog = new SelectMigrationServiceDialog(this.serviceContextChangedEvent);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ import * as vscode from 'vscode';
|
||||
import * as loc from '../constants/strings';
|
||||
import { getSqlServerName, getMigrationStatusImage } from '../api/utils';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
|
||||
import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
|
||||
import { getResourceName } from '../api/azure';
|
||||
import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
|
||||
import { EmptySettingValue } from './tabBase';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
import { DashboardStatusBar } from './DashboardStatusBar';
|
||||
|
||||
const MigrationDetailsBlobContainerTabId = 'MigrationDetailsBlobContainerTab';
|
||||
|
||||
@@ -36,13 +36,13 @@ export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<Mi
|
||||
public async create(
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
onClosedCallback: () => Promise<void>,
|
||||
openMigrationsListFcn: () => Promise<void>,
|
||||
statusBar: DashboardStatusBar,
|
||||
): Promise<MigrationDetailsBlobContainerTab> {
|
||||
|
||||
this.view = view;
|
||||
this.context = context;
|
||||
this.onClosedCallback = onClosedCallback;
|
||||
this.openMigrationsListFcn = openMigrationsListFcn;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize(this.view);
|
||||
@@ -51,12 +51,14 @@ export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<Mi
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing || this.model?.migration === undefined) {
|
||||
if (this.isRefreshing ||
|
||||
this.refreshLoader === undefined ||
|
||||
this.model?.migration === undefined) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this.refreshButton.enabled = false;
|
||||
this.refreshLoader.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
|
||||
@@ -95,7 +97,7 @@ export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<Mi
|
||||
this._targetServerInfoField.text.value = targetServerName;
|
||||
this._targetVersionInfoField.text.value = targetServerVersion;
|
||||
|
||||
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue;
|
||||
this._migrationStatusInfoField.text.value = getMigrationStatusString(migration);
|
||||
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
|
||||
|
||||
const storageAccountResourceId = migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
|
||||
@@ -114,9 +116,8 @@ export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase<Mi
|
||||
this.cancelButton.enabled = canCancelMigration(migration);
|
||||
this.retryButton.enabled = canRetryMigration(migration);
|
||||
|
||||
this.isRefreshing = false;
|
||||
this.refreshLoader.loading = false;
|
||||
this.refreshButton.enabled = true;
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
|
||||
protected async initialize(view: azdata.ModelView): Promise<void> {
|
||||
|
||||
@@ -10,11 +10,11 @@ import * as loc from '../constants/strings';
|
||||
import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage } from '../api/utils';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import * as styles from '../constants/styles';
|
||||
import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
|
||||
import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper';
|
||||
import { getResourceName } from '../api/azure';
|
||||
import { EmptySettingValue } from './tabBase';
|
||||
import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
import { DashboardStatusBar } from './DashboardStatusBar';
|
||||
|
||||
const MigrationDetailsFileShareTabId = 'MigrationDetailsFileShareTab';
|
||||
|
||||
@@ -43,7 +43,6 @@ export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<Migrat
|
||||
private _lastAppliedBackupInfoField!: InfoFieldSchema;
|
||||
private _lastAppliedBackupTakenOnInfoField!: InfoFieldSchema;
|
||||
private _currentRestoringFileInfoField!: InfoFieldSchema;
|
||||
|
||||
private _fileCount!: azdata.TextComponent;
|
||||
private _fileTable!: azdata.TableComponent;
|
||||
private _emptyTableFill!: azdata.FlexContainer;
|
||||
@@ -56,12 +55,12 @@ export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<Migrat
|
||||
public async create(
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
onClosedCallback: () => Promise<void>,
|
||||
openMigrationsListFcn: () => Promise<void>,
|
||||
statusBar: DashboardStatusBar): Promise<MigrationDetailsFileShareTab> {
|
||||
|
||||
this.view = view;
|
||||
this.context = context;
|
||||
this.onClosedCallback = onClosedCallback;
|
||||
this.openMigrationsListFcn = openMigrationsListFcn;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize(this.view);
|
||||
@@ -70,126 +69,128 @@ export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase<Migrat
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing || this.model?.migration === undefined) {
|
||||
if (this.isRefreshing ||
|
||||
this.refreshLoader === undefined ||
|
||||
this.model?.migration === undefined) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this.refreshButton.enabled = false;
|
||||
this.refreshLoader.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
await this._fileTable.updateProperty('data', []);
|
||||
|
||||
try {
|
||||
this.isRefreshing = true;
|
||||
this.refreshLoader.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
await this._fileTable.updateProperty('data', []);
|
||||
|
||||
await this.model.fetchStatus();
|
||||
|
||||
const migration = this.model?.migration;
|
||||
await this.cutoverButton.updateCssStyles(
|
||||
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
|
||||
|
||||
await this.showMigrationErrors(migration);
|
||||
|
||||
const sqlServerName = migration.properties.sourceServerName;
|
||||
const sourceDatabaseName = migration.properties.sourceDatabaseName;
|
||||
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
|
||||
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
|
||||
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
|
||||
const targetDatabaseName = migration.name;
|
||||
const targetServerName = getResourceName(migration.properties.scope);
|
||||
|
||||
const targetType = getMigrationTargetTypeEnum(migration);
|
||||
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
|
||||
|
||||
let lastAppliedSSN: string;
|
||||
let lastAppliedBackupFileTakenOn: string;
|
||||
|
||||
const tableData: ActiveBackupFileSchema[] = [];
|
||||
migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(
|
||||
(activeBackupSet) => {
|
||||
tableData.push(
|
||||
...activeBackupSet.listOfBackupFiles.map(f => {
|
||||
return {
|
||||
fileName: f.fileName,
|
||||
type: activeBackupSet.backupType,
|
||||
status: f.status,
|
||||
dataUploaded: `${convertByteSizeToReadableUnit(f.dataWritten)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
|
||||
copyThroughput: (f.copyThroughput) ? (f.copyThroughput / 1024).toFixed(2) : EmptySettingValue,
|
||||
backupStartTime: activeBackupSet.backupStartDate,
|
||||
firstLSN: activeBackupSet.firstLSN,
|
||||
lastLSN: activeBackupSet.lastLSN
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
if (activeBackupSet.listOfBackupFiles[0].fileName === migration.properties.migrationStatusDetails?.lastRestoredFilename) {
|
||||
lastAppliedSSN = activeBackupSet.lastLSN;
|
||||
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
|
||||
}
|
||||
});
|
||||
|
||||
this.databaseLabel.value = sourceDatabaseName;
|
||||
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
|
||||
this._sourceDetailsInfoField.text.value = sqlServerName;
|
||||
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
|
||||
|
||||
this._targetDatabaseInfoField.text.value = targetDatabaseName;
|
||||
this._targetServerInfoField.text.value = targetServerName;
|
||||
this._targetVersionInfoField.text.value = targetServerVersion;
|
||||
|
||||
this._migrationStatusInfoField.text.value = getMigrationStatusString(migration);
|
||||
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
|
||||
|
||||
this._fullBackupFileOnInfoField.text.value = migration?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? EmptySettingValue;
|
||||
|
||||
const fileShare = migration.properties.backupConfiguration?.sourceLocation?.fileShare;
|
||||
const backupLocation = fileShare?.path! ?? EmptySettingValue;
|
||||
this._backupLocationInfoField.text.value = backupLocation ?? EmptySettingValue;
|
||||
|
||||
this._lastLSNInfoField.text.value = lastAppliedSSN! ?? EmptySettingValue;
|
||||
this._lastAppliedBackupInfoField.text.value = migration.properties.migrationStatusDetails?.lastRestoredFilename ?? EmptySettingValue;
|
||||
this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : EmptySettingValue;
|
||||
this._currentRestoringFileInfoField.text.value = this.getMigrationCurrentlyRestoringFile(migration) ?? EmptySettingValue;
|
||||
|
||||
await this._fileCount.updateCssStyles({ ...styles.SECTION_HEADER_CSS, display: 'inline' });
|
||||
|
||||
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
|
||||
if (tableData.length === 0) {
|
||||
await this._emptyTableFill.updateCssStyles({ 'display': 'flex' });
|
||||
this._fileTable.height = '50px';
|
||||
await this._fileTable.updateProperty('data', []);
|
||||
} else {
|
||||
await this._emptyTableFill.updateCssStyles({ 'display': 'none' });
|
||||
this._fileTable.height = '300px';
|
||||
|
||||
// Sorting files in descending order of backupStartTime
|
||||
tableData.sort((file1, file2) => new Date(file1.backupStartTime) > new Date(file2.backupStartTime) ? - 1 : 1);
|
||||
}
|
||||
|
||||
const data = tableData.map(row => [
|
||||
row.fileName,
|
||||
row.type,
|
||||
row.status,
|
||||
row.dataUploaded,
|
||||
row.copyThroughput,
|
||||
convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(),
|
||||
row.firstLSN,
|
||||
row.lastLSN
|
||||
]) || [];
|
||||
|
||||
await this._fileTable.updateProperty('data', data);
|
||||
|
||||
this.cutoverButton.enabled = canCutoverMigration(migration);
|
||||
this.cancelButton.enabled = canCancelMigration(migration);
|
||||
this.retryButton.enabled = canRetryMigration(migration);
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
e.message);
|
||||
} finally {
|
||||
this.refreshLoader.loading = false;
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
|
||||
const migration = this.model?.migration;
|
||||
await this.cutoverButton.updateCssStyles(
|
||||
{ 'display': isOfflineMigation(migration) ? 'none' : 'block' });
|
||||
|
||||
await this.showMigrationErrors(migration);
|
||||
|
||||
const sqlServerName = migration.properties.sourceServerName;
|
||||
const sourceDatabaseName = migration.properties.sourceDatabaseName;
|
||||
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
|
||||
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
|
||||
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
|
||||
const targetDatabaseName = migration.name;
|
||||
const targetServerName = getResourceName(migration.properties.scope);
|
||||
|
||||
const targetType = getMigrationTargetTypeEnum(migration);
|
||||
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
|
||||
|
||||
let lastAppliedSSN: string;
|
||||
let lastAppliedBackupFileTakenOn: string;
|
||||
|
||||
const tableData: ActiveBackupFileSchema[] = [];
|
||||
migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(
|
||||
(activeBackupSet) => {
|
||||
tableData.push(
|
||||
...activeBackupSet.listOfBackupFiles.map(f => {
|
||||
return {
|
||||
fileName: f.fileName,
|
||||
type: activeBackupSet.backupType,
|
||||
status: f.status,
|
||||
dataUploaded: `${convertByteSizeToReadableUnit(f.dataWritten)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
|
||||
copyThroughput: (f.copyThroughput) ? (f.copyThroughput / 1024).toFixed(2) : EmptySettingValue,
|
||||
backupStartTime: activeBackupSet.backupStartDate,
|
||||
firstLSN: activeBackupSet.firstLSN,
|
||||
lastLSN: activeBackupSet.lastLSN
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
if (activeBackupSet.listOfBackupFiles[0].fileName === migration.properties.migrationStatusDetails?.lastRestoredFilename) {
|
||||
lastAppliedSSN = activeBackupSet.lastLSN;
|
||||
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
|
||||
}
|
||||
});
|
||||
|
||||
this.databaseLabel.value = sourceDatabaseName;
|
||||
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
|
||||
this._sourceDetailsInfoField.text.value = sqlServerName;
|
||||
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
|
||||
|
||||
this._targetDatabaseInfoField.text.value = targetDatabaseName;
|
||||
this._targetServerInfoField.text.value = targetServerName;
|
||||
this._targetVersionInfoField.text.value = targetServerVersion;
|
||||
|
||||
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue;
|
||||
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
|
||||
|
||||
this._fullBackupFileOnInfoField.text.value = migration?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? EmptySettingValue;
|
||||
|
||||
const fileShare = migration.properties.backupConfiguration?.sourceLocation?.fileShare;
|
||||
const backupLocation = fileShare?.path! ?? EmptySettingValue;
|
||||
this._backupLocationInfoField.text.value = backupLocation ?? EmptySettingValue;
|
||||
|
||||
this._lastLSNInfoField.text.value = lastAppliedSSN! ?? EmptySettingValue;
|
||||
this._lastAppliedBackupInfoField.text.value = migration.properties.migrationStatusDetails?.lastRestoredFilename ?? EmptySettingValue;
|
||||
this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : EmptySettingValue;
|
||||
this._currentRestoringFileInfoField.text.value = this.getMigrationCurrentlyRestoringFile(migration) ?? EmptySettingValue;
|
||||
|
||||
await this._fileCount.updateCssStyles({ ...styles.SECTION_HEADER_CSS, display: 'inline' });
|
||||
|
||||
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
|
||||
if (tableData.length === 0) {
|
||||
await this._emptyTableFill.updateCssStyles({ 'display': 'flex' });
|
||||
this._fileTable.height = '50px';
|
||||
await this._fileTable.updateProperty('data', []);
|
||||
} else {
|
||||
await this._emptyTableFill.updateCssStyles({ 'display': 'none' });
|
||||
this._fileTable.height = '300px';
|
||||
|
||||
// Sorting files in descending order of backupStartTime
|
||||
tableData.sort((file1, file2) => new Date(file1.backupStartTime) > new Date(file2.backupStartTime) ? - 1 : 1);
|
||||
}
|
||||
|
||||
const data = tableData.map(row => [
|
||||
row.fileName,
|
||||
row.type,
|
||||
row.status,
|
||||
row.dataUploaded,
|
||||
row.copyThroughput,
|
||||
convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(),
|
||||
row.firstLSN,
|
||||
row.lastLSN
|
||||
]) || [];
|
||||
|
||||
await this._fileTable.updateProperty('data', data);
|
||||
|
||||
this.cutoverButton.enabled = canCutoverMigration(migration);
|
||||
this.cancelButton.enabled = canCancelMigration(migration);
|
||||
this.retryButton.enabled = canRetryMigration(migration);
|
||||
this.isRefreshing = false;
|
||||
this.refreshLoader.loading = false;
|
||||
this.refreshButton.enabled = true;
|
||||
}
|
||||
|
||||
protected async initialize(view: azdata.ModelView): Promise<void> {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migratio
|
||||
import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
|
||||
import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
|
||||
import { MigrationTargetType } from '../models/stateMachine';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
import { DashboardStatusBar } from './DashboardStatusBar';
|
||||
|
||||
export const infoFieldLgWidth: string = '330px';
|
||||
export const infoFieldWidth: string = '250px';
|
||||
@@ -38,8 +38,7 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
|
||||
protected model!: MigrationCutoverDialogModel;
|
||||
protected databaseLabel!: azdata.TextComponent;
|
||||
protected serviceContext!: MigrationServiceContext;
|
||||
protected onClosedCallback!: () => Promise<void>;
|
||||
|
||||
protected openMigrationsListFcn!: () => Promise<void>;
|
||||
protected cutoverButton!: azdata.ButtonComponent;
|
||||
protected refreshButton!: azdata.ButtonComponent;
|
||||
protected cancelButton!: azdata.ButtonComponent;
|
||||
@@ -49,7 +48,11 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
|
||||
protected retryButton!: azdata.ButtonComponent;
|
||||
protected summaryTextComponent: azdata.TextComponent[] = [];
|
||||
|
||||
public abstract create(context: vscode.ExtensionContext, view: azdata.ModelView, onClosedCallback: () => Promise<void>, statusBar: DashboardStatusBar): Promise<T>;
|
||||
public abstract create(
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
openMigrationsListFcn: () => Promise<void>,
|
||||
statusBar: DashboardStatusBar): Promise<T>;
|
||||
|
||||
protected abstract migrationInfoGrid(): Promise<azdata.FlexContainer>;
|
||||
|
||||
@@ -80,7 +83,7 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
|
||||
.component();
|
||||
this.disposables.push(
|
||||
migrationsTabLink.onDidClick(
|
||||
async (e) => await this.onClosedCallback()));
|
||||
async (e) => await this.openMigrationsListFcn()));
|
||||
|
||||
const breadCrumbImage = this.view.modelBuilder.image()
|
||||
.withProps({
|
||||
@@ -202,7 +205,7 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
|
||||
this.context,
|
||||
this.serviceContext,
|
||||
this.model.migration,
|
||||
this.onClosedCallback);
|
||||
this.serviceContextChangedEvent);
|
||||
await retryMigrationDialog.openDialog();
|
||||
}
|
||||
));
|
||||
@@ -254,12 +257,10 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
|
||||
async (e) => await this.refresh()));
|
||||
|
||||
this.refreshLoader = this.view.modelBuilder.loadingComponent()
|
||||
.withItem(this.refreshButton)
|
||||
.withProps({
|
||||
loading: false,
|
||||
CSSStyles: {
|
||||
'height': '8px',
|
||||
'margin-top': '4px'
|
||||
}
|
||||
CSSStyles: { 'height': '8px', 'margin-top': '4px' }
|
||||
}).component();
|
||||
|
||||
toolbarContainer.addToolbarItems([
|
||||
@@ -268,7 +269,6 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
|
||||
<azdata.ToolbarComponent>{ component: this.retryButton },
|
||||
<azdata.ToolbarComponent>{ component: this.copyDatabaseMigrationDetails, toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this.newSupportRequest, toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this.refreshButton },
|
||||
<azdata.ToolbarComponent>{ component: this.refreshLoader },
|
||||
]);
|
||||
|
||||
|
||||
@@ -8,13 +8,12 @@ import * as vscode from 'vscode';
|
||||
import * as loc from '../constants/strings';
|
||||
import { getSqlServerName, getMigrationStatusImage, getPipelineStatusImage, debounce } from '../api/utils';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import { canCancelMigration, canCutoverMigration, canRetryMigration, formatDateTimeString, formatNumber, formatSizeBytes, formatSizeKb, formatTime, getMigrationStatus, getMigrationTargetTypeEnum, isOfflineMigation, PipelineStatusCodes } from '../constants/helper';
|
||||
import { canCancelMigration, canCutoverMigration, canRetryMigration, formatDateTimeString, formatNumber, formatSizeBytes, formatSizeKb, formatTime, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation, PipelineStatusCodes } from '../constants/helper';
|
||||
import { CopyProgressDetail, getResourceName } from '../api/azure';
|
||||
import { InfoFieldSchema, infoFieldLgWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase';
|
||||
import { EmptySettingValue } from './tabBase';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
import { EOL } from 'os';
|
||||
import { DashboardStatusBar } from './DashboardStatusBar';
|
||||
|
||||
const MigrationDetailsTableTabId = 'MigrationDetailsTableTab';
|
||||
|
||||
@@ -63,12 +62,12 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
|
||||
public async create(
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
onClosedCallback: () => Promise<void>,
|
||||
openMigrationsListFcn: () => Promise<void>,
|
||||
statusBar: DashboardStatusBar): Promise<MigrationDetailsTableTab> {
|
||||
|
||||
this.view = view;
|
||||
this.context = context;
|
||||
this.onClosedCallback = onClosedCallback;
|
||||
this.openMigrationsListFcn = openMigrationsListFcn;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize(this.view);
|
||||
@@ -78,16 +77,17 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
|
||||
|
||||
@debounce(500)
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
if (this.isRefreshing ||
|
||||
this.refreshLoader === undefined) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this.refreshButton.enabled = false;
|
||||
this.refreshLoader.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
|
||||
try {
|
||||
this.isRefreshing = true;
|
||||
this.refreshLoader.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
|
||||
await this.model.fetchStatus();
|
||||
await this._loadData();
|
||||
} catch (e) {
|
||||
@@ -95,11 +95,10 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
e.message);
|
||||
} finally {
|
||||
this.refreshLoader.loading = false;
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
|
||||
this.isRefreshing = false;
|
||||
this.refreshLoader.loading = false;
|
||||
this.refreshButton.enabled = true;
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
@@ -120,8 +119,9 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
|
||||
const targetType = getMigrationTargetTypeEnum(migration);
|
||||
const targetServerVersion = MigrationTargetTypeName[targetType ?? ''];
|
||||
|
||||
const hashSet: loc.LookupTable<number> = {};
|
||||
this._progressDetail = migration?.properties.migrationStatusDetails?.listOfCopyProgressDetails ?? [];
|
||||
|
||||
const hashSet: loc.LookupTable<number> = {};
|
||||
await this._populateTableData(hashSet);
|
||||
|
||||
const successCount = hashSet[PipelineStatusCodes.Succeeded] ?? 0;
|
||||
@@ -138,7 +138,7 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
|
||||
(hashSet[PipelineStatusCodes.RebuildingIndexes] ?? 0) +
|
||||
(hashSet[PipelineStatusCodes.InProgress] ?? 0);
|
||||
|
||||
const totalCount = migration.properties.migrationStatusDetails?.listOfCopyProgressDetails.length ?? 0;
|
||||
const totalCount = this._progressDetail.length;
|
||||
|
||||
this._updateSummaryComponent(SummaryCardIndex.TotalTables, totalCount);
|
||||
this._updateSummaryComponent(SummaryCardIndex.InProgressTables, inProgressCount);
|
||||
@@ -155,7 +155,7 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase<MigrationD
|
||||
this._targetServerInfoField.text.value = targetServerName;
|
||||
this._targetVersionInfoField.text.value = targetServerVersion;
|
||||
|
||||
this._migrationStatusInfoField.text.value = getMigrationStatus(migration) ?? EmptySettingValue;
|
||||
this._migrationStatusInfoField.text.value = getMigrationStatusString(migration);
|
||||
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migration);
|
||||
this._serverObjectsInfoField.text.value = totalCount.toLocaleString();
|
||||
|
||||
|
||||
@@ -6,20 +6,16 @@
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { getCurrentMigrations, getSelectedServiceStatus, MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
import { getCurrentMigrations, getSelectedServiceStatus } from '../models/migrationLocalStorage';
|
||||
import * as loc from '../constants/strings';
|
||||
import { filterMigrations, getMigrationDuration, getMigrationStatusImage, getMigrationStatusWithErrors, getMigrationTime } from '../api/utils';
|
||||
import { SqlMigrationServiceDetailsDialog } from '../dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog';
|
||||
import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
|
||||
import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel';
|
||||
import { getMigrationTargetType, getMigrationMode, canRetryMigration, getMigrationModeEnum, canCancelMigration, canCutoverMigration, getMigrationStatus } from '../constants/helper';
|
||||
import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
|
||||
import { filterMigrations, getMigrationDuration, getMigrationStatusImage, getMigrationStatusWithErrors, getMigrationTime, MenuCommands } from '../api/utils';
|
||||
import { getMigrationTargetType, getMigrationMode, getMigrationModeEnum, canCancelMigration, canCutoverMigration, getMigrationStatus } from '../constants/helper';
|
||||
import { DatabaseMigration, getResourceName } from '../api/azure';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
|
||||
import { AdsMigrationStatus, EmptySettingValue, MenuCommands, TabBase } from './tabBase';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
import { AdsMigrationStatus, EmptySettingValue, ServiceContextChangeEvent, TabBase } from './tabBase';
|
||||
import { MigrationMode } from '../models/stateMachine';
|
||||
import { DashboardStatusBar } from './DashboardStatusBar';
|
||||
|
||||
export const MigrationsListTabId = 'MigrationsListTab';
|
||||
|
||||
@@ -58,12 +54,14 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
openMigrationDetails: (migration: DatabaseMigration) => Promise<void>,
|
||||
serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>,
|
||||
statusBar: DashboardStatusBar,
|
||||
): Promise<MigrationsListTab> {
|
||||
|
||||
this.view = view;
|
||||
this.context = context;
|
||||
this._openMigrationDetails = openMigrationDetails;
|
||||
this.serviceContextChangedEvent = serviceContextChangedEvent;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize();
|
||||
@@ -71,29 +69,28 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
return this;
|
||||
}
|
||||
|
||||
public onDialogClosed = async (): Promise<void> =>
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
|
||||
public async setMigrationFilter(filter: AdsMigrationStatus): Promise<void> {
|
||||
if (this._statusDropdown.values && this._statusDropdown.values.length > 0) {
|
||||
const statusFilter = (<azdata.CategoryValue[]>this._statusDropdown.values)
|
||||
.find(value => value.name === filter.toString());
|
||||
|
||||
this._statusDropdown.value = statusFilter;
|
||||
await this._statusDropdown.updateProperties({ 'value': statusFilter });
|
||||
}
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
if (this.isRefreshing) {
|
||||
if (this.isRefreshing ||
|
||||
this._refreshLoader === undefined) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRefreshing = true;
|
||||
this._refresh.enabled = false;
|
||||
this._refreshLoader.loading = true;
|
||||
await this.statusBar.clearError();
|
||||
|
||||
try {
|
||||
this.isRefreshing = true;
|
||||
this._refreshLoader.loading = true;
|
||||
|
||||
await this.statusBar.clearError();
|
||||
|
||||
await this._statusTable.updateProperty('data', []);
|
||||
this._migrations = await getCurrentMigrations();
|
||||
await this._populateMigrationTable();
|
||||
@@ -105,26 +102,22 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
logError(TelemetryViews.MigrationsTab, 'refreshMigrations', e);
|
||||
} finally {
|
||||
this._refreshLoader.loading = false;
|
||||
this._refresh.enabled = true;
|
||||
this.isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected async initialize(): Promise<void> {
|
||||
this._registerCommands();
|
||||
|
||||
this._createStatusTable();
|
||||
this.content = this.view.modelBuilder.flexContainer()
|
||||
.withItems(
|
||||
[
|
||||
this._createToolbar(),
|
||||
await this._createSearchAndSortContainer(),
|
||||
this._createStatusTable()
|
||||
this._statusTable,
|
||||
],
|
||||
{ CSSStyles: { 'width': '100%' } }
|
||||
).withLayout({
|
||||
width: '100%',
|
||||
flexFlow: 'column',
|
||||
}).withProps({ CSSStyles: { 'padding': '0px' } })
|
||||
).withLayout({ width: '100%', flexFlow: 'column' })
|
||||
.withProps({ CSSStyles: { 'padding': '0px' } })
|
||||
.component();
|
||||
}
|
||||
|
||||
@@ -144,20 +137,16 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
async (e) => await this.refresh()));
|
||||
|
||||
this._refreshLoader = this.view.modelBuilder.loadingComponent()
|
||||
.withItem(this._refresh)
|
||||
.withProps({
|
||||
loading: false,
|
||||
CSSStyles: {
|
||||
'height': '8px',
|
||||
'margin-top': '6px'
|
||||
}
|
||||
})
|
||||
.component();
|
||||
CSSStyles: { 'height': '8px', 'margin-top': '6px' }
|
||||
}).component();
|
||||
|
||||
toolbar.addToolbarItems([
|
||||
<azdata.ToolbarComponent>{ component: this.createNewMigrationButton(), toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this.createNewSupportRequestButton() },
|
||||
<azdata.ToolbarComponent>{ component: this.createFeedbackButton(), toolbarSeparatorAfter: true },
|
||||
<azdata.ToolbarComponent>{ component: this._refresh },
|
||||
<azdata.ToolbarComponent>{ component: this._refreshLoader },
|
||||
]);
|
||||
|
||||
@@ -178,16 +167,25 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
width: 230,
|
||||
}).component();
|
||||
|
||||
const onDialogClosed = async (): Promise<void> =>
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
|
||||
this.disposables.push(
|
||||
this._serviceContextButton.onDidClick(
|
||||
async () => {
|
||||
const dialog = new SelectMigrationServiceDialog(onDialogClosed);
|
||||
const dialog = new SelectMigrationServiceDialog(this.serviceContextChangedEvent);
|
||||
await dialog.initialize();
|
||||
}));
|
||||
|
||||
const connectionProfile = await azdata.connection.getCurrentConnection();
|
||||
this.disposables.push(
|
||||
this.serviceContextChangedEvent.event(
|
||||
async (e) => {
|
||||
if (e.connectionId === connectionProfile.connectionId) {
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
));
|
||||
await this.updateServiceContext(this._serviceContextButton);
|
||||
|
||||
this._searchBox = this.view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
stopEnterPropagation: true,
|
||||
@@ -212,7 +210,9 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
.withProps({
|
||||
ariaLabel: loc.MIGRATION_STATUS_FILTER,
|
||||
values: this._statusDropdownValues,
|
||||
width: '150px'
|
||||
width: '150px',
|
||||
fireOnTextChange: true,
|
||||
value: this._statusDropdownValues[0],
|
||||
}).component();
|
||||
this.disposables.push(
|
||||
this._statusDropdown.onValueChanged(
|
||||
@@ -311,173 +311,6 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
return container;
|
||||
}
|
||||
|
||||
private _registerCommands(): void {
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.Cutover,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(
|
||||
migration => migration.id === migrationId);
|
||||
|
||||
if (canRetryMigration(migration)) {
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
const dialog = new ConfirmCutoverDialog(cutoverDialogModel);
|
||||
await dialog.initialize();
|
||||
if (cutoverDialogModel.CutoverError) {
|
||||
void vscode.window.showErrorMessage(loc.MIGRATION_CUTOVER_ERROR);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, cutoverDialogModel.CutoverError);
|
||||
}
|
||||
} else {
|
||||
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CUTOVER);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_CUTOVER_ERROR,
|
||||
loc.MIGRATION_CUTOVER_ERROR,
|
||||
e.message);
|
||||
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.ViewDatabase,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(m => m.id === migrationId);
|
||||
await this._openMigrationDetails(migration!);
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.OPEN_MIGRATION_DETAILS_ERROR,
|
||||
loc.OPEN_MIGRATION_DETAILS_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewDatabase, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.ViewTarget,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
const url = 'https://portal.azure.com/#resource/' + migration!.properties.scope;
|
||||
await vscode.env.openExternal(vscode.Uri.parse(url));
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.OPEN_MIGRATION_TARGET_ERROR,
|
||||
loc.OPEN_MIGRATION_TARGET_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewTarget, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.ViewService,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
const dialog = new SqlMigrationServiceDetailsDialog(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await dialog.initialize();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.OPEN_MIGRATION_SERVICE_ERROR,
|
||||
loc.OPEN_MIGRATION_SERVICE_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewService, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.CopyMigration,
|
||||
async (migrationId: string) => {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
|
||||
try {
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.CopyMigration, e);
|
||||
}
|
||||
|
||||
await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migration, undefined, 2));
|
||||
await vscode.window.showInformationMessage(loc.DETAILS_COPIED);
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.CancelMigration,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
if (canCancelMigration(migration)) {
|
||||
void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => {
|
||||
if (v === loc.YES) {
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
await cutoverDialogModel.cancelMigration();
|
||||
|
||||
if (cutoverDialogModel.CancelMigrationError) {
|
||||
void vscode.window.showErrorMessage(loc.MIGRATION_CANNOT_CANCEL);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, cutoverDialogModel.CancelMigrationError);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CANCEL);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_CANCELLATION_ERROR,
|
||||
loc.MIGRATION_CANCELLATION_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this.disposables.push(vscode.commands.registerCommand(
|
||||
MenuCommands.RetryMigration,
|
||||
async (migrationId: string) => {
|
||||
try {
|
||||
await this.statusBar.clearError();
|
||||
const migration = this._migrations.find(migration => migration.id === migrationId);
|
||||
if (canRetryMigration(migration)) {
|
||||
let retryMigrationDialog = new RetryMigrationDialog(
|
||||
this.context,
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!,
|
||||
async () => await this.onDialogClosed());
|
||||
await retryMigrationDialog.openDialog();
|
||||
}
|
||||
else {
|
||||
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.statusBar.showError(
|
||||
loc.MIGRATION_RETRY_ERROR,
|
||||
loc.MIGRATION_RETRY_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.RetryMigration, e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _sortMigrations(migrations: DatabaseMigration[], columnName: string, ascending: boolean): void {
|
||||
const sortDir = ascending ? -1 : 1;
|
||||
switch (columnName) {
|
||||
@@ -575,6 +408,7 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
(<azdata.CategoryValue>this._columnSortDropdown.value).name,
|
||||
this._columnSortCheckbox.checked === true);
|
||||
|
||||
const connectionProfile = await azdata.connection.getCurrentConnection();
|
||||
const data: any[] = this._filteredMigrations.map((migration, index) => {
|
||||
return [
|
||||
<azdata.HyperlinkColumnCellValue>{
|
||||
@@ -597,7 +431,11 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
getMigrationTime(migration.properties.endedOn), // finishTime
|
||||
<azdata.ContextMenuColumnCellValue>{
|
||||
title: '',
|
||||
context: migration.id,
|
||||
context: {
|
||||
connectionId: connectionProfile.connectionId,
|
||||
migrationId: migration.id,
|
||||
migrationOperationId: migration.properties.migrationOperationId,
|
||||
},
|
||||
commands: this._getMenuCommands(migration), // context menu
|
||||
},
|
||||
];
|
||||
@@ -632,7 +470,6 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
value: 'sourceDatabase',
|
||||
width: 190,
|
||||
type: azdata.ColumnType.hyperlink,
|
||||
showText: true,
|
||||
},
|
||||
{
|
||||
cssClass: rowCssStyles,
|
||||
@@ -717,25 +554,26 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
|
||||
]
|
||||
}).component();
|
||||
|
||||
this.disposables.push(this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
|
||||
const buttonState = <azdata.ICellActionEventArgs>rowState;
|
||||
const migration = this._filteredMigrations[rowState.row];
|
||||
switch (buttonState?.column) {
|
||||
case 2:
|
||||
const status = getMigrationStatus(migration);
|
||||
const statusMessage = loc.DATABASE_MIGRATION_STATUS_LABEL(status);
|
||||
const errors = this.getMigrationErrors(migration!);
|
||||
this.disposables.push(
|
||||
this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
|
||||
const buttonState = <azdata.ICellActionEventArgs>rowState;
|
||||
const migration = this._filteredMigrations[rowState.row];
|
||||
switch (buttonState?.column) {
|
||||
case 2:
|
||||
const status = getMigrationStatus(migration);
|
||||
const statusMessage = loc.DATABASE_MIGRATION_STATUS_LABEL(status);
|
||||
const errors = this.getMigrationErrors(migration!);
|
||||
|
||||
this.showDialogMessage(
|
||||
loc.DATABASE_MIGRATION_STATUS_TITLE,
|
||||
statusMessage,
|
||||
errors);
|
||||
break;
|
||||
case 0:
|
||||
await this._openMigrationDetails(migration);
|
||||
break;
|
||||
}
|
||||
}));
|
||||
this.showDialogMessage(
|
||||
loc.DATABASE_MIGRATION_STATUS_TITLE,
|
||||
statusMessage,
|
||||
errors);
|
||||
break;
|
||||
case 0:
|
||||
await this._openMigrationDetails(migration);
|
||||
break;
|
||||
}
|
||||
}));
|
||||
|
||||
return this._statusTable;
|
||||
}
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as loc from '../constants/strings';
|
||||
import { AdsMigrationStatus, TabBase } from './tabBase';
|
||||
import { AdsMigrationStatus, MigrationDetailsEvent, ServiceContextChangeEvent, TabBase } from './tabBase';
|
||||
import { MigrationsListTab, MigrationsListTabId } from './migrationsListTab';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { DatabaseMigration, getMigrationDetails } from '../api/azure';
|
||||
import { MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
import { FileStorageType } from '../models/stateMachine';
|
||||
import { MigrationDetailsTabBase } from './migrationDetailsTabBase';
|
||||
import { MigrationDetailsFileShareTab } from './migrationDetailsFileShareTab';
|
||||
import { MigrationDetailsBlobContainerTab } from './migrationDetailsBlobContainerTab';
|
||||
import { MigrationDetailsTableTab } from './migrationDetailsTableTab';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
import { DashboardStatusBar } from './DashboardStatusBar';
|
||||
|
||||
export const MigrationsTabId = 'MigrationsTab';
|
||||
|
||||
@@ -27,6 +27,7 @@ export class MigrationsTab extends TabBase<MigrationsTab> {
|
||||
private _migrationDetailsBlobTab!: MigrationDetailsTabBase<any>;
|
||||
private _migrationDetailsTableTab!: MigrationDetailsTabBase<any>;
|
||||
private _selectedTabId: string | undefined = undefined;
|
||||
private _migrationDetailsEvent!: vscode.EventEmitter<MigrationDetailsEvent>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -34,16 +35,17 @@ export class MigrationsTab extends TabBase<MigrationsTab> {
|
||||
this.id = MigrationsTabId;
|
||||
}
|
||||
|
||||
public onDialogClosed = async (): Promise<void> =>
|
||||
await this._migrationsListTab.onDialogClosed();
|
||||
|
||||
public async create(
|
||||
context: vscode.ExtensionContext,
|
||||
view: azdata.ModelView,
|
||||
serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>,
|
||||
migrationDetailsEvent: vscode.EventEmitter<MigrationDetailsEvent>,
|
||||
statusBar: DashboardStatusBar): Promise<MigrationsTab> {
|
||||
|
||||
this.context = context;
|
||||
this.view = view;
|
||||
this.serviceContextChangedEvent = serviceContextChangedEvent;
|
||||
this._migrationDetailsEvent = migrationDetailsEvent;
|
||||
this.statusBar = statusBar;
|
||||
|
||||
await this.initialize(view);
|
||||
@@ -56,9 +58,9 @@ export class MigrationsTab extends TabBase<MigrationsTab> {
|
||||
switch (this._selectedTabId) {
|
||||
case undefined:
|
||||
case MigrationsListTabId:
|
||||
return await this._migrationsListTab?.refresh();
|
||||
return this._migrationsListTab.refresh();
|
||||
default:
|
||||
return await this._migrationDetailsTab?.refresh();
|
||||
return this._migrationDetailsTab.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,41 +79,58 @@ export class MigrationsTab extends TabBase<MigrationsTab> {
|
||||
this._migrationsListTab = await new MigrationsListTab().create(
|
||||
this.context,
|
||||
this.view,
|
||||
async (migration) => await this._openMigrationDetails(migration),
|
||||
async (migration) => await this.openMigrationDetails(migration),
|
||||
this.serviceContextChangedEvent,
|
||||
this.statusBar);
|
||||
this.disposables.push(this._migrationsListTab);
|
||||
|
||||
const openMigrationsListTab = async (): Promise<void> => {
|
||||
await this.statusBar.clearError();
|
||||
await this._openTab(this._migrationsListTab);
|
||||
};
|
||||
|
||||
this._migrationDetailsBlobTab = await new MigrationDetailsBlobContainerTab().create(
|
||||
this.context,
|
||||
this.view,
|
||||
async () => await this._openMigrationsListTab(),
|
||||
openMigrationsListTab,
|
||||
this.statusBar);
|
||||
this.disposables.push(this._migrationDetailsBlobTab);
|
||||
|
||||
this._migrationDetailsFileShareTab = await new MigrationDetailsFileShareTab().create(
|
||||
this.context,
|
||||
this.view,
|
||||
async () => await this._openMigrationsListTab(),
|
||||
openMigrationsListTab,
|
||||
this.statusBar);
|
||||
this.disposables.push(this._migrationDetailsFileShareTab);
|
||||
|
||||
this._migrationDetailsTableTab = await new MigrationDetailsTableTab().create(
|
||||
this.context,
|
||||
this.view,
|
||||
async () => await this._openMigrationsListTab(),
|
||||
openMigrationsListTab,
|
||||
this.statusBar);
|
||||
this.disposables.push(this._migrationDetailsFileShareTab);
|
||||
|
||||
const connectionProfile = await azdata.connection.getCurrentConnection();
|
||||
const connectionId = connectionProfile.connectionId;
|
||||
this.disposables.push(
|
||||
this._migrationDetailsEvent.event(async e => {
|
||||
if (e.connectionId === connectionId) {
|
||||
const migration = await this._getMigrationDetails(e.migrationId, e.migrationOperationId);
|
||||
if (migration) {
|
||||
await this.openMigrationDetails(migration);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.content = this._tab;
|
||||
}
|
||||
|
||||
public async setMigrationFilter(filter: AdsMigrationStatus): Promise<void> {
|
||||
await this._migrationsListTab?.setMigrationFilter(filter);
|
||||
await this._openTab(this._migrationsListTab);
|
||||
await this._migrationsListTab?.setMigrationFilter(filter);
|
||||
await this._migrationsListTab.setMigrationFilter(filter);
|
||||
}
|
||||
|
||||
private async _openMigrationDetails(migration: DatabaseMigration): Promise<void> {
|
||||
public async openMigrationDetails(migration: DatabaseMigration): Promise<void> {
|
||||
switch (migration.properties.backupConfiguration?.sourceLocation?.fileStorageType) {
|
||||
case FileStorageType.AzureBlob:
|
||||
this._migrationDetailsTab = this._migrationDetailsBlobTab;
|
||||
@@ -128,12 +147,21 @@ export class MigrationsTab extends TabBase<MigrationsTab> {
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration);
|
||||
|
||||
const promise = this._migrationDetailsTab.refresh();
|
||||
await this._openTab(this._migrationDetailsTab);
|
||||
await promise;
|
||||
}
|
||||
|
||||
private async _openMigrationsListTab(): Promise<void> {
|
||||
await this.statusBar.clearError();
|
||||
await this._openTab(this._migrationsListTab);
|
||||
private async _getMigrationDetails(migrationId: string, migrationOperationId: string): Promise<DatabaseMigration | undefined> {
|
||||
const context = await MigrationLocalStorage.getMigrationServiceContext();
|
||||
if (context.azureAccount && context.subscription) {
|
||||
return getMigrationDetails(
|
||||
context.azureAccount,
|
||||
context.subscription,
|
||||
migrationId,
|
||||
migrationOperationId);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async _openTab(tab: azdata.Tab): Promise<void> {
|
||||
@@ -141,6 +169,7 @@ export class MigrationsTab extends TabBase<MigrationsTab> {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.statusBar.clearError();
|
||||
this._tab.clearItems();
|
||||
this._tab.addItem(tab.content);
|
||||
this._selectedTabId = tab.id;
|
||||
|
||||
@@ -5,82 +5,121 @@
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as mssql from 'mssql';
|
||||
import { promises as fs } from 'fs';
|
||||
import { DatabaseMigration, getMigrationDetails } from '../api/azure';
|
||||
import { MenuCommands, SqlMigrationExtensionId } from '../api/utils';
|
||||
import { canCancelMigration, canRetryMigration } from '../constants/helper';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { MigrationNotebookInfo, NotebookPathHelper } from '../constants/notebookPathHelper';
|
||||
import * as loc from '../constants/strings';
|
||||
import { SavedAssessmentDialog } from '../dialog/assessmentResults/savedAssessmentDialog';
|
||||
import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
|
||||
import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel';
|
||||
import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
|
||||
import { SqlMigrationServiceDetailsDialog } from '../dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog';
|
||||
import { MigrationLocalStorage } from '../models/migrationLocalStorage';
|
||||
import { MigrationStateModel, SavedInfo } from '../models/stateMachine';
|
||||
import { logError, TelemetryViews } from '../telemtery';
|
||||
import { WizardController } from '../wizard/wizardController';
|
||||
import { DashboardStatusBar, ErrorEvent } from './DashboardStatusBar';
|
||||
import { DashboardTab } from './dashboardTab';
|
||||
import { MigrationsTab, MigrationsTabId } from './migrationsTab';
|
||||
import { AdsMigrationStatus } from './tabBase';
|
||||
import { AdsMigrationStatus, MigrationDetailsEvent, ServiceContextChangeEvent } from './tabBase';
|
||||
|
||||
export interface DashboardStatusBar {
|
||||
showError: (errorTitle: string, errorLable: string, errorDescription: string) => Promise<void>;
|
||||
clearError: () => Promise<void>;
|
||||
errorTitle: string;
|
||||
errorLabel: string;
|
||||
errorDescription: string;
|
||||
export interface MenuCommandArgs {
|
||||
connectionId: string,
|
||||
migrationId: string,
|
||||
migrationOperationId: string,
|
||||
}
|
||||
|
||||
export class DashboardWidget implements DashboardStatusBar {
|
||||
private _context: vscode.ExtensionContext;
|
||||
private _view!: azdata.ModelView;
|
||||
private _tabs!: azdata.TabbedPanelComponent;
|
||||
private _statusInfoBox!: azdata.InfoBoxComponent;
|
||||
private _dashboardTab!: DashboardTab;
|
||||
private _migrationsTab!: MigrationsTab;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
export class DashboardWidget {
|
||||
public stateModel!: MigrationStateModel;
|
||||
private readonly _context: vscode.ExtensionContext;
|
||||
private readonly _onServiceContextChanged: vscode.EventEmitter<ServiceContextChangeEvent>;
|
||||
private readonly _migrationDetailsEvent: vscode.EventEmitter<MigrationDetailsEvent>;
|
||||
private readonly _errorEvent: vscode.EventEmitter<ErrorEvent>;
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this._context = context;
|
||||
NotebookPathHelper.setExtensionContext(context);
|
||||
IconPathHelper.setExtensionContext(context);
|
||||
MigrationLocalStorage.setExtensionContext(context);
|
||||
|
||||
this._onServiceContextChanged = new vscode.EventEmitter<ServiceContextChangeEvent>();
|
||||
this._errorEvent = new vscode.EventEmitter<ErrorEvent>();
|
||||
this._migrationDetailsEvent = new vscode.EventEmitter<MigrationDetailsEvent>();
|
||||
|
||||
context.subscriptions.push(this._onServiceContextChanged);
|
||||
context.subscriptions.push(this._errorEvent);
|
||||
context.subscriptions.push(this._migrationDetailsEvent);
|
||||
}
|
||||
|
||||
public errorTitle: string = '';
|
||||
public errorLabel: string = '';
|
||||
public errorDescription: string = '';
|
||||
public async register(): Promise<void> {
|
||||
await this._registerCommands();
|
||||
|
||||
public async showError(errorTitle: string, errorLabel: string, errorDescription: string): Promise<void> {
|
||||
this.errorTitle = errorTitle;
|
||||
this.errorLabel = errorLabel;
|
||||
this.errorDescription = errorDescription;
|
||||
this._statusInfoBox.style = 'error';
|
||||
this._statusInfoBox.text = errorTitle;
|
||||
await this._updateStatusDisplay(this._statusInfoBox, true);
|
||||
}
|
||||
|
||||
public async clearError(): Promise<void> {
|
||||
await this._updateStatusDisplay(this._statusInfoBox, false);
|
||||
this.errorTitle = '';
|
||||
this.errorLabel = '';
|
||||
this.errorDescription = '';
|
||||
this._statusInfoBox.style = 'success';
|
||||
this._statusInfoBox.text = '';
|
||||
}
|
||||
|
||||
public register(): void {
|
||||
azdata.ui.registerModelViewProvider('migration-dashboard', async (view) => {
|
||||
this._view = view;
|
||||
this._disposables.push(
|
||||
this._view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
const disposables: vscode.Disposable[] = [];
|
||||
const _view = view;
|
||||
|
||||
const statusInfoBox = view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
style: 'error',
|
||||
text: '',
|
||||
clickableButtonAriaLabel: loc.ERROR_DIALOG_ARIA_CLICK_VIEW_ERROR_DETAILS,
|
||||
announceText: true,
|
||||
isClickable: true,
|
||||
display: 'none',
|
||||
CSSStyles: { 'font-size': '14px', 'display': 'none', },
|
||||
}).component();
|
||||
|
||||
const connectionProfile = await azdata.connection.getCurrentConnection();
|
||||
const statusBar = new DashboardStatusBar(
|
||||
this._context,
|
||||
connectionProfile.connectionId,
|
||||
statusInfoBox,
|
||||
this._errorEvent);
|
||||
|
||||
disposables.push(
|
||||
statusInfoBox.onDidClick(
|
||||
async e => await statusBar.openErrorDialog()));
|
||||
|
||||
disposables.push(
|
||||
_view.onClosed(e =>
|
||||
disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } })));
|
||||
|
||||
const openMigrationFcn = async (filter: AdsMigrationStatus): Promise<void> => {
|
||||
this._tabs.selectTab(MigrationsTabId);
|
||||
await this._migrationsTab.setMigrationFilter(filter);
|
||||
if (!migrationsTabInitialized) {
|
||||
migrationsTabInitialized = true;
|
||||
tabs.selectTab(MigrationsTabId);
|
||||
await migrationsTab.setMigrationFilter(AdsMigrationStatus.ALL);
|
||||
await migrationsTab.refresh();
|
||||
await migrationsTab.setMigrationFilter(filter);
|
||||
} else {
|
||||
const promise = migrationsTab.setMigrationFilter(filter);
|
||||
tabs.selectTab(MigrationsTabId);
|
||||
await promise;
|
||||
}
|
||||
};
|
||||
|
||||
this._dashboardTab = await new DashboardTab().create(
|
||||
const dashboardTab = await new DashboardTab().create(
|
||||
view,
|
||||
async (filter: AdsMigrationStatus) => await openMigrationFcn(filter),
|
||||
this);
|
||||
this._disposables.push(this._dashboardTab);
|
||||
this._onServiceContextChanged,
|
||||
statusBar);
|
||||
disposables.push(dashboardTab);
|
||||
|
||||
this._migrationsTab = await new MigrationsTab().create(
|
||||
const migrationsTab = await new MigrationsTab().create(
|
||||
this._context,
|
||||
view,
|
||||
this);
|
||||
this._disposables.push(this._migrationsTab);
|
||||
this._onServiceContextChanged,
|
||||
this._migrationDetailsEvent,
|
||||
statusBar);
|
||||
disposables.push(migrationsTab);
|
||||
|
||||
this._tabs = view.modelBuilder.tabbedPanel()
|
||||
.withTabs([this._dashboardTab, this._migrationsTab])
|
||||
const tabs = view.modelBuilder.tabbedPanel()
|
||||
.withTabs([dashboardTab, migrationsTab])
|
||||
.withLayout({ alwaysShowTabs: true, orientation: azdata.TabOrientation.Horizontal })
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
@@ -91,107 +130,338 @@ export class DashboardWidget implements DashboardStatusBar {
|
||||
})
|
||||
.component();
|
||||
|
||||
this._disposables.push(
|
||||
this._tabs.onTabChanged(
|
||||
async id => {
|
||||
await this.clearError();
|
||||
await this.onDialogClosed();
|
||||
}));
|
||||
|
||||
this._statusInfoBox = view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
style: 'error',
|
||||
text: '',
|
||||
announceText: true,
|
||||
isClickable: true,
|
||||
display: 'none',
|
||||
CSSStyles: { 'font-size': '14px' },
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
this._statusInfoBox.onDidClick(
|
||||
async e => await this.openErrorDialog()));
|
||||
let migrationsTabInitialized = false;
|
||||
disposables.push(
|
||||
tabs.onTabChanged(async tabId => {
|
||||
const connectionProfile = await azdata.connection.getCurrentConnection();
|
||||
await this.clearError(connectionProfile.connectionId);
|
||||
if (tabId === MigrationsTabId && !migrationsTabInitialized) {
|
||||
migrationsTabInitialized = true;
|
||||
await migrationsTab.refresh();
|
||||
}
|
||||
}));
|
||||
|
||||
const flexContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.withItems([this._statusInfoBox, this._tabs])
|
||||
.withItems([statusInfoBox, tabs])
|
||||
.component();
|
||||
await view.initializeModel(flexContainer);
|
||||
|
||||
await this.refresh();
|
||||
await dashboardTab.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
void this._migrationsTab.refresh();
|
||||
await this._dashboardTab.refresh();
|
||||
}
|
||||
private async _registerCommands(): Promise<void> {
|
||||
this._context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
MenuCommands.Cutover,
|
||||
async (args: MenuCommandArgs) => {
|
||||
try {
|
||||
await this.clearError(args.connectionId);
|
||||
const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId);
|
||||
if (canRetryMigration(migration)) {
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
const dialog = new ConfirmCutoverDialog(cutoverDialogModel);
|
||||
await dialog.initialize();
|
||||
if (cutoverDialogModel.CutoverError) {
|
||||
void vscode.window.showErrorMessage(loc.MIGRATION_CUTOVER_ERROR);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, cutoverDialogModel.CutoverError);
|
||||
}
|
||||
} else {
|
||||
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CUTOVER);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.showError(
|
||||
args.connectionId,
|
||||
loc.MIGRATION_CUTOVER_ERROR,
|
||||
loc.MIGRATION_CUTOVER_ERROR,
|
||||
e.message);
|
||||
|
||||
public async onDialogClosed(): Promise<void> {
|
||||
await this._dashboardTab.onDialogClosed();
|
||||
await this._migrationsTab.onDialogClosed();
|
||||
}
|
||||
|
||||
private _errorDialogIsOpen: boolean = false;
|
||||
|
||||
protected async openErrorDialog(): Promise<void> {
|
||||
if (this._errorDialogIsOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const tab = azdata.window.createTab(this.errorTitle);
|
||||
tab.registerContent(async (view) => {
|
||||
const flex = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
view.modelBuilder.text()
|
||||
.withProps({ value: this.errorLabel, CSSStyles: { 'margin': '0px 0px 5px 5px' } })
|
||||
.component(),
|
||||
view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
value: this.errorDescription,
|
||||
readOnly: true,
|
||||
multiline: true,
|
||||
inputType: 'text',
|
||||
rows: 20,
|
||||
CSSStyles: { 'overflow': 'hidden auto', 'margin': '0px 0px 0px 5px' },
|
||||
})
|
||||
.component()
|
||||
])
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
width: 420,
|
||||
})
|
||||
.withProps({ CSSStyles: { 'margin': '0 10px 0 10px' } })
|
||||
.component();
|
||||
|
||||
await view.initializeModel(flex);
|
||||
});
|
||||
|
||||
const dialog = azdata.window.createModelViewDialog(
|
||||
this.errorTitle,
|
||||
'errorDialog',
|
||||
450,
|
||||
'flyout');
|
||||
dialog.content = [tab];
|
||||
dialog.okButton.label = loc.ERROR_DIALOG_CLEAR_BUTTON_LABEL;
|
||||
dialog.okButton.focused = true;
|
||||
dialog.cancelButton.label = loc.CLOSE;
|
||||
this._disposables.push(
|
||||
dialog.onClosed(async e => {
|
||||
if (e === 'ok') {
|
||||
await this.clearError();
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.Cutover, e);
|
||||
}
|
||||
this._errorDialogIsOpen = false;
|
||||
}));
|
||||
|
||||
azdata.window.openDialog(dialog);
|
||||
} catch (error) {
|
||||
this._errorDialogIsOpen = false;
|
||||
this._context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
MenuCommands.ViewDatabase,
|
||||
async (args: MenuCommandArgs) => {
|
||||
try {
|
||||
await this.clearError(args.connectionId);
|
||||
this._migrationDetailsEvent.fire({
|
||||
connectionId: args.connectionId,
|
||||
migrationId: args.migrationId,
|
||||
migrationOperationId: args.migrationOperationId,
|
||||
});
|
||||
} catch (e) {
|
||||
await this.showError(
|
||||
args.connectionId,
|
||||
loc.OPEN_MIGRATION_DETAILS_ERROR,
|
||||
loc.OPEN_MIGRATION_DETAILS_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewDatabase, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this._context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
MenuCommands.ViewTarget,
|
||||
async (args: MenuCommandArgs) => {
|
||||
try {
|
||||
const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId);
|
||||
const url = 'https://portal.azure.com/#resource/' + migration!.properties.scope;
|
||||
await vscode.env.openExternal(vscode.Uri.parse(url));
|
||||
} catch (e) {
|
||||
await this.showError(
|
||||
args.connectionId,
|
||||
loc.OPEN_MIGRATION_TARGET_ERROR,
|
||||
loc.OPEN_MIGRATION_TARGET_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewTarget, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this._context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
MenuCommands.ViewService,
|
||||
async (args: MenuCommandArgs) => {
|
||||
try {
|
||||
await this.clearError(args.connectionId);
|
||||
const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId);
|
||||
const dialog = new SqlMigrationServiceDetailsDialog(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await dialog.initialize();
|
||||
} catch (e) {
|
||||
await this.showError(
|
||||
args.connectionId,
|
||||
loc.OPEN_MIGRATION_SERVICE_ERROR,
|
||||
loc.OPEN_MIGRATION_SERVICE_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.ViewService, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this._context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
MenuCommands.CopyMigration,
|
||||
async (args: MenuCommandArgs) => {
|
||||
await this.clearError(args.connectionId);
|
||||
const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId);
|
||||
if (migration) {
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration);
|
||||
try {
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
} catch (e) {
|
||||
await this.showError(
|
||||
args.connectionId,
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
loc.MIGRATION_STATUS_REFRESH_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.CopyMigration, e);
|
||||
}
|
||||
|
||||
await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migration, undefined, 2));
|
||||
await vscode.window.showInformationMessage(loc.DETAILS_COPIED);
|
||||
}
|
||||
}));
|
||||
|
||||
this._context.subscriptions.push(vscode.commands.registerCommand(
|
||||
MenuCommands.CancelMigration,
|
||||
async (args: MenuCommandArgs) => {
|
||||
try {
|
||||
await this.clearError(args.connectionId);
|
||||
const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId);
|
||||
if (canCancelMigration(migration)) {
|
||||
void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO)
|
||||
.then(async (v) => {
|
||||
if (v === loc.YES) {
|
||||
const cutoverDialogModel = new MigrationCutoverDialogModel(
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!);
|
||||
await cutoverDialogModel.fetchStatus();
|
||||
await cutoverDialogModel.cancelMigration();
|
||||
|
||||
if (cutoverDialogModel.CancelMigrationError) {
|
||||
void vscode.window.showErrorMessage(loc.MIGRATION_CANNOT_CANCEL);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, cutoverDialogModel.CancelMigrationError);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CANCEL);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.showError(
|
||||
args.connectionId,
|
||||
loc.MIGRATION_CANCELLATION_ERROR,
|
||||
loc.MIGRATION_CANCELLATION_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this._context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
MenuCommands.RetryMigration,
|
||||
async (args: MenuCommandArgs) => {
|
||||
try {
|
||||
await this.clearError(args.connectionId);
|
||||
const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId);
|
||||
if (canRetryMigration(migration)) {
|
||||
const retryMigrationDialog = new RetryMigrationDialog(
|
||||
this._context,
|
||||
await MigrationLocalStorage.getMigrationServiceContext(),
|
||||
migration!,
|
||||
this._onServiceContextChanged);
|
||||
await retryMigrationDialog.openDialog();
|
||||
}
|
||||
else {
|
||||
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.showError(
|
||||
args.connectionId,
|
||||
loc.MIGRATION_RETRY_ERROR,
|
||||
loc.MIGRATION_RETRY_ERROR,
|
||||
e.message);
|
||||
logError(TelemetryViews.MigrationsTab, MenuCommands.RetryMigration, e);
|
||||
}
|
||||
}));
|
||||
|
||||
this._context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
MenuCommands.StartMigration,
|
||||
async () => await this.launchMigrationWizard()));
|
||||
|
||||
this._context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
MenuCommands.OpenNotebooks,
|
||||
async () => {
|
||||
const input = vscode.window.createQuickPick<MigrationNotebookInfo>();
|
||||
input.placeholder = loc.NOTEBOOK_QUICK_PICK_PLACEHOLDER;
|
||||
input.items = NotebookPathHelper.getAllMigrationNotebooks();
|
||||
|
||||
this._context.subscriptions.push(
|
||||
input.onDidAccept(async (e) => {
|
||||
const selectedNotebook = input.selectedItems[0];
|
||||
if (selectedNotebook) {
|
||||
try {
|
||||
await azdata.nb.showNotebookDocument(vscode.Uri.parse(`untitled: ${selectedNotebook.label}`), {
|
||||
preview: false,
|
||||
initialContent: (await fs.readFile(selectedNotebook.notebookPath)).toString(),
|
||||
initialDirtyState: false
|
||||
});
|
||||
} catch (e) {
|
||||
void vscode.window.showErrorMessage(`${loc.NOTEBOOK_OPEN_ERROR} - ${e.toString()}`);
|
||||
}
|
||||
input.hide();
|
||||
}
|
||||
}));
|
||||
|
||||
input.show();
|
||||
}));
|
||||
|
||||
this._context.subscriptions.push(azdata.tasks.registerTask(
|
||||
MenuCommands.StartMigration,
|
||||
async () => await this.launchMigrationWizard()));
|
||||
|
||||
this._context.subscriptions.push(
|
||||
azdata.tasks.registerTask(
|
||||
MenuCommands.NewSupportRequest,
|
||||
async () => await this.launchNewSupportRequest()));
|
||||
|
||||
this._context.subscriptions.push(
|
||||
azdata.tasks.registerTask(
|
||||
MenuCommands.SendFeedback,
|
||||
async () => {
|
||||
const actionId = MenuCommands.IssueReporter;
|
||||
const args = {
|
||||
extensionId: SqlMigrationExtensionId,
|
||||
issueTitle: loc.FEEDBACK_ISSUE_TITLE,
|
||||
};
|
||||
return await vscode.commands.executeCommand(actionId, args);
|
||||
}));
|
||||
}
|
||||
|
||||
private async clearError(connectionId: string): Promise<void> {
|
||||
this._errorEvent.fire({
|
||||
connectionId: connectionId,
|
||||
title: '',
|
||||
label: '',
|
||||
message: '',
|
||||
});
|
||||
}
|
||||
|
||||
private async showError(connectionId: string, title: string, label: string, message: string): Promise<void> {
|
||||
this._errorEvent.fire({
|
||||
connectionId: connectionId,
|
||||
title: title,
|
||||
label: label,
|
||||
message: message,
|
||||
});
|
||||
}
|
||||
|
||||
private async _getMigrationById(migrationId: string, migrationOperationId: string): Promise<DatabaseMigration | undefined> {
|
||||
const context = await MigrationLocalStorage.getMigrationServiceContext();
|
||||
if (context.azureAccount && context.subscription) {
|
||||
return getMigrationDetails(
|
||||
context.azureAccount,
|
||||
context.subscription,
|
||||
migrationId,
|
||||
migrationOperationId);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async launchMigrationWizard(): Promise<void> {
|
||||
const activeConnection = await azdata.connection.getCurrentConnection();
|
||||
let connectionId: string = '';
|
||||
let serverName: string = '';
|
||||
if (!activeConnection) {
|
||||
const connection = await azdata.connection.openConnectionDialog();
|
||||
if (connection) {
|
||||
connectionId = connection.connectionId;
|
||||
serverName = connection.options.server;
|
||||
}
|
||||
} else {
|
||||
connectionId = activeConnection.connectionId;
|
||||
serverName = activeConnection.serverName;
|
||||
}
|
||||
if (serverName) {
|
||||
const api = (await vscode.extensions.getExtension(mssql.extension.name)?.activate()) as mssql.IExtension;
|
||||
if (api) {
|
||||
this.stateModel = new MigrationStateModel(this._context, connectionId, api.sqlMigration);
|
||||
this._context.subscriptions.push(this.stateModel);
|
||||
const savedInfo = this.checkSavedInfo(serverName);
|
||||
if (savedInfo) {
|
||||
this.stateModel.savedInfo = savedInfo;
|
||||
this.stateModel.serverName = serverName;
|
||||
const savedAssessmentDialog = new SavedAssessmentDialog(
|
||||
this._context,
|
||||
this.stateModel,
|
||||
this._onServiceContextChanged);
|
||||
await savedAssessmentDialog.openDialog();
|
||||
} else {
|
||||
const wizardController = new WizardController(
|
||||
this._context,
|
||||
this.stateModel,
|
||||
this._onServiceContextChanged);
|
||||
await wizardController.openWizard(connectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateStatusDisplay(control: azdata.Component, visible: boolean): Promise<void> {
|
||||
await control.updateCssStyles({ 'display': visible ? 'inline' : 'none' });
|
||||
private checkSavedInfo(serverName: string): SavedInfo | undefined {
|
||||
return this._context.globalState.get<SavedInfo>(`${this.stateModel.mementoString}.${serverName}`);
|
||||
}
|
||||
|
||||
public async launchNewSupportRequest(): Promise<void> {
|
||||
await vscode.env.openExternal(vscode.Uri.parse(
|
||||
`https://portal.azure.com/#blade/Microsoft_Azure_Support/HelpAndSupportBlade/newsupportrequest`));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import * as loc from '../constants/strings';
|
||||
import { IconPathHelper } from '../constants/iconPathHelper';
|
||||
import { EOL } from 'os';
|
||||
import { DatabaseMigration } from '../api/azure';
|
||||
import { DashboardStatusBar } from './sqlServerDashboard';
|
||||
import { getSelectedServiceStatus } from '../models/migrationLocalStorage';
|
||||
import { MenuCommands, SqlMigrationExtensionId } from '../api/utils';
|
||||
import { DashboardStatusBar } from './DashboardStatusBar';
|
||||
|
||||
export const SqlMigrationExtensionId = 'microsoft.sql-migration';
|
||||
export const EmptySettingValue = '-';
|
||||
|
||||
export enum AdsMigrationStatus {
|
||||
@@ -23,17 +23,15 @@ export enum AdsMigrationStatus {
|
||||
COMPLETING = 'completing'
|
||||
}
|
||||
|
||||
export const MenuCommands = {
|
||||
Cutover: 'sqlmigration.cutover',
|
||||
ViewDatabase: 'sqlmigration.view.database',
|
||||
ViewTarget: 'sqlmigration.view.target',
|
||||
ViewService: 'sqlmigration.view.service',
|
||||
CopyMigration: 'sqlmigration.copy.migration',
|
||||
CancelMigration: 'sqlmigration.cancel.migration',
|
||||
RetryMigration: 'sqlmigration.retry.migration',
|
||||
StartMigration: 'sqlmigration.start',
|
||||
IssueReporter: 'workbench.action.openIssueReporter',
|
||||
};
|
||||
export interface ServiceContextChangeEvent {
|
||||
connectionId: string;
|
||||
}
|
||||
|
||||
export interface MigrationDetailsEvent {
|
||||
connectionId: string,
|
||||
migrationId: string,
|
||||
migrationOperationId: string,
|
||||
}
|
||||
|
||||
export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
|
||||
public content!: azdata.Component;
|
||||
@@ -45,7 +43,8 @@ export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
|
||||
protected view!: azdata.ModelView;
|
||||
protected disposables: vscode.Disposable[] = [];
|
||||
protected isRefreshing: boolean = false;
|
||||
protected openMigrationFcn!: (status: AdsMigrationStatus) => Promise<void>;
|
||||
protected openMigrationsFcn!: (status: AdsMigrationStatus) => Promise<void>;
|
||||
protected serviceContextChangedEvent!: vscode.EventEmitter<ServiceContextChangeEvent>;
|
||||
protected statusBar!: DashboardStatusBar;
|
||||
|
||||
protected abstract initialize(view: azdata.ModelView): Promise<void>;
|
||||
@@ -165,8 +164,9 @@ export abstract class TabBase<T> implements azdata.Tab, vscode.Disposable {
|
||||
const errors = [];
|
||||
errors.push(migration.properties.provisioningError);
|
||||
errors.push(migration.properties.migrationFailureError?.message);
|
||||
errors.push(...migration.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
|
||||
errors.push(migration.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
|
||||
errors.push(migration.properties.migrationStatusDetails?.restoreBlockingReason);
|
||||
errors.push(migration.properties.migrationStatusDetails?.sqlDataCopyErrors);
|
||||
|
||||
// remove undefined and duplicate error entries
|
||||
return errors
|
||||
|
||||
@@ -118,12 +118,16 @@ export class AssessmentResultsDialog {
|
||||
this._model._miDbs = selectedDbs;
|
||||
break;
|
||||
}
|
||||
|
||||
case MigrationTargetType.SQLVM: {
|
||||
this.didUpdateDatabasesForMigration(this._model._vmDbs, selectedDbs);
|
||||
this._model._vmDbs = selectedDbs;
|
||||
break;
|
||||
}
|
||||
case MigrationTargetType.SQLDB: {
|
||||
this.didUpdateDatabasesForMigration(this._model._sqldbDbs, selectedDbs);
|
||||
this._model._sqldbDbs = selectedDbs;
|
||||
break;
|
||||
}
|
||||
}
|
||||
await this._skuRecommendationPage.refreshCardText();
|
||||
this.model.refreshDatabaseBackupPage = true;
|
||||
|
||||
@@ -9,25 +9,28 @@ import * as constants from '../../constants/strings';
|
||||
import { MigrationStateModel } from '../../models/stateMachine';
|
||||
import { WizardController } from '../../wizard/wizardController';
|
||||
import * as styles from '../../constants/styles';
|
||||
import { ServiceContextChangeEvent } from '../../dashboard/tabBase';
|
||||
|
||||
export class SavedAssessmentDialog {
|
||||
|
||||
private static readonly OkButtonText: string = constants.NEXT_LABEL;
|
||||
private static readonly CancelButtonText: string = constants.CANCEL_LABEL;
|
||||
|
||||
private _isOpen: boolean = false;
|
||||
private dialog: azdata.window.Dialog | undefined;
|
||||
private _rootContainer!: azdata.FlexContainer;
|
||||
private stateModel: MigrationStateModel;
|
||||
private context: vscode.ExtensionContext;
|
||||
private _serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
private _isOpen: boolean = false;
|
||||
private _rootContainer!: azdata.FlexContainer;
|
||||
|
||||
constructor(
|
||||
context: vscode.ExtensionContext,
|
||||
stateModel: MigrationStateModel,
|
||||
private readonly _onClosedCallback: () => Promise<void>) {
|
||||
serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>) {
|
||||
this.stateModel = stateModel;
|
||||
this.context = context;
|
||||
this._serviceContextChangedEvent = serviceContextChangedEvent;
|
||||
}
|
||||
|
||||
private async initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
|
||||
@@ -36,18 +39,18 @@ export class SavedAssessmentDialog {
|
||||
try {
|
||||
this._rootContainer = this.initializePageContent(view);
|
||||
await view.initializeModel(this._rootContainer);
|
||||
this._disposables.push(dialog.okButton.onClick(async e => {
|
||||
await this.execute();
|
||||
}));
|
||||
this._disposables.push(dialog.cancelButton.onClick(e => {
|
||||
this.cancel();
|
||||
}));
|
||||
this._disposables.push(
|
||||
dialog.okButton.onClick(
|
||||
async e => await this.execute()));
|
||||
|
||||
this._disposables.push(
|
||||
dialog.cancelButton.onClick(
|
||||
e => this.cancel()));
|
||||
this._disposables.push(
|
||||
view.onClosed(
|
||||
e => this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } })));
|
||||
|
||||
this._disposables.push(view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } }
|
||||
);
|
||||
}));
|
||||
resolve();
|
||||
} catch (ex) {
|
||||
reject(ex);
|
||||
@@ -83,7 +86,7 @@ export class SavedAssessmentDialog {
|
||||
const wizardController = new WizardController(
|
||||
this.context,
|
||||
this.stateModel,
|
||||
this._onClosedCallback);
|
||||
this._serviceContextChangedEvent);
|
||||
|
||||
await wizardController.openWizard(this.stateModel.sourceConnectionId);
|
||||
this._isOpen = false;
|
||||
@@ -100,44 +103,39 @@ export class SavedAssessmentDialog {
|
||||
public initializePageContent(view: azdata.ModelView): azdata.FlexContainer {
|
||||
const buttonGroup = 'resumeMigration';
|
||||
|
||||
const radioStart = view.modelBuilder.radioButton().withProps({
|
||||
label: constants.START_NEW_SESSION,
|
||||
name: buttonGroup,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin-bottom': '8px'
|
||||
},
|
||||
checked: true
|
||||
}).component();
|
||||
const radioStart = view.modelBuilder.radioButton()
|
||||
.withProps({
|
||||
label: constants.START_NEW_SESSION,
|
||||
name: buttonGroup,
|
||||
CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '8px' },
|
||||
checked: true
|
||||
}).component();
|
||||
|
||||
this._disposables.push(radioStart.onDidChangeCheckedState((e) => {
|
||||
if (e) {
|
||||
this.stateModel.resumeAssessment = false;
|
||||
}
|
||||
}));
|
||||
const radioContinue = view.modelBuilder.radioButton().withProps({
|
||||
label: constants.RESUME_SESSION,
|
||||
name: buttonGroup,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
},
|
||||
checked: false
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
radioStart.onDidChangeCheckedState(checked => {
|
||||
if (checked) {
|
||||
this.stateModel.resumeAssessment = false;
|
||||
}
|
||||
}));
|
||||
const radioContinue = view.modelBuilder.radioButton()
|
||||
.withProps({
|
||||
label: constants.RESUME_SESSION,
|
||||
name: buttonGroup,
|
||||
CSSStyles: { ...styles.BODY_CSS },
|
||||
checked: false
|
||||
}).component();
|
||||
|
||||
this._disposables.push(radioContinue.onDidChangeCheckedState((e) => {
|
||||
if (e) {
|
||||
this.stateModel.resumeAssessment = true;
|
||||
}
|
||||
}));
|
||||
this._disposables.push(
|
||||
radioContinue.onDidChangeCheckedState(checked => {
|
||||
if (checked) {
|
||||
this.stateModel.resumeAssessment = true;
|
||||
}
|
||||
}));
|
||||
|
||||
const flex = view.modelBuilder.flexContainer()
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'padding': '20px 15px',
|
||||
}
|
||||
}).component();
|
||||
.withLayout({ flexFlow: 'column', })
|
||||
.withProps({ CSSStyles: { 'padding': '20px 15px', } })
|
||||
.component();
|
||||
flex.addItem(radioStart, { flex: '0 0 auto' });
|
||||
flex.addItem(radioContinue, { flex: '0 0 auto' });
|
||||
|
||||
|
||||
@@ -91,7 +91,14 @@ export class SqlDatabaseTree {
|
||||
|
||||
const selectDbMessage = this.createSelectDbMessage();
|
||||
this._resultComponent = await this.createComponentResult(view);
|
||||
const treeComponent = await this.createComponent(view, this._targetType === MigrationTargetType.SQLVM ? this._model._vmDbs : this._model._miDbs);
|
||||
const treeComponent = await this.createComponent(
|
||||
view,
|
||||
(this._targetType === MigrationTargetType.SQLVM)
|
||||
? this._model._vmDbs
|
||||
: (this._targetType === MigrationTargetType.SQLMI)
|
||||
? this._model._miDbs
|
||||
: this._model._sqldbDbs);
|
||||
|
||||
this._rootContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'row',
|
||||
height: '100%',
|
||||
@@ -101,7 +108,8 @@ export class SqlDatabaseTree {
|
||||
this._rootContainer.addItem(this._resultComponent, { flex: '0 0 auto' });
|
||||
this._rootContainer.addItem(selectDbMessage, { flex: '1 1 auto' });
|
||||
|
||||
if (this._targetType === MigrationTargetType.SQLMI) {
|
||||
if (this._targetType === MigrationTargetType.SQLMI ||
|
||||
this._targetType === MigrationTargetType.SQLDB) {
|
||||
if (!!this._model._assessmentResults?.issues.find(value => value.databaseRestoreFails) ||
|
||||
!!this._model._assessmentResults?.databaseAssessments.find(d => !!d.issues.find(issue => issue.databaseRestoreFails))) {
|
||||
dialog.message = {
|
||||
@@ -192,7 +200,8 @@ export class SqlDatabaseTree {
|
||||
}));
|
||||
|
||||
this._disposables.push(this._databaseTable.onRowSelected(async (e) => {
|
||||
if (this._targetType === MigrationTargetType.SQLMI) {
|
||||
if (this._targetType === MigrationTargetType.SQLMI ||
|
||||
this._targetType === MigrationTargetType.SQLDB) {
|
||||
this._activeIssues = this._model._assessmentResults?.databaseAssessments[e.row].issues;
|
||||
} else {
|
||||
this._activeIssues = [];
|
||||
@@ -306,7 +315,8 @@ export class SqlDatabaseTree {
|
||||
});
|
||||
this._recommendation.value = constants.WARNINGS_DETAILS;
|
||||
this._recommendationTitle.value = constants.WARNINGS_COUNT(this._activeIssues?.length);
|
||||
if (this._targetType === MigrationTargetType.SQLMI) {
|
||||
if (this._targetType === MigrationTargetType.SQLMI ||
|
||||
this._targetType === MigrationTargetType.SQLDB) {
|
||||
await this.refreshResults();
|
||||
}
|
||||
}));
|
||||
@@ -388,42 +398,34 @@ export class SqlDatabaseTree {
|
||||
}
|
||||
|
||||
private createNoIssuesText(): azdata.FlexContainer {
|
||||
let message: azdata.TextComponent;
|
||||
const failedAssessment = this.handleFailedAssessment();
|
||||
if (this._targetType === MigrationTargetType.SQLVM) {
|
||||
message = this._view.modelBuilder.text().withProps({
|
||||
value: failedAssessment
|
||||
? constants.NO_RESULTS_AVAILABLE
|
||||
: constants.NO_ISSUES_FOUND_VM,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
}).component();
|
||||
} else {
|
||||
message = this._view.modelBuilder.text().withProps({
|
||||
value: failedAssessment
|
||||
? constants.NO_RESULTS_AVAILABLE
|
||||
: constants.NO_ISSUES_FOUND_MI,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
}).component();
|
||||
}
|
||||
//TODO: will need to add a SQL DB condition here in the future
|
||||
|
||||
this._noIssuesContainer = this._view.modelBuilder.flexContainer().withItems([message]).withProps({
|
||||
CSSStyles: {
|
||||
'margin-top': '8px',
|
||||
'display': 'none'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const value = failedAssessment
|
||||
? constants.NO_RESULTS_AVAILABLE
|
||||
: (this._targetType === MigrationTargetType.SQLVM)
|
||||
? constants.NO_ISSUES_FOUND_VM
|
||||
: (this._targetType === MigrationTargetType.SQLMI)
|
||||
? constants.NO_ISSUES_FOUND_MI
|
||||
: constants.NO_ISSUES_FOUND_SQLDB;
|
||||
|
||||
const message = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: value,
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
}).component();
|
||||
|
||||
this._noIssuesContainer = this._view.modelBuilder.flexContainer()
|
||||
.withItems([message])
|
||||
.withProps({ CSSStyles: { 'margin-top': '8px', 'display': 'none' } })
|
||||
.component();
|
||||
|
||||
return this._noIssuesContainer;
|
||||
}
|
||||
|
||||
private handleFailedAssessment(): boolean {
|
||||
const failedAssessment: boolean = this._model._assessmentResults?.assessmentError !== undefined
|
||||
|| (this._model._assessmentResults?.errors?.length || 0) > 0;
|
||||
|| (this._model._assessmentResults?.errors?.length ?? 0) > 0;
|
||||
if (failedAssessment) {
|
||||
this._dialog.message = {
|
||||
level: azdata.window.MessageLevel.Warning,
|
||||
@@ -471,16 +473,12 @@ export class SqlDatabaseTree {
|
||||
|
||||
private createAssessmentContainer(): azdata.FlexContainer {
|
||||
const title = this.createAssessmentTitle();
|
||||
|
||||
const bottomContainer = this.createDescriptionContainer();
|
||||
|
||||
const container = this._view.modelBuilder.flexContainer().withItems([title, bottomContainer]).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'margin-left': '24px'
|
||||
}
|
||||
}).component();
|
||||
const container = this._view.modelBuilder.flexContainer()
|
||||
.withItems([title, bottomContainer])
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.withProps({ CSSStyles: { 'margin-left': '24px' } })
|
||||
.component();
|
||||
|
||||
return container;
|
||||
}
|
||||
@@ -488,14 +486,10 @@ export class SqlDatabaseTree {
|
||||
private createDescriptionContainer(): azdata.FlexContainer {
|
||||
const description = this.createDescription();
|
||||
const impactedObjects = this.createImpactedObjectsDescription();
|
||||
|
||||
const container = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'row'
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'height': '100%'
|
||||
}
|
||||
}).component();
|
||||
const container = this._view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'row' })
|
||||
.withProps({ CSSStyles: { 'height': '100%' } })
|
||||
.component();
|
||||
container.addItem(description, { flex: '0 0 auto', CSSStyles: { 'width': '200px', 'margin-right': '35px' } });
|
||||
container.addItem(impactedObjects, { flex: '0 0 auto', CSSStyles: { 'width': '280px' } });
|
||||
|
||||
@@ -541,19 +535,8 @@ export class SqlDatabaseTree {
|
||||
rowCssStyles: rowStyle
|
||||
},
|
||||
],
|
||||
dataValues: [
|
||||
[
|
||||
{
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
value: ''
|
||||
}
|
||||
]
|
||||
],
|
||||
CSSStyles: {
|
||||
'margin-top': '12px'
|
||||
}
|
||||
dataValues: [[{ value: '' }, { value: '' }]],
|
||||
CSSStyles: { 'margin-top': '12px' }
|
||||
}
|
||||
).component();
|
||||
|
||||
@@ -562,36 +545,47 @@ export class SqlDatabaseTree {
|
||||
this.refreshImpactedObject(impactedObject);
|
||||
}));
|
||||
|
||||
const objectDetailsTitle = this._view.modelBuilder.text().withProps({
|
||||
value: constants.OBJECT_DETAILS,
|
||||
CSSStyles: {
|
||||
...styles.LIGHT_LABEL_CSS,
|
||||
'margin': '12px 0px 0px 0px',
|
||||
}
|
||||
}).component();
|
||||
const objectDetailsTitle = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.OBJECT_DETAILS,
|
||||
CSSStyles: {
|
||||
...styles.LIGHT_LABEL_CSS,
|
||||
'margin': '12px 0px 0px 0px',
|
||||
}
|
||||
}).component();
|
||||
const objectDescriptionStyle = {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '5px 0px 0px 0px',
|
||||
'word-wrap': 'break-word'
|
||||
};
|
||||
this._objectDetailsType = this._view.modelBuilder.text().withProps({
|
||||
value: constants.TYPES_LABEL,
|
||||
CSSStyles: objectDescriptionStyle
|
||||
}).component();
|
||||
this._objectDetailsType = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.TYPES_LABEL,
|
||||
CSSStyles: objectDescriptionStyle
|
||||
}).component();
|
||||
|
||||
this._objectDetailsName = this._view.modelBuilder.text().withProps({
|
||||
value: constants.NAMES_LABEL,
|
||||
CSSStyles: objectDescriptionStyle
|
||||
}).component();
|
||||
this._objectDetailsName = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.NAMES_LABEL,
|
||||
CSSStyles: objectDescriptionStyle
|
||||
}).component();
|
||||
|
||||
this._objectDetailsSample = this._view.modelBuilder.text().withProps({
|
||||
value: '',
|
||||
CSSStyles: objectDescriptionStyle
|
||||
}).component();
|
||||
this._objectDetailsSample = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: '',
|
||||
CSSStyles: objectDescriptionStyle
|
||||
}).component();
|
||||
|
||||
const container = this._view.modelBuilder.flexContainer().withItems([impactedObjectsTitle, this._impactedObjectsTable, objectDetailsTitle, this._objectDetailsType, this._objectDetailsName, this._objectDetailsSample]).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
const container = this._view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
impactedObjectsTitle,
|
||||
this._impactedObjectsTable,
|
||||
objectDetailsTitle,
|
||||
this._objectDetailsType,
|
||||
this._objectDetailsName,
|
||||
this._objectDetailsSample])
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
|
||||
return container;
|
||||
}
|
||||
@@ -607,76 +601,91 @@ export class SqlDatabaseTree {
|
||||
'width': '200px',
|
||||
'word-wrap': 'break-word'
|
||||
};
|
||||
const descriptionTitle = this._view.modelBuilder.text().withProps({
|
||||
value: constants.DESCRIPTION,
|
||||
CSSStyles: LABEL_CSS
|
||||
}).component();
|
||||
this._descriptionText = this._view.modelBuilder.text().withProps({
|
||||
CSSStyles: textStyle
|
||||
}).component();
|
||||
const descriptionTitle = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.DESCRIPTION,
|
||||
CSSStyles: LABEL_CSS
|
||||
}).component();
|
||||
this._descriptionText = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
CSSStyles: textStyle
|
||||
}).component();
|
||||
|
||||
const recommendationTitle = this._view.modelBuilder.text().withProps({
|
||||
value: constants.RECOMMENDATION,
|
||||
CSSStyles: LABEL_CSS
|
||||
}).component();
|
||||
this._recommendationText = this._view.modelBuilder.text().withProps({
|
||||
CSSStyles: textStyle
|
||||
}).component();
|
||||
const moreInfo = this._view.modelBuilder.text().withProps({
|
||||
value: constants.MORE_INFO,
|
||||
CSSStyles: LABEL_CSS
|
||||
}).component();
|
||||
this._moreInfo = this._view.modelBuilder.hyperlink().withProps({
|
||||
label: '',
|
||||
url: '',
|
||||
CSSStyles: textStyle,
|
||||
ariaLabel: constants.MORE_INFO,
|
||||
showLinkIcon: true
|
||||
}).component();
|
||||
const recommendationTitle = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.RECOMMENDATION,
|
||||
CSSStyles: LABEL_CSS
|
||||
}).component();
|
||||
this._recommendationText = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
CSSStyles: textStyle
|
||||
}).component();
|
||||
const moreInfo = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.MORE_INFO,
|
||||
CSSStyles: LABEL_CSS
|
||||
}).component();
|
||||
this._moreInfo = this._view.modelBuilder.hyperlink()
|
||||
.withProps({
|
||||
label: '',
|
||||
url: '',
|
||||
CSSStyles: textStyle,
|
||||
ariaLabel: constants.MORE_INFO,
|
||||
showLinkIcon: true
|
||||
}).component();
|
||||
|
||||
const container = this._view.modelBuilder.flexContainer().withItems([descriptionTitle, this._descriptionText, recommendationTitle, this._recommendationText, moreInfo, this._moreInfo]).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
const container = this._view.modelBuilder.flexContainer()
|
||||
.withItems([descriptionTitle,
|
||||
this._descriptionText,
|
||||
recommendationTitle,
|
||||
this._recommendationText,
|
||||
moreInfo,
|
||||
this._moreInfo])
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private createAssessmentTitle(): azdata.TextComponent {
|
||||
this._assessmentTitle = this._view.modelBuilder.text().withProps({
|
||||
value: '',
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin-top': '12px',
|
||||
'height': '48px',
|
||||
'width': '540px',
|
||||
'border-bottom': 'solid 1px'
|
||||
}
|
||||
}).component();
|
||||
this._assessmentTitle = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: '',
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin-top': '12px',
|
||||
'height': '48px',
|
||||
'width': '540px',
|
||||
'border-bottom': 'solid 1px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
return this._assessmentTitle;
|
||||
}
|
||||
|
||||
private createTitleComponent(): azdata.TextComponent {
|
||||
const title = this._view.modelBuilder.text().withProps({
|
||||
value: constants.TARGET_PLATFORM,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0 0 4px 0'
|
||||
}
|
||||
});
|
||||
|
||||
return title.component();
|
||||
return this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.TARGET_PLATFORM,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0 0 4px 0'
|
||||
}
|
||||
}).component();
|
||||
}
|
||||
|
||||
private createPlatformComponent(): azdata.TextComponent {
|
||||
const impact = this._view.modelBuilder.text().withProps({
|
||||
value: (this._targetType === MigrationTargetType.SQLVM) ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE,
|
||||
CSSStyles: {
|
||||
...styles.PAGE_SUBTITLE_CSS
|
||||
}
|
||||
});
|
||||
const target = (this._targetType === MigrationTargetType.SQLVM)
|
||||
? constants.SUMMARY_VM_TYPE
|
||||
: (this._targetType === MigrationTargetType.SQLMI)
|
||||
? constants.SUMMARY_MI_TYPE
|
||||
: constants.SUMMARY_SQLDB_TYPE;
|
||||
|
||||
return impact.component();
|
||||
return this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: target,
|
||||
CSSStyles: { ...styles.PAGE_SUBTITLE_CSS }
|
||||
}).component();
|
||||
}
|
||||
|
||||
private createRecommendationComponent(): azdata.TextComponent {
|
||||
@@ -718,7 +727,6 @@ export class SqlDatabaseTree {
|
||||
}
|
||||
|
||||
private createImpactedObjectsTable(): azdata.FlexContainer {
|
||||
|
||||
const headerStyle: azdata.CssStyles = {
|
||||
'border': 'none',
|
||||
'text-align': 'left'
|
||||
@@ -732,13 +740,11 @@ export class SqlDatabaseTree {
|
||||
'overflow': 'hidden',
|
||||
};
|
||||
|
||||
this._assessmentResultsTable = this._view.modelBuilder.declarativeTable().withProps(
|
||||
{
|
||||
this._assessmentResultsTable = this._view.modelBuilder.declarativeTable()
|
||||
.withProps({
|
||||
enableRowSelection: true,
|
||||
width: '200px',
|
||||
CSSStyles: {
|
||||
'table-layout': 'fixed'
|
||||
},
|
||||
CSSStyles: { 'table-layout': 'fixed' },
|
||||
columns: [
|
||||
{
|
||||
displayName: '',
|
||||
@@ -758,21 +764,21 @@ export class SqlDatabaseTree {
|
||||
}
|
||||
]
|
||||
}
|
||||
).component();
|
||||
).component();
|
||||
|
||||
this._disposables.push(this._assessmentResultsTable.onRowSelected(async (e) => {
|
||||
const selectedIssue = e.row > -1 ? this._activeIssues[e.row] : undefined;
|
||||
await this.refreshAssessmentDetails(selectedIssue);
|
||||
}));
|
||||
|
||||
const container = this._view.modelBuilder.flexContainer().withItems([this._assessmentResultsTable]).withLayout({
|
||||
flexFlow: 'column',
|
||||
height: '100%'
|
||||
}).withProps({
|
||||
CSSStyles: {
|
||||
'border-right': 'solid 1px'
|
||||
}
|
||||
}).component();
|
||||
const container = this._view.modelBuilder.flexContainer()
|
||||
.withItems([this._assessmentResultsTable])
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
height: '100%'
|
||||
})
|
||||
.withProps({ CSSStyles: { 'border-right': 'solid 1px' } })
|
||||
.component();
|
||||
|
||||
return container;
|
||||
}
|
||||
@@ -788,42 +794,23 @@ export class SqlDatabaseTree {
|
||||
}
|
||||
|
||||
public async refreshResults(): Promise<void> {
|
||||
if (this._targetType === MigrationTargetType.SQLMI) {
|
||||
if (this._targetType === MigrationTargetType.SQLMI ||
|
||||
this._targetType === MigrationTargetType.SQLDB) {
|
||||
if (this._activeIssues?.length === 0) {
|
||||
/// show no issues here
|
||||
await this._assessmentsTable.updateCssStyles({
|
||||
'display': 'none',
|
||||
'border-right': 'none'
|
||||
});
|
||||
await this._assessmentContainer.updateCssStyles({
|
||||
'display': 'none'
|
||||
});
|
||||
await this._noIssuesContainer.updateCssStyles({
|
||||
'display': 'flex'
|
||||
});
|
||||
await this._assessmentsTable.updateCssStyles({ 'display': 'none', 'border-right': 'none' });
|
||||
await this._assessmentContainer.updateCssStyles({ 'display': 'none' });
|
||||
await this._noIssuesContainer.updateCssStyles({ 'display': 'flex' });
|
||||
} else {
|
||||
await this._assessmentContainer.updateCssStyles({
|
||||
'display': 'flex'
|
||||
});
|
||||
await this._assessmentsTable.updateCssStyles({
|
||||
'display': 'flex',
|
||||
'border-right': 'solid 1px'
|
||||
});
|
||||
await this._noIssuesContainer.updateCssStyles({
|
||||
'display': 'none'
|
||||
});
|
||||
await this._assessmentContainer.updateCssStyles({ 'display': 'flex' });
|
||||
await this._assessmentsTable.updateCssStyles({ 'display': 'flex', 'border-right': 'solid 1px' });
|
||||
await this._noIssuesContainer.updateCssStyles({ 'display': 'none' });
|
||||
}
|
||||
} else {
|
||||
await this._assessmentsTable.updateCssStyles({
|
||||
'display': 'none',
|
||||
'border-right': 'none'
|
||||
});
|
||||
await this._assessmentContainer.updateCssStyles({
|
||||
'display': 'none'
|
||||
});
|
||||
await this._noIssuesContainer.updateCssStyles({
|
||||
'display': 'flex'
|
||||
});
|
||||
await this._assessmentsTable.updateCssStyles({ 'display': 'none', 'border-right': 'none' });
|
||||
await this._assessmentContainer.updateCssStyles({ 'display': 'none' });
|
||||
await this._noIssuesContainer.updateCssStyles({ 'display': 'flex' });
|
||||
|
||||
this._recommendationTitle.value = constants.ASSESSMENT_RESULTS;
|
||||
this._recommendation.value = '';
|
||||
}
|
||||
@@ -868,8 +855,9 @@ export class SqlDatabaseTree {
|
||||
this._impactedObjects = selectedIssue?.impactedObjects || [];
|
||||
this._recommendationText.value = selectedIssue?.message || constants.NA;
|
||||
|
||||
await this._impactedObjectsTable.setDataValues(this._impactedObjects.map(
|
||||
(object) => [{ value: object.objectType }, { value: object.name }]));
|
||||
await this._impactedObjectsTable.setDataValues(
|
||||
this._impactedObjects.map(
|
||||
(object) => [{ value: object.objectType }, { value: object.name }]));
|
||||
|
||||
this._impactedObjectsTable.selectedRow = this._impactedObjects?.length > 0 ? 0 : -1;
|
||||
}
|
||||
@@ -884,56 +872,55 @@ export class SqlDatabaseTree {
|
||||
let instanceTableValues: azdata.DeclarativeTableCellValue[][] = [];
|
||||
this._databaseTableValues = [];
|
||||
this._dbNames = this._model._databasesForAssessment;
|
||||
const selectedDbs = (this._targetType === MigrationTargetType.SQLVM) ? this._model._vmDbs : this._model._miDbs;
|
||||
const selectedDbs = (this._targetType === MigrationTargetType.SQLVM)
|
||||
? this._model._vmDbs
|
||||
: (this._targetType === MigrationTargetType.SQLMI)
|
||||
? this._model._miDbs
|
||||
: this._model._sqldbDbs;
|
||||
|
||||
this._serverName = (await this._model.getSourceConnectionProfile()).serverName;
|
||||
|
||||
if (this._targetType === MigrationTargetType.SQLVM || !this._model._assessmentResults) {
|
||||
instanceTableValues = [
|
||||
[
|
||||
instanceTableValues = [[
|
||||
{
|
||||
value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName),
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: '0',
|
||||
style: styleRight
|
||||
}
|
||||
]];
|
||||
this._dbNames.forEach((db) => {
|
||||
this._databaseTableValues.push([
|
||||
{
|
||||
value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName),
|
||||
value: selectedDbs.includes(db),
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: this.createIconTextCell(IconPathHelper.sqlDatabaseLogo, db),
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: '0',
|
||||
style: styleRight
|
||||
}
|
||||
]
|
||||
];
|
||||
this._dbNames.forEach((db) => {
|
||||
this._databaseTableValues.push(
|
||||
[
|
||||
{
|
||||
value: selectedDbs.includes(db),
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: this.createIconTextCell(IconPathHelper.sqlDatabaseLogo, db),
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: '0',
|
||||
style: styleRight
|
||||
}
|
||||
]
|
||||
);
|
||||
]);
|
||||
});
|
||||
} else {
|
||||
instanceTableValues = [
|
||||
[
|
||||
{
|
||||
value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName),
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: this._model._assessmentResults?.issues?.length,
|
||||
style: styleRight
|
||||
}
|
||||
]
|
||||
];
|
||||
this._model._assessmentResults?.databaseAssessments.sort((db1, db2) => {
|
||||
return db2.issues?.length - db1.issues?.length;
|
||||
});
|
||||
instanceTableValues = [[
|
||||
{
|
||||
value: this.createIconTextCell(IconPathHelper.sqlServerLogo, this._serverName),
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: this._model._assessmentResults?.issues?.length,
|
||||
style: styleRight
|
||||
}
|
||||
]];
|
||||
this._model._assessmentResults?.databaseAssessments
|
||||
.sort((db1, db2) => db2.issues?.length - db1.issues?.length);
|
||||
|
||||
// Reset the dbName list so that it is in sync with the table
|
||||
this._dbNames = this._model._assessmentResults?.databaseAssessments.map(da => da.name);
|
||||
this._model._assessmentResults?.databaseAssessments.forEach((db) => {
|
||||
@@ -941,23 +928,21 @@ export class SqlDatabaseTree {
|
||||
if (db.issues.find(item => item.databaseRestoreFails)) {
|
||||
selectable = false;
|
||||
}
|
||||
this._databaseTableValues.push(
|
||||
[
|
||||
{
|
||||
value: selectedDbs.includes(db.name),
|
||||
style: styleLeft,
|
||||
enabled: selectable
|
||||
},
|
||||
{
|
||||
value: this.createIconTextCell((selectable) ? IconPathHelper.sqlDatabaseLogo : IconPathHelper.sqlDatabaseWarningLogo, db.name),
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: db.issues?.length,
|
||||
style: styleRight
|
||||
}
|
||||
]
|
||||
);
|
||||
this._databaseTableValues.push([
|
||||
{
|
||||
value: selectedDbs.includes(db.name),
|
||||
style: styleLeft,
|
||||
enabled: selectable
|
||||
},
|
||||
{
|
||||
value: this.createIconTextCell((selectable) ? IconPathHelper.sqlDatabaseLogo : IconPathHelper.sqlDatabaseWarningLogo, db.name),
|
||||
style: styleLeft
|
||||
},
|
||||
{
|
||||
value: db.issues?.length,
|
||||
style: styleRight
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
await this._instanceTable.setDataValues(instanceTableValues);
|
||||
@@ -973,47 +958,7 @@ export class SqlDatabaseTree {
|
||||
});
|
||||
}
|
||||
|
||||
// undo when bug #16445 is fixed
|
||||
private createIconTextCell(icon: IconPath, text: string): string {
|
||||
return text;
|
||||
}
|
||||
// private createIconTextCell(icon: IconPath, text: string): azdata.FlexContainer {
|
||||
// const cellContainer = this._view.modelBuilder.flexContainer().withProps({
|
||||
// CSSStyles: {
|
||||
// 'justify-content': 'left'
|
||||
// }
|
||||
// }).component();
|
||||
|
||||
// const iconComponent = this._view.modelBuilder.image().withProps({
|
||||
// iconPath: icon,
|
||||
// iconWidth: '16px',
|
||||
// iconHeight: '16px',
|
||||
// width: '20px',
|
||||
// height: '20px'
|
||||
// }).component();
|
||||
// cellContainer.addItem(iconComponent, {
|
||||
// flex: '0',
|
||||
// CSSStyles: {
|
||||
// 'width': '32px'
|
||||
// }
|
||||
// });
|
||||
|
||||
// const textComponent = this._view.modelBuilder.text().withProps({
|
||||
// value: text,
|
||||
// title: text,
|
||||
// CSSStyles: {
|
||||
// 'margin': '0px',
|
||||
// 'width': '100%',
|
||||
// }
|
||||
// }).component();
|
||||
|
||||
// cellContainer.addItem(textComponent, {
|
||||
// CSSStyles: {
|
||||
// 'width': 'auto'
|
||||
// }
|
||||
// });
|
||||
|
||||
// return cellContainer;
|
||||
// }
|
||||
// undo when bug #16445 is fixed
|
||||
}
|
||||
|
||||
@@ -390,8 +390,12 @@ export class CreateSqlMigrationServiceDialog {
|
||||
private async populateResourceGroups(): Promise<void> {
|
||||
this.migrationServiceResourceGroupDropdown.loading = true;
|
||||
try {
|
||||
this._resourceGroups = await utils.getAllResourceGroups(this._model._azureAccount, this._model._targetSubscription);
|
||||
this.migrationServiceResourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this._resourceGroups);
|
||||
this._resourceGroups = await utils.getAllResourceGroups(
|
||||
this._model._azureAccount,
|
||||
this._model._targetSubscription);
|
||||
this.migrationServiceResourceGroupDropdown.values = utils.getResourceDropdownValues(
|
||||
this._resourceGroups,
|
||||
constants.RESOURCE_GROUP_NOT_FOUND);
|
||||
|
||||
const selectedResourceGroupValue = this.migrationServiceResourceGroupDropdown.values.find(v => v.name.toLowerCase() === this._resourceGroupPreset.toLowerCase());
|
||||
this.migrationServiceResourceGroupDropdown.value = (selectedResourceGroupValue) ? selectedResourceGroupValue : this.migrationServiceResourceGroupDropdown.values[0];
|
||||
|
||||
@@ -156,20 +156,21 @@ export class ConfirmCutoverDialog {
|
||||
height: 20,
|
||||
label: constants.REFRESH,
|
||||
}).component();
|
||||
this._disposables.push(refreshButton.onDidClick(async e => {
|
||||
refreshLoader.loading = true;
|
||||
try {
|
||||
await this.migrationCutoverModel.fetchStatus();
|
||||
containerHeading.value = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
|
||||
} catch (e) {
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: e.message
|
||||
};
|
||||
} finally {
|
||||
refreshLoader.loading = false;
|
||||
}
|
||||
}));
|
||||
this._disposables.push(
|
||||
refreshButton.onDidClick(async e => {
|
||||
try {
|
||||
refreshLoader.loading = true;
|
||||
await this.migrationCutoverModel.fetchStatus();
|
||||
containerHeading.value = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
|
||||
} catch (e) {
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: e.message
|
||||
};
|
||||
} finally {
|
||||
refreshLoader.loading = false;
|
||||
}
|
||||
}));
|
||||
container.addItem(refreshButton, { flex: '0' });
|
||||
|
||||
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
|
||||
@@ -232,22 +233,23 @@ export class ConfirmCutoverDialog {
|
||||
|
||||
headingRow.addItem(containerHeading, { flex: '0' });
|
||||
|
||||
this._disposables.push(refreshButton.onDidClick(async e => {
|
||||
refreshLoader.loading = true;
|
||||
try {
|
||||
await this.migrationCutoverModel.fetchStatus();
|
||||
containerHeading.label = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
|
||||
lastScanCompleted.value = constants.LAST_SCAN_COMPLETED(get12HourTime(new Date()));
|
||||
this.refreshFileTable(fileTable);
|
||||
} catch (e) {
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: e.message
|
||||
};
|
||||
} finally {
|
||||
refreshLoader.loading = false;
|
||||
}
|
||||
}));
|
||||
this._disposables.push(
|
||||
refreshButton.onDidClick(async e => {
|
||||
try {
|
||||
refreshLoader.loading = true;
|
||||
await this.migrationCutoverModel.fetchStatus();
|
||||
containerHeading.label = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
|
||||
lastScanCompleted.value = constants.LAST_SCAN_COMPLETED(get12HourTime(new Date()));
|
||||
this.refreshFileTable(fileTable);
|
||||
} catch (e) {
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: e.message
|
||||
};
|
||||
} finally {
|
||||
refreshLoader.loading = false;
|
||||
}
|
||||
}));
|
||||
headingRow.addItem(refreshButton, { flex: '0' });
|
||||
|
||||
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DatabaseMigration, startMigrationCutover, stopMigration, BackupFileInfo, getResourceGroupFromId, getMigrationDetails, getMigrationTargetName } from '../../api/azure';
|
||||
import { BackupFileInfoStatus, MigrationServiceContext } from '../../models/migrationLocalStorage';
|
||||
import { MigrationServiceContext } from '../../models/migrationLocalStorage';
|
||||
import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery';
|
||||
import * as constants from '../../constants/strings';
|
||||
import { getMigrationTargetType, getMigrationMode, isBlobMigration } from '../../constants/helper';
|
||||
@@ -110,7 +110,7 @@ export class MigrationCutoverDialogModel {
|
||||
const files: BackupFileInfo[] = [];
|
||||
this.migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(abs => {
|
||||
abs.listOfBackupFiles.forEach(f => {
|
||||
if (f.status !== BackupFileInfoStatus.Restored) {
|
||||
if (f.status !== constants.BackupFileInfoStatus.Restored) {
|
||||
files.push(f);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import { MigrationServiceContext } from '../../models/migrationLocalStorage';
|
||||
import { WizardController } from '../../wizard/wizardController';
|
||||
import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper';
|
||||
import * as constants from '../../constants/strings';
|
||||
import { ServiceContextChangeEvent } from '../../dashboard/tabBase';
|
||||
|
||||
export class RetryMigrationDialog {
|
||||
|
||||
@@ -20,15 +21,20 @@ export class RetryMigrationDialog {
|
||||
private readonly _context: vscode.ExtensionContext,
|
||||
private readonly _serviceContext: MigrationServiceContext,
|
||||
private readonly _migration: DatabaseMigration,
|
||||
private readonly _onClosedCallback: () => Promise<void>) {
|
||||
private readonly _serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>) {
|
||||
}
|
||||
|
||||
private async createMigrationStateModel(serviceContext: MigrationServiceContext, migration: DatabaseMigration, connectionId: string, serverName: string, api: mssql.IExtension, location: azureResource.AzureLocation): Promise<MigrationStateModel> {
|
||||
let stateModel = new MigrationStateModel(this._context, connectionId, api.sqlMigration);
|
||||
private async createMigrationStateModel(
|
||||
serviceContext: MigrationServiceContext,
|
||||
migration: DatabaseMigration,
|
||||
connectionId: string,
|
||||
serverName: string,
|
||||
api: mssql.IExtension,
|
||||
location: azureResource.AzureLocation): Promise<MigrationStateModel> {
|
||||
|
||||
const stateModel = new MigrationStateModel(this._context, connectionId, api.sqlMigration);
|
||||
const sourceDatabaseName = migration.properties.sourceDatabaseName;
|
||||
let savedInfo: SavedInfo;
|
||||
savedInfo = {
|
||||
const savedInfo: SavedInfo = {
|
||||
closedPage: 0,
|
||||
|
||||
// DatabaseSelector
|
||||
@@ -142,7 +148,7 @@ export class RetryMigrationDialog {
|
||||
}
|
||||
});
|
||||
|
||||
let activeConnection = await azdata.connection.getCurrentConnection();
|
||||
const activeConnection = await azdata.connection.getCurrentConnection();
|
||||
let connectionId: string = '';
|
||||
let serverName: string = '';
|
||||
if (!activeConnection) {
|
||||
@@ -163,7 +169,7 @@ export class RetryMigrationDialog {
|
||||
const wizardController = new WizardController(
|
||||
this._context,
|
||||
stateModel,
|
||||
this._onClosedCallback);
|
||||
this._serviceContextChangedEvent);
|
||||
await wizardController.openWizard(stateModel.sourceConnectionId);
|
||||
} else {
|
||||
void vscode.window.showInformationMessage(constants.MIGRATION_CANNOT_RETRY);
|
||||
|
||||
@@ -12,6 +12,7 @@ import * as constants from '../../constants/strings';
|
||||
import * as utils from '../../api/utils';
|
||||
import { SqlMigrationService } from '../../api/azure';
|
||||
import { logError, TelemetryViews } from '../../telemtery';
|
||||
import { ServiceContextChangeEvent } from '../../dashboard/tabBase';
|
||||
|
||||
const CONTROL_MARGIN = '20px';
|
||||
const INPUT_COMPONENT_WIDTH = '100%';
|
||||
@@ -56,7 +57,7 @@ export class SelectMigrationServiceDialog {
|
||||
private _deleteButton!: azdata.window.Button;
|
||||
|
||||
constructor(
|
||||
private readonly _onClosedCallback: () => Promise<void>) {
|
||||
private readonly onServiceContextChanged: vscode.EventEmitter<ServiceContextChangeEvent>) {
|
||||
this._dialog = azdata.window.createModelViewDialog(
|
||||
constants.MIGRATION_SERVICE_SELECT_TITLE,
|
||||
'SelectMigraitonServiceDialog',
|
||||
@@ -85,10 +86,10 @@ export class SelectMigrationServiceDialog {
|
||||
'left');
|
||||
this._disposables.push(
|
||||
this._deleteButton.onClick(async (value) => {
|
||||
await MigrationLocalStorage.saveMigrationServiceContext({});
|
||||
await this._onClosedCallback();
|
||||
await MigrationLocalStorage.saveMigrationServiceContext({}, this.onServiceContextChanged);
|
||||
azdata.window.closeDialog(this._dialog);
|
||||
}));
|
||||
|
||||
this._dialog.customButtons = [this._deleteButton];
|
||||
|
||||
azdata.window.openDialog(this._dialog);
|
||||
@@ -262,7 +263,7 @@ export class SelectMigrationServiceDialog {
|
||||
? utils.deepClone(selectedLocation)
|
||||
: undefined!;
|
||||
await this._populateResourceGroupDropdown();
|
||||
await this._populateMigrationServiceDropdown();
|
||||
this._populateMigrationServiceDropdown();
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -290,7 +291,7 @@ export class SelectMigrationServiceDialog {
|
||||
this._serviceContext.resourceGroup = (selectedResourceGroup)
|
||||
? utils.deepClone(selectedResourceGroup)
|
||||
: undefined!;
|
||||
await this._populateMigrationServiceDropdown();
|
||||
this._populateMigrationServiceDropdown();
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -323,10 +324,10 @@ export class SelectMigrationServiceDialog {
|
||||
}));
|
||||
|
||||
this._disposables.push(
|
||||
this._dialog.okButton.onClick(async (value) => {
|
||||
await MigrationLocalStorage.saveMigrationServiceContext(this._serviceContext);
|
||||
await this._onClosedCallback();
|
||||
}));
|
||||
this._dialog.okButton.onClick(async (value) =>
|
||||
await MigrationLocalStorage.saveMigrationServiceContext(
|
||||
this._serviceContext,
|
||||
this.onServiceContextChanged)));
|
||||
|
||||
return this._view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
@@ -417,8 +418,14 @@ export class SelectMigrationServiceDialog {
|
||||
private async _populateLocationDropdown(): Promise<void> {
|
||||
try {
|
||||
this._azureLocationDropdown.loading = true;
|
||||
this._sqlMigrationServices = await utils.getAzureSqlMigrationServices(this._serviceContext.azureAccount, this._serviceContext.subscription);
|
||||
this._locations = await utils.getSqlMigrationServiceLocations(this._serviceContext.azureAccount, this._serviceContext.subscription, this._sqlMigrationServices);
|
||||
this._sqlMigrationServices = await utils.getAzureSqlMigrationServices(
|
||||
this._serviceContext.azureAccount,
|
||||
this._serviceContext.subscription);
|
||||
this._locations = await utils.getResourceLocations(
|
||||
this._serviceContext.azureAccount,
|
||||
this._serviceContext.subscription,
|
||||
this._sqlMigrationServices);
|
||||
|
||||
this._azureLocationDropdown.values = await utils.getAzureLocationsDropdownValues(this._locations);
|
||||
if (this._azureLocationDropdown.values.length > 0) {
|
||||
utils.selectDefaultDropdownValue(
|
||||
@@ -439,8 +446,13 @@ export class SelectMigrationServiceDialog {
|
||||
private async _populateResourceGroupDropdown(): Promise<void> {
|
||||
try {
|
||||
this._azureResourceGroupDropdown.loading = true;
|
||||
this._resourceGroups = await utils.getSqlMigrationServiceResourceGroups(this._sqlMigrationServices, this._serviceContext.location!);
|
||||
this._azureResourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this._resourceGroups);
|
||||
this._resourceGroups = utils.getServiceResourceGroupsByLocation(
|
||||
this._sqlMigrationServices,
|
||||
this._serviceContext.location!);
|
||||
this._azureResourceGroupDropdown.values = utils.getResourceDropdownValues(
|
||||
this._resourceGroups,
|
||||
constants.RESOURCE_GROUP_NOT_FOUND);
|
||||
|
||||
if (this._azureResourceGroupDropdown.values.length > 0) {
|
||||
utils.selectDefaultDropdownValue(
|
||||
this._azureResourceGroupDropdown,
|
||||
@@ -457,10 +469,15 @@ export class SelectMigrationServiceDialog {
|
||||
}
|
||||
}
|
||||
|
||||
private async _populateMigrationServiceDropdown(): Promise<void> {
|
||||
private _populateMigrationServiceDropdown(): void {
|
||||
try {
|
||||
this._azureServiceDropdown.loading = true;
|
||||
this._azureServiceDropdown.values = await utils.getAzureSqlMigrationServicesDropdownValues(this._sqlMigrationServices, this._serviceContext.location!, this._serviceContext.resourceGroup!);
|
||||
this._azureServiceDropdown.values = utils.getAzureResourceDropdownValues(
|
||||
this._sqlMigrationServices,
|
||||
this._serviceContext.location!,
|
||||
this._serviceContext.resourceGroup?.name,
|
||||
constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR);
|
||||
|
||||
if (this._azureServiceDropdown.values.length > 0) {
|
||||
utils.selectDefaultDropdownValue(
|
||||
this._azureServiceDropdown,
|
||||
|
||||
@@ -111,93 +111,86 @@ export class GetAzureRecommendationDialog {
|
||||
'margin': '0'
|
||||
},
|
||||
}).component();
|
||||
this._disposables.push(collectDataButton.onDidChangeCheckedState(async (e) => {
|
||||
if (e) {
|
||||
await this.switchDataSourceContainerFields(PerformanceDataSourceOptions.CollectData);
|
||||
}
|
||||
}));
|
||||
this._disposables.push(
|
||||
collectDataButton.onDidChangeCheckedState(async checked => {
|
||||
if (checked) {
|
||||
await this.switchDataSourceContainerFields(
|
||||
PerformanceDataSourceOptions.CollectData);
|
||||
}
|
||||
}));
|
||||
|
||||
const openExistingButton = _view.modelBuilder.radioButton()
|
||||
.withProps({
|
||||
name: buttonGroup,
|
||||
label: constants.AZURE_RECOMMENDATION_OPEN_EXISTING,
|
||||
checked: this._performanceDataSource === PerformanceDataSourceOptions.OpenExisting,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0 12px',
|
||||
}
|
||||
CSSStyles: { ...styles.BODY_CSS, 'margin': '0 12px' }
|
||||
}).component();
|
||||
this._disposables.push(openExistingButton.onDidChangeCheckedState(async (e) => {
|
||||
if (e) {
|
||||
await this.switchDataSourceContainerFields(PerformanceDataSourceOptions.OpenExisting);
|
||||
}
|
||||
}));
|
||||
this._disposables.push(
|
||||
openExistingButton.onDidChangeCheckedState(async checked => {
|
||||
if (checked) {
|
||||
await this.switchDataSourceContainerFields(
|
||||
PerformanceDataSourceOptions.OpenExisting);
|
||||
}
|
||||
}));
|
||||
|
||||
radioButtonContainer.addItems([
|
||||
collectDataButton,
|
||||
openExistingButton
|
||||
]);
|
||||
openExistingButton]);
|
||||
|
||||
this._collectDataContainer = this.createCollectDataContainer(_view);
|
||||
this._openExistingContainer = this.createOpenExistingContainer(_view);
|
||||
|
||||
const container = _view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).withItems([
|
||||
chooseMethodText,
|
||||
radioButtonContainer,
|
||||
this._openExistingContainer,
|
||||
this._collectDataContainer,
|
||||
]).component();
|
||||
const container = _view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.withItems([
|
||||
chooseMethodText,
|
||||
radioButtonContainer,
|
||||
this._openExistingContainer,
|
||||
this._collectDataContainer])
|
||||
.component();
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private createCollectDataContainer(_view: azdata.ModelView): azdata.FlexContainer {
|
||||
const container = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'column',
|
||||
'display': 'inline',
|
||||
}
|
||||
}).component();
|
||||
const container = _view.modelBuilder.flexContainer()
|
||||
.withProps(
|
||||
{ CSSStyles: { 'flex-direction': 'column', 'display': 'inline' } })
|
||||
.component();
|
||||
|
||||
const instructions = _view.modelBuilder.text().withProps({
|
||||
value: constants.AZURE_RECOMMENDATION_COLLECT_DATA_FOLDER,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin-bottom': '8px',
|
||||
}
|
||||
}).component();
|
||||
const instructions = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.AZURE_RECOMMENDATION_COLLECT_DATA_FOLDER,
|
||||
CSSStyles: { ...styles.LABEL_CSS, 'margin-bottom': '8px' }
|
||||
}).component();
|
||||
|
||||
const selectFolderContainer = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'row',
|
||||
'align-items': 'center',
|
||||
}
|
||||
}).component();
|
||||
const selectFolderContainer = _view.modelBuilder.flexContainer()
|
||||
.withProps(
|
||||
{ CSSStyles: { 'flex-direction': 'row', 'align-items': 'center' } })
|
||||
.component();
|
||||
|
||||
this._collectDataFolderInput = _view.modelBuilder.inputBox().withProps({
|
||||
placeHolder: constants.FOLDER_NAME,
|
||||
readOnly: true,
|
||||
width: 320,
|
||||
CSSStyles: {
|
||||
'margin-right': '12px'
|
||||
},
|
||||
}).component();
|
||||
this._disposables.push(this._collectDataFolderInput.onTextChanged(async (value) => {
|
||||
if (value) {
|
||||
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
|
||||
this.dialog!.okButton.enabled = true;
|
||||
}
|
||||
}));
|
||||
this._collectDataFolderInput = _view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
placeHolder: constants.FOLDER_NAME,
|
||||
readOnly: true,
|
||||
width: 320,
|
||||
CSSStyles: { 'margin-right': '12px' },
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
this._collectDataFolderInput.onTextChanged(async (value) => {
|
||||
if (value) {
|
||||
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
|
||||
this.dialog!.okButton.enabled = true;
|
||||
}
|
||||
}));
|
||||
|
||||
const browseButton = _view.modelBuilder.button().withProps({
|
||||
label: constants.BROWSE,
|
||||
width: 100,
|
||||
CSSStyles: {
|
||||
'margin': '0'
|
||||
}
|
||||
}).component();
|
||||
const browseButton = _view.modelBuilder.button()
|
||||
.withProps({
|
||||
label: constants.BROWSE,
|
||||
width: 100,
|
||||
CSSStyles: { 'margin': '0' }
|
||||
}).component();
|
||||
this._disposables.push(browseButton.onDidClick(async (e) => {
|
||||
let folder = await utils.promptUserForFolder();
|
||||
this._collectDataFolderInput.value = folder;
|
||||
@@ -205,74 +198,61 @@ export class GetAzureRecommendationDialog {
|
||||
|
||||
selectFolderContainer.addItems([
|
||||
this._collectDataFolderInput,
|
||||
browseButton,
|
||||
]);
|
||||
browseButton]);
|
||||
|
||||
container.addItems([
|
||||
instructions,
|
||||
selectFolderContainer,
|
||||
]);
|
||||
selectFolderContainer]);
|
||||
return container;
|
||||
}
|
||||
|
||||
private createOpenExistingContainer(_view: azdata.ModelView): azdata.FlexContainer {
|
||||
const container = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'column',
|
||||
'display': 'none',
|
||||
}
|
||||
}).component();
|
||||
const container = _view.modelBuilder.flexContainer()
|
||||
.withProps(
|
||||
{ CSSStyles: { 'flex-direction': 'column', 'display': 'none', } })
|
||||
.component();
|
||||
|
||||
const instructions = _view.modelBuilder.text().withProps({
|
||||
value: constants.AZURE_RECOMMENDATION_OPEN_EXISTING_FOLDER,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin-bottom': '8px',
|
||||
}
|
||||
}).component();
|
||||
const instructions = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.AZURE_RECOMMENDATION_OPEN_EXISTING_FOLDER,
|
||||
CSSStyles: { ...styles.LABEL_CSS, 'margin-bottom': '8px' }
|
||||
}).component();
|
||||
|
||||
const selectFolderContainer = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'row',
|
||||
'align-items': 'center',
|
||||
}
|
||||
}).component();
|
||||
const selectFolderContainer = _view.modelBuilder.flexContainer()
|
||||
.withProps(
|
||||
{ CSSStyles: { 'flex-direction': 'row', 'align-items': 'center' } })
|
||||
.component();
|
||||
|
||||
this._openExistingFolderInput = _view.modelBuilder.inputBox().withProps({
|
||||
placeHolder: constants.FOLDER_NAME,
|
||||
readOnly: true,
|
||||
width: 320,
|
||||
CSSStyles: {
|
||||
'margin-right': '12px'
|
||||
},
|
||||
CSSStyles: { 'margin-right': '12px' },
|
||||
}).component();
|
||||
this._disposables.push(this._openExistingFolderInput.onTextChanged(async (value) => {
|
||||
if (value) {
|
||||
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
|
||||
this.dialog!.okButton.enabled = true;
|
||||
}
|
||||
}));
|
||||
this._disposables.push(
|
||||
this._openExistingFolderInput.onTextChanged(async (value) => {
|
||||
if (value) {
|
||||
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
|
||||
this.dialog!.okButton.enabled = true;
|
||||
}
|
||||
}));
|
||||
|
||||
const openButton = _view.modelBuilder.button().withProps({
|
||||
label: constants.OPEN,
|
||||
width: 100,
|
||||
CSSStyles: {
|
||||
'margin': '0'
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(openButton.onDidClick(async (e) => {
|
||||
let folder = await utils.promptUserForFolder();
|
||||
this._openExistingFolderInput.value = folder;
|
||||
}));
|
||||
const openButton = _view.modelBuilder.button()
|
||||
.withProps({
|
||||
label: constants.OPEN,
|
||||
width: 100,
|
||||
CSSStyles: { 'margin': '0' }
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
openButton.onDidClick(
|
||||
async (e) => this._openExistingFolderInput.value = await utils.promptUserForFolder()));
|
||||
|
||||
selectFolderContainer.addItems([
|
||||
this._openExistingFolderInput,
|
||||
openButton,
|
||||
]);
|
||||
openButton]);
|
||||
container.addItems([
|
||||
instructions,
|
||||
selectFolderContainer,
|
||||
]);
|
||||
selectFolderContainer]);
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -281,24 +261,22 @@ export class GetAzureRecommendationDialog {
|
||||
|
||||
let okButtonEnabled = false;
|
||||
switch (containerType) {
|
||||
case PerformanceDataSourceOptions.CollectData: {
|
||||
await this._collectDataContainer.updateCssStyles({ 'display': 'inline' });
|
||||
await this._openExistingContainer.updateCssStyles({ 'display': 'none' });
|
||||
case PerformanceDataSourceOptions.CollectData:
|
||||
await utils.updateControlDisplay(this._collectDataContainer, true);
|
||||
await utils.updateControlDisplay(this._openExistingContainer, false);
|
||||
|
||||
if (this._collectDataFolderInput.value) {
|
||||
okButtonEnabled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PerformanceDataSourceOptions.OpenExisting: {
|
||||
await this._collectDataContainer.updateCssStyles({ 'display': 'none' });
|
||||
await this._openExistingContainer.updateCssStyles({ 'display': 'inline' });
|
||||
case PerformanceDataSourceOptions.OpenExisting:
|
||||
await utils.updateControlDisplay(this._collectDataContainer, false);
|
||||
await utils.updateControlDisplay(this._openExistingContainer, true);
|
||||
|
||||
if (this._openExistingFolderInput.value) {
|
||||
okButtonEnabled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.dialog!.okButton.enabled = okButtonEnabled;
|
||||
}
|
||||
@@ -306,27 +284,32 @@ export class GetAzureRecommendationDialog {
|
||||
public async openDialog(dialogName?: string) {
|
||||
if (!this._isOpen) {
|
||||
this._isOpen = true;
|
||||
this.dialog = azdata.window.createModelViewDialog(constants.GET_AZURE_RECOMMENDATION, 'GetAzureRecommendationsDialog', 'narrow');
|
||||
this.dialog = azdata.window.createModelViewDialog(
|
||||
constants.GET_AZURE_RECOMMENDATION,
|
||||
'GetAzureRecommendationsDialog',
|
||||
'narrow');
|
||||
|
||||
this.dialog.okButton.label = GetAzureRecommendationDialog.StartButtonText;
|
||||
this._disposables.push(this.dialog.okButton.onClick(async () => await this.execute()));
|
||||
this._disposables.push(this.dialog.cancelButton.onClick(() => this._isOpen = false));
|
||||
this._disposables.push(
|
||||
this.dialog.okButton.onClick(
|
||||
async () => await this.execute()));
|
||||
|
||||
const dialogSetupPromises: Thenable<void>[] = [];
|
||||
dialogSetupPromises.push(this.initializeDialog(this.dialog));
|
||||
this._disposables.push(
|
||||
this.dialog.cancelButton.onClick(
|
||||
() => this._isOpen = false));
|
||||
|
||||
const promise = this.initializeDialog(this.dialog);
|
||||
azdata.window.openDialog(this.dialog);
|
||||
await Promise.all(dialogSetupPromises);
|
||||
await promise;
|
||||
|
||||
// if data source was previously selected, default folder value to previously selected
|
||||
switch (this.migrationStateModel._skuRecommendationPerformanceDataSource) {
|
||||
case PerformanceDataSourceOptions.CollectData: {
|
||||
case PerformanceDataSourceOptions.CollectData:
|
||||
this._collectDataFolderInput.value = this.migrationStateModel._skuRecommendationPerformanceLocation;
|
||||
break;
|
||||
}
|
||||
case PerformanceDataSourceOptions.OpenExisting: {
|
||||
case PerformanceDataSourceOptions.OpenExisting:
|
||||
this._openExistingFolderInput.value = this.migrationStateModel._skuRecommendationPerformanceLocation;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await this.switchDataSourceContainerFields(this._performanceDataSource);
|
||||
@@ -338,16 +321,14 @@ export class GetAzureRecommendationDialog {
|
||||
|
||||
this.migrationStateModel._skuRecommendationPerformanceDataSource = this._performanceDataSource;
|
||||
switch (this.migrationStateModel._skuRecommendationPerformanceDataSource) {
|
||||
case PerformanceDataSourceOptions.CollectData: {
|
||||
case PerformanceDataSourceOptions.CollectData:
|
||||
await this.migrationStateModel.startPerfDataCollection(
|
||||
this.migrationStateModel._skuRecommendationPerformanceLocation,
|
||||
this.migrationStateModel._performanceDataQueryIntervalInSeconds,
|
||||
this.migrationStateModel._staticDataQueryIntervalInSeconds,
|
||||
this.migrationStateModel._numberOfPerformanceDataQueryIterations,
|
||||
this.skuRecommendationPage
|
||||
);
|
||||
this.skuRecommendationPage);
|
||||
break;
|
||||
}
|
||||
case PerformanceDataSourceOptions.OpenExisting: {
|
||||
const serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName;
|
||||
const errors: string[] = [];
|
||||
|
||||
@@ -25,7 +25,10 @@ export class SkuEditParametersDialog {
|
||||
private _targetPercentileDropdown!: azdata.DropDownComponent;
|
||||
private _enablePreviewValue!: boolean;
|
||||
|
||||
constructor(public skuRecommendationPage: SKURecommendationPage, public migrationStateModel: MigrationStateModel) {
|
||||
constructor(
|
||||
public skuRecommendationPage: SKURecommendationPage,
|
||||
public migrationStateModel: MigrationStateModel) {
|
||||
|
||||
this._enablePreviewValue = true;
|
||||
}
|
||||
|
||||
@@ -35,10 +38,10 @@ export class SkuEditParametersDialog {
|
||||
try {
|
||||
const flex = this.createContainer(view);
|
||||
|
||||
this._disposables.push(view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
this._disposables.push(
|
||||
view.onClosed(e =>
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } })));
|
||||
|
||||
await view.initializeModel(flex);
|
||||
resolve();
|
||||
@@ -50,56 +53,50 @@ export class SkuEditParametersDialog {
|
||||
}
|
||||
|
||||
private createContainer(_view: azdata.ModelView): azdata.FlexContainer {
|
||||
const container = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'margin': '8px 16px',
|
||||
'flex-direction': 'column',
|
||||
}
|
||||
}).component();
|
||||
const container = _view.modelBuilder.flexContainer()
|
||||
.withProps(
|
||||
{ CSSStyles: { 'margin': '8px 16px', 'flex-direction': 'column' } })
|
||||
.component();
|
||||
|
||||
const description = _view.modelBuilder.text().withProps({
|
||||
value: constants.EDIT_PARAMETERS_TEXT,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
const description = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.EDIT_PARAMETERS_TEXT,
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
})
|
||||
.component();
|
||||
|
||||
const WIZARD_INPUT_COMPONENT_WIDTH = '300px';
|
||||
const scaleFactorLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.SCALE_FACTOR,
|
||||
description: constants.SCALE_FACTOR_TOOLTIP,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS
|
||||
}
|
||||
}).component();
|
||||
this._scaleFactorInput = _view.modelBuilder.inputBox().withProps({
|
||||
required: true,
|
||||
validationErrorMessage: constants.INVALID_SCALE_FACTOR,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
CSSStyles: {
|
||||
'margin-top': '-1em',
|
||||
'margin-bottom': '8px',
|
||||
},
|
||||
}).withValidation(c => {
|
||||
if (Number(c.value) && Number(c.value) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).component();
|
||||
const scaleFactorLabel = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.SCALE_FACTOR,
|
||||
description: constants.SCALE_FACTOR_TOOLTIP,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: { ...styles.LABEL_CSS }
|
||||
}).component();
|
||||
this._scaleFactorInput = _view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
required: true,
|
||||
validationErrorMessage: constants.INVALID_SCALE_FACTOR,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
CSSStyles: { 'margin-top': '-1em', 'margin-bottom': '8px' },
|
||||
}).withValidation(c => {
|
||||
if (Number(c.value) && Number(c.value) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).component();
|
||||
|
||||
const targetPercentileLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.PERCENTAGE_UTILIZATION,
|
||||
description: constants.PERCENTAGE_UTILIZATION_TOOLTIP,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
}
|
||||
}).component();
|
||||
const targetPercentileLabel = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.PERCENTAGE_UTILIZATION,
|
||||
description: constants.PERCENTAGE_UTILIZATION_TOOLTIP,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: { ...styles.LABEL_CSS }
|
||||
}).component();
|
||||
const createPercentageValues = () => {
|
||||
let values: azdata.CategoryValue[] = [];
|
||||
const values: azdata.CategoryValue[] = [];
|
||||
TARGET_PERCENTILE_VALUES.forEach(n => {
|
||||
const val = n.toString();
|
||||
values.push({
|
||||
@@ -109,27 +106,27 @@ export class SkuEditParametersDialog {
|
||||
});
|
||||
return values;
|
||||
};
|
||||
this._targetPercentileDropdown = _view.modelBuilder.dropDown().withProps({
|
||||
values: createPercentageValues(),
|
||||
ariaLabel: constants.PERCENTAGE_UTILIZATION,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: false,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
CSSStyles: {
|
||||
'margin-top': '-1em',
|
||||
'margin-bottom': '8px',
|
||||
},
|
||||
}).component();
|
||||
this._targetPercentileDropdown = _view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
values: createPercentageValues(),
|
||||
ariaLabel: constants.PERCENTAGE_UTILIZATION,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: false,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
CSSStyles: {
|
||||
'margin-top': '-1em',
|
||||
'margin-bottom': '8px',
|
||||
},
|
||||
}).component();
|
||||
|
||||
const enablePreviewLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.ENABLE_PREVIEW_SKU,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
}
|
||||
}).component();
|
||||
const enablePreviewLabel = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.ENABLE_PREVIEW_SKU,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: { ...styles.LABEL_CSS, }
|
||||
}).component();
|
||||
const buttonGroup = 'enablePreviewSKUs';
|
||||
const enablePreviewRadioButtonContainer = _view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
@@ -151,11 +148,12 @@ export class SkuEditParametersDialog {
|
||||
'margin': '0'
|
||||
},
|
||||
}).component();
|
||||
this._disposables.push(enablePreviewButton.onDidChangeCheckedState(async (e) => {
|
||||
if (e) {
|
||||
this._enablePreviewValue = true;
|
||||
}
|
||||
}));
|
||||
this._disposables.push(
|
||||
enablePreviewButton.onDidChangeCheckedState(async checked => {
|
||||
if (checked) {
|
||||
this._enablePreviewValue = true;
|
||||
}
|
||||
}));
|
||||
const disablePreviewButton = _view.modelBuilder.radioButton()
|
||||
.withProps({
|
||||
name: buttonGroup,
|
||||
@@ -167,23 +165,21 @@ export class SkuEditParametersDialog {
|
||||
'margin': '0 12px',
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(disablePreviewButton.onDidChangeCheckedState(async (e) => {
|
||||
if (e) {
|
||||
this._enablePreviewValue = false;
|
||||
}
|
||||
}));
|
||||
this._disposables.push(
|
||||
disablePreviewButton.onDidChangeCheckedState(checked => {
|
||||
if (checked) {
|
||||
this._enablePreviewValue = false;
|
||||
}
|
||||
}));
|
||||
enablePreviewRadioButtonContainer.addItems([
|
||||
enablePreviewButton,
|
||||
disablePreviewButton
|
||||
]);
|
||||
disablePreviewButton]);
|
||||
|
||||
const enablePreviewInfoBox = _view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
text: constants.ENABLE_PREVIEW_SKU_INFO,
|
||||
style: 'information',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
CSSStyles: { ...styles.BODY_CSS, }
|
||||
}).component();
|
||||
|
||||
container.addItems([
|
||||
@@ -202,12 +198,19 @@ export class SkuEditParametersDialog {
|
||||
public async openDialog(dialogName?: string) {
|
||||
if (!this._isOpen) {
|
||||
this._isOpen = true;
|
||||
this.dialog = azdata.window.createModelViewDialog(constants.EDIT_RECOMMENDATION_PARAMETERS, 'SkuEditParametersDialog', 'narrow');
|
||||
this.dialog = azdata.window.createModelViewDialog(
|
||||
constants.EDIT_RECOMMENDATION_PARAMETERS,
|
||||
'SkuEditParametersDialog',
|
||||
'narrow');
|
||||
|
||||
this.dialog.okButton.label = SkuEditParametersDialog.UpdateButtonText;
|
||||
this._disposables.push(this.dialog.okButton.onClick(async () => await this.execute()));
|
||||
this._disposables.push(
|
||||
this.dialog.okButton.onClick(
|
||||
async () => await this.execute()));
|
||||
|
||||
this._disposables.push(this.dialog.cancelButton.onClick(() => this._isOpen = false));
|
||||
this._disposables.push(
|
||||
this.dialog.cancelButton.onClick(
|
||||
() => this._isOpen = false));
|
||||
|
||||
const dialogSetupPromises: Thenable<void>[] = [];
|
||||
dialogSetupPromises.push(this.initializeDialog(this.dialog));
|
||||
|
||||
@@ -34,15 +34,13 @@ export class SkuRecommendationResultsDialog {
|
||||
constructor(public model: MigrationStateModel, public _targetType: MigrationTargetType) {
|
||||
switch (this._targetType) {
|
||||
case MigrationTargetType.SQLMI:
|
||||
this.targetName = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
|
||||
this.targetName = constants.SKU_RECOMMENDATION_MI_CARD_TEXT;
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLVM:
|
||||
this.targetName = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
|
||||
this.targetName = constants.SKU_RECOMMENDATION_VM_CARD_TEXT;
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLDB:
|
||||
this.targetName = constants.AZURE_SQL_DATABASE;
|
||||
this.targetName = constants.SKU_RECOMMENDATION_SQLDB_CARD_TEXT;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -79,7 +77,9 @@ export class SkuRecommendationResultsDialog {
|
||||
|
||||
this.targetRecommendations?.forEach((recommendation, index) => {
|
||||
if (index > 0) {
|
||||
const separator = _view.modelBuilder.separator().withProps({ width: 750 }).component();
|
||||
const separator = _view.modelBuilder.separator()
|
||||
.withProps({ width: 750 })
|
||||
.component();
|
||||
container.addItem(separator);
|
||||
}
|
||||
|
||||
@@ -101,7 +101,9 @@ export class SkuRecommendationResultsDialog {
|
||||
recommendation = <mssql.IaaSSkuRecommendationResultItem>recommendationItem;
|
||||
|
||||
if (recommendation.targetSku) {
|
||||
configuration = constants.VM_CONFIGURATION(recommendation.targetSku.virtualMachineSize!.azureSkuName, recommendation.targetSku.virtualMachineSize!.vCPUsAvailable);
|
||||
configuration = constants.VM_CONFIGURATION(
|
||||
recommendation.targetSku.virtualMachineSize!.azureSkuName,
|
||||
recommendation.targetSku.virtualMachineSize!.vCPUsAvailable);
|
||||
|
||||
storageSection = this.createSqlVmTargetStorageSection(_view, recommendation);
|
||||
}
|
||||
@@ -123,84 +125,73 @@ export class SkuRecommendationResultsDialog {
|
||||
: constants.PREMIUM_SERIES_MEMORY_OPTIMIZED;
|
||||
|
||||
configuration = this._targetType === MigrationTargetType.SQLDB
|
||||
? constants.DB_CONFIGURATION(serviceTier, recommendation.targetSku.computeSize!)
|
||||
? constants.SQLDB_CONFIGURATION(serviceTier, recommendation.targetSku.computeSize!)
|
||||
: constants.MI_CONFIGURATION(hardwareType, serviceTier, recommendation.targetSku.computeSize!);
|
||||
|
||||
const storageLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.STORAGE_HEADER,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin': '12px 0 0',
|
||||
}
|
||||
}).component();
|
||||
const storageValue = _view.modelBuilder.text().withProps({
|
||||
value: constants.STORAGE_GB(recommendation.targetSku.storageMaxSizeInMb! / 1024),
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
const storageLabel = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.STORAGE_HEADER,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin': '12px 0 0',
|
||||
}
|
||||
}).component();
|
||||
const storageValue = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.STORAGE_GB(recommendation.targetSku.storageMaxSizeInMb! / 1024),
|
||||
CSSStyles: { ...styles.BODY_CSS, }
|
||||
}).component();
|
||||
|
||||
storageSection.addItems([
|
||||
storageLabel,
|
||||
storageValue,
|
||||
]);
|
||||
storageValue]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
const recommendationContainer = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'margin-bottom': '20px',
|
||||
'flex-direction': 'column',
|
||||
}
|
||||
}).component();
|
||||
|
||||
if (this._targetType === MigrationTargetType.SQLDB) {
|
||||
const databaseNameLabel = _view.modelBuilder.text().withProps({
|
||||
value: recommendation.databaseName!,
|
||||
const recommendationContainer = _view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'margin-bottom': '20px',
|
||||
'flex-direction': 'column',
|
||||
}
|
||||
}).component();
|
||||
|
||||
if (this._targetType === MigrationTargetType.SQLDB) {
|
||||
const databaseNameLabel = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: recommendation.databaseName!,
|
||||
CSSStyles: { ...styles.SECTION_HEADER_CSS, }
|
||||
}).component();
|
||||
recommendationContainer.addItem(databaseNameLabel);
|
||||
}
|
||||
|
||||
const targetDeploymentTypeLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.TARGET_DEPLOYMENT_TYPE,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin': '0',
|
||||
}
|
||||
}).component();
|
||||
const targetDeploymentTypeValue = _view.modelBuilder.text().withProps({
|
||||
value: this.targetName,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0',
|
||||
}
|
||||
}).component();
|
||||
const targetDeploymentTypeLabel = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.TARGET_DEPLOYMENT_TYPE,
|
||||
CSSStyles: { ...styles.LABEL_CSS, 'margin': '0', }
|
||||
}).component();
|
||||
const targetDeploymentTypeValue = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: this.targetName,
|
||||
CSSStyles: { ...styles.BODY_CSS, 'margin': '0', }
|
||||
}).component();
|
||||
|
||||
const azureConfigurationLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.AZURE_CONFIGURATION,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin': '12px 0 0',
|
||||
}
|
||||
}).component();
|
||||
const azureConfigurationValue = _view.modelBuilder.text().withProps({
|
||||
value: configuration,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0',
|
||||
}
|
||||
}).component();
|
||||
const azureConfigurationLabel = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.AZURE_CONFIGURATION,
|
||||
CSSStyles: { ...styles.LABEL_CSS, 'margin': '12px 0 0', }
|
||||
}).component();
|
||||
const azureConfigurationValue = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: configuration,
|
||||
CSSStyles: { ...styles.BODY_CSS, 'margin': '0', }
|
||||
}).component();
|
||||
|
||||
recommendationContainer.addItems([
|
||||
targetDeploymentTypeLabel,
|
||||
targetDeploymentTypeValue,
|
||||
|
||||
targetDeploymentTypeLabel,
|
||||
targetDeploymentTypeValue,
|
||||
|
||||
azureConfigurationLabel,
|
||||
azureConfigurationValue,
|
||||
|
||||
@@ -209,23 +200,21 @@ export class SkuRecommendationResultsDialog {
|
||||
|
||||
const recommendationsReasonSection = _view.modelBuilder.text().withProps({
|
||||
value: constants.RECOMMENDATION_REASON,
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'margin': '12px 0 0'
|
||||
}
|
||||
CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin': '12px 0 0' }
|
||||
}).component();
|
||||
|
||||
const reasonsContainer = _view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
const justifications: string[] = recommendation?.positiveJustifications?.concat(recommendation?.negativeJustifications) || [constants.SKU_RECOMMENDATION_NO_RECOMMENDATION_REASON];
|
||||
const reasonsContainer = _view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
|
||||
const justifications: string[] = recommendation?.positiveJustifications?.concat(recommendation?.negativeJustifications)
|
||||
|| [constants.SKU_RECOMMENDATION_NO_RECOMMENDATION_REASON];
|
||||
|
||||
justifications?.forEach(text => {
|
||||
reasonsContainer.addItem(
|
||||
_view.modelBuilder.text().withProps({
|
||||
value: text,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
CSSStyles: { ...styles.BODY_CSS, }
|
||||
}).component()
|
||||
);
|
||||
});
|
||||
@@ -235,26 +224,23 @@ export class SkuRecommendationResultsDialog {
|
||||
recommendationContainer.addItems([
|
||||
recommendationsReasonSection,
|
||||
reasonsContainer,
|
||||
storagePropertiesContainer,
|
||||
]);
|
||||
storagePropertiesContainer]);
|
||||
|
||||
return recommendationContainer;
|
||||
}
|
||||
|
||||
private createSqlVmTargetStorageSection(_view: azdata.ModelView, recommendation: mssql.IaaSSkuRecommendationResultItem): azdata.FlexContainer {
|
||||
const recommendedTargetStorageSection = _view.modelBuilder.text().withProps({
|
||||
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'margin-top': '12px'
|
||||
}
|
||||
}).component();
|
||||
const recommendedTargetStorageInfo = _view.modelBuilder.text().withProps({
|
||||
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION_INFO,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
const recommendedTargetStorageSection = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
|
||||
CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '12px' }
|
||||
}).component();
|
||||
|
||||
const recommendedTargetStorageInfo = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION_INFO,
|
||||
CSSStyles: { ...styles.BODY_CSS, }
|
||||
}).component();
|
||||
|
||||
const headerCssStyle = {
|
||||
'border': 'none',
|
||||
@@ -333,20 +319,21 @@ export class SkuRecommendationResultsDialog {
|
||||
logDiskTableRow,
|
||||
];
|
||||
|
||||
const storageConfigurationTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable().withProps({
|
||||
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
|
||||
columns: columns,
|
||||
dataValues: storageConfigurationTableRows,
|
||||
width: 700
|
||||
}).component();
|
||||
const storageConfigurationTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable()
|
||||
.withProps({
|
||||
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
|
||||
columns: columns,
|
||||
dataValues: storageConfigurationTableRows,
|
||||
width: 700
|
||||
}).component();
|
||||
|
||||
const container = _view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).withItems([
|
||||
recommendedTargetStorageSection,
|
||||
recommendedTargetStorageInfo,
|
||||
storageConfigurationTable,
|
||||
]).component();
|
||||
const container = _view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.withItems([
|
||||
recommendedTargetStorageSection,
|
||||
recommendedTargetStorageInfo,
|
||||
storageConfigurationTable])
|
||||
.component();
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -375,19 +362,16 @@ export class SkuRecommendationResultsDialog {
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLDB:
|
||||
instanceRequirements = this.instanceRequirements?.databaseLevelRequirements.filter(d => {
|
||||
return databaseName === d.databaseName;
|
||||
})[0]!;
|
||||
instanceRequirements = this.instanceRequirements?.databaseLevelRequirements
|
||||
.filter((d) => databaseName === d.databaseName)[0]!;
|
||||
break;
|
||||
}
|
||||
|
||||
const storagePropertiesSection = _view.modelBuilder.text().withProps({
|
||||
value: constants.SOURCE_PROPERTIES,
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'margin-top': '12px'
|
||||
}
|
||||
}).component();
|
||||
const storagePropertiesSection = _view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.SOURCE_PROPERTIES,
|
||||
CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '12px' }
|
||||
}).component();
|
||||
|
||||
const headerCssStyle = {
|
||||
'border': 'none',
|
||||
@@ -407,7 +391,7 @@ export class SkuRecommendationResultsDialog {
|
||||
};
|
||||
|
||||
const columnWidth = 80;
|
||||
let columns: azdata.DeclarativeTableColumn[] = [
|
||||
const columns: azdata.DeclarativeTableColumn[] = [
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.DIMENSION,
|
||||
@@ -450,19 +434,18 @@ export class SkuRecommendationResultsDialog {
|
||||
ioLatencyRow,
|
||||
];
|
||||
|
||||
const storagePropertiesTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable().withProps({
|
||||
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
|
||||
columns: columns,
|
||||
dataValues: storagePropertiesTableRows,
|
||||
width: 300
|
||||
}).component();
|
||||
const storagePropertiesTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable()
|
||||
.withProps({
|
||||
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
|
||||
columns: columns,
|
||||
dataValues: storagePropertiesTableRows,
|
||||
width: 300
|
||||
}).component();
|
||||
|
||||
const container = _view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).withItems([
|
||||
storagePropertiesSection,
|
||||
storagePropertiesTable,
|
||||
]).component();
|
||||
const container = _view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.withItems([storagePropertiesSection, storagePropertiesTable])
|
||||
.component();
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -537,10 +520,9 @@ export class SkuRecommendationResultsDialog {
|
||||
}));
|
||||
this.dialog.customButtons = [this._saveButton];
|
||||
|
||||
const dialogSetupPromises: Thenable<void>[] = [];
|
||||
dialogSetupPromises.push(this.initializeDialog(this.dialog));
|
||||
const promise = this.initializeDialog(this.dialog);
|
||||
azdata.window.openDialog(this.dialog);
|
||||
await Promise.all(dialogSetupPromises);
|
||||
await promise;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import * as constants from '../../constants/strings';
|
||||
import { AzureSqlDatabaseServer } from '../../api/azure';
|
||||
import { collectSourceDatabaseTableInfo, collectTargetDatabaseTableInfo, TableInfo } from '../../api/sqlUtils';
|
||||
import { MigrationStateModel } from '../../models/stateMachine';
|
||||
|
||||
const DialogName = 'TableMigrationSelection';
|
||||
|
||||
export class TableMigrationSelectionDialog {
|
||||
private _dialog: azdata.window.Dialog | undefined;
|
||||
private _headingText!: azdata.TextComponent;
|
||||
private _filterInputBox!: azdata.InputBoxComponent;
|
||||
private _tableSelectionTable!: azdata.TableComponent;
|
||||
private _tableLoader!: azdata.LoadingComponent;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
private _isOpen: boolean = false;
|
||||
private _model: MigrationStateModel;
|
||||
private _sourceDatabaseName: string;
|
||||
private _tableSelectionMap!: Map<string, TableInfo>;
|
||||
private _targetTableMap!: Map<string, TableInfo>;
|
||||
private _onSaveCallback: () => Promise<void>;
|
||||
|
||||
constructor(
|
||||
model: MigrationStateModel,
|
||||
sourceDatabaseName: string,
|
||||
onSaveCallback: () => Promise<void>
|
||||
) {
|
||||
this._model = model;
|
||||
this._sourceDatabaseName = sourceDatabaseName;
|
||||
this._onSaveCallback = onSaveCallback;
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
try {
|
||||
this._tableLoader.loading = true;
|
||||
const targetDatabaseInfo = this._model._sourceTargetMapping.get(this._sourceDatabaseName);
|
||||
if (targetDatabaseInfo) {
|
||||
const sourceTableList: TableInfo[] = await collectSourceDatabaseTableInfo(
|
||||
this._model.sourceConnectionId,
|
||||
this._sourceDatabaseName);
|
||||
|
||||
this._tableSelectionMap = new Map();
|
||||
sourceTableList.forEach(table => {
|
||||
const sourceTable = targetDatabaseInfo.sourceTables.get(table.tableName);
|
||||
const isSelected = sourceTable?.selectedForMigration === true;
|
||||
const tableInfo: TableInfo = {
|
||||
databaseName: table.databaseName,
|
||||
rowCount: table.rowCount,
|
||||
selectedForMigration: isSelected,
|
||||
tableName: table.tableName,
|
||||
};
|
||||
this._tableSelectionMap.set(table.tableName, tableInfo);
|
||||
});
|
||||
|
||||
const targetTableList: TableInfo[] = await collectTargetDatabaseTableInfo(
|
||||
this._model._targetServerInstance as AzureSqlDatabaseServer,
|
||||
targetDatabaseInfo.databaseName,
|
||||
this._model._azureTenant.id,
|
||||
this._model._targetUserName,
|
||||
this._model._targetPassword);
|
||||
|
||||
this._targetTableMap = new Map();
|
||||
targetTableList.forEach(table =>
|
||||
this._targetTableMap.set(
|
||||
table.tableName, {
|
||||
databaseName: table.databaseName,
|
||||
rowCount: table.rowCount,
|
||||
selectedForMigration: false,
|
||||
tableName: table.tableName,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
this._dialog!.message = {
|
||||
text: constants.DATABASE_TABLE_CONNECTION_ERROR,
|
||||
description: constants.DATABASE_TABLE_CONNECTION_ERROR_MESSAGE(error.message),
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
} finally {
|
||||
this._tableLoader.loading = false;
|
||||
await this._loadControls();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadControls(): Promise<void> {
|
||||
const data: any[][] = [];
|
||||
const filterText = this._filterInputBox.value ?? '';
|
||||
const selectedItems: number[] = [];
|
||||
let tableRow = 0;
|
||||
this._tableSelectionMap.forEach(sourceTable => {
|
||||
if (filterText?.length === 0 || sourceTable.tableName.indexOf(filterText) > -1) {
|
||||
let tableStatus = constants.TARGET_TABLE_MISSING;
|
||||
const targetTable = this._targetTableMap.get(sourceTable.tableName);
|
||||
if (targetTable) {
|
||||
const targetTableRowCount = targetTable?.rowCount ?? 0;
|
||||
tableStatus = targetTableRowCount > 0
|
||||
? constants.TARGET_TABLE_NOT_EMPTY
|
||||
: '--';
|
||||
}
|
||||
|
||||
data.push([
|
||||
sourceTable.selectedForMigration,
|
||||
sourceTable.tableName,
|
||||
tableStatus]);
|
||||
if (sourceTable.selectedForMigration && targetTable) {
|
||||
selectedItems.push(tableRow);
|
||||
}
|
||||
tableRow++;
|
||||
}
|
||||
});
|
||||
await this._tableSelectionTable.updateProperty('data', data);
|
||||
this._tableSelectionTable.selectedRows = selectedItems;
|
||||
this._updateRowSelection();
|
||||
}
|
||||
|
||||
private async _initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
|
||||
dialog.registerContent(async (view) => {
|
||||
this._filterInputBox = view.modelBuilder.inputBox()
|
||||
.withProps({
|
||||
inputType: 'search',
|
||||
placeHolder: constants.TABLE_SELECTION_FILTER,
|
||||
width: 268,
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
this._filterInputBox.onTextChanged(
|
||||
async e => await this._loadControls()));
|
||||
|
||||
this._headingText = view.modelBuilder.text()
|
||||
.withProps({ value: constants.DATABASE_LOADING_TABLES })
|
||||
.component();
|
||||
|
||||
this._tableSelectionTable = await this._createSelectionTable(view);
|
||||
this._tableLoader = view.modelBuilder.loadingComponent()
|
||||
.withItem(this._tableSelectionTable)
|
||||
.withProps({
|
||||
loading: false,
|
||||
loadingText: constants.DATABASE_TABLE_DATA_LOADING
|
||||
}).component();
|
||||
|
||||
const flex = view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
this._filterInputBox,
|
||||
this._headingText,
|
||||
this._tableLoader],
|
||||
{ flex: '0 0 auto' })
|
||||
.withProps({ CSSStyles: { 'margin': '0 0 0 15px' } })
|
||||
.withLayout({
|
||||
flexFlow: 'column',
|
||||
height: '100%',
|
||||
width: 565,
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
view.onClosed(e =>
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } })));
|
||||
|
||||
await view.initializeModel(flex);
|
||||
await this._loadData();
|
||||
});
|
||||
}
|
||||
|
||||
public async openDialog(dialogTitle: string) {
|
||||
if (!this._isOpen) {
|
||||
this._isOpen = true;
|
||||
this._dialog = azdata.window.createModelViewDialog(
|
||||
dialogTitle,
|
||||
DialogName,
|
||||
600);
|
||||
|
||||
this._dialog.okButton.label = constants.TABLE_SELECTION_UPDATE_BUTTON;
|
||||
this._disposables.push(
|
||||
this._dialog.okButton.onClick(
|
||||
async () => this._save()));
|
||||
|
||||
this._dialog.cancelButton.label = constants.TABLE_SELECTION_CANCEL_BUTTON;
|
||||
this._disposables.push(
|
||||
this._dialog.cancelButton.onClick(
|
||||
async () => this._isOpen = false));
|
||||
|
||||
const promise = this._initializeDialog(this._dialog);
|
||||
azdata.window.openDialog(this._dialog);
|
||||
await promise;
|
||||
}
|
||||
}
|
||||
|
||||
private async _createSelectionTable(view: azdata.ModelView): Promise<azdata.TableComponent> {
|
||||
const cssClass = 'no-borders';
|
||||
const table = view.modelBuilder.table()
|
||||
.withProps({
|
||||
data: [],
|
||||
width: 565,
|
||||
height: '600px',
|
||||
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
|
||||
columns: [
|
||||
<azdata.CheckboxColumn>{
|
||||
value: '',
|
||||
width: 10,
|
||||
type: azdata.ColumnType.checkBox,
|
||||
action: azdata.ActionOnCellCheckboxCheck.selectRow,
|
||||
resizable: false,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
},
|
||||
{
|
||||
name: constants.TABLE_SELECTION_TABLENAME_COLUMN,
|
||||
value: 'tableName',
|
||||
type: azdata.ColumnType.text,
|
||||
width: 300,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
},
|
||||
{
|
||||
name: constants.TABLE_SELECTION_HASROWS_COLUMN,
|
||||
value: 'hasRows',
|
||||
type: azdata.ColumnType.text,
|
||||
width: 255,
|
||||
cssClass: cssClass,
|
||||
headerCssClass: cssClass,
|
||||
}]
|
||||
})
|
||||
.withValidation(() => true)
|
||||
.component();
|
||||
|
||||
let updating: boolean = false;
|
||||
this._disposables.push(
|
||||
table.onRowSelected(e => {
|
||||
if (updating) {
|
||||
return;
|
||||
}
|
||||
updating = true;
|
||||
|
||||
// collect table list selected for migration
|
||||
const selectedRows = this._tableSelectionTable.selectedRows ?? [];
|
||||
const keepSelectedRows: number[] = [];
|
||||
// determine if selected rows have a matching target and can be selected
|
||||
selectedRows.forEach(rowIndex => {
|
||||
// get selected source table name
|
||||
const sourceTableName = this._tableSelectionTable.data[rowIndex][1] as string;
|
||||
// get source table info
|
||||
const sourceTableInfo = this._tableSelectionMap.get(sourceTableName);
|
||||
if (sourceTableInfo) {
|
||||
// see if source table exists on target database
|
||||
const targetTableInfo = this._targetTableMap.get(sourceTableName);
|
||||
// keep source table selected
|
||||
sourceTableInfo.selectedForMigration = targetTableInfo !== undefined;
|
||||
// update table selection map with new selectedForMigration value
|
||||
this._tableSelectionMap.set(sourceTableName, sourceTableInfo);
|
||||
// keep row selected
|
||||
if (sourceTableInfo.selectedForMigration) {
|
||||
keepSelectedRows.push(rowIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// if the selected rows are different, update the selectedRows property
|
||||
if (!this._areEqual(this._tableSelectionTable.selectedRows ?? [], keepSelectedRows)) {
|
||||
this._tableSelectionTable.selectedRows = keepSelectedRows;
|
||||
}
|
||||
|
||||
this._updateRowSelection();
|
||||
updating = false;
|
||||
}));
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
private _areEqual(source: number[], target: number[]): boolean {
|
||||
if (source.length === target.length) {
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
if (source[i] !== target[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _updateRowSelection(): void {
|
||||
this._headingText.value = this._tableSelectionTable.data.length > 0
|
||||
? constants.TABLE_SELECTED_COUNT(
|
||||
this._tableSelectionTable.selectedRows?.length ?? 0,
|
||||
this._tableSelectionTable.data.length)
|
||||
: this._tableLoader.loading
|
||||
? constants.DATABASE_LOADING_TABLES
|
||||
: constants.DATABASE_MISSING_TABLES;
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
const targetDatabaseInfo = this._model._sourceTargetMapping.get(this._sourceDatabaseName);
|
||||
if (targetDatabaseInfo) {
|
||||
// collect table list selected for migration
|
||||
const selectedRows = this._tableSelectionTable.selectedRows ?? [];
|
||||
const selectedTables = new Map<String, TableInfo>();
|
||||
selectedRows.forEach(rowIndex => {
|
||||
const tableName = this._tableSelectionTable.data[rowIndex][1] as string;
|
||||
const tableInfo = this._tableSelectionMap.get(tableName);
|
||||
if (tableInfo) {
|
||||
selectedTables.set(tableName, tableInfo);
|
||||
}
|
||||
});
|
||||
|
||||
// copy table map selection status from grid
|
||||
this._tableSelectionMap.forEach(tableInfo => {
|
||||
const selectedTableInfo = selectedTables.get(tableInfo.tableName);
|
||||
tableInfo.selectedForMigration = selectedTableInfo?.selectedForMigration === true;
|
||||
this._tableSelectionMap.set(tableInfo.tableName, tableInfo);
|
||||
});
|
||||
|
||||
// save table selection changes to migration source target map
|
||||
targetDatabaseInfo.sourceTables = this._tableSelectionMap;
|
||||
this._model._sourceTargetMapping.set(this._sourceDatabaseName, targetDatabaseInfo);
|
||||
}
|
||||
await this._onSaveCallback();
|
||||
this._isOpen = false;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { MigrationMode, MigrationStateModel, NetworkContainerType } from '../../models/stateMachine';
|
||||
import { MigrationMode, MigrationStateModel, MigrationTargetType, NetworkContainerType } from '../../models/stateMachine';
|
||||
import * as constants from '../../constants/strings';
|
||||
import * as styles from '../../constants/styles';
|
||||
|
||||
@@ -25,22 +25,20 @@ export class TargetDatabaseSummaryDialog {
|
||||
this._dialogObject = azdata.window.createModelViewDialog(
|
||||
constants.DATABASE_TO_BE_MIGRATED,
|
||||
'TargetDatabaseSummaryDialog',
|
||||
dialogWidth
|
||||
);
|
||||
dialogWidth);
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
let tab = azdata.window.createTab('sql.migration.CreateResourceGroupDialog');
|
||||
const tab = azdata.window.createTab('sql.migration.CreateResourceGroupDialog');
|
||||
tab.registerContent(async (view: azdata.ModelView) => {
|
||||
this._view = view;
|
||||
|
||||
const databaseCount = this._view.modelBuilder.text().withProps({
|
||||
value: constants.COUNT_DATABASES(this._model._databasesForMigration.length),
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin-bottom': '20px'
|
||||
}
|
||||
}).component();
|
||||
const isSqlDbMigration = this._model._targetType === MigrationTargetType.SQLDB;
|
||||
const databaseCount = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.COUNT_DATABASES(this._model._databasesForMigration.length),
|
||||
CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '20px' }
|
||||
}).component();
|
||||
|
||||
const headerCssStyle = {
|
||||
'border': 'none',
|
||||
@@ -61,7 +59,7 @@ export class TargetDatabaseSummaryDialog {
|
||||
|
||||
const columnWidth = 150;
|
||||
|
||||
let columns: azdata.DeclarativeTableColumn[] = [
|
||||
const columns: azdata.DeclarativeTableColumn[] = [
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.SOURCE_DATABASE,
|
||||
@@ -70,7 +68,6 @@ export class TargetDatabaseSummaryDialog {
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
},
|
||||
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.TARGET_DATABASE_NAME,
|
||||
@@ -78,46 +75,59 @@ export class TargetDatabaseSummaryDialog {
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
}
|
||||
];
|
||||
}];
|
||||
|
||||
if (this._model._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
|
||||
columns.push(
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.LOCATION,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
},
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.RESOURCE_GROUP,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
},
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.SUMMARY_AZURE_STORAGE,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
},
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.BLOB_CONTAINER,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
},
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.BLOB_CONTAINER_LAST_BACKUP_FILE,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle,
|
||||
hidden: this._model._databaseBackup.migrationMode === MigrationMode.ONLINE
|
||||
});
|
||||
} else if (isSqlDbMigration) {
|
||||
columns.push({
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.LOCATION,
|
||||
displayName: constants.TARGET_TABLE_COUNT_NAME,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
}, {
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.RESOURCE_GROUP,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
}, {
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.SUMMARY_AZURE_STORAGE,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
}, {
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.BLOB_CONTAINER,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
}, {
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.BLOB_CONTAINER_LAST_BACKUP_FILE,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle,
|
||||
hidden: this._model._databaseBackup.migrationMode === MigrationMode.ONLINE
|
||||
});
|
||||
} else {
|
||||
columns.push({
|
||||
@@ -134,59 +144,54 @@ export class TargetDatabaseSummaryDialog {
|
||||
|
||||
this._model._databasesForMigration.forEach((db, index) => {
|
||||
const tableRow: azdata.DeclarativeTableCellValue[] = [];
|
||||
tableRow.push({
|
||||
value: db
|
||||
}, {
|
||||
value: this._model._targetDatabaseNames[index]
|
||||
});
|
||||
tableRow.push(
|
||||
{ value: db },
|
||||
{ value: this._model._targetDatabaseNames[index] });
|
||||
|
||||
if (this._model._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
|
||||
tableRow.push({
|
||||
value: this._model._databaseBackup.blobs[index].storageAccount.location
|
||||
}, {
|
||||
value: this._model._databaseBackup.blobs[index].storageAccount.resourceGroup!
|
||||
}, {
|
||||
value: this._model._databaseBackup.blobs[index].storageAccount.name
|
||||
}, {
|
||||
value: this._model._databaseBackup.blobs[index].blobContainer.name
|
||||
});
|
||||
tableRow.push(
|
||||
{ value: this._model._databaseBackup.blobs[index].storageAccount.location },
|
||||
{ value: this._model._databaseBackup.blobs[index].storageAccount.resourceGroup! },
|
||||
{ value: this._model._databaseBackup.blobs[index].storageAccount.name },
|
||||
{ value: this._model._databaseBackup.blobs[index].blobContainer.name });
|
||||
|
||||
if (this._model._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
|
||||
tableRow.push({
|
||||
value: this._model._databaseBackup.blobs[index].lastBackupFile!
|
||||
});
|
||||
tableRow.push(
|
||||
{ value: this._model._databaseBackup.blobs[index].lastBackupFile! });
|
||||
}
|
||||
} else if (isSqlDbMigration) {
|
||||
const totalTables = this._model._sourceTargetMapping.get(db)?.sourceTables.size ?? 0;
|
||||
let selectedTables = 0;
|
||||
this._model._sourceTargetMapping.get(db)?.sourceTables.forEach(
|
||||
tableInfo => selectedTables += tableInfo.selectedForMigration ? 1 : 0);
|
||||
tableRow.push(
|
||||
{ value: constants.TOTAL_TABLES_SELECTED(selectedTables, totalTables) });
|
||||
} else {
|
||||
tableRow.push({
|
||||
value: this._model._databaseBackup.networkShares[index].networkShareLocation
|
||||
});
|
||||
tableRow.push(
|
||||
{ value: this._model._databaseBackup.networkShares[index].networkShareLocation });
|
||||
}
|
||||
tableRows.push(tableRow);
|
||||
});
|
||||
|
||||
const databaseTable: azdata.DeclarativeTableComponent = this._view.modelBuilder.declarativeTable().withProps({
|
||||
ariaLabel: constants.DATABASE_TO_BE_MIGRATED,
|
||||
columns: columns,
|
||||
dataValues: tableRows,
|
||||
width: this._tableLength
|
||||
}).component();
|
||||
const databaseTable: azdata.DeclarativeTableComponent = this._view.modelBuilder.declarativeTable()
|
||||
.withProps({
|
||||
ariaLabel: constants.DATABASE_TO_BE_MIGRATED,
|
||||
columns: columns,
|
||||
dataValues: tableRows,
|
||||
width: this._tableLength
|
||||
}).component();
|
||||
|
||||
const container = this._view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.withItems([databaseCount, databaseTable])
|
||||
.component();
|
||||
const form = this._view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
[{ component: container }],
|
||||
{ horizontal: false })
|
||||
.withLayout({ width: '100%' })
|
||||
.component();
|
||||
|
||||
const container = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column',
|
||||
}).withItems([
|
||||
databaseCount,
|
||||
databaseTable
|
||||
]).component();
|
||||
const formBuilder = this._view.modelBuilder.formContainer().withFormItems(
|
||||
[
|
||||
{
|
||||
component: container
|
||||
}
|
||||
],
|
||||
{
|
||||
horizontal: false
|
||||
}
|
||||
);
|
||||
const form = formBuilder.withLayout({ width: '100%' }).component();
|
||||
return view.initializeModel(form);
|
||||
});
|
||||
this._dialogObject.content = [tab];
|
||||
|
||||
@@ -4,154 +4,14 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import { WizardController } from './wizard/wizardController';
|
||||
import * as mssql from 'mssql';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as loc from './constants/strings';
|
||||
import { MigrationNotebookInfo, NotebookPathHelper } from './constants/notebookPathHelper';
|
||||
import { IconPathHelper } from './constants/iconPathHelper';
|
||||
import { DashboardWidget } from './dashboard/sqlServerDashboard';
|
||||
import { MigrationLocalStorage } from './models/migrationLocalStorage';
|
||||
import { MigrationStateModel, SavedInfo } from './models/stateMachine';
|
||||
import { SavedAssessmentDialog } from './dialog/assessmentResults/savedAssessmentDialog';
|
||||
|
||||
class SQLMigration {
|
||||
|
||||
public stateModel!: MigrationStateModel;
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext) {
|
||||
NotebookPathHelper.setExtensionContext(context);
|
||||
IconPathHelper.setExtensionContext(context);
|
||||
MigrationLocalStorage.setExtensionContext(context);
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
await this.registerCommands();
|
||||
}
|
||||
|
||||
async registerCommands(): Promise<void> {
|
||||
const commandDisposables: vscode.Disposable[] = [ // Array of disposables returned by registerCommand
|
||||
vscode.commands.registerCommand(
|
||||
'sqlmigration.start',
|
||||
async () => await this.launchMigrationWizard()),
|
||||
vscode.commands.registerCommand(
|
||||
'sqlmigration.openNotebooks',
|
||||
async () => {
|
||||
const input = vscode.window.createQuickPick<MigrationNotebookInfo>();
|
||||
input.placeholder = loc.NOTEBOOK_QUICK_PICK_PLACEHOLDER;
|
||||
|
||||
input.items = NotebookPathHelper.getAllMigrationNotebooks();
|
||||
|
||||
this.context.subscriptions.push(input.onDidAccept(async (e) => {
|
||||
const selectedNotebook = input.selectedItems[0];
|
||||
if (selectedNotebook) {
|
||||
try {
|
||||
await azdata.nb.showNotebookDocument(vscode.Uri.parse(`untitled: ${selectedNotebook.label}`), {
|
||||
preview: false,
|
||||
initialContent: (await fs.readFile(selectedNotebook.notebookPath)).toString(),
|
||||
initialDirtyState: false
|
||||
});
|
||||
} catch (e) {
|
||||
void vscode.window.showErrorMessage(`${loc.NOTEBOOK_OPEN_ERROR} - ${e.toString()}`);
|
||||
}
|
||||
input.hide();
|
||||
}
|
||||
}));
|
||||
|
||||
input.show();
|
||||
}),
|
||||
azdata.tasks.registerTask(
|
||||
'sqlmigration.start',
|
||||
async () => await this.launchMigrationWizard()),
|
||||
azdata.tasks.registerTask(
|
||||
'sqlmigration.newsupportrequest',
|
||||
async () => await this.launchNewSupportRequest()),
|
||||
azdata.tasks.registerTask(
|
||||
'sqlmigration.sendfeedback',
|
||||
async () => {
|
||||
const actionId = 'workbench.action.openIssueReporter';
|
||||
const args = {
|
||||
extensionId: 'microsoft.sql-migration',
|
||||
issueTitle: loc.FEEDBACK_ISSUE_TITLE,
|
||||
};
|
||||
return await vscode.commands.executeCommand(actionId, args);
|
||||
}),
|
||||
azdata.tasks.registerTask(
|
||||
'sqlmigration.refreshmigrations',
|
||||
async (e) => await widget.refresh()),
|
||||
];
|
||||
|
||||
this.context.subscriptions.push(...commandDisposables);
|
||||
}
|
||||
|
||||
async launchMigrationWizard(): Promise<void> {
|
||||
let activeConnection = await azdata.connection.getCurrentConnection();
|
||||
let connectionId: string = '';
|
||||
let serverName: string = '';
|
||||
if (!activeConnection) {
|
||||
const connection = await azdata.connection.openConnectionDialog();
|
||||
if (connection) {
|
||||
connectionId = connection.connectionId;
|
||||
serverName = connection.options.server;
|
||||
}
|
||||
} else {
|
||||
connectionId = activeConnection.connectionId;
|
||||
serverName = activeConnection.serverName;
|
||||
}
|
||||
if (serverName) {
|
||||
const api = (await vscode.extensions.getExtension(mssql.extension.name)?.activate()) as mssql.IExtension;
|
||||
if (api) {
|
||||
this.stateModel = new MigrationStateModel(this.context, connectionId, api.sqlMigration);
|
||||
this.context.subscriptions.push(this.stateModel);
|
||||
const savedInfo = this.checkSavedInfo(serverName);
|
||||
if (savedInfo) {
|
||||
this.stateModel.savedInfo = savedInfo;
|
||||
this.stateModel.serverName = serverName;
|
||||
const savedAssessmentDialog = new SavedAssessmentDialog(
|
||||
this.context,
|
||||
this.stateModel,
|
||||
async () => await widget?.onDialogClosed());
|
||||
await savedAssessmentDialog.openDialog();
|
||||
} else {
|
||||
const wizardController = new WizardController(
|
||||
this.context,
|
||||
this.stateModel,
|
||||
async () => await widget?.onDialogClosed());
|
||||
await wizardController.openWizard(connectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkSavedInfo(serverName: string): SavedInfo | undefined {
|
||||
let savedInfo: SavedInfo | undefined = this.context.globalState.get(`${this.stateModel.mementoString}.${serverName}`);
|
||||
if (savedInfo) {
|
||||
return savedInfo;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async launchNewSupportRequest(): Promise<void> {
|
||||
await vscode.env.openExternal(vscode.Uri.parse(
|
||||
`https://portal.azure.com/#blade/Microsoft_Azure_Support/HelpAndSupportBlade/newsupportrequest`));
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
let sqlMigration: SQLMigration;
|
||||
let widget: DashboardWidget;
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
sqlMigration = new SQLMigration(context);
|
||||
await sqlMigration.registerCommands();
|
||||
export async function activate(context: vscode.ExtensionContext): Promise<DashboardWidget> {
|
||||
widget = new DashboardWidget(context);
|
||||
widget.register();
|
||||
await widget.register();
|
||||
return widget;
|
||||
}
|
||||
|
||||
export function deactivate(): void {
|
||||
sqlMigration.stop();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as azurecore from 'azurecore';
|
||||
import { DatabaseMigration, SqlMigrationService, getSubscriptions, getServiceMigrations } from '../api/azure';
|
||||
import { deepClone } from '../api/utils';
|
||||
import * as loc from '../constants/strings';
|
||||
import { ServiceContextChangeEvent } from '../dashboard/tabBase';
|
||||
|
||||
export class MigrationLocalStorage {
|
||||
private static context: vscode.ExtensionContext;
|
||||
@@ -26,15 +27,16 @@ export class MigrationLocalStorage {
|
||||
return {};
|
||||
}
|
||||
|
||||
public static async saveMigrationServiceContext(serviceContext: MigrationServiceContext): Promise<void> {
|
||||
public static async saveMigrationServiceContext(serviceContext: MigrationServiceContext, serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>): Promise<void> {
|
||||
const connectionProfile = await azdata.connection.getCurrentConnection();
|
||||
if (connectionProfile) {
|
||||
const serverContextKey = `${this.mementoToken}.${connectionProfile.serverName}.serviceContext`;
|
||||
return await this.context.globalState.update(serverContextKey, deepClone(serviceContext));
|
||||
await this.context.globalState.update(serverContextKey, deepClone(serviceContext));
|
||||
serviceContextChangedEvent.fire({ connectionId: connectionProfile.connectionId });
|
||||
}
|
||||
}
|
||||
|
||||
public static async refreshMigrationAzureAccount(serviceContext: MigrationServiceContext, migration: DatabaseMigration): Promise<void> {
|
||||
public static async refreshMigrationAzureAccount(serviceContext: MigrationServiceContext, migration: DatabaseMigration, serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>): Promise<void> {
|
||||
if (serviceContext.azureAccount?.isStale) {
|
||||
const accounts = await azdata.accounts.getAllAccounts();
|
||||
const account = accounts.find(a => !a.isStale && a.key.accountId === serviceContext.azureAccount?.key.accountId);
|
||||
@@ -43,7 +45,7 @@ export class MigrationLocalStorage {
|
||||
const subscription = subscriptions.find(s => s.id === serviceContext.subscription?.id);
|
||||
if (subscription) {
|
||||
serviceContext.azureAccount = account;
|
||||
await this.saveMigrationServiceContext(serviceContext);
|
||||
await this.saveMigrationServiceContext(serviceContext, serviceContextChangedEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,30 +89,3 @@ export interface MigrationServiceContext {
|
||||
resourceGroup?: azurecore.azureResource.AzureResourceResourceGroup,
|
||||
migrationService?: SqlMigrationService,
|
||||
}
|
||||
|
||||
export enum MigrationStatus {
|
||||
Failed = 'Failed',
|
||||
Succeeded = 'Succeeded',
|
||||
InProgress = 'InProgress',
|
||||
Canceled = 'Canceled',
|
||||
Completing = 'Completing',
|
||||
Creating = 'Creating',
|
||||
Canceling = 'Canceling',
|
||||
Retriable = 'Retriable',
|
||||
}
|
||||
|
||||
export enum ProvisioningState {
|
||||
Failed = 'Failed',
|
||||
Succeeded = 'Succeeded',
|
||||
Creating = 'Creating'
|
||||
}
|
||||
|
||||
export enum BackupFileInfoStatus {
|
||||
Arrived = 'Arrived',
|
||||
Uploading = 'Uploading',
|
||||
Uploaded = 'Uploaded',
|
||||
Restoring = 'Restoring',
|
||||
Restored = 'Restored',
|
||||
Cancelled = 'Cancelled',
|
||||
Ignored = 'Ignored'
|
||||
}
|
||||
|
||||
@@ -80,6 +80,4 @@ export abstract class MigrationWizardPage {
|
||||
const current = this.wizard.currentPage;
|
||||
await this.wizard.setCurrentPage(current + 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,14 @@ import * as azdata from 'azdata';
|
||||
import * as azurecore from 'azurecore';
|
||||
import * as vscode from 'vscode';
|
||||
import * as mssql from 'mssql';
|
||||
import { SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, SqlVMServer, getLocationDisplayName, getSqlManagedInstanceDatabases } from '../api/azure';
|
||||
import { SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, SqlVMServer, getLocationDisplayName, getSqlManagedInstanceDatabases, AzureSqlDatabaseServer } from '../api/azure';
|
||||
import * as constants from '../constants/strings';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery';
|
||||
import { hashString, deepClone } from '../api/utils';
|
||||
import { SKURecommendationPage } from '../wizard/skuRecommendationPage';
|
||||
import { excludeDatabses, TargetDatabaseInfo } from '../api/sqlUtils';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export enum State {
|
||||
@@ -136,7 +137,7 @@ export interface SavedInfo {
|
||||
subscription: azurecore.azureResource.AzureResourceSubscription | null;
|
||||
location: azurecore.azureResource.AzureLocation | null;
|
||||
resourceGroup: azurecore.azureResource.AzureResourceResourceGroup | null;
|
||||
targetServerInstance: azurecore.azureResource.AzureSqlManagedInstance | SqlVMServer | null;
|
||||
targetServerInstance: azurecore.azureResource.AzureSqlManagedInstance | SqlVMServer | AzureSqlDatabaseServer | null;
|
||||
migrationMode: MigrationMode | null;
|
||||
networkContainerType: NetworkContainerType | null;
|
||||
networkShares: NetworkShare[];
|
||||
@@ -176,7 +177,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
public _resourceGroup!: azurecore.azureResource.AzureResourceResourceGroup;
|
||||
public _targetManagedInstances!: SqlManagedInstance[];
|
||||
public _targetSqlVirtualMachines!: SqlVMServer[];
|
||||
public _targetServerInstance!: SqlManagedInstance | SqlVMServer;
|
||||
public _targetSqlDatabaseServers!: AzureSqlDatabaseServer[];
|
||||
public _targetServerInstance!: SqlManagedInstance | SqlVMServer | AzureSqlDatabaseServer;
|
||||
public _databaseBackup!: DatabaseBackupModel;
|
||||
public _storageAccounts!: StorageAccount[];
|
||||
public _fileShares!: azurecore.azureResource.FileShare[];
|
||||
@@ -185,15 +187,15 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
public _sourceDatabaseNames!: string[];
|
||||
public _targetDatabaseNames!: string[];
|
||||
|
||||
public _targetUserName!: string;
|
||||
public _targetPassword!: string;
|
||||
public _sourceTargetMapping: Map<string, TargetDatabaseInfo | undefined> = new Map();
|
||||
|
||||
public _sqlMigrationServiceResourceGroup!: azurecore.azureResource.AzureResourceResourceGroup;
|
||||
public _sqlMigrationService!: SqlMigrationService | undefined;
|
||||
public _sqlMigrationServices!: SqlMigrationService[];
|
||||
public _nodeNames!: string[];
|
||||
|
||||
private _stateChangeEventEmitter = new vscode.EventEmitter<StateChangeEvent>();
|
||||
private _currentState: State;
|
||||
private _gatheringInformationError: string | undefined;
|
||||
|
||||
public _databasesForAssessment!: string[];
|
||||
public _assessmentResults!: ServerAssessment;
|
||||
public _assessedDatabaseList!: string[];
|
||||
@@ -204,8 +206,10 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
|
||||
public _databasesForMigration: string[] = [];
|
||||
public _didUpdateDatabasesForMigration: boolean = false;
|
||||
public _didDatabaseMappingChange: boolean = false;
|
||||
public _vmDbs: string[] = [];
|
||||
public _miDbs: string[] = [];
|
||||
public _sqldbDbs: string[] = [];
|
||||
public _targetType!: MigrationTargetType;
|
||||
|
||||
public _skuRecommendationResults!: SkuRecommendation;
|
||||
@@ -213,10 +217,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
private _skuRecommendationApiResponse!: mssql.SkuRecommendationResult;
|
||||
public _skuRecommendationReportFilePaths: string[];
|
||||
public _skuRecommendationPerformanceLocation!: string;
|
||||
private _skuRecommendationRecommendedDatabaseList!: string[];
|
||||
private _startPerfDataCollectionApiResponse!: mssql.StartPerfDataCollectionResult;
|
||||
private _stopPerfDataCollectionApiResponse!: mssql.StopPerfDataCollectionResult;
|
||||
private _refreshPerfDataCollectionApiResponse!: mssql.RefreshPerfDataCollectionResult;
|
||||
|
||||
public _perfDataCollectionStartDate!: Date | undefined;
|
||||
public _perfDataCollectionStopDate!: Date | undefined;
|
||||
public _perfDataCollectionLastRefreshedDate!: Date;
|
||||
@@ -232,9 +233,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
public readonly _recommendationTargetPlatforms = [MigrationTargetType.SQLDB, MigrationTargetType.SQLMI, MigrationTargetType.SQLVM];
|
||||
|
||||
public refreshPerfDataCollectionFrequency = this._performanceDataQueryIntervalInSeconds * 1000;
|
||||
private _autoRefreshPerfDataCollectionHandle!: NodeJS.Timeout;
|
||||
public refreshGetSkuRecommendationFrequency = constants.TIME_IN_MINUTES(10);
|
||||
private _autoRefreshGetSkuRecommendationHandle!: NodeJS.Timeout;
|
||||
|
||||
public _skuScalingFactor!: number;
|
||||
public _skuTargetPercentile!: number;
|
||||
@@ -246,15 +245,18 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
public savedInfo!: SavedInfo;
|
||||
public closedPage!: number;
|
||||
public _sessionId: string = uuidv4();
|
||||
|
||||
public excludeDbs: string[] = [
|
||||
'master',
|
||||
'tempdb',
|
||||
'msdb',
|
||||
'model'
|
||||
];
|
||||
public serverName!: string;
|
||||
|
||||
private _stateChangeEventEmitter = new vscode.EventEmitter<StateChangeEvent>();
|
||||
private _currentState: State;
|
||||
private _gatheringInformationError: string | undefined;
|
||||
private _skuRecommendationRecommendedDatabaseList!: string[];
|
||||
private _startPerfDataCollectionApiResponse!: mssql.StartPerfDataCollectionResult;
|
||||
private _stopPerfDataCollectionApiResponse!: mssql.StopPerfDataCollectionResult;
|
||||
private _refreshPerfDataCollectionApiResponse!: mssql.RefreshPerfDataCollectionResult;
|
||||
private _autoRefreshPerfDataCollectionHandle!: NodeJS.Timeout;
|
||||
private _autoRefreshGetSkuRecommendationHandle!: NodeJS.Timeout;
|
||||
|
||||
constructor(
|
||||
public extensionContext: vscode.ExtensionContext,
|
||||
private readonly _sourceConnectionId: string,
|
||||
@@ -288,8 +290,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
this._stateChangeEventEmitter.fire({ oldState, newState: this.currentState });
|
||||
}
|
||||
public async getDatabases(): Promise<string[]> {
|
||||
let temp = await azdata.connection.listDatabases(this.sourceConnectionId);
|
||||
let finalResult = temp.filter((name) => !this.excludeDbs.includes(name));
|
||||
const temp = await azdata.connection.listDatabases(this.sourceConnectionId);
|
||||
const finalResult = temp.filter((name) => !excludeDatabses.includes(name));
|
||||
return finalResult;
|
||||
}
|
||||
public hasRecommendedDatabaseListChanged(): boolean {
|
||||
@@ -304,7 +306,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
}));
|
||||
}
|
||||
|
||||
public async getDatabaseAssessments(targetType: MigrationTargetType): Promise<ServerAssessment> {
|
||||
public async getDatabaseAssessments(targetType: MigrationTargetType[]): Promise<ServerAssessment> {
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this.sourceConnectionId);
|
||||
try {
|
||||
const response = (await this.migrationService.getAssessments(ownerUri, this._databasesForAssessment))!;
|
||||
@@ -313,11 +315,14 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
|
||||
if (response?.assessmentResult) {
|
||||
response.assessmentResult.items = response.assessmentResult.items?.filter(
|
||||
issue => issue.appliesToMigrationTargetPlatform === targetType);
|
||||
issue => targetType.includes(
|
||||
<MigrationTargetType>issue.appliesToMigrationTargetPlatform));
|
||||
|
||||
response.assessmentResult.databases?.forEach(
|
||||
database => database.items = database.items?.filter(
|
||||
issue => issue.appliesToMigrationTargetPlatform === targetType));
|
||||
issue => targetType.includes(
|
||||
<MigrationTargetType>issue.appliesToMigrationTargetPlatform)));
|
||||
|
||||
this._assessmentResults = {
|
||||
issues: this._assessmentApiResponse?.assessmentResult?.items || [],
|
||||
databaseAssessments: this._assessmentApiResponse?.assessmentResult?.databases?.map(d => {
|
||||
@@ -447,8 +452,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
'sessionId': this._sessionId,
|
||||
'recommendedSku': JSON.stringify(resultItem?.targetSku)
|
||||
},
|
||||
{}
|
||||
);
|
||||
{});
|
||||
});
|
||||
|
||||
this._skuRecommendationResults?.recommendations?.sqlVmRecommendationResults?.forEach(resultItem => {
|
||||
@@ -460,8 +464,19 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
'sessionId': this._sessionId,
|
||||
'recommendedSku': JSON.stringify(resultItem?.targetSku)
|
||||
},
|
||||
{}
|
||||
);
|
||||
{});
|
||||
});
|
||||
|
||||
this._skuRecommendationResults?.recommendations?.sqlDbRecommendationResults?.forEach(resultItem => {
|
||||
// Send telemetry for recommended SQLDB SKU
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.SkuRecommendationWizard,
|
||||
TelemetryAction.GetSqlDbSkuRecommendation,
|
||||
{
|
||||
'sessionId': this._sessionId,
|
||||
'recommendedSku': JSON.stringify(resultItem?.targetSku)
|
||||
},
|
||||
{});
|
||||
});
|
||||
|
||||
// Send Instance requirements used for calculating recommendations
|
||||
@@ -513,8 +528,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
'tempDBSizeInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.tempDBSizeInMB,
|
||||
'aggregationTargetPercentile': this._skuRecommendationResults?.recommendations?.instanceRequirements?.aggregationTargetPercentile,
|
||||
'numberOfDataPointsAnalyzed': this._skuRecommendationResults?.recommendations?.instanceRequirements?.numberOfDataPointsAnalyzed,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.SkuRecommendationWizard, 'GetSkuRecommendationTelemetryFailed', e);
|
||||
@@ -530,7 +544,12 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
try {
|
||||
if (!this.performanceCollectionInProgress()) {
|
||||
const ownerUri = await azdata.connection.getUriForConnection(this.sourceConnectionId);
|
||||
const response = await this.migrationService.startPerfDataCollection(ownerUri, dataFolder, perfQueryIntervalInSec, staticQueryIntervalInSec, numberOfIterations);
|
||||
const response = await this.migrationService.startPerfDataCollection(
|
||||
ownerUri,
|
||||
dataFolder,
|
||||
perfQueryIntervalInSec,
|
||||
staticQueryIntervalInSec,
|
||||
numberOfIterations);
|
||||
|
||||
this._startPerfDataCollectionApiResponse = response!;
|
||||
this._perfDataCollectionStartDate = this._startPerfDataCollectionApiResponse.dateTimeStarted;
|
||||
@@ -560,8 +579,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
'sessionId': this._sessionId,
|
||||
'timeDataCollectionStarted': this._perfDataCollectionStartDate?.toString() || ''
|
||||
},
|
||||
{}
|
||||
);
|
||||
{});
|
||||
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.DataCollectionWizard, 'StartDataCollectionTelemetryFailed', e);
|
||||
@@ -574,13 +592,14 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
if (!this._autoRefreshPerfDataCollectionHandle) {
|
||||
clearInterval(this._autoRefreshPerfDataCollectionHandle);
|
||||
if (this.refreshPerfDataCollectionFrequency !== -1) {
|
||||
this._autoRefreshPerfDataCollectionHandle = setInterval(async function () {
|
||||
await classVariable.refreshPerfDataCollection();
|
||||
|
||||
if (await classVariable.isWaitingForFirstTimeRefresh()) {
|
||||
await page.refreshSkuRecommendationComponents(); // update timer
|
||||
}
|
||||
}, refreshIntervalInMs);
|
||||
this._autoRefreshPerfDataCollectionHandle = setInterval(
|
||||
async function () {
|
||||
await classVariable.refreshPerfDataCollection();
|
||||
if (await classVariable.isWaitingForFirstTimeRefresh()) {
|
||||
await page.refreshSkuRecommendationComponents(); // update timer
|
||||
}
|
||||
},
|
||||
refreshIntervalInMs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,9 +607,11 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
// start one-time timer to get SKU recommendation
|
||||
clearTimeout(this._autoRefreshGetSkuRecommendationHandle);
|
||||
if (this.refreshGetSkuRecommendationFrequency !== -1) {
|
||||
this._autoRefreshGetSkuRecommendationHandle = setTimeout(async function () {
|
||||
await page.refreshAzureRecommendation();
|
||||
}, this.refreshGetSkuRecommendationFrequency);
|
||||
this._autoRefreshGetSkuRecommendationHandle = setTimeout(
|
||||
async function () {
|
||||
await page.refreshAzureRecommendation();
|
||||
},
|
||||
this.refreshGetSkuRecommendationFrequency);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -612,7 +633,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
}
|
||||
|
||||
// Generate telemetry for stop data collection request
|
||||
this.generateStopDataCollectionTelemetry().catch(e => console.error(e));
|
||||
this.generateStopDataCollectionTelemetry()
|
||||
.catch(e => console.error(e));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -625,8 +647,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
'sessionId': this._sessionId,
|
||||
'timeDataCollectionStopped': this._perfDataCollectionStopDate?.toString() || ''
|
||||
},
|
||||
{}
|
||||
);
|
||||
{});
|
||||
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.DataCollectionWizard, 'StopDataCollectionTelemetryFailed', e);
|
||||
@@ -643,7 +664,10 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
this._perfDataCollectionIsCollecting = this._refreshPerfDataCollectionApiResponse.isCollecting;
|
||||
|
||||
if (this._perfDataCollectionErrors?.length > 0) {
|
||||
void vscode.window.showInformationMessage(constants.PERF_DATA_COLLECTION_ERROR(this._assessmentApiResponse?.assessmentResult?.name, this._perfDataCollectionErrors));
|
||||
void vscode.window.showInformationMessage(
|
||||
constants.PERF_DATA_COLLECTION_ERROR(
|
||||
this._assessmentApiResponse?.assessmentResult?.name,
|
||||
this._perfDataCollectionErrors));
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@@ -661,33 +685,24 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
}
|
||||
|
||||
public performanceCollectionNotStarted(): boolean {
|
||||
if (!this._perfDataCollectionStartDate
|
||||
&& !this._perfDataCollectionStopDate) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return !this._perfDataCollectionStartDate
|
||||
&& !this._perfDataCollectionStopDate;
|
||||
}
|
||||
|
||||
public performanceCollectionInProgress(): boolean {
|
||||
if (this._perfDataCollectionStartDate
|
||||
&& !this._perfDataCollectionStopDate) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return this._perfDataCollectionStartDate !== undefined
|
||||
&& this._perfDataCollectionStopDate === undefined;
|
||||
}
|
||||
|
||||
public performanceCollectionStopped(): boolean {
|
||||
if (this._perfDataCollectionStartDate
|
||||
&& this._perfDataCollectionStopDate) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return this._perfDataCollectionStartDate !== undefined
|
||||
&& this._perfDataCollectionStopDate !== undefined;
|
||||
}
|
||||
|
||||
private async generateAssessmentTelemetry(): Promise<void> {
|
||||
try {
|
||||
|
||||
let serverIssues = this._assessmentResults?.issues.map(i => {
|
||||
const serverIssues = this._assessmentResults?.issues.map(i => {
|
||||
return {
|
||||
ruleId: i.ruleId,
|
||||
count: i.impactedObjects.length
|
||||
@@ -696,18 +711,15 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
|
||||
const serverAssessmentErrorsMap: Map<number, number> = new Map();
|
||||
this._assessmentApiResponse?.assessmentResult?.errors?.forEach(e => {
|
||||
serverAssessmentErrorsMap.set(e.errorId, serverAssessmentErrorsMap.get(e.errorId) ?? 0 + 1);
|
||||
serverAssessmentErrorsMap.set(
|
||||
e.errorId,
|
||||
serverAssessmentErrorsMap.get(e.errorId) ?? 0 + 1);
|
||||
});
|
||||
|
||||
let serverErrors: { errorId: number, count: number }[] = [];
|
||||
serverAssessmentErrorsMap.forEach((v, k) => {
|
||||
serverErrors.push(
|
||||
{
|
||||
errorId: k,
|
||||
count: v
|
||||
}
|
||||
);
|
||||
});
|
||||
const serverErrors: { errorId: number, count: number }[] = [];
|
||||
serverAssessmentErrorsMap.forEach(
|
||||
(v, k) => serverErrors.push(
|
||||
{ errorId: k, count: v }));
|
||||
|
||||
const startTime = new Date(this._assessmentApiResponse?.startTime);
|
||||
const endTime = new Date(this._assessmentApiResponse?.endedTime);
|
||||
@@ -759,27 +771,26 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
'assessmentTimeMs': d.assessmentTimeInMilliseconds,
|
||||
'numberOfBlockerIssues': d.sqlManagedInstanceTargetReadiness.numOfBlockerIssues,
|
||||
'databaseSizeInMb': d.databaseSize
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
d.items.forEach(i => {
|
||||
databaseWarningsMap.set(i.ruleId, databaseWarningsMap.get(i.ruleId) ?? 0 + i.impactedObjects.length);
|
||||
databaseWarningsMap.set(
|
||||
i.ruleId,
|
||||
databaseWarningsMap.get(i.ruleId) ?? 0 + i.impactedObjects.length);
|
||||
});
|
||||
|
||||
d.errors.forEach(e => {
|
||||
databaseErrorsMap.set(e.errorId, databaseErrorsMap.get(e.errorId) ?? 0 + 1);
|
||||
});
|
||||
d.errors.forEach(
|
||||
e => databaseErrorsMap.set(
|
||||
e.errorId,
|
||||
databaseErrorsMap.get(e.errorId) ?? 0 + 1));
|
||||
|
||||
});
|
||||
|
||||
let databaseWarnings: { warningId: string, count: number }[] = [];
|
||||
const databaseWarnings: { warningId: string, count: number }[] = [];
|
||||
|
||||
databaseWarningsMap.forEach((v, k) => {
|
||||
databaseWarnings.push({
|
||||
warningId: k,
|
||||
count: v
|
||||
});
|
||||
});
|
||||
databaseWarningsMap.forEach(
|
||||
(v, k) => databaseWarnings.push(
|
||||
{ warningId: k, count: v }));
|
||||
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.MigrationWizardTargetSelectionPage,
|
||||
@@ -788,16 +799,12 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
'sessionId': this._sessionId,
|
||||
'warnings': JSON.stringify(databaseWarnings)
|
||||
},
|
||||
{}
|
||||
);
|
||||
{});
|
||||
|
||||
let databaseErrors: { errorId: number, count: number }[] = [];
|
||||
databaseErrorsMap.forEach((v, k) => {
|
||||
databaseErrors.push({
|
||||
errorId: k,
|
||||
count: v
|
||||
});
|
||||
});
|
||||
const databaseErrors: { errorId: number, count: number }[] = [];
|
||||
databaseErrorsMap.forEach(
|
||||
(v, k) => databaseErrors.push(
|
||||
{ errorId: k, count: v }));
|
||||
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.MigrationWizardTargetSelectionPage,
|
||||
@@ -806,8 +813,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
'sessionId': this._sessionId,
|
||||
'errors': JSON.stringify(databaseErrors)
|
||||
},
|
||||
{}
|
||||
);
|
||||
{});
|
||||
|
||||
} catch (e) {
|
||||
console.log('error during assessment telemetry:');
|
||||
@@ -837,13 +843,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
|
||||
public async getSourceConnectionProfile(): Promise<azdata.connection.ConnectionProfile> {
|
||||
const sqlConnections = await azdata.connection.getConnections();
|
||||
return sqlConnections.find((value) => {
|
||||
if (value.connectionId === this.sourceConnectionId) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})!;
|
||||
return sqlConnections.find(
|
||||
value => value.connectionId === this.sourceConnectionId)!;
|
||||
}
|
||||
|
||||
public getLocationDisplayName(location: string): Promise<string> {
|
||||
@@ -851,22 +852,20 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
}
|
||||
|
||||
public async getManagedDatabases(): Promise<string[]> {
|
||||
return (await getSqlManagedInstanceDatabases(this._azureAccount,
|
||||
this._targetSubscription,
|
||||
<SqlManagedInstance>this._targetServerInstance)).map(t => t.name);
|
||||
return (
|
||||
await getSqlManagedInstanceDatabases(this._azureAccount,
|
||||
this._targetSubscription,
|
||||
<SqlManagedInstance>this._targetServerInstance)
|
||||
).map(t => t.name);
|
||||
}
|
||||
|
||||
public async startMigration() {
|
||||
const sqlConnections = await azdata.connection.getConnections();
|
||||
const currentConnection = sqlConnections.find((value) => {
|
||||
if (value.connectionId === this.sourceConnectionId) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const currentConnection = sqlConnections.find(
|
||||
value => value.connectionId === this.sourceConnectionId);
|
||||
|
||||
const isOfflineMigration = this._databaseBackup.migrationMode === MigrationMode.OFFLINE;
|
||||
const isSqlDbTarget = this._targetType === MigrationTargetType.SQLDB;
|
||||
|
||||
const requestBody: StartDatabaseMigrationRequest = {
|
||||
location: this._sqlMigrationService?.location!,
|
||||
@@ -877,7 +876,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
sourceSqlConnection: {
|
||||
dataSource: currentConnection?.serverName!,
|
||||
authentication: this._authenticationType,
|
||||
username: this._sqlServerUsername,
|
||||
userName: this._sqlServerUsername,
|
||||
password: this._sqlServerPassword
|
||||
},
|
||||
scope: this._targetServerInstance.id,
|
||||
@@ -926,6 +925,55 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
}
|
||||
};
|
||||
break;
|
||||
default:
|
||||
if (isSqlDbTarget) {
|
||||
const sourceDatabaseName = this._databasesForMigration[i];
|
||||
const targetDatabaseInfo = this._sourceTargetMapping.get(sourceDatabaseName);
|
||||
const totalTables = targetDatabaseInfo?.sourceTables.size ?? 0;
|
||||
// skip databases that don't have tables
|
||||
if (totalTables === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourceTables: string[] = [];
|
||||
let selectedTables = 0;
|
||||
targetDatabaseInfo?.sourceTables.forEach(sourceTableInfo => {
|
||||
if (sourceTableInfo.selectedForMigration) {
|
||||
selectedTables++;
|
||||
sourceTables.push(sourceTableInfo.tableName);
|
||||
}
|
||||
});
|
||||
|
||||
// skip databases that don't have tables selected
|
||||
if (selectedTables === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sqlDbTarget = this._targetServerInstance as AzureSqlDatabaseServer;
|
||||
requestBody.properties.offlineConfiguration = undefined;
|
||||
requestBody.properties.sourceSqlConnection = {
|
||||
dataSource: currentConnection?.serverName!,
|
||||
authentication: this._authenticationType,
|
||||
userName: this._sqlServerUsername,
|
||||
password: this._sqlServerPassword,
|
||||
encryptConnection: true,
|
||||
trustServerCertificate: false,
|
||||
};
|
||||
requestBody.properties.targetSqlConnection = {
|
||||
dataSource: sqlDbTarget.properties.fullyQualifiedDomainName,
|
||||
authentication: MigrationSourceAuthenticationType.Sql,
|
||||
userName: this._targetUserName,
|
||||
password: this._targetPassword,
|
||||
encryptConnection: true,
|
||||
trustServerCertificate: false,
|
||||
};
|
||||
|
||||
// send an empty array when 'all' tables are selected for migration
|
||||
requestBody.properties.tableList = selectedTables === totalTables
|
||||
? []
|
||||
: sourceTables;
|
||||
}
|
||||
break;
|
||||
}
|
||||
requestBody.properties.sourceDatabaseName = this._databasesForMigration[i];
|
||||
const response = await startDatabaseMigration(
|
||||
@@ -969,8 +1017,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
'wizardEntryPoint': wizardEntryPoint,
|
||||
},
|
||||
{
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
void vscode.window.showInformationMessage(
|
||||
localize(
|
||||
@@ -982,7 +1029,10 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
}
|
||||
} catch (e) {
|
||||
void vscode.window.showErrorMessage(
|
||||
localize('sql.migration.starting.migration.error', "An error occurred while starting the migration: '{0}'", e.message));
|
||||
localize(
|
||||
'sql.migration.starting.migration.error',
|
||||
"An error occurred while starting the migration: '{0}'",
|
||||
e.message));
|
||||
logError(TelemetryViews.MigrationLocalStorage, 'StartMigrationFailed', e);
|
||||
}
|
||||
finally {
|
||||
@@ -990,15 +1040,15 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
await this.refreshPerfDataCollection();
|
||||
if ((!this.resumeAssessment || this.retryMigration) && this._perfDataCollectionIsCollecting) {
|
||||
void this.stopPerfDataCollection();
|
||||
void vscode.window.showInformationMessage(constants.AZURE_RECOMMENDATION_STOP_POPUP);
|
||||
void vscode.window.showInformationMessage(
|
||||
constants.AZURE_RECOMMENDATION_STOP_POPUP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async saveInfo(serverName: string, currentPage: Page): Promise<void> {
|
||||
let saveInfo: SavedInfo;
|
||||
saveInfo = {
|
||||
const saveInfo: SavedInfo = {
|
||||
closedPage: currentPage,
|
||||
databaseAssessment: [],
|
||||
databaseList: [],
|
||||
@@ -1047,7 +1097,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
saveInfo.serverAssessment = this._assessmentResults;
|
||||
|
||||
if (this._skuRecommendationPerformanceDataSource) {
|
||||
let skuRecommendation: SkuRecommendationSavedInfo = {
|
||||
const skuRecommendation: SkuRecommendationSavedInfo = {
|
||||
skuRecommendationPerformanceDataSource: this._skuRecommendationPerformanceDataSource,
|
||||
skuRecommendationPerformanceLocation: this._skuRecommendationPerformanceLocation,
|
||||
perfDataCollectionStartDate: this._perfDataCollectionStartDate,
|
||||
@@ -1072,6 +1122,9 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
this._databasesForAssessment = this.savedInfo.databaseAssessment;
|
||||
this._databasesForMigration = this.savedInfo.databaseList;
|
||||
this._didUpdateDatabasesForMigration = true;
|
||||
this._didDatabaseMappingChange = true;
|
||||
this.refreshDatabaseBackupPage = true;
|
||||
|
||||
switch (this._targetType) {
|
||||
case MigrationTargetType.SQLMI:
|
||||
this._miDbs = this._databasesForMigration;
|
||||
@@ -1079,6 +1132,9 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
case MigrationTargetType.SQLVM:
|
||||
this._vmDbs = this._databasesForMigration;
|
||||
break;
|
||||
case MigrationTargetType.SQLDB:
|
||||
this._sqldbDbs = this._databasesForMigration;
|
||||
break;
|
||||
}
|
||||
|
||||
this._azureAccount = this.savedInfo.azureAccount || undefined!;
|
||||
@@ -1091,7 +1147,6 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
|
||||
this._databaseBackup.migrationMode = this.savedInfo.migrationMode || undefined!;
|
||||
|
||||
this.refreshDatabaseBackupPage = true;
|
||||
this._sourceDatabaseNames = this._databasesForMigration;
|
||||
this._targetDatabaseNames = this.savedInfo.targetDatabaseNames;
|
||||
this._databaseBackup.networkContainerType = this.savedInfo.networkContainerType || undefined!;
|
||||
|
||||
@@ -57,6 +57,7 @@ export enum TelemetryAction {
|
||||
OnPageLeave = 'OnPageLeave',
|
||||
GetMISkuRecommendation = 'GetMISkuRecommendation',
|
||||
GetVMSkuRecommendation = 'GetVMSkuRecommendation',
|
||||
GetSqlDbSkuRecommendation = 'GetSqlDbSkuRecommendation',
|
||||
GetInstanceRequirements = 'GetInstanceRequirements',
|
||||
StartDataCollection = 'StartDataCollection',
|
||||
StopDataCollection = 'StopDataCollection'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -219,9 +219,9 @@ export class DatabaseSelectorPage extends MigrationWizardPage {
|
||||
]
|
||||
}).component();
|
||||
|
||||
this._disposables.push(this._databaseSelectorTable.onRowSelected(async (e) => {
|
||||
await this.updateValuesOnSelection();
|
||||
}));
|
||||
this._disposables.push(
|
||||
this._databaseSelectorTable.onRowSelected(
|
||||
async (e) => await this.updateValuesOnSelection()));
|
||||
|
||||
// load unfiltered table list and pre-select list of databases saved in state
|
||||
await this._filterTableList('', this.migrationStateModel._databasesForAssessment);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
|
||||
import { MigrationStateModel, MigrationTargetType, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
|
||||
import { CreateSqlMigrationServiceDialog } from '../dialog/createSqlMigrationService/createSqlMigrationServiceDialog';
|
||||
import * as constants from '../constants/strings';
|
||||
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
|
||||
@@ -17,20 +17,17 @@ import * as utils from '../api/utils';
|
||||
import * as styles from '../constants/styles';
|
||||
|
||||
export class IntergrationRuntimePage extends MigrationWizardPage {
|
||||
|
||||
private _view!: azdata.ModelView;
|
||||
private _statusLoadingComponent!: azdata.LoadingComponent;
|
||||
private _subscription!: azdata.TextComponent;
|
||||
private _location!: azdata.TextComponent;
|
||||
private _resourceGroupDropdown!: azdata.DropDownComponent;
|
||||
private _dmsDropdown!: azdata.DropDownComponent;
|
||||
|
||||
private _dmsInfoContainer!: azdata.FlexContainer;
|
||||
private _dmsStatusInfoBox!: azdata.InfoBoxComponent;
|
||||
private _authKeyTable!: azdata.DeclarativeTableComponent;
|
||||
private _refreshButton!: azdata.ButtonComponent;
|
||||
private _connectionStatusLoader!: azdata.LoadingComponent;
|
||||
|
||||
private _copy1!: azdata.ButtonComponent;
|
||||
private _copy2!: azdata.ButtonComponent;
|
||||
private _refresh1!: azdata.ButtonComponent;
|
||||
@@ -55,31 +52,39 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
|
||||
const form = view.modelBuilder.formContainer()
|
||||
.withFormItems([
|
||||
{ component: this.migrationServiceDropdownContainer() },
|
||||
{ component: this._dmsInfoContainer }
|
||||
])
|
||||
{ component: this._dmsInfoContainer }])
|
||||
.withProps({ CSSStyles: { 'padding-top': '0' } })
|
||||
.component();
|
||||
|
||||
this._disposables.push(this._view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
this._disposables.push(
|
||||
this._view.onClosed(e =>
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } })));
|
||||
|
||||
await view.initializeModel(form);
|
||||
}
|
||||
|
||||
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
|
||||
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._subscription.value = this.migrationStateModel._targetSubscription.name;
|
||||
this._location.value = await getLocationDisplayName(this.migrationStateModel._targetServerInstance.location);
|
||||
this._dmsInfoContainer.display = (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE && this.migrationStateModel._sqlMigrationService) ? 'inline' : 'none';
|
||||
this._location.value = await getLocationDisplayName(
|
||||
this.migrationStateModel._targetServerInstance.location);
|
||||
|
||||
await utils.updateControlDisplay(
|
||||
this._dmsInfoContainer,
|
||||
this.migrationStateModel._targetType === MigrationTargetType.SQLDB ||
|
||||
this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE);
|
||||
|
||||
await this.loadResourceGroupDropdown();
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
||||
this.wizard.message = { text: '' };
|
||||
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
|
||||
this.wizard.message = {
|
||||
text: ''
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
const state = this.migrationStateModel._sqlMigrationService?.properties?.integrationRuntimeState;
|
||||
if (!this.migrationStateModel._sqlMigrationService) {
|
||||
this.wizard.message = {
|
||||
@@ -88,327 +93,325 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
|
||||
};
|
||||
return false;
|
||||
}
|
||||
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE && state !== 'Online') {
|
||||
if ((this.migrationStateModel._targetType === MigrationTargetType.SQLDB ||
|
||||
this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE)
|
||||
&& state !== 'Online') {
|
||||
|
||||
this.wizard.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: constants.SERVICE_OFFLINE_ERROR
|
||||
};
|
||||
return false;
|
||||
} else {
|
||||
this.wizard.message = {
|
||||
text: ''
|
||||
};
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
||||
return true;
|
||||
});
|
||||
this.wizard.registerNavigationValidator((pageChangeInfo) => true);
|
||||
}
|
||||
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
}
|
||||
|
||||
private migrationServiceDropdownContainer(): azdata.FlexContainer {
|
||||
const descriptionText = this._view.modelBuilder.text().withProps({
|
||||
value: constants.IR_PAGE_DESCRIPTION,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin-bottom': '16px'
|
||||
}
|
||||
}).component();
|
||||
const descriptionText = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.IR_PAGE_DESCRIPTION,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '16px' }
|
||||
}).component();
|
||||
|
||||
const subscriptionLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.SUBSCRIPTION,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS
|
||||
}
|
||||
}).component();
|
||||
this._subscription = this._view.modelBuilder.text().withProps({
|
||||
enabled: false,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
CSSStyles: {
|
||||
'margin': '0'
|
||||
}
|
||||
}).component();
|
||||
const subscriptionLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.SUBSCRIPTION,
|
||||
CSSStyles: { ...styles.LABEL_CSS }
|
||||
}).component();
|
||||
this._subscription = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
enabled: false,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
CSSStyles: { 'margin': '0' }
|
||||
}).component();
|
||||
|
||||
const locationLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.LOCATION,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin-top': '1em'
|
||||
}
|
||||
}).component();
|
||||
this._location = this._view.modelBuilder.text().withProps({
|
||||
enabled: false,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
CSSStyles: {
|
||||
'margin': '0'
|
||||
}
|
||||
}).component();
|
||||
const locationLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.LOCATION,
|
||||
CSSStyles: { ...styles.LABEL_CSS, 'margin-top': '1em' }
|
||||
}).component();
|
||||
this._location = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
enabled: false,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
CSSStyles: { 'margin': '0' }
|
||||
}).component();
|
||||
|
||||
const resourceGroupLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.RESOURCE_GROUP,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS
|
||||
}
|
||||
}).component();
|
||||
this._resourceGroupDropdown = this._view.modelBuilder.dropDown().withProps({
|
||||
ariaLabel: constants.RESOURCE_GROUP,
|
||||
placeholder: constants.SELECT_RESOURCE_GROUP,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
CSSStyles: {
|
||||
'margin-top': '-1em'
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(this._resourceGroupDropdown.onValueChanged(async (value) => {
|
||||
if (value && value !== 'undefined' && value !== constants.RESOURCE_GROUP_NOT_FOUND) {
|
||||
const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value);
|
||||
this.migrationStateModel._sqlMigrationServiceResourceGroup = (selectedResourceGroup)
|
||||
? selectedResourceGroup
|
||||
: undefined!;
|
||||
await this.populateDms();
|
||||
}
|
||||
}));
|
||||
const resourceGroupLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.RESOURCE_GROUP,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: { ...styles.LABEL_CSS }
|
||||
}).component();
|
||||
this._resourceGroupDropdown = this._view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
ariaLabel: constants.RESOURCE_GROUP,
|
||||
placeholder: constants.SELECT_RESOURCE_GROUP,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
CSSStyles: { 'margin-top': '-1em' }
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
this._resourceGroupDropdown.onValueChanged(
|
||||
async (value) => {
|
||||
if (value && value !== 'undefined' && value !== constants.RESOURCE_GROUP_NOT_FOUND) {
|
||||
const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value);
|
||||
this.migrationStateModel._sqlMigrationServiceResourceGroup = (selectedResourceGroup)
|
||||
? selectedResourceGroup
|
||||
: undefined!;
|
||||
this.populateDms();
|
||||
}
|
||||
}));
|
||||
|
||||
const migrationServiceDropdownLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.IR_PAGE_TITLE,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS
|
||||
}
|
||||
}).component();
|
||||
this._dmsDropdown = this._view.modelBuilder.dropDown().withProps({
|
||||
ariaLabel: constants.IR_PAGE_TITLE,
|
||||
placeholder: constants.SELECT_RESOURCE_GROUP_PROMPT,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
CSSStyles: {
|
||||
'margin-top': '-1em'
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(this._dmsDropdown.onValueChanged(async (value) => {
|
||||
if (value && value !== 'undefined' && value !== constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR) {
|
||||
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
|
||||
this._dmsInfoContainer.display = 'inline';
|
||||
}
|
||||
this.wizard.message = {
|
||||
text: ''
|
||||
};
|
||||
const selectedDms = this.migrationStateModel._sqlMigrationServices.find(dms => dms.name === value && dms.properties.resourceGroup.toLowerCase() === this.migrationStateModel._sqlMigrationServiceResourceGroup.name.toLowerCase());
|
||||
if (selectedDms) {
|
||||
this.migrationStateModel._sqlMigrationService = selectedDms;
|
||||
await this.loadMigrationServiceStatus();
|
||||
}
|
||||
} else {
|
||||
this.migrationStateModel._sqlMigrationService = undefined;
|
||||
this._dmsInfoContainer.display = 'none';
|
||||
}
|
||||
}));
|
||||
const migrationServiceDropdownLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.IR_PAGE_TITLE,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: { ...styles.LABEL_CSS }
|
||||
}).component();
|
||||
this._dmsDropdown = this._view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
ariaLabel: constants.IR_PAGE_TITLE,
|
||||
placeholder: constants.SELECT_RESOURCE_GROUP_PROMPT,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: true,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
CSSStyles: { 'margin-top': '-1em' }
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
this._dmsDropdown.onValueChanged(
|
||||
async (value) => {
|
||||
if (value && value !== 'undefined' && value !== constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR) {
|
||||
this.wizard.message = { text: '' };
|
||||
|
||||
const createNewMigrationService = this._view.modelBuilder.hyperlink().withProps({
|
||||
label: constants.CREATE_NEW,
|
||||
ariaLabel: constants.CREATE_NEW_MIGRATION_SERVICE,
|
||||
url: '',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
}).component();
|
||||
await utils.updateControlDisplay(
|
||||
this._dmsInfoContainer,
|
||||
this.migrationStateModel._targetType === MigrationTargetType.SQLDB ||
|
||||
this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE);
|
||||
|
||||
this._disposables.push(createNewMigrationService.onDidClick(async (e) => {
|
||||
const dialog = new CreateSqlMigrationServiceDialog();
|
||||
const createdDmsResult = await dialog.createNewDms(this.migrationStateModel, (<azdata.CategoryValue>this._resourceGroupDropdown.value).displayName);
|
||||
this.migrationStateModel._sqlMigrationServiceResourceGroup = createdDmsResult.resourceGroup;
|
||||
this.migrationStateModel._sqlMigrationService = createdDmsResult.service;
|
||||
await this.loadResourceGroupDropdown();
|
||||
await this.populateDms();
|
||||
}));
|
||||
const selectedDms = this.migrationStateModel._sqlMigrationServices.find(
|
||||
dms => dms.name === value && dms.properties.resourceGroup.toLowerCase() === this.migrationStateModel._sqlMigrationServiceResourceGroup.name.toLowerCase());
|
||||
if (selectedDms) {
|
||||
this.migrationStateModel._sqlMigrationService = selectedDms;
|
||||
await this.loadStatus();
|
||||
}
|
||||
} else {
|
||||
this.migrationStateModel._sqlMigrationService = undefined;
|
||||
await utils.updateControlDisplay(this._dmsInfoContainer, false);
|
||||
}
|
||||
}));
|
||||
|
||||
const flexContainer = this._view.modelBuilder.flexContainer().withItems([
|
||||
descriptionText,
|
||||
subscriptionLabel,
|
||||
this._subscription,
|
||||
locationLabel,
|
||||
this._location,
|
||||
resourceGroupLabel,
|
||||
this._resourceGroupDropdown,
|
||||
migrationServiceDropdownLabel,
|
||||
this._dmsDropdown,
|
||||
createNewMigrationService
|
||||
]).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
return flexContainer;
|
||||
const createNewMigrationService = this._view.modelBuilder.hyperlink()
|
||||
.withProps({
|
||||
label: constants.CREATE_NEW,
|
||||
ariaLabel: constants.CREATE_NEW_MIGRATION_SERVICE,
|
||||
url: '',
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
createNewMigrationService.onDidClick(
|
||||
async (e) => {
|
||||
const dialog = new CreateSqlMigrationServiceDialog();
|
||||
const createdDmsResult = await dialog.createNewDms(
|
||||
this.migrationStateModel,
|
||||
(<azdata.CategoryValue>this._resourceGroupDropdown.value).displayName);
|
||||
|
||||
this.migrationStateModel._sqlMigrationServiceResourceGroup = createdDmsResult.resourceGroup;
|
||||
this.migrationStateModel._sqlMigrationService = createdDmsResult.service;
|
||||
await this.loadResourceGroupDropdown();
|
||||
this.populateDms();
|
||||
}));
|
||||
|
||||
return this._view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
descriptionText,
|
||||
subscriptionLabel,
|
||||
this._subscription,
|
||||
locationLabel,
|
||||
this._location,
|
||||
resourceGroupLabel,
|
||||
this._resourceGroupDropdown,
|
||||
migrationServiceDropdownLabel,
|
||||
this._dmsDropdown,
|
||||
createNewMigrationService])
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
}
|
||||
|
||||
private createDMSDetailsContainer(): azdata.FlexContainer {
|
||||
const container = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
const container = this._view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
|
||||
const connectionStatusLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.SERVICE_CONNECTION_STATUS,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'width': '130px'
|
||||
}
|
||||
}).component();
|
||||
const connectionStatusLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.SERVICE_CONNECTION_STATUS,
|
||||
CSSStyles: { ...styles.LABEL_CSS, 'width': '130px' }
|
||||
}).component();
|
||||
|
||||
this._refreshButton = this._view.modelBuilder.button().withProps({
|
||||
iconWidth: '18px',
|
||||
iconHeight: '18px',
|
||||
iconPath: IconPathHelper.refresh,
|
||||
height: '18px',
|
||||
width: '18px',
|
||||
ariaLabel: constants.REFRESH,
|
||||
}).component();
|
||||
this._refreshButton = this._view.modelBuilder.button()
|
||||
.withProps({
|
||||
iconWidth: '18px',
|
||||
iconHeight: '18px',
|
||||
iconPath: IconPathHelper.refresh,
|
||||
height: '18px',
|
||||
width: '18px',
|
||||
ariaLabel: constants.REFRESH,
|
||||
}).component();
|
||||
|
||||
this._disposables.push(this._refreshButton.onDidClick(async (e) => {
|
||||
this._connectionStatusLoader.loading = true;
|
||||
try {
|
||||
await this.loadStatus();
|
||||
} finally {
|
||||
this._connectionStatusLoader.loading = false;
|
||||
}
|
||||
}));
|
||||
this._disposables.push(
|
||||
this._refreshButton.onDidClick(
|
||||
async (e) => this.loadStatus()));
|
||||
|
||||
const connectionLabelContainer = this._view.modelBuilder.flexContainer().component();
|
||||
connectionLabelContainer.addItem(connectionStatusLabel, {
|
||||
flex: '0'
|
||||
});
|
||||
connectionLabelContainer.addItem(this._refreshButton, {
|
||||
flex: '0',
|
||||
CSSStyles: { 'margin-right': '10px' }
|
||||
});
|
||||
const connectionLabelContainer = this._view.modelBuilder.flexContainer()
|
||||
.component();
|
||||
connectionLabelContainer.addItem(
|
||||
connectionStatusLabel,
|
||||
{ flex: '0' });
|
||||
connectionLabelContainer.addItem(
|
||||
this._refreshButton,
|
||||
{ flex: '0', CSSStyles: { 'margin-right': '10px' } });
|
||||
|
||||
const statusContainer = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
const statusContainer = this._view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
|
||||
this._dmsStatusInfoBox = this._view.modelBuilder.infoBox().withProps({
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
style: 'error',
|
||||
text: '',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
}).component();
|
||||
this._dmsStatusInfoBox = this._view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
style: 'error',
|
||||
text: '',
|
||||
CSSStyles: { ...styles.BODY_CSS }
|
||||
}).component();
|
||||
|
||||
const authenticationKeysLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.AUTHENTICATION_KEYS,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS
|
||||
}
|
||||
}).component();
|
||||
const authenticationKeysLabel = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.AUTHENTICATION_KEYS,
|
||||
CSSStyles: { ...styles.LABEL_CSS }
|
||||
}).component();
|
||||
|
||||
this._copy1 = this._view.modelBuilder.button().withProps({
|
||||
title: constants.COPY_KEY1,
|
||||
iconPath: IconPathHelper.copy,
|
||||
ariaLabel: constants.COPY_KEY1,
|
||||
}).component();
|
||||
this._copy1 = this._view.modelBuilder.button()
|
||||
.withProps({
|
||||
title: constants.COPY_KEY1,
|
||||
iconPath: IconPathHelper.copy,
|
||||
ariaLabel: constants.COPY_KEY1,
|
||||
}).component();
|
||||
|
||||
this._disposables.push(this._copy1.onDidClick(async (e) => {
|
||||
await vscode.env.clipboard.writeText(<string>this._authKeyTable.dataValues![0][1].value);
|
||||
void vscode.window.showInformationMessage(constants.SERVICE_KEY1_COPIED_HELP);
|
||||
}));
|
||||
this._disposables.push(
|
||||
this._copy1.onDidClick(
|
||||
async (e) => {
|
||||
await vscode.env.clipboard.writeText(<string>this._authKeyTable.dataValues![0][1].value);
|
||||
void vscode.window.showInformationMessage(constants.SERVICE_KEY1_COPIED_HELP);
|
||||
}));
|
||||
|
||||
this._copy2 = this._view.modelBuilder.button().withProps({
|
||||
title: constants.COPY_KEY2,
|
||||
iconPath: IconPathHelper.copy,
|
||||
ariaLabel: constants.COPY_KEY2,
|
||||
}).component();
|
||||
this._copy2 = this._view.modelBuilder.button()
|
||||
.withProps({
|
||||
title: constants.COPY_KEY2,
|
||||
iconPath: IconPathHelper.copy,
|
||||
ariaLabel: constants.COPY_KEY2,
|
||||
}).component();
|
||||
|
||||
this._disposables.push(this._copy2.onDidClick(async (e) => {
|
||||
await vscode.env.clipboard.writeText(<string>this._authKeyTable.dataValues![1][1].value);
|
||||
void vscode.window.showInformationMessage(constants.SERVICE_KEY2_COPIED_HELP);
|
||||
}));
|
||||
this._disposables.push(
|
||||
this._copy2.onDidClick(async (e) => {
|
||||
await vscode.env.clipboard.writeText(<string>this._authKeyTable.dataValues![1][1].value);
|
||||
void vscode.window.showInformationMessage(constants.SERVICE_KEY2_COPIED_HELP);
|
||||
}));
|
||||
|
||||
this._refresh1 = this._view.modelBuilder.button().withProps({
|
||||
title: constants.REFRESH_KEY1,
|
||||
iconPath: IconPathHelper.refresh,
|
||||
ariaLabel: constants.REFRESH_KEY1,
|
||||
}).component();
|
||||
this._refresh1 = this._view.modelBuilder.button()
|
||||
.withProps({
|
||||
title: constants.REFRESH_KEY1,
|
||||
iconPath: IconPathHelper.refresh,
|
||||
ariaLabel: constants.REFRESH_KEY1,
|
||||
}).component();
|
||||
|
||||
this._refresh2 = this._view.modelBuilder.button().withProps({
|
||||
title: constants.REFRESH_KEY2,
|
||||
iconPath: IconPathHelper.refresh,
|
||||
ariaLabel: constants.REFRESH_KEY2,
|
||||
}).component();
|
||||
this._refresh2 = this._view.modelBuilder.button()
|
||||
.withProps({
|
||||
title: constants.REFRESH_KEY2,
|
||||
iconPath: IconPathHelper.refresh,
|
||||
ariaLabel: constants.REFRESH_KEY2,
|
||||
}).component();
|
||||
|
||||
this._authKeyTable = createAuthenticationKeyTable(this._view);
|
||||
|
||||
statusContainer.addItems([
|
||||
this._dmsStatusInfoBox,
|
||||
authenticationKeysLabel,
|
||||
this._authKeyTable
|
||||
]);
|
||||
this._authKeyTable]);
|
||||
|
||||
this._connectionStatusLoader = this._view.modelBuilder.loadingComponent().withItem(
|
||||
statusContainer
|
||||
).withProps({
|
||||
loading: false
|
||||
}).component();
|
||||
this._connectionStatusLoader = this._view.modelBuilder.loadingComponent()
|
||||
.withItem(statusContainer)
|
||||
.withProps({ loading: false })
|
||||
.component();
|
||||
|
||||
container.addItems(
|
||||
[
|
||||
connectionLabelContainer,
|
||||
this._connectionStatusLoader
|
||||
]
|
||||
);
|
||||
container.addItems([
|
||||
connectionLabelContainer,
|
||||
this._connectionStatusLoader]);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
public async loadResourceGroupDropdown(): Promise<void> {
|
||||
this._resourceGroupDropdown.loading = true;
|
||||
this._dmsDropdown.loading = true;
|
||||
try {
|
||||
this.migrationStateModel._sqlMigrationServices = await utils.getAzureSqlMigrationServices(this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription);
|
||||
this.migrationStateModel._resourceGroups = await utils.getSqlMigrationServiceResourceGroups(this.migrationStateModel._sqlMigrationServices, this.migrationStateModel._location);
|
||||
this._resourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this.migrationStateModel._resourceGroups);
|
||||
const resourceGroup = (this.migrationStateModel._sqlMigrationService)
|
||||
this._resourceGroupDropdown.loading = true;
|
||||
this._dmsDropdown.loading = true;
|
||||
|
||||
this.migrationStateModel._sqlMigrationServices = await utils.getAzureSqlMigrationServices(
|
||||
this.migrationStateModel._azureAccount,
|
||||
this.migrationStateModel._targetSubscription);
|
||||
|
||||
this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation(
|
||||
this.migrationStateModel._sqlMigrationServices,
|
||||
this.migrationStateModel._location);
|
||||
|
||||
this._resourceGroupDropdown.values = utils.getResourceDropdownValues(
|
||||
this.migrationStateModel._resourceGroups,
|
||||
constants.RESOURCE_GROUP_NOT_FOUND);
|
||||
|
||||
const resourceGroup = this.migrationStateModel._sqlMigrationService
|
||||
? getFullResourceGroupFromId(this.migrationStateModel._sqlMigrationService?.id)
|
||||
: undefined;
|
||||
utils.selectDefaultDropdownValue(this._resourceGroupDropdown, resourceGroup, false);
|
||||
} finally {
|
||||
this._dmsDropdown.loading = false;
|
||||
this._resourceGroupDropdown.loading = false;
|
||||
this._dmsDropdown.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async populateDms(): Promise<void> {
|
||||
this._dmsDropdown.loading = true;
|
||||
public populateDms(): void {
|
||||
try {
|
||||
this._dmsDropdown.values = await utils.getAzureSqlMigrationServicesDropdownValues(this.migrationStateModel._sqlMigrationServices, this.migrationStateModel._location, this.migrationStateModel._sqlMigrationServiceResourceGroup);
|
||||
utils.selectDefaultDropdownValue(this._dmsDropdown, this.migrationStateModel._sqlMigrationService?.id, false);
|
||||
this._dmsDropdown.loading = true;
|
||||
this._dmsDropdown.values = utils.getAzureResourceDropdownValues(
|
||||
this.migrationStateModel._sqlMigrationServices,
|
||||
this.migrationStateModel._location,
|
||||
this.migrationStateModel._sqlMigrationServiceResourceGroup.name,
|
||||
constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR);
|
||||
|
||||
utils.selectDefaultDropdownValue(
|
||||
this._dmsDropdown,
|
||||
this.migrationStateModel._sqlMigrationService?.id,
|
||||
false);
|
||||
} finally {
|
||||
this._dmsDropdown.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadMigrationServiceStatus(): Promise<void> {
|
||||
this._statusLoadingComponent.loading = true;
|
||||
try {
|
||||
await this.loadStatus();
|
||||
} catch (error) {
|
||||
logError(TelemetryViews.MigrationWizardIntegrationRuntimePage, 'ErrorLoadingMigrationServiceStatus', error);
|
||||
} finally {
|
||||
this._statusLoadingComponent.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadStatus(): Promise<void> {
|
||||
try {
|
||||
this._statusLoadingComponent.loading = true;
|
||||
|
||||
if (this.migrationStateModel._sqlMigrationService) {
|
||||
const migrationService = await getSqlMigrationService(
|
||||
this.migrationStateModel._azureAccount,
|
||||
@@ -436,12 +439,15 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
|
||||
const state = migrationService.properties.integrationRuntimeState;
|
||||
if (state === 'Online') {
|
||||
await this._dmsStatusInfoBox.updateProperties(<azdata.InfoBoxComponentProperties>{
|
||||
text: constants.SERVICE_READY(this.migrationStateModel._sqlMigrationService!.name, this.migrationStateModel._nodeNames.join(', ')),
|
||||
text: constants.SERVICE_READY(
|
||||
this.migrationStateModel._sqlMigrationService!.name,
|
||||
this.migrationStateModel._nodeNames.join(', ')),
|
||||
style: 'success'
|
||||
});
|
||||
} else {
|
||||
await this._dmsStatusInfoBox.updateProperties(<azdata.InfoBoxComponentProperties>{
|
||||
text: constants.SERVICE_NOT_READY(this.migrationStateModel._sqlMigrationService!.name),
|
||||
text: constants.SERVICE_NOT_READY(
|
||||
this.migrationStateModel._sqlMigrationService!.name),
|
||||
style: 'error'
|
||||
});
|
||||
}
|
||||
@@ -464,65 +470,49 @@ export class IntergrationRuntimePage extends MigrationWizardPage {
|
||||
.withItems([this._copy2, this._refresh2])
|
||||
.component()
|
||||
}
|
||||
]
|
||||
];
|
||||
]];
|
||||
|
||||
await this._authKeyTable.setDataValues(data);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.IntegrationRuntimePage, 'ErrorLoadingStatus', e);
|
||||
} finally {
|
||||
this._statusLoadingComponent.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createAuthenticationKeyTable(view: azdata.ModelView,): azdata.DeclarativeTableComponent {
|
||||
const authKeyTable = view.modelBuilder.declarativeTable().withProps({
|
||||
ariaLabel: constants.DATABASE_MIGRATION_SERVICE_AUTHENTICATION_KEYS,
|
||||
columns: [
|
||||
{
|
||||
displayName: constants.NAME,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '50px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: {
|
||||
...styles.BODY_CSS
|
||||
const authKeyTable = view.modelBuilder.declarativeTable()
|
||||
.withProps({
|
||||
ariaLabel: constants.DATABASE_MIGRATION_SERVICE_AUTHENTICATION_KEYS,
|
||||
columns: [
|
||||
{
|
||||
displayName: constants.NAME,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '50px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: { ...styles.BODY_CSS },
|
||||
headerCssStyles: { ...styles.BODY_CSS, 'font-weight': '600' }
|
||||
},
|
||||
headerCssStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'font-weight': '600'
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: constants.AUTH_KEY_COLUMN_HEADER,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '500px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: {
|
||||
...styles.BODY_CSS,
|
||||
|
||||
{
|
||||
displayName: constants.AUTH_KEY_COLUMN_HEADER,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: '500px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: { ...styles.BODY_CSS },
|
||||
headerCssStyles: { ...styles.BODY_CSS, 'font-weight': '600' }
|
||||
},
|
||||
headerCssStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'font-weight': '600'
|
||||
{
|
||||
displayName: '',
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
width: '30px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: { ...styles.BODY_CSS },
|
||||
headerCssStyles: { ...styles.BODY_CSS }
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: '',
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
width: '30px',
|
||||
isReadOnly: true,
|
||||
rowCssStyles: {
|
||||
...styles.BODY_CSS
|
||||
},
|
||||
headerCssStyles: {
|
||||
...styles.BODY_CSS
|
||||
}
|
||||
}
|
||||
],
|
||||
CSSStyles: {
|
||||
'margin-top': '5px',
|
||||
'width': WIZARD_INPUT_COMPONENT_WIDTH
|
||||
}
|
||||
}).component();
|
||||
],
|
||||
CSSStyles: { 'margin-top': '5px', 'width': WIZARD_INPUT_COMPONENT_WIDTH }
|
||||
}).component();
|
||||
return authKeyTable;
|
||||
}
|
||||
|
||||
@@ -6,17 +6,22 @@
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationMode, MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
|
||||
import { MigrationMode, MigrationStateModel, MigrationTargetType, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../constants/strings';
|
||||
import * as styles from '../constants/styles';
|
||||
|
||||
export class MigrationModePage extends MigrationWizardPage {
|
||||
private _view!: azdata.ModelView;
|
||||
private originalMigrationMode!: MigrationMode;
|
||||
private _onlineButton!: azdata.RadioButtonComponent;
|
||||
private _offlineButton!: azdata.RadioButtonComponent;
|
||||
private _originalMigrationMode!: MigrationMode;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||
super(wizard, azdata.window.createWizardPage(constants.DATABASE_BACKUP_MIGRATION_MODE_LABEL, 'MigrationModePage'), migrationStateModel);
|
||||
super(
|
||||
wizard,
|
||||
azdata.window.createWizardPage(constants.DATABASE_BACKUP_MIGRATION_MODE_LABEL, 'MigrationModePage'),
|
||||
migrationStateModel);
|
||||
this.migrationStateModel._databaseBackup.migrationMode = this.migrationStateModel._databaseBackup.migrationMode || MigrationMode.ONLINE;
|
||||
}
|
||||
|
||||
@@ -25,115 +30,103 @@ export class MigrationModePage extends MigrationWizardPage {
|
||||
|
||||
const pageDescription = {
|
||||
title: '',
|
||||
component: view.modelBuilder.text().withProps({
|
||||
value: constants.DATABASE_BACKUP_MIGRATION_MODE_DESCRIPTION,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0'
|
||||
}
|
||||
}).component()
|
||||
component: view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.DATABASE_BACKUP_MIGRATION_MODE_DESCRIPTION,
|
||||
CSSStyles: { ...styles.BODY_CSS, 'margin': '0' }
|
||||
}).component()
|
||||
};
|
||||
|
||||
const form = view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
[
|
||||
pageDescription,
|
||||
this.migrationModeContainer(),
|
||||
]
|
||||
).withProps({
|
||||
CSSStyles: {
|
||||
'padding-top': '0'
|
||||
}
|
||||
}).component();
|
||||
.withFormItems([
|
||||
pageDescription,
|
||||
this.migrationModeContainer()])
|
||||
.withProps({ CSSStyles: { 'padding-top': '0' } })
|
||||
.component();
|
||||
|
||||
this._disposables.push(
|
||||
this._view.onClosed(
|
||||
e => this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } })));
|
||||
|
||||
this._disposables.push(this._view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
await view.initializeModel(form);
|
||||
}
|
||||
|
||||
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
|
||||
this.originalMigrationMode = this.migrationStateModel._databaseBackup.migrationMode;
|
||||
this.wizard.registerNavigationValidator((e) => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
|
||||
if (this.originalMigrationMode !== this.migrationStateModel._databaseBackup.migrationMode) {
|
||||
this.migrationStateModel.refreshDatabaseBackupPage = true;
|
||||
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.wizard.registerNavigationValidator((e) => {
|
||||
return true;
|
||||
});
|
||||
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
|
||||
this._onlineButton.enabled = !isSqlDbTarget;
|
||||
if (isSqlDbTarget) {
|
||||
this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.OFFLINE;
|
||||
this._offlineButton.checked = true;
|
||||
await this._offlineButton.focus();
|
||||
}
|
||||
this._originalMigrationMode = this.migrationStateModel._databaseBackup.migrationMode;
|
||||
this.wizard.registerNavigationValidator((e) => true);
|
||||
}
|
||||
|
||||
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
|
||||
if (this._originalMigrationMode !== this.migrationStateModel._databaseBackup.migrationMode) {
|
||||
this.migrationStateModel.refreshDatabaseBackupPage = true;
|
||||
}
|
||||
this.wizard.registerNavigationValidator((e) => true);
|
||||
}
|
||||
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
}
|
||||
|
||||
private migrationModeContainer(): azdata.FormComponent {
|
||||
const buttonGroup = 'migrationMode';
|
||||
this._onlineButton = this._view.modelBuilder.radioButton()
|
||||
.withProps({
|
||||
label: constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL,
|
||||
name: buttonGroup,
|
||||
checked: this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.ONLINE,
|
||||
CSSStyles: { ...styles.LABEL_CSS, },
|
||||
}).component();
|
||||
const onlineDescription = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_DESCRIPTION,
|
||||
CSSStyles: { ...styles.NOTE_CSS, 'margin-left': '20px' }
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
this._onlineButton.onDidChangeCheckedState(checked => {
|
||||
if (checked) {
|
||||
this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.ONLINE;
|
||||
}
|
||||
}));
|
||||
|
||||
const onlineButton = this._view.modelBuilder.radioButton().withProps({
|
||||
label: constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL,
|
||||
name: buttonGroup,
|
||||
checked: this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.ONLINE,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
},
|
||||
}).component();
|
||||
this._offlineButton = this._view.modelBuilder.radioButton()
|
||||
.withProps({
|
||||
label: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL,
|
||||
name: buttonGroup,
|
||||
checked: this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE,
|
||||
CSSStyles: { ...styles.LABEL_CSS, 'margin-top': '12px' },
|
||||
}).component();
|
||||
const offlineDescription = this._view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_DESCRIPTION,
|
||||
CSSStyles: { ...styles.NOTE_CSS, 'margin-left': '20px' }
|
||||
}).component();
|
||||
this._disposables.push(
|
||||
this._offlineButton.onDidChangeCheckedState(checked => {
|
||||
if (checked) {
|
||||
this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.OFFLINE;
|
||||
}
|
||||
}));
|
||||
|
||||
const onlineDescription = this._view.modelBuilder.text().withProps({
|
||||
value: constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_DESCRIPTION,
|
||||
CSSStyles: {
|
||||
...styles.NOTE_CSS,
|
||||
'margin-left': '20px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._disposables.push(onlineButton.onDidChangeCheckedState((e) => {
|
||||
if (e) {
|
||||
this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.ONLINE;
|
||||
}
|
||||
}));
|
||||
|
||||
const offlineButton = this._view.modelBuilder.radioButton().withProps({
|
||||
label: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL,
|
||||
name: buttonGroup,
|
||||
checked: this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin-top': '12px'
|
||||
},
|
||||
}).component();
|
||||
|
||||
const offlineDescription = this._view.modelBuilder.text().withProps({
|
||||
value: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_DESCRIPTION,
|
||||
CSSStyles: {
|
||||
...styles.NOTE_CSS,
|
||||
'margin-left': '20px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._disposables.push(offlineButton.onDidChangeCheckedState((e) => {
|
||||
if (e) {
|
||||
this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.OFFLINE;
|
||||
}
|
||||
}));
|
||||
|
||||
const flexContainer = this._view.modelBuilder.flexContainer().withItems(
|
||||
[
|
||||
onlineButton,
|
||||
const flexContainer = this._view.modelBuilder.flexContainer()
|
||||
.withItems([
|
||||
this._onlineButton,
|
||||
onlineDescription,
|
||||
offlineButton,
|
||||
offlineDescription
|
||||
]
|
||||
).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
this._offlineButton,
|
||||
offlineDescription]
|
||||
).withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
|
||||
return {
|
||||
component: flexContainer
|
||||
};
|
||||
return { component: flexContainer };
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationSourceAuthenticationType, MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../constants/strings';
|
||||
import { createLabelTextComponent, createHeadingTextComponent, WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
|
||||
import { AuthenticationType } from '../api/sqlUtils';
|
||||
|
||||
export class SqlSourceConfigurationPage extends MigrationWizardPage {
|
||||
private _view!: azdata.ModelView;
|
||||
@@ -59,10 +60,13 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage {
|
||||
const query = 'select SUSER_NAME()';
|
||||
const results = await queryProvider.runQueryAndReturn(await (azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId)), query);
|
||||
const username = results.rows[0][0].displayValue;
|
||||
this.migrationStateModel._authenticationType = connectionProfile.authenticationType === 'SqlLogin' ? MigrationSourceAuthenticationType.Sql : connectionProfile.authenticationType === 'Integrated' ? MigrationSourceAuthenticationType.Integrated : undefined!;
|
||||
this.migrationStateModel._authenticationType = connectionProfile.authenticationType === AuthenticationType.SqlLogin
|
||||
? MigrationSourceAuthenticationType.Sql
|
||||
: connectionProfile.authenticationType === AuthenticationType.Integrated
|
||||
? MigrationSourceAuthenticationType.Integrated
|
||||
: undefined!;
|
||||
|
||||
const sourceCredText = await createHeadingTextComponent(this._view, constants.SOURCE_CREDENTIALS);
|
||||
|
||||
const enterYourCredText = createLabelTextComponent(
|
||||
this._view,
|
||||
constants.ENTER_YOUR_SQL_CREDS,
|
||||
|
||||
@@ -24,134 +24,204 @@ export class SummaryPage extends MigrationWizardPage {
|
||||
|
||||
protected async registerContent(view: azdata.ModelView): Promise<void> {
|
||||
this._view = view;
|
||||
this._flexContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
this._flexContainer = view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
const form = view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
[
|
||||
{
|
||||
component: this._flexContainer
|
||||
}
|
||||
]
|
||||
);
|
||||
.withFormItems([{ component: this._flexContainer }])
|
||||
.component();
|
||||
|
||||
this._disposables.push(this._view.onClosed(e => {
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } });
|
||||
}));
|
||||
this._disposables.push(
|
||||
this._view.onClosed(e =>
|
||||
this._disposables.forEach(
|
||||
d => { try { d.dispose(); } catch { } })));
|
||||
|
||||
await view.initializeModel(form.component());
|
||||
await view.initializeModel(form);
|
||||
}
|
||||
|
||||
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
|
||||
const targetDatabaseSummary = new TargetDatabaseSummaryDialog(this.migrationStateModel);
|
||||
const targetDatabaseHyperlink = this._view.modelBuilder.hyperlink().withProps({
|
||||
url: '',
|
||||
label: this.migrationStateModel._databasesForMigration?.length.toString(),
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0px',
|
||||
'width': '300px',
|
||||
}
|
||||
}).component();
|
||||
const isSqlVmTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLVM;
|
||||
const isSqlMiTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLMI;
|
||||
const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB;
|
||||
const isNetworkShare = this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE;
|
||||
|
||||
this._disposables.push(targetDatabaseHyperlink.onDidClick(async e => {
|
||||
await targetDatabaseSummary.initialize();
|
||||
}));
|
||||
const targetDatabaseHyperlink = this._view.modelBuilder.hyperlink()
|
||||
.withProps({
|
||||
url: '',
|
||||
label: (this.migrationStateModel._databasesForMigration?.length ?? 0).toString(),
|
||||
CSSStyles: { ...styles.BODY_CSS, 'margin': '0px', 'width': '300px', }
|
||||
}).component();
|
||||
|
||||
this._disposables.push(
|
||||
targetDatabaseHyperlink.onDidClick(
|
||||
async e => await targetDatabaseSummary.initialize()));
|
||||
|
||||
const targetDatabaseRow = this._view.modelBuilder.flexContainer()
|
||||
.withLayout(
|
||||
{
|
||||
flexFlow: 'row',
|
||||
alignItems: 'center',
|
||||
})
|
||||
.withItems(
|
||||
[
|
||||
createLabelTextComponent(this._view, constants.SUMMARY_DATABASE_COUNT_LABEL,
|
||||
{
|
||||
...styles.BODY_CSS,
|
||||
'width': '300px',
|
||||
}
|
||||
),
|
||||
targetDatabaseHyperlink
|
||||
],
|
||||
{
|
||||
CSSStyles: {
|
||||
'margin-right': '5px'
|
||||
}
|
||||
})
|
||||
.withLayout({ flexFlow: 'row', alignItems: 'center', })
|
||||
.withItems([
|
||||
createLabelTextComponent(
|
||||
this._view,
|
||||
constants.SUMMARY_DATABASE_COUNT_LABEL,
|
||||
{ ...styles.BODY_CSS, 'width': '300px' }),
|
||||
targetDatabaseHyperlink],
|
||||
{ CSSStyles: { 'margin-right': '5px' } })
|
||||
.component();
|
||||
|
||||
this._flexContainer.addItems(
|
||||
[
|
||||
await createHeadingTextComponent(this._view, constants.ACCOUNTS_SELECTION_PAGE_TITLE, true),
|
||||
createInformationRow(this._view, constants.ACCOUNTS_SELECTION_PAGE_TITLE, this.migrationStateModel._azureAccount.displayInfo.displayName),
|
||||
|
||||
await createHeadingTextComponent(this._view, constants.SOURCE_DATABASES),
|
||||
this._flexContainer
|
||||
.addItems([
|
||||
await createHeadingTextComponent(
|
||||
this._view,
|
||||
constants.SOURCE_DATABASES),
|
||||
targetDatabaseRow,
|
||||
|
||||
await createHeadingTextComponent(this._view, constants.AZURE_SQL_TARGET_PAGE_TITLE),
|
||||
createInformationRow(this._view, constants.AZURE_SQL_TARGET_PAGE_TITLE, (this.migrationStateModel._targetType === MigrationTargetType.SQLVM) ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE),
|
||||
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name),
|
||||
createInformationRow(this._view, constants.LOCATION, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.location)),
|
||||
createInformationRow(this._view, constants.RESOURCE_GROUP, getResourceGroupFromId(this.migrationStateModel._targetServerInstance.id)),
|
||||
createInformationRow(this._view, (this.migrationStateModel._targetType === MigrationTargetType.SQLVM) ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.name!)),
|
||||
await createHeadingTextComponent(
|
||||
this._view,
|
||||
constants.AZURE_SQL_TARGET_PAGE_TITLE),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.ACCOUNTS_SELECTION_PAGE_TITLE,
|
||||
this.migrationStateModel._azureAccount.displayInfo.displayName),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.AZURE_SQL_TARGET_PAGE_TITLE,
|
||||
isSqlVmTarget
|
||||
? constants.SUMMARY_VM_TYPE
|
||||
: isSqlMiTarget
|
||||
? constants.SUMMARY_MI_TYPE
|
||||
: constants.SUMMARY_SQLDB_TYPE),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.SUBSCRIPTION,
|
||||
this.migrationStateModel._targetSubscription.name),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.LOCATION,
|
||||
await this.migrationStateModel.getLocationDisplayName(
|
||||
this.migrationStateModel._targetServerInstance.location)),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.RESOURCE_GROUP,
|
||||
getResourceGroupFromId(
|
||||
this.migrationStateModel._targetServerInstance.id)),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
(isSqlVmTarget)
|
||||
? constants.SUMMARY_VM_TYPE
|
||||
: (isSqlMiTarget)
|
||||
? constants.SUMMARY_MI_TYPE
|
||||
: constants.SUMMARY_SQLDB_TYPE,
|
||||
await this.migrationStateModel.getLocationDisplayName(
|
||||
this.migrationStateModel._targetServerInstance.name!)),
|
||||
await createHeadingTextComponent(
|
||||
this._view,
|
||||
constants.DATABASE_BACKUP_MIGRATION_MODE_LABEL),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.MODE,
|
||||
this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.ONLINE
|
||||
? constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL
|
||||
: constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL),
|
||||
]);
|
||||
|
||||
await createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_MIGRATION_MODE_LABEL),
|
||||
createInformationRow(this._view, constants.MODE, this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.ONLINE ? constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL : constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL),
|
||||
if (this.migrationStateModel._targetType !== MigrationTargetType.SQLDB) {
|
||||
this._flexContainer.addItems([
|
||||
await createHeadingTextComponent(
|
||||
this._view,
|
||||
constants.DATABASE_BACKUP_PAGE_TITLE),
|
||||
await this.createNetworkContainerRows()]);
|
||||
}
|
||||
|
||||
await createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_PAGE_TITLE),
|
||||
await this.createNetworkContainerRows(),
|
||||
this._flexContainer.addItems([
|
||||
|
||||
await createHeadingTextComponent(this._view, constants.IR_PAGE_TITLE),
|
||||
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name),
|
||||
createInformationRow(this._view, constants.LOCATION, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._sqlMigrationService?.location!)),
|
||||
createInformationRow(this._view, constants.RESOURCE_GROUP, this.migrationStateModel._sqlMigrationService?.properties?.resourceGroup!),
|
||||
createInformationRow(this._view, constants.IR_PAGE_TITLE, this.migrationStateModel._sqlMigrationService?.name!)
|
||||
]
|
||||
);
|
||||
|
||||
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE && this.migrationStateModel._nodeNames?.length > 0) {
|
||||
this._flexContainer.addItem(createInformationRow(this._view, constants.SHIR, this.migrationStateModel._nodeNames.join(', ')));
|
||||
await createHeadingTextComponent(
|
||||
this._view,
|
||||
constants.IR_PAGE_TITLE),
|
||||
createInformationRow(
|
||||
this._view, constants.SUBSCRIPTION,
|
||||
this.migrationStateModel._targetSubscription.name),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.LOCATION,
|
||||
await this.migrationStateModel.getLocationDisplayName(
|
||||
this.migrationStateModel._sqlMigrationService?.location!)),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.RESOURCE_GROUP,
|
||||
this.migrationStateModel._sqlMigrationService?.properties?.resourceGroup!),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.IR_PAGE_TITLE,
|
||||
this.migrationStateModel._sqlMigrationService?.name!)]);
|
||||
|
||||
if (isSqlDbTarget ||
|
||||
(isNetworkShare && this.migrationStateModel._nodeNames?.length > 0)) {
|
||||
|
||||
this._flexContainer.addItem(
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.SHIR,
|
||||
this.migrationStateModel._nodeNames.join(', ')));
|
||||
}
|
||||
}
|
||||
|
||||
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
|
||||
this._flexContainer.clearItems();
|
||||
this.wizard.registerNavigationValidator(async (pageChangeInfo) => {
|
||||
return true;
|
||||
});
|
||||
this.wizard.registerNavigationValidator(async (pageChangeInfo) => true);
|
||||
}
|
||||
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
}
|
||||
|
||||
private async createNetworkContainerRows(): Promise<azdata.FlexContainer> {
|
||||
const flexContainer = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
const flexContainer = this._view.modelBuilder.flexContainer()
|
||||
.withLayout({ flexFlow: 'column' })
|
||||
.component();
|
||||
|
||||
const networkShare = this.migrationStateModel._databaseBackup.networkShares[0];
|
||||
switch (this.migrationStateModel._databaseBackup.networkContainerType) {
|
||||
case NetworkContainerType.NETWORK_SHARE:
|
||||
flexContainer.addItems(
|
||||
[
|
||||
createInformationRow(this._view, constants.BACKUP_LOCATION, constants.NETWORK_SHARE),
|
||||
createInformationRow(this._view, constants.USER_ACCOUNT, this.migrationStateModel._databaseBackup.networkShares[0].windowsUser),
|
||||
await createHeadingTextComponent(this._view, constants.AZURE_STORAGE_ACCOUNT_TO_UPLOAD_BACKUPS),
|
||||
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name),
|
||||
createInformationRow(this._view, constants.LOCATION, this.migrationStateModel._databaseBackup.networkShares[0].storageAccount?.location),
|
||||
createInformationRow(this._view, constants.RESOURCE_GROUP, this.migrationStateModel._databaseBackup.networkShares[0].storageAccount?.resourceGroup!),
|
||||
createInformationRow(this._view, constants.STORAGE_ACCOUNT, this.migrationStateModel._databaseBackup.networkShares[0].storageAccount?.name!),
|
||||
]
|
||||
);
|
||||
flexContainer.addItems([
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.BACKUP_LOCATION,
|
||||
constants.NETWORK_SHARE),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.USER_ACCOUNT,
|
||||
networkShare.windowsUser),
|
||||
await createHeadingTextComponent(
|
||||
this._view,
|
||||
constants.AZURE_STORAGE_ACCOUNT_TO_UPLOAD_BACKUPS),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.SUBSCRIPTION,
|
||||
this.migrationStateModel._databaseBackup.subscription.name),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.LOCATION,
|
||||
networkShare.storageAccount?.location),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.RESOURCE_GROUP,
|
||||
networkShare.storageAccount?.resourceGroup!),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.STORAGE_ACCOUNT,
|
||||
networkShare.storageAccount?.name!),
|
||||
]);
|
||||
break;
|
||||
case NetworkContainerType.BLOB_CONTAINER:
|
||||
flexContainer.addItems(
|
||||
[
|
||||
createInformationRow(this._view, constants.TYPE, constants.BLOB_CONTAINER),
|
||||
createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name)
|
||||
]
|
||||
);
|
||||
flexContainer.addItems([
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.TYPE,
|
||||
constants.BLOB_CONTAINER),
|
||||
createInformationRow(
|
||||
this._view,
|
||||
constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION,
|
||||
this.migrationStateModel._databaseBackup.subscription.name)]);
|
||||
}
|
||||
return flexContainer;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@ import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError
|
||||
import * as styles from '../constants/styles';
|
||||
import { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage';
|
||||
import { azureResource } from 'azurecore';
|
||||
import { ServiceContextChangeEvent } from '../dashboard/tabBase';
|
||||
|
||||
export const WIZARD_INPUT_COMPONENT_WIDTH = '600px';
|
||||
export class WizardController {
|
||||
@@ -27,7 +28,7 @@ export class WizardController {
|
||||
constructor(
|
||||
private readonly extensionContext: vscode.ExtensionContext,
|
||||
private readonly _model: MigrationStateModel,
|
||||
private readonly _onClosedCallback: () => Promise<void>) {
|
||||
private readonly _serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>) {
|
||||
}
|
||||
|
||||
public async openWizard(connectionId: string): Promise<void> {
|
||||
@@ -40,7 +41,11 @@ export class WizardController {
|
||||
|
||||
private async createWizard(stateModel: MigrationStateModel): Promise<void> {
|
||||
const serverName = (await stateModel.getSourceConnectionProfile()).serverName;
|
||||
this._wizardObject = azdata.window.createWizard(loc.WIZARD_TITLE(serverName), 'MigrationWizard', 'wide');
|
||||
this._wizardObject = azdata.window.createWizard(
|
||||
loc.WIZARD_TITLE(serverName),
|
||||
'MigrationWizard',
|
||||
'wide');
|
||||
|
||||
this._wizardObject.generateScriptButton.enabled = false;
|
||||
this._wizardObject.generateScriptButton.hidden = true;
|
||||
const saveAndCloseButton = azdata.window.createButton(loc.SAVE_AND_CLOSE);
|
||||
@@ -60,8 +65,7 @@ export class WizardController {
|
||||
migrationModePage,
|
||||
databaseBackupPage,
|
||||
integrationRuntimePage,
|
||||
summaryPage
|
||||
];
|
||||
summaryPage];
|
||||
|
||||
this._wizardObject.pages = pages.map(p => p.getwizardPage());
|
||||
|
||||
@@ -82,20 +86,26 @@ export class WizardController {
|
||||
|
||||
// if the user selected network share and selected save & close afterwards, it should always return to the database backup page so that
|
||||
// the user can input their password again
|
||||
if (this._model.savedInfo.closedPage >= Page.DatabaseBackup && this._model.savedInfo.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
|
||||
if (this._model.savedInfo.closedPage >= Page.DatabaseBackup &&
|
||||
this._model.savedInfo.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
|
||||
wizardSetupPromises.push(this._wizardObject.setCurrentPage(Page.DatabaseBackup));
|
||||
} else {
|
||||
wizardSetupPromises.push(this._wizardObject.setCurrentPage(this._model.savedInfo.closedPage));
|
||||
}
|
||||
}
|
||||
|
||||
this._model.extensionContext.subscriptions.push(this._wizardObject.onPageChanged(async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => {
|
||||
const newPage = pageChangeInfo.newPage;
|
||||
const lastPage = pageChangeInfo.lastPage;
|
||||
this.sendPageButtonClickEvent(pageChangeInfo).catch(e => logError(TelemetryViews.MigrationWizardController, 'ErrorSendingPageButtonClick', e));
|
||||
await pages[lastPage]?.onPageLeave(pageChangeInfo);
|
||||
await pages[newPage]?.onPageEnter(pageChangeInfo);
|
||||
}));
|
||||
this._model.extensionContext.subscriptions.push(
|
||||
this._wizardObject.onPageChanged(
|
||||
async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => {
|
||||
const newPage = pageChangeInfo.newPage;
|
||||
const lastPage = pageChangeInfo.lastPage;
|
||||
this.sendPageButtonClickEvent(pageChangeInfo)
|
||||
.catch(e => logError(
|
||||
TelemetryViews.MigrationWizardController,
|
||||
'ErrorSendingPageButtonClick', e));
|
||||
await pages[lastPage]?.onPageLeave(pageChangeInfo);
|
||||
await pages[newPage]?.onPageEnter(pageChangeInfo);
|
||||
}));
|
||||
|
||||
this._wizardObject.registerNavigationValidator(async validator => {
|
||||
// const lastPage = validator.lastPage;
|
||||
@@ -110,50 +120,59 @@ export class WizardController {
|
||||
await Promise.all(wizardSetupPromises);
|
||||
this._model.extensionContext.subscriptions.push(
|
||||
this._wizardObject.onPageChanged(
|
||||
async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => {
|
||||
await pages[0].onPageEnter(pageChangeInfo);
|
||||
}));
|
||||
|
||||
this._disposables.push(saveAndCloseButton.onClick(async () => {
|
||||
await stateModel.saveInfo(serverName, this._wizardObject.currentPage);
|
||||
await this._wizardObject.close();
|
||||
|
||||
if (stateModel.performanceCollectionInProgress()) {
|
||||
void vscode.window.showInformationMessage(loc.SAVE_AND_CLOSE_POPUP);
|
||||
}
|
||||
}));
|
||||
|
||||
this._disposables.push(this._wizardObject.cancelButton.onClick(e => {
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.SqlMigrationWizard,
|
||||
TelemetryAction.PageButtonClick,
|
||||
{
|
||||
...this.getTelemetryProps(),
|
||||
'buttonPressed': TelemetryAction.Cancel,
|
||||
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
|
||||
}, {});
|
||||
}));
|
||||
|
||||
this._wizardObject.doneButton.label = loc.START_MIGRATION_TEXT;
|
||||
async (pageChangeInfo: azdata.window.WizardPageChangeInfo) =>
|
||||
await pages[0].onPageEnter(pageChangeInfo)));
|
||||
|
||||
this._disposables.push(
|
||||
this._wizardObject.doneButton.onClick(async (e) => {
|
||||
await stateModel.startMigration();
|
||||
await this.updateServiceContext(stateModel);
|
||||
await this._onClosedCallback();
|
||||
saveAndCloseButton.onClick(async () => {
|
||||
await stateModel.saveInfo(serverName, this._wizardObject.currentPage);
|
||||
await this._wizardObject.close();
|
||||
|
||||
if (stateModel.performanceCollectionInProgress()) {
|
||||
void vscode.window.showInformationMessage(loc.SAVE_AND_CLOSE_POPUP);
|
||||
}
|
||||
}));
|
||||
|
||||
this._disposables.push(
|
||||
this._wizardObject.cancelButton.onClick(e => {
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.SqlMigrationWizard,
|
||||
TelemetryAction.PageButtonClick,
|
||||
{
|
||||
...this.getTelemetryProps(),
|
||||
'buttonPressed': TelemetryAction.Done,
|
||||
'buttonPressed': TelemetryAction.Cancel,
|
||||
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
|
||||
}, {});
|
||||
},
|
||||
{});
|
||||
}));
|
||||
|
||||
this._wizardObject.doneButton.label = loc.START_MIGRATION_TEXT;
|
||||
|
||||
this._disposables.push(
|
||||
this._wizardObject.doneButton.onClick(async (e) => {
|
||||
try {
|
||||
await stateModel.startMigration();
|
||||
await this.updateServiceContext(stateModel, this._serviceContextChangedEvent);
|
||||
} catch (e) {
|
||||
logError(TelemetryViews.MigrationWizardController, 'StartMigrationFailed', e);
|
||||
} finally {
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.SqlMigrationWizard,
|
||||
TelemetryAction.PageButtonClick,
|
||||
{
|
||||
...this.getTelemetryProps(),
|
||||
'buttonPressed': TelemetryAction.Done,
|
||||
'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title
|
||||
},
|
||||
{});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private async updateServiceContext(stateModel: MigrationStateModel): Promise<void> {
|
||||
private async updateServiceContext(
|
||||
stateModel: MigrationStateModel,
|
||||
serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>): Promise<void> {
|
||||
|
||||
const resourceGroup = this._getResourceGroupByName(
|
||||
stateModel._resourceGroups,
|
||||
stateModel._sqlMigrationService?.properties.resourceGroup);
|
||||
@@ -174,18 +193,28 @@ export class WizardController {
|
||||
location: location,
|
||||
resourceGroup: resourceGroup,
|
||||
migrationService: stateModel._sqlMigrationService,
|
||||
});
|
||||
},
|
||||
serviceContextChangedEvent);
|
||||
}
|
||||
|
||||
private _getResourceGroupByName(resourceGroups: azureResource.AzureResourceResourceGroup[], displayName?: string): azureResource.AzureResourceResourceGroup | undefined {
|
||||
private _getResourceGroupByName(
|
||||
resourceGroups: azureResource.AzureResourceResourceGroup[],
|
||||
displayName?: string): azureResource.AzureResourceResourceGroup | undefined {
|
||||
|
||||
return resourceGroups.find(rg => rg.name === displayName);
|
||||
}
|
||||
|
||||
private _getLocationByValue(locations: azureResource.AzureLocation[], name?: string): azureResource.AzureLocation | undefined {
|
||||
private _getLocationByValue(
|
||||
locations: azureResource.AzureLocation[],
|
||||
name?: string): azureResource.AzureLocation | undefined {
|
||||
|
||||
return locations.find(loc => loc.name === name);
|
||||
}
|
||||
|
||||
private _getSubscriptionFromResourceId(subscriptions: azureResource.AzureResourceSubscription[], resourceId?: string): azureResource.AzureResourceSubscription | undefined {
|
||||
private _getSubscriptionFromResourceId(
|
||||
subscriptions: azureResource.AzureResourceSubscription[],
|
||||
resourceId?: string): azureResource.AzureResourceSubscription | undefined {
|
||||
|
||||
let parts = resourceId?.split('/subscriptions/');
|
||||
if (parts?.length && parts?.length > 1) {
|
||||
parts = parts[1]?.split('/resourcegroups/');
|
||||
@@ -198,7 +227,9 @@ export class WizardController {
|
||||
}
|
||||
|
||||
private async sendPageButtonClickEvent(pageChangeInfo: azdata.window.WizardPageChangeInfo) {
|
||||
const buttonPressed = pageChangeInfo.newPage > pageChangeInfo.lastPage ? TelemetryAction.Next : TelemetryAction.Prev;
|
||||
const buttonPressed = pageChangeInfo.newPage > pageChangeInfo.lastPage
|
||||
? TelemetryAction.Next
|
||||
: TelemetryAction.Prev;
|
||||
const pageTitle = this._wizardObject.pages[pageChangeInfo.lastPage]?.title;
|
||||
sendSqlMigrationActionEvent(
|
||||
TelemetryViews.SqlMigrationWizard,
|
||||
@@ -207,7 +238,8 @@ export class WizardController {
|
||||
...this.getTelemetryProps(),
|
||||
'buttonPressed': buttonPressed,
|
||||
'pageTitle': pageTitle
|
||||
}, {});
|
||||
},
|
||||
{});
|
||||
}
|
||||
|
||||
private getTelemetryProps() {
|
||||
@@ -221,33 +253,38 @@ export class WizardController {
|
||||
}
|
||||
}
|
||||
|
||||
export function createInformationRow(view: azdata.ModelView, label: string, value: string): azdata.FlexContainer {
|
||||
export function createInformationRow(
|
||||
view: azdata.ModelView,
|
||||
label: string,
|
||||
value: string): azdata.FlexContainer {
|
||||
|
||||
return view.modelBuilder.flexContainer()
|
||||
.withLayout(
|
||||
{
|
||||
flexFlow: 'row',
|
||||
alignItems: 'center',
|
||||
})
|
||||
.withItems(
|
||||
[
|
||||
createLabelTextComponent(view, label,
|
||||
{
|
||||
...styles.BODY_CSS,
|
||||
'margin': '4px 0px',
|
||||
'width': '300px',
|
||||
}
|
||||
),
|
||||
createTextComponent(view, value,
|
||||
{
|
||||
...styles.BODY_CSS,
|
||||
'margin': '4px 0px',
|
||||
'width': '300px',
|
||||
}
|
||||
)
|
||||
]).component();
|
||||
.withLayout({ flexFlow: 'row', alignItems: 'center', })
|
||||
.withItems([
|
||||
createLabelTextComponent(
|
||||
view,
|
||||
label,
|
||||
{
|
||||
...styles.BODY_CSS,
|
||||
'margin': '4px 0px',
|
||||
'width': '300px',
|
||||
}),
|
||||
createTextComponent(
|
||||
view,
|
||||
value,
|
||||
{
|
||||
...styles.BODY_CSS,
|
||||
'margin': '4px 0px',
|
||||
'width': '300px',
|
||||
})])
|
||||
.component();
|
||||
}
|
||||
|
||||
export async function createHeadingTextComponent(view: azdata.ModelView, value: string, firstElement: boolean = false): Promise<azdata.TextComponent> {
|
||||
export async function createHeadingTextComponent(
|
||||
view: azdata.ModelView,
|
||||
value: string,
|
||||
firstElement: boolean = false): Promise<azdata.TextComponent> {
|
||||
|
||||
const component = createTextComponent(view, value);
|
||||
await component.updateCssStyles({
|
||||
...styles.LABEL_CSS,
|
||||
@@ -256,14 +293,20 @@ export async function createHeadingTextComponent(view: azdata.ModelView, value:
|
||||
return component;
|
||||
}
|
||||
|
||||
export function createLabelTextComponent(view: azdata.ModelView, value: string, styles: { [key: string]: string; } = { 'width': '300px' }): azdata.TextComponent {
|
||||
const component = createTextComponent(view, value, styles);
|
||||
return component;
|
||||
export function createLabelTextComponent(
|
||||
view: azdata.ModelView,
|
||||
value: string,
|
||||
styles: { [key: string]: string; } = { 'width': '300px' }): azdata.TextComponent {
|
||||
|
||||
return createTextComponent(view, value, styles);
|
||||
}
|
||||
|
||||
export function createTextComponent(view: azdata.ModelView, value: string, styles: { [key: string]: string; } = { 'width': '300px' }): azdata.TextComponent {
|
||||
return view.modelBuilder.text().withProps({
|
||||
value: value,
|
||||
CSSStyles: styles
|
||||
}).component();
|
||||
export function createTextComponent(
|
||||
view: azdata.ModelView,
|
||||
value: string,
|
||||
styles: { [key: string]: string; } = { 'width': '300px' }): azdata.TextComponent {
|
||||
|
||||
return view.modelBuilder.text()
|
||||
.withProps({ value: value, CSSStyles: styles })
|
||||
.component();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user