diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index 50eacfc415..81f1ec3a3f 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -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 { @@ -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 { + 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 { + 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 { 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 { 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 { +export async function startDatabaseMigration( + account: azdata.Account, + subscription: Subscription, + regionName: string, + targetServer: SqlManagedInstance | SqlVMServer | AzureSqlDatabaseServer, + targetDatabaseName: string, + requestBody: StartDatabaseMigrationRequest, + sessionId: string): Promise { + 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 { diff --git a/extensions/sql-migration/src/api/sqlUtils.ts b/extensions/sql-migration/src/api/sqlUtils.ts new file mode 100644 index 0000000000..98e1d7412e --- /dev/null +++ b/extensions/sql-migration/src/api/sqlUtils.ts @@ -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; + targetTables: Map; +} + +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 { + const ownerUri = await azdata.connection.getUriForConnection(sourceConnectionId); + const connectionProvider = azdata.dataprotocol.getProvider( + 'MSSQL', + azdata.DataProviderType.ConnectionProvider); + await connectionProvider.changeDatabase(ownerUri, sourceDatabase); + const queryProvider = azdata.dataprotocol.getProvider( + '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 { + 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( + '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 { + + 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( + '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 { + 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'; +} diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts index 890e83183a..86668dad7b 100644 --- a/extensions/sql-migration/src/api/utils.ts +++ b/extensions/sql-migration/src/api/utils.ts @@ -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(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 { - 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 { @@ -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 { - let locations: azureResource.AzureLocation[] = []; +export async function getResourceLocations( + account?: Account, + subscription?: azureResource.AzureResourceSubscription, + resources?: { location: string }[]): Promise { + 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 { - 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 { - 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 { - 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 { let resourceGroups: azureResource.AzureResourceResourceGroup[] = []; - try { - if (managedInstances && location) { - resourceGroups = managedInstances - .filter((mi) => mi.location.toLowerCase() === location.name.toLowerCase()) - .map((mi) => { - return { - 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 { + 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 { - let resourceGroups: azureResource.AzureResourceResourceGroup[] = []; - try { - if (virtualMachines && location) { - resourceGroups = virtualMachines - .filter((vm) => vm.location.toLowerCase() === location.name.toLowerCase()) - .map((vm) => { - return { - 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 { - let resourceGroups: azureResource.AzureResourceResourceGroup[] = []; - try { - if (storageAccounts && location) { - resourceGroups = storageAccounts - .filter((sa) => sa.location.toLowerCase() === location.name.toLowerCase()) - .map((sa) => { - return { - 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 { - 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 { - 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 { @@ -622,25 +503,6 @@ export async function getAllResourceGroups(account?: Account, subscription?: azu return resourceGroups; } -export async function getAzureResourceGroupsDropdownValues(resourceGroups: azureResource.AzureResourceResourceGroup[]): Promise { - 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 { 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 { + 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 { + 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 { 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 { - 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 { 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 { - 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 { - 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 { - 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 { @@ -816,25 +632,6 @@ export async function getBlobContainer(account?: Account, subscription?: azureRe return blobContainers; } -export async function getBlobContainersValues(blobContainers: azureResource.BlobContainer[]): Promise { - 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 { 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 { - 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 { + 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 { + 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 { + 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 { + 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 { - 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 ''; } diff --git a/extensions/sql-migration/src/constants/helper.ts b/extensions/sql-migration/src/constants/helper.ts index 90d032b9a7..afaeabc2be 100644 --- a/extensions/sql-migration/src/constants/helper.ts +++ b/extensions/sql-migration/src/constants/helper.ts @@ -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 { diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 2e29a35b61..b9b2d08700 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -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 { [key: string]: T; } - export const StatusLookup: LookupTable = { [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 { diff --git a/extensions/sql-migration/src/dashboard/DashboardStatusBar.ts b/extensions/sql-migration/src/dashboard/DashboardStatusBar.ts new file mode 100644 index 0000000000..52ed5e66c1 --- /dev/null +++ b/extensions/sql-migration/src/dashboard/DashboardStatusBar.ts @@ -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 = new vscode.EventEmitter(); + private _disposables: vscode.Disposable[] = []; + + constructor(context: vscode.ExtensionContext, connectionId: string, statusInfoBox: azdata.InfoBoxComponent, errorEvent: vscode.EventEmitter) { + 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 { + 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 { + await this._updateStatusDisplay(this._statusInfoBox, false); + this._errorTitle = ''; + this._errorLabel = ''; + this._errorDescription = ''; + this._statusInfoBox.style = 'success'; + this._statusInfoBox.text = ''; + } + + public async openErrorDialog(): Promise { + 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 { + await control.updateCssStyles({ 'display': visible ? 'inline' : 'none' }); + } +} diff --git a/extensions/sql-migration/src/dashboard/dashboardTab.ts b/extensions/sql-migration/src/dashboard/dashboardTab.ts index 4d91decc68..172535d94b 100644 --- a/extensions/sql-migration/src/dashboard/dashboardTab.ts +++ b/extensions/sql-migration/src/dashboard/dashboardTab.ts @@ -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 { this.icon = IconPathHelper.sqlMigrationLogo; } - public onDialogClosed = async (): Promise => - await this.updateServiceContext(this._serviceContextButton); - public async create( view: azdata.ModelView, openMigrationsFcn: (status: AdsMigrationStatus) => Promise, + serviceContextChangedEvent: vscode.EventEmitter, statusBar: DashboardStatusBar): Promise { 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 { } public async refresh(): Promise { - 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 { @@ -616,11 +617,8 @@ export class DashboardTab extends TabBase { }).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 { 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 { 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 { 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 { 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 { 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 { 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 { }) .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(); })); diff --git a/extensions/sql-migration/src/dashboard/migrationDetailsBlobContainerTab.ts b/extensions/sql-migration/src/dashboard/migrationDetailsBlobContainerTab.ts index 3508ec2753..e86801fc19 100644 --- a/extensions/sql-migration/src/dashboard/migrationDetailsBlobContainerTab.ts +++ b/extensions/sql-migration/src/dashboard/migrationDetailsBlobContainerTab.ts @@ -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 Promise, + openMigrationsListFcn: () => Promise, statusBar: DashboardStatusBar, ): Promise { 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 { - 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 { diff --git a/extensions/sql-migration/src/dashboard/migrationDetailsFileShareTab.ts b/extensions/sql-migration/src/dashboard/migrationDetailsFileShareTab.ts index 4912fd027a..7b408a9ab9 100644 --- a/extensions/sql-migration/src/dashboard/migrationDetailsFileShareTab.ts +++ b/extensions/sql-migration/src/dashboard/migrationDetailsFileShareTab.ts @@ -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 Promise, + openMigrationsListFcn: () => Promise, statusBar: DashboardStatusBar): Promise { 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 { - 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 { diff --git a/extensions/sql-migration/src/dashboard/migrationDetailsTabBase.ts b/extensions/sql-migration/src/dashboard/migrationDetailsTabBase.ts index e2bdd589e6..c93222d438 100644 --- a/extensions/sql-migration/src/dashboard/migrationDetailsTabBase.ts +++ b/extensions/sql-migration/src/dashboard/migrationDetailsTabBase.ts @@ -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 extends TabBase { protected model!: MigrationCutoverDialogModel; protected databaseLabel!: azdata.TextComponent; protected serviceContext!: MigrationServiceContext; - protected onClosedCallback!: () => Promise; - + protected openMigrationsListFcn!: () => Promise; protected cutoverButton!: azdata.ButtonComponent; protected refreshButton!: azdata.ButtonComponent; protected cancelButton!: azdata.ButtonComponent; @@ -49,7 +48,11 @@ export abstract class MigrationDetailsTabBase extends TabBase { protected retryButton!: azdata.ButtonComponent; protected summaryTextComponent: azdata.TextComponent[] = []; - public abstract create(context: vscode.ExtensionContext, view: azdata.ModelView, onClosedCallback: () => Promise, statusBar: DashboardStatusBar): Promise; + public abstract create( + context: vscode.ExtensionContext, + view: azdata.ModelView, + openMigrationsListFcn: () => Promise, + statusBar: DashboardStatusBar): Promise; protected abstract migrationInfoGrid(): Promise; @@ -80,7 +83,7 @@ export abstract class MigrationDetailsTabBase extends TabBase { .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 extends TabBase { this.context, this.serviceContext, this.model.migration, - this.onClosedCallback); + this.serviceContextChangedEvent); await retryMigrationDialog.openDialog(); } )); @@ -254,12 +257,10 @@ export abstract class MigrationDetailsTabBase extends TabBase { 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 extends TabBase { { component: this.retryButton }, { component: this.copyDatabaseMigrationDetails, toolbarSeparatorAfter: true }, { component: this.newSupportRequest, toolbarSeparatorAfter: true }, - { component: this.refreshButton }, { component: this.refreshLoader }, ]); diff --git a/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts b/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts index fab2b4395c..95d1b5992c 100644 --- a/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts +++ b/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts @@ -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 Promise, + openMigrationsListFcn: () => Promise, statusBar: DashboardStatusBar): Promise { 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 { - 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 { @@ -120,8 +119,9 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase = {}; this._progressDetail = migration?.properties.migrationStatusDetails?.listOfCopyProgressDetails ?? []; + + const hashSet: loc.LookupTable = {}; await this._populateTableData(hashSet); const successCount = hashSet[PipelineStatusCodes.Succeeded] ?? 0; @@ -138,7 +138,7 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase { context: vscode.ExtensionContext, view: azdata.ModelView, openMigrationDetails: (migration: DatabaseMigration) => Promise, + serviceContextChangedEvent: vscode.EventEmitter, statusBar: DashboardStatusBar, ): Promise { 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 { return this; } - public onDialogClosed = async (): Promise => - await this.updateServiceContext(this._serviceContextButton); - public async setMigrationFilter(filter: AdsMigrationStatus): Promise { if (this._statusDropdown.values && this._statusDropdown.values.length > 0) { const statusFilter = (this._statusDropdown.values) .find(value => value.name === filter.toString()); - this._statusDropdown.value = statusFilter; + await this._statusDropdown.updateProperties({ 'value': statusFilter }); } } public async refresh(): Promise { - 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 { logError(TelemetryViews.MigrationsTab, 'refreshMigrations', e); } finally { this._refreshLoader.loading = false; - this._refresh.enabled = true; this.isRefreshing = false; } } protected async initialize(): Promise { - 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 { 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([ { component: this.createNewMigrationButton(), toolbarSeparatorAfter: true }, { component: this.createNewSupportRequestButton() }, { component: this.createFeedbackButton(), toolbarSeparatorAfter: true }, - { component: this._refresh }, { component: this._refreshLoader }, ]); @@ -178,16 +167,25 @@ export class MigrationsListTab extends TabBase { width: 230, }).component(); - const onDialogClosed = async (): Promise => - 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 { .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 { 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 { (this._columnSortDropdown.value).name, this._columnSortCheckbox.checked === true); + const connectionProfile = await azdata.connection.getCurrentConnection(); const data: any[] = this._filteredMigrations.map((migration, index) => { return [ { @@ -597,7 +431,11 @@ export class MigrationsListTab extends TabBase { getMigrationTime(migration.properties.endedOn), // finishTime { 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 { value: 'sourceDatabase', width: 190, type: azdata.ColumnType.hyperlink, - showText: true, }, { cssClass: rowCssStyles, @@ -717,25 +554,26 @@ export class MigrationsListTab extends TabBase { ] }).component(); - this.disposables.push(this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => { - const buttonState = 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 = 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; } diff --git a/extensions/sql-migration/src/dashboard/migrationsTab.ts b/extensions/sql-migration/src/dashboard/migrationsTab.ts index b56ef3712c..b1c08692a7 100644 --- a/extensions/sql-migration/src/dashboard/migrationsTab.ts +++ b/extensions/sql-migration/src/dashboard/migrationsTab.ts @@ -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 { private _migrationDetailsBlobTab!: MigrationDetailsTabBase; private _migrationDetailsTableTab!: MigrationDetailsTabBase; private _selectedTabId: string | undefined = undefined; + private _migrationDetailsEvent!: vscode.EventEmitter; constructor() { super(); @@ -34,16 +35,17 @@ export class MigrationsTab extends TabBase { this.id = MigrationsTabId; } - public onDialogClosed = async (): Promise => - await this._migrationsListTab.onDialogClosed(); - public async create( context: vscode.ExtensionContext, view: azdata.ModelView, + serviceContextChangedEvent: vscode.EventEmitter, + migrationDetailsEvent: vscode.EventEmitter, statusBar: DashboardStatusBar): Promise { 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 { 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 { 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 => { + 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 { - 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 { + public async openMigrationDetails(migration: DatabaseMigration): Promise { switch (migration.properties.backupConfiguration?.sourceLocation?.fileStorageType) { case FileStorageType.AzureBlob: this._migrationDetailsTab = this._migrationDetailsBlobTab; @@ -128,12 +147,21 @@ export class MigrationsTab extends TabBase { await MigrationLocalStorage.getMigrationServiceContext(), migration); + const promise = this._migrationDetailsTab.refresh(); await this._openTab(this._migrationDetailsTab); + await promise; } - private async _openMigrationsListTab(): Promise { - await this.statusBar.clearError(); - await this._openTab(this._migrationsListTab); + private async _getMigrationDetails(migrationId: string, migrationOperationId: string): Promise { + 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 { @@ -141,6 +169,7 @@ export class MigrationsTab extends TabBase { return; } + await this.statusBar.clearError(); this._tab.clearItems(); this._tab.addItem(tab.content); this._selectedTabId = tab.id; diff --git a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts index 304a2e2a1d..125435e449 100644 --- a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts +++ b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts @@ -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; - clearError: () => Promise; - 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; + private readonly _migrationDetailsEvent: vscode.EventEmitter; + private readonly _errorEvent: vscode.EventEmitter; constructor(context: vscode.ExtensionContext) { this._context = context; + NotebookPathHelper.setExtensionContext(context); + IconPathHelper.setExtensionContext(context); + MigrationLocalStorage.setExtensionContext(context); + + this._onServiceContextChanged = new vscode.EventEmitter(); + this._errorEvent = new vscode.EventEmitter(); + this._migrationDetailsEvent = new vscode.EventEmitter(); + + 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 { + await this._registerCommands(); - public async showError(errorTitle: string, errorLabel: string, errorDescription: string): Promise { - 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 { - 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 => { - 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 this._migrationsTab.refresh(); - await this._dashboardTab.refresh(); - } + private async _registerCommands(): Promise { + 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 { - await this._dashboardTab.onDialogClosed(); - await this._migrationsTab.onDialogClosed(); - } - - private _errorDialogIsOpen: boolean = false; - - protected async openErrorDialog(): Promise { - 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(); + 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 { + this._errorEvent.fire({ + connectionId: connectionId, + title: '', + label: '', + message: '', + }); + } + + private async showError(connectionId: string, title: string, label: string, message: string): Promise { + this._errorEvent.fire({ + connectionId: connectionId, + title: title, + label: label, + message: message, + }); + } + + private async _getMigrationById(migrationId: string, migrationOperationId: string): Promise { + const context = await MigrationLocalStorage.getMigrationServiceContext(); + if (context.azureAccount && context.subscription) { + return getMigrationDetails( + context.azureAccount, + context.subscription, + migrationId, + migrationOperationId); + } + return undefined; + } + + public async launchMigrationWizard(): Promise { + 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 { - await control.updateCssStyles({ 'display': visible ? 'inline' : 'none' }); + private checkSavedInfo(serverName: string): SavedInfo | undefined { + return this._context.globalState.get(`${this.stateModel.mementoString}.${serverName}`); + } + + public async launchNewSupportRequest(): Promise { + await vscode.env.openExternal(vscode.Uri.parse( + `https://portal.azure.com/#blade/Microsoft_Azure_Support/HelpAndSupportBlade/newsupportrequest`)); } } diff --git a/extensions/sql-migration/src/dashboard/tabBase.ts b/extensions/sql-migration/src/dashboard/tabBase.ts index 1f3cabc8bd..a009b24dcf 100644 --- a/extensions/sql-migration/src/dashboard/tabBase.ts +++ b/extensions/sql-migration/src/dashboard/tabBase.ts @@ -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 implements azdata.Tab, vscode.Disposable { public content!: azdata.Component; @@ -45,7 +43,8 @@ export abstract class TabBase implements azdata.Tab, vscode.Disposable { protected view!: azdata.ModelView; protected disposables: vscode.Disposable[] = []; protected isRefreshing: boolean = false; - protected openMigrationFcn!: (status: AdsMigrationStatus) => Promise; + protected openMigrationsFcn!: (status: AdsMigrationStatus) => Promise; + protected serviceContextChangedEvent!: vscode.EventEmitter; protected statusBar!: DashboardStatusBar; protected abstract initialize(view: azdata.ModelView): Promise; @@ -165,8 +164,9 @@ export abstract class TabBase 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 diff --git a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts index e70f2aa661..5b9a915e7a 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts @@ -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; diff --git a/extensions/sql-migration/src/dialog/assessmentResults/savedAssessmentDialog.ts b/extensions/sql-migration/src/dialog/assessmentResults/savedAssessmentDialog.ts index 10de5103ed..0ef67c2c88 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/savedAssessmentDialog.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/savedAssessmentDialog.ts @@ -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; private _disposables: vscode.Disposable[] = []; + private _isOpen: boolean = false; + private _rootContainer!: azdata.FlexContainer; constructor( context: vscode.ExtensionContext, stateModel: MigrationStateModel, - private readonly _onClosedCallback: () => Promise) { + serviceContextChangedEvent: vscode.EventEmitter) { this.stateModel = stateModel; this.context = context; + this._serviceContextChangedEvent = serviceContextChangedEvent; } private async initializeDialog(dialog: azdata.window.Dialog): Promise { @@ -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' }); diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts index c6ab469592..bb0045de59 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts @@ -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 { - 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 } diff --git a/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts b/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts index 7c11c940fc..84c674e8ba 100644 --- a/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts +++ b/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts @@ -390,8 +390,12 @@ export class CreateSqlMigrationServiceDialog { private async populateResourceGroups(): Promise { 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]; diff --git a/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts index 25c5448b78..56c0355777 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts @@ -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({ diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts index b80661f797..044acfdf3b 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts @@ -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); } }); diff --git a/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts b/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts index 0103d791ef..0fb0c93a57 100644 --- a/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts +++ b/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts @@ -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) { + private readonly _serviceContextChangedEvent: vscode.EventEmitter) { } - private async createMigrationStateModel(serviceContext: MigrationServiceContext, migration: DatabaseMigration, connectionId: string, serverName: string, api: mssql.IExtension, location: azureResource.AzureLocation): Promise { - 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 { + 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); diff --git a/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts b/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts index 2273bf5490..373bcfdeab 100644 --- a/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts +++ b/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts @@ -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) { + private readonly onServiceContextChanged: vscode.EventEmitter) { 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 { 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 { 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 { + 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, diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/getAzureRecommendationDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/getAzureRecommendationDialog.ts index e27258047b..c250089f29 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/getAzureRecommendationDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/getAzureRecommendationDialog.ts @@ -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[] = []; - 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[] = []; diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/skuEditParametersDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/skuEditParametersDialog.ts index 715f7dc1b2..6ef0c1eea8 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/skuEditParametersDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/skuEditParametersDialog.ts @@ -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[] = []; dialogSetupPromises.push(this.initializeDialog(this.dialog)); diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts index 5abdd2fe3c..f681406e58 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts @@ -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 = 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[] = []; - dialogSetupPromises.push(this.initializeDialog(this.dialog)); + const promise = this.initializeDialog(this.dialog); azdata.window.openDialog(this.dialog); - await Promise.all(dialogSetupPromises); + await promise; } } diff --git a/extensions/sql-migration/src/dialog/tableMigrationSelection/tableMigrationSelectionDialog.ts b/extensions/sql-migration/src/dialog/tableMigrationSelection/tableMigrationSelectionDialog.ts new file mode 100644 index 0000000000..1decf27460 --- /dev/null +++ b/extensions/sql-migration/src/dialog/tableMigrationSelection/tableMigrationSelectionDialog.ts @@ -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; + private _targetTableMap!: Map; + private _onSaveCallback: () => Promise; + + constructor( + model: MigrationStateModel, + sourceDatabaseName: string, + onSaveCallback: () => Promise + ) { + this._model = model; + this._sourceDatabaseName = sourceDatabaseName; + this._onSaveCallback = onSaveCallback; + } + + private async _loadData(): Promise { + 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 { + 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 { + 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 { + const cssClass = 'no-borders'; + const table = view.modelBuilder.table() + .withProps({ + data: [], + width: 565, + height: '600px', + forceFitColumns: azdata.ColumnSizingMode.ForceFit, + columns: [ + { + 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 { + 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(); + 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; + } +} diff --git a/extensions/sql-migration/src/dialog/targetDatabaseSummary/targetDatabaseSummaryDialog.ts b/extensions/sql-migration/src/dialog/targetDatabaseSummary/targetDatabaseSummaryDialog.ts index 0b2ae413a8..57b9157465 100644 --- a/extensions/sql-migration/src/dialog/targetDatabaseSummary/targetDatabaseSummaryDialog.ts +++ b/extensions/sql-migration/src/dialog/targetDatabaseSummary/targetDatabaseSummaryDialog.ts @@ -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 { - 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]; diff --git a/extensions/sql-migration/src/main.ts b/extensions/sql-migration/src/main.ts index 59b8242dc5..aa68994c42 100644 --- a/extensions/sql-migration/src/main.ts +++ b/extensions/sql-migration/src/main.ts @@ -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 { - await this.registerCommands(); - } - - async registerCommands(): Promise { - 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(); - 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 { - 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 { - 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 { widget = new DashboardWidget(context); - widget.register(); + await widget.register(); + return widget; } export function deactivate(): void { - sqlMigration.stop(); } diff --git a/extensions/sql-migration/src/models/migrationLocalStorage.ts b/extensions/sql-migration/src/models/migrationLocalStorage.ts index b763cee84b..c2fd3c32ea 100644 --- a/extensions/sql-migration/src/models/migrationLocalStorage.ts +++ b/extensions/sql-migration/src/models/migrationLocalStorage.ts @@ -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 { + public static async saveMigrationServiceContext(serviceContext: MigrationServiceContext, serviceContextChangedEvent: vscode.EventEmitter): Promise { 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 { + public static async refreshMigrationAzureAccount(serviceContext: MigrationServiceContext, migration: DatabaseMigration, serviceContextChangedEvent: vscode.EventEmitter): Promise { 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' -} diff --git a/extensions/sql-migration/src/models/migrationWizardPage.ts b/extensions/sql-migration/src/models/migrationWizardPage.ts index c47ffdc653..abdf6e4f7d 100644 --- a/extensions/sql-migration/src/models/migrationWizardPage.ts +++ b/extensions/sql-migration/src/models/migrationWizardPage.ts @@ -80,6 +80,4 @@ export abstract class MigrationWizardPage { const current = this.wizard.currentPage; await this.wizard.setCurrentPage(current + 1); } - } - diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 4d06153eb1..189d62a746 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -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 = new Map(); + public _sqlMigrationServiceResourceGroup!: azurecore.azureResource.AzureResourceResourceGroup; public _sqlMigrationService!: SqlMigrationService | undefined; public _sqlMigrationServices!: SqlMigrationService[]; public _nodeNames!: string[]; - private _stateChangeEventEmitter = new vscode.EventEmitter(); - 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(); + 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 { - 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 { + public async getDatabaseAssessments(targetType: MigrationTargetType[]): Promise { 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( + issue.appliesToMigrationTargetPlatform)); response.assessmentResult.databases?.forEach( database => database.items = database.items?.filter( - issue => issue.appliesToMigrationTargetPlatform === targetType)); + issue => targetType.includes( + 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 { 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 = 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 { 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 { @@ -851,22 +852,20 @@ export class MigrationStateModel implements Model, vscode.Disposable { } public async getManagedDatabases(): Promise { - return (await getSqlManagedInstanceDatabases(this._azureAccount, - this._targetSubscription, - this._targetServerInstance)).map(t => t.name); + return ( + await getSqlManagedInstanceDatabases(this._azureAccount, + this._targetSubscription, + 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 { - 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!; diff --git a/extensions/sql-migration/src/telemtery.ts b/extensions/sql-migration/src/telemtery.ts index 35d73abac9..98dd4605c9 100644 --- a/extensions/sql-migration/src/telemtery.ts +++ b/extensions/sql-migration/src/telemtery.ts @@ -57,6 +57,7 @@ export enum TelemetryAction { OnPageLeave = 'OnPageLeave', GetMISkuRecommendation = 'GetMISkuRecommendation', GetVMSkuRecommendation = 'GetVMSkuRecommendation', + GetSqlDbSkuRecommendation = 'GetSqlDbSkuRecommendation', GetInstanceRequirements = 'GetInstanceRequirements', StartDataCollection = 'StartDataCollection', StopDataCollection = 'StopDataCollection' diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index e83df3d048..0e428b338c 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -15,6 +15,8 @@ import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; import * as utils from '../api/utils'; import { logError, TelemetryViews } from '../telemtery'; import * as styles from '../constants/styles'; +import { TableMigrationSelectionDialog } from '../dialog/tableMigrationSelection/tableMigrationSelectionDialog'; +import { AuthenticationType } from '../api/sqlUtils'; const WIZARD_TABLE_COLUMN_WIDTH = '200px'; const WIZARD_TABLE_COLUMN_WIDTH_SMALL = '170px'; @@ -30,6 +32,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { private _networkShareButton!: azdata.RadioButtonComponent; private _blobContainerButton!: azdata.RadioButtonComponent; + private _sourceConnectionContainer!: azdata.FlexContainer; private _networkShareContainer!: azdata.FlexContainer; private _windowsUserAccountText!: azdata.InputBoxComponent; private _passwordText!: azdata.InputBoxComponent; @@ -60,10 +63,18 @@ export class DatabaseBackupPage extends MigrationWizardPage { private _networkShareTargetDatabaseNames: azdata.InputBoxComponent[] = []; private _blobContainerTargetDatabaseNames: azdata.InputBoxComponent[] = []; private _networkShareLocations: azdata.InputBoxComponent[] = []; + private _radioButtonContainer!: azdata.FlexContainer; + private _networkDetailsContainer!: azdata.FlexContainer; private _existingDatabases: string[] = []; private _disposables: vscode.Disposable[] = []; + // SQL DB table selection + private _refreshLoading!: azdata.LoadingComponent; + private _refreshButton!: azdata.ButtonComponent; + private _databaseTable!: azdata.TableComponent; + private _migrationTableSection!: azdata.FlexContainer; + constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.DATABASE_BACKUP_PAGE_TITLE), migrationStateModel); } @@ -71,44 +82,29 @@ export class DatabaseBackupPage extends MigrationWizardPage { protected async registerContent(view: azdata.ModelView): Promise { this._view = view; - const radioButtonContainer = this.createBackupLocationComponent(); - - const networkDetailsContainer = this.createNetworkDetailsContainer(); - + this._radioButtonContainer = this.createBackupLocationComponent(); + this._sourceConnectionContainer = this.createSourceCredentialsContainer(); + this._networkDetailsContainer = this.createNetworkDetailsContainer(); this._targetDatabaseContainer = this.createTargetDatabaseContainer(); - this._networkShareStorageAccountDetails = this.createNetworkShareStorageAccountDetailsContainer(); + this._migrationTableSection = this._migrationTableSelectionContainer(); const form = this._view.modelBuilder.formContainer() - .withFormItems( - [ - { - title: '', - component: radioButtonContainer - }, - { - title: '', - component: networkDetailsContainer - }, - { - title: '', - component: this._targetDatabaseContainer - }, - { - title: '', - component: this._networkShareStorageAccountDetails - } - ] - ).withProps({ - CSSStyles: { - 'padding-top': '0' - } - }).component(); + .withFormItems([ + { title: '', component: this._radioButtonContainer }, + { title: '', component: this._sourceConnectionContainer }, + { title: '', component: this._networkDetailsContainer }, + { title: '', component: this._migrationTableSection }, + { title: '', component: this._targetDatabaseContainer }, + { title: '', component: this._networkShareStorageAccountDetails }]) + .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); } @@ -116,66 +112,58 @@ export class DatabaseBackupPage extends MigrationWizardPage { private createBackupLocationComponent(): azdata.FlexContainer { const buttonGroup = 'networkContainer'; - const selectLocationText = this._view.modelBuilder.text().withProps({ - value: constants.DATABASE_BACKUP_PAGE_DESCRIPTION, - CSSStyles: { - ...styles.BODY_CSS - } - }).component(); + const selectLocationText = this._view.modelBuilder.text() + .withProps({ + value: constants.DATABASE_BACKUP_PAGE_DESCRIPTION, + CSSStyles: { ...styles.BODY_CSS } + }).component(); - const backupChecksumInfoBox = this._view.modelBuilder.infoBox().withProps({ - text: constants.DATABASE_BACKUP_CHECKSUM_INFO_TEXT, - style: 'information', - width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.BODY_CSS - } - }).component(); + const backupChecksumInfoBox = this._view.modelBuilder.infoBox() + .withProps({ + text: constants.DATABASE_BACKUP_CHECKSUM_INFO_TEXT, + style: 'information', + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { ...styles.BODY_CSS } + }).component(); this._networkShareButton = this._view.modelBuilder.radioButton() .withProps({ name: buttonGroup, label: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL, checked: this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE, - CSSStyles: { - ...styles.BODY_CSS, - 'margin': '0' - } + CSSStyles: { ...styles.BODY_CSS, 'margin': '0' } }).component(); - this._disposables.push(this._networkShareButton.onDidChangeCheckedState(async (e) => { - if (e) { - await this.switchNetworkContainerFields(NetworkContainerType.NETWORK_SHARE); - } - })); + this._disposables.push( + this._networkShareButton.onDidChangeCheckedState(async checked => { + if (checked) { + await this.switchNetworkContainerFields(NetworkContainerType.NETWORK_SHARE); + } + })); this._blobContainerButton = this._view.modelBuilder.radioButton() .withProps({ name: buttonGroup, label: constants.DATABASE_BACKUP_NC_BLOB_STORAGE_RADIO_LABEL, checked: this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER, - CSSStyles: { - ...styles.BODY_CSS, - 'margin': '0' - } + CSSStyles: { ...styles.BODY_CSS, 'margin': '0' } }).component(); - this._disposables.push(this._blobContainerButton.onDidChangeCheckedState(async (e) => { - if (e) { - await this.switchNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER); - } - })); + this._disposables.push( + this._blobContainerButton.onDidChangeCheckedState(async checked => { + if (checked) { + await this.switchNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER); + } + })); - const flexContainer = this._view.modelBuilder.flexContainer().withItems( - [ + const flexContainer = this._view.modelBuilder.flexContainer() + .withItems([ selectLocationText, backupChecksumInfoBox, this._networkShareButton, - this._blobContainerButton - ] - ).withLayout({ - flexFlow: 'column' - }).component(); + this._blobContainerButton]) + .withLayout({ flexFlow: 'column' }) + .component(); return flexContainer; } @@ -184,99 +172,99 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._networkShareContainer = this.createNetworkShareContainer(); this._blobContainer = this.createBlobContainer(); - const networkContainer = this._view.modelBuilder.flexContainer().withLayout({ - flexFlow: 'column' - }).withItems([ - this._networkShareContainer, - this._blobContainer, - ]).component(); - return networkContainer; + return this._view.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'column' }) + .withItems([ + this._networkShareContainer, + this._blobContainer]) + .component(); + } + + private createSourceCredentialsContainer(): azdata.FlexContainer { + const sqlSourceHeader = this._view.modelBuilder.text() + .withProps({ + value: constants.SOURCE_CREDENTIALS, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '4px' } + }).component(); + + this._sourceHelpText = this._view.modelBuilder.text() + .withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { ...styles.BODY_CSS } + }).component(); + + const usernameLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.USERNAME, + width: WIZARD_INPUT_COMPONENT_WIDTH, + requiredIndicator: true, + CSSStyles: { ...styles.LABEL_CSS } + }).component(); + this._sqlSourceUsernameInput = this._view.modelBuilder.inputBox() + .withProps({ + required: true, + enabled: false, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { 'margin-top': '-1em' }, + }).component(); + this._disposables.push( + this._sqlSourceUsernameInput.onTextChanged( + value => this.migrationStateModel._sqlServerUsername = value)); + + const sqlPasswordLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL, + width: WIZARD_INPUT_COMPONENT_WIDTH, + requiredIndicator: true, + CSSStyles: { ...styles.LABEL_CSS } + }).component(); + this._sqlSourcePassword = this._view.modelBuilder.inputBox() + .withProps({ + required: true, + inputType: 'password', + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { 'margin-top': '-1em' }, + }).component(); + this._disposables.push( + this._sqlSourcePassword.onTextChanged( + value => this.migrationStateModel._sqlServerPassword = value)); + + return this._view.modelBuilder.flexContainer() + .withItems([ + sqlSourceHeader, + this._sourceHelpText, + usernameLabel, + this._sqlSourceUsernameInput, + sqlPasswordLabel, + this._sqlSourcePassword]) + .withLayout({ flexFlow: 'column' }) + .withProps({ display: 'none' }) + .component(); } private createNetworkShareContainer(): azdata.FlexContainer { + const networkShareHeading = this._view.modelBuilder.text() + .withProps({ + value: constants.DATABASE_BACKUP_NETWORK_SHARE_HEADER_TEXT, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '24px' } + }).component(); - const sqlSourceHeader = this._view.modelBuilder.text().withProps({ - value: constants.SOURCE_CREDENTIALS, - width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.SECTION_HEADER_CSS, - 'margin-top': '4px' - } - }).component(); + const networkShareHelpText = this._view.modelBuilder.text() + .withProps({ + value: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { ...styles.BODY_CSS } + }).component(); - this._sourceHelpText = this._view.modelBuilder.text().withProps({ - width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.BODY_CSS - } - }).component(); - - const usernameLabel = this._view.modelBuilder.text().withProps({ - value: constants.USERNAME, - width: WIZARD_INPUT_COMPONENT_WIDTH, - requiredIndicator: true, - CSSStyles: { - ...styles.LABEL_CSS - } - }).component(); - this._sqlSourceUsernameInput = this._view.modelBuilder.inputBox().withProps({ - required: true, - enabled: false, - width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - 'margin-top': '-1em' - }, - }).component(); - this._disposables.push(this._sqlSourceUsernameInput.onTextChanged(value => { - this.migrationStateModel._sqlServerUsername = value; - })); - - const sqlPasswordLabel = this._view.modelBuilder.text().withProps({ - value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL, - width: WIZARD_INPUT_COMPONENT_WIDTH, - requiredIndicator: true, - CSSStyles: { - ...styles.LABEL_CSS - } - }).component(); - this._sqlSourcePassword = this._view.modelBuilder.inputBox().withProps({ - required: true, - inputType: 'password', - width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - 'margin-top': '-1em' - }, - }).component(); - this._disposables.push(this._sqlSourcePassword.onTextChanged(value => { - this.migrationStateModel._sqlServerPassword = value; - })); - - - const networkShareHeading = this._view.modelBuilder.text().withProps({ - value: constants.DATABASE_BACKUP_NETWORK_SHARE_HEADER_TEXT, - width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.SECTION_HEADER_CSS, - 'margin-top': '24px' - } - }).component(); - - const networkShareHelpText = this._view.modelBuilder.text().withProps({ - value: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT, - width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.BODY_CSS - } - }).component(); - - const networkShareInfoBox = this._view.modelBuilder.infoBox().withProps({ - text: constants.DATABASE_SERVICE_ACCOUNT_INFO_TEXT, - style: 'information', - width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.BODY_CSS - } - }).component(); + const networkShareInfoBox = this._view.modelBuilder.infoBox() + .withProps({ + text: constants.DATABASE_SERVICE_ACCOUNT_INFO_TEXT, + style: 'information', + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { ...styles.BODY_CSS } + }).component(); const windowsUserAccountLabel = this._view.modelBuilder.text() .withProps({ @@ -284,9 +272,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { description: constants.DATABASE_BACKUP_NETWORK_SHARE_WINDOWS_USER_INFO, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, - CSSStyles: { - ...styles.LABEL_CSS - } + CSSStyles: { ...styles.LABEL_CSS } }).component(); this._windowsUserAccountText = this._view.modelBuilder.inputBox() .withProps({ @@ -294,10 +280,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { required: true, validationErrorMessage: constants.INVALID_USER_ACCOUNT, width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.BODY_CSS, - 'margin-top': '-1em' - } + CSSStyles: { ...styles.BODY_CSS, 'margin-top': '-1em' } }) .withValidation((component) => { if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { @@ -309,20 +292,19 @@ export class DatabaseBackupPage extends MigrationWizardPage { } return true; }).component(); - this._disposables.push(this._windowsUserAccountText.onTextChanged((value) => { - for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) { - this.migrationStateModel._databaseBackup.networkShares[i].windowsUser = value; - } - })); + this._disposables.push( + this._windowsUserAccountText.onTextChanged((value) => { + for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) { + this.migrationStateModel._databaseBackup.networkShares[i].windowsUser = value; + } + })); const passwordLabel = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, - CSSStyles: { - ...styles.LABEL_CSS, - } + CSSStyles: { ...styles.LABEL_CSS, } }).component(); this._passwordText = this._view.modelBuilder.inputBox() .withProps({ @@ -330,107 +312,77 @@ export class DatabaseBackupPage extends MigrationWizardPage { inputType: 'password', required: true, width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.BODY_CSS, - 'margin-top': '-1em' - } + CSSStyles: { ...styles.BODY_CSS, 'margin-top': '-1em' } }).component(); - this._disposables.push(this._passwordText.onTextChanged((value) => { - for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) { - this.migrationStateModel._databaseBackup.networkShares[i].password = value; - } - })); + this._disposables.push( + this._passwordText.onTextChanged((value) => { + for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) { + this.migrationStateModel._databaseBackup.networkShares[i].password = value; + } + })); - const flexContainer = this._view.modelBuilder.flexContainer().withItems( - [ - sqlSourceHeader, - this._sourceHelpText, - usernameLabel, - this._sqlSourceUsernameInput, - sqlPasswordLabel, - this._sqlSourcePassword, + return this._view.modelBuilder.flexContainer() + .withItems([ networkShareHeading, networkShareHelpText, networkShareInfoBox, windowsUserAccountLabel, this._windowsUserAccountText, passwordLabel, - this._passwordText, - ] - ).withLayout({ - flexFlow: 'column' - }).withProps({ - display: 'none' - }).component(); - - return flexContainer; + this._passwordText]) + .withLayout({ flexFlow: 'column' }) + .withProps({ display: 'none' }) + .component(); } private createBlobContainer(): azdata.FlexContainer { - const blobHeading = this._view.modelBuilder.text().withProps({ - value: constants.DATABASE_BACKUP_BLOB_STORAGE_HEADER_TEXT, - width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.SECTION_HEADER_CSS - } - }).component(); + const blobHeading = this._view.modelBuilder.text() + .withProps({ + value: constants.DATABASE_BACKUP_BLOB_STORAGE_HEADER_TEXT, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { ...styles.SECTION_HEADER_CSS } + }).component(); - const blobHelpText = this._view.modelBuilder.text().withProps({ - value: constants.DATABASE_BACKUP_BLOB_STORAGE_HELP_TEXT, - width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.BODY_CSS, - 'margin-bottom': '12px' - } - }).component(); + const blobHelpText = this._view.modelBuilder.text() + .withProps({ + value: constants.DATABASE_BACKUP_BLOB_STORAGE_HELP_TEXT, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '12px' } + }).component(); const subscriptionLabel = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL, - CSSStyles: { - ...styles.LABEL_CSS - } + CSSStyles: { ...styles.LABEL_CSS } }).component(); this._blobContainerSubscription = this._view.modelBuilder.text() .withProps({ enabled: false, - CSSStyles: { - ...styles.BODY_CSS, - 'margin': '0 0 12px 0' - } + CSSStyles: { ...styles.BODY_CSS, 'margin': '0 0 12px 0' } }).component(); const locationLabel = this._view.modelBuilder.text() .withProps({ value: constants.LOCATION, - CSSStyles: { - ...styles.LABEL_CSS - } + CSSStyles: { ...styles.LABEL_CSS } }).component(); this._blobContainerLocation = this._view.modelBuilder.text() .withProps({ enabled: false, - CSSStyles: { - ...styles.BODY_CSS, - 'margin': '0px' - } + CSSStyles: { ...styles.BODY_CSS, 'margin': '0px' } }).component(); const flexContainer = this._view.modelBuilder.flexContainer() - .withItems( - [ - blobHeading, - blobHelpText, - subscriptionLabel, - this._blobContainerSubscription, - locationLabel, - this._blobContainerLocation, - ] - ).withLayout({ - flexFlow: 'column' - }).withProps({ - display: 'none' - }).component(); + .withItems([ + blobHeading, + blobHelpText, + subscriptionLabel, + this._blobContainerSubscription, + locationLabel, + this._blobContainerLocation]) + .withLayout({ flexFlow: 'column' }) + .withProps({ display: 'none' }) + .component(); return flexContainer; } @@ -441,7 +393,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { 'border': 'none', 'text-align': 'left', 'box-shadow': 'inset 0px -1px 0px #F3F2F1', - }; const rowCssStyle: azdata.CssStyles = { ...styles.BODY_CSS, @@ -453,150 +404,137 @@ export class DatabaseBackupPage extends MigrationWizardPage { const networkShareTableText = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_TABLE_HELP_TEXT, - CSSStyles: { - ...styles.SECTION_HEADER_CSS, - 'margin-top': '8px' - } + CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '8px' } }).component(); const blobTableText = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_BLOB_STORAGE_TABLE_HELP_TEXT, - CSSStyles: { - ...styles.SECTION_HEADER_CSS - } + CSSStyles: { ...styles.SECTION_HEADER_CSS } }).component(); - const azureStoragePrivateEndpointInfoBox = this._view.modelBuilder.infoBox().withProps({ - text: constants.DATABASE_BACKUP_PRIVATE_ENDPOINT_INFO_TEXT, - style: 'information', - width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.BODY_CSS - } - }).component(); + const azureStoragePrivateEndpointInfoBox = this._view.modelBuilder.infoBox() + .withProps({ + text: constants.DATABASE_BACKUP_PRIVATE_ENDPOINT_INFO_TEXT, + style: 'information', + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { ...styles.BODY_CSS } + }).component(); - this._networkShareTargetDatabaseNamesTable = this._view.modelBuilder.declarativeTable().withProps({ - columns: [ - { - displayName: constants.SOURCE_DATABASE, - valueType: azdata.DeclarativeDataType.string, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles, - isReadOnly: true, - width: '250px' - }, - { - displayName: constants.TARGET_DATABASE_NAME, - valueType: azdata.DeclarativeDataType.component, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles, - isReadOnly: true, - width: WIZARD_TABLE_COLUMN_WIDTH - }, - { - displayName: constants.NETWORK_SHARE_PATH, - valueType: azdata.DeclarativeDataType.component, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles, - isReadOnly: true, - width: '300px' - } - ] - }).component(); + this._networkShareTargetDatabaseNamesTable = this._view.modelBuilder.declarativeTable() + .withProps({ + columns: [ + { + displayName: constants.SOURCE_DATABASE, + valueType: azdata.DeclarativeDataType.string, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: '250px' + }, + { + displayName: constants.TARGET_DATABASE_NAME, + valueType: azdata.DeclarativeDataType.component, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: WIZARD_TABLE_COLUMN_WIDTH + }, + { + displayName: constants.NETWORK_SHARE_PATH, + valueType: azdata.DeclarativeDataType.component, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: '300px' + } + ] + }).component(); - this._blobContainerTargetDatabaseNamesTable = this._view.modelBuilder.declarativeTable().withProps({ - columns: [ - { - displayName: constants.SOURCE_DATABASE, - valueType: azdata.DeclarativeDataType.string, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles, - isReadOnly: true, - width: WIZARD_TABLE_COLUMN_WIDTH, - }, - { - displayName: constants.TARGET_DATABASE_NAME, - valueType: azdata.DeclarativeDataType.component, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles, - isReadOnly: true, - width: WIZARD_TABLE_COLUMN_WIDTH - }, - { - displayName: constants.RESOURCE_GROUP, - valueType: azdata.DeclarativeDataType.component, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles, - isReadOnly: true, - width: WIZARD_TABLE_COLUMN_WIDTH - }, - { - displayName: constants.STORAGE_ACCOUNT, - valueType: azdata.DeclarativeDataType.component, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles, - isReadOnly: true, - width: WIZARD_TABLE_COLUMN_WIDTH - }, - { - displayName: constants.BLOB_CONTAINER, - valueType: azdata.DeclarativeDataType.component, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles, - isReadOnly: true, - width: WIZARD_TABLE_COLUMN_WIDTH - }, - { - displayName: constants.BLOB_CONTAINER_LAST_BACKUP_FILE, - valueType: azdata.DeclarativeDataType.component, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles, - isReadOnly: true, - width: WIZARD_TABLE_COLUMN_WIDTH, - hidden: true - } - ] - }).component(); + this._blobContainerTargetDatabaseNamesTable = this._view.modelBuilder.declarativeTable() + .withProps({ + columns: [ + { + displayName: constants.SOURCE_DATABASE, + valueType: azdata.DeclarativeDataType.string, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: WIZARD_TABLE_COLUMN_WIDTH, + }, + { + displayName: constants.TARGET_DATABASE_NAME, + valueType: azdata.DeclarativeDataType.component, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: WIZARD_TABLE_COLUMN_WIDTH + }, + { + displayName: constants.RESOURCE_GROUP, + valueType: azdata.DeclarativeDataType.component, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: WIZARD_TABLE_COLUMN_WIDTH + }, + { + displayName: constants.STORAGE_ACCOUNT, + valueType: azdata.DeclarativeDataType.component, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: WIZARD_TABLE_COLUMN_WIDTH + }, + { + displayName: constants.BLOB_CONTAINER, + valueType: azdata.DeclarativeDataType.component, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: WIZARD_TABLE_COLUMN_WIDTH + }, + { + displayName: constants.BLOB_CONTAINER_LAST_BACKUP_FILE, + valueType: azdata.DeclarativeDataType.component, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: WIZARD_TABLE_COLUMN_WIDTH, + hidden: true + } + ] + }).component(); - this._networkTableContainer = this._view.modelBuilder.flexContainer().withItems([ - networkShareTableText, - this._networkShareTargetDatabaseNamesTable - ]).withProps({ - CSSStyles: { - 'display': 'none', - } - }).component(); + this._networkTableContainer = this._view.modelBuilder.flexContainer() + .withItems([ + networkShareTableText, + this._networkShareTargetDatabaseNamesTable]) + .withProps({ CSSStyles: { 'display': 'none', } }) + .component(); const allFieldsRequiredLabel = this._view.modelBuilder.text() .withProps({ value: constants.ALL_FIELDS_REQUIRED, - CSSStyles: { - ...styles.BODY_CSS - } + CSSStyles: { ...styles.BODY_CSS } }).component(); - this._blobTableContainer = this._view.modelBuilder.flexContainer().withItems([ - blobTableText, - allFieldsRequiredLabel, - azureStoragePrivateEndpointInfoBox, - this._blobContainerTargetDatabaseNamesTable - ]).withProps({ - CSSStyles: { - 'display': 'none', - } - }).component(); + this._blobTableContainer = this._view.modelBuilder.flexContainer() + .withItems([ + blobTableText, + allFieldsRequiredLabel, + azureStoragePrivateEndpointInfoBox, + this._blobContainerTargetDatabaseNamesTable]) + .withProps({ CSSStyles: { 'display': 'none', } }) + .component(); - const container = this._view.modelBuilder.flexContainer().withLayout({ - flexFlow: 'column' - }).withItems([ - this._networkTableContainer, - this._blobTableContainer - ]).withProps({ - CSSStyles: { - 'display': 'none', - } - }).component(); + const container = this._view.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'column' }) + .withItems([ + this._networkTableContainer, + this._blobTableContainer]) + .withProps({ CSSStyles: { 'display': 'none' } }) + .component(); return container; } @@ -605,65 +543,48 @@ export class DatabaseBackupPage extends MigrationWizardPage { .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HEADER, width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.SECTION_HEADER_CSS, - 'margin-top': '12px' - } + CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '12px' } }).component(); const azureAccountHelpText = this._view.modelBuilder.text() .withProps({ value: constants.DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP, width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.BODY_CSS, - 'margin-bottom': '12px' - } + CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '12px' } }).component(); - const azureStoragePrivateEndpointInfoBox = this._view.modelBuilder.infoBox().withProps({ - text: constants.DATABASE_BACKUP_PRIVATE_ENDPOINT_INFO_TEXT, - style: 'information', - width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.BODY_CSS - } - }).component(); + const azureStoragePrivateEndpointInfoBox = this._view.modelBuilder.infoBox() + .withProps({ + text: constants.DATABASE_BACKUP_PRIVATE_ENDPOINT_INFO_TEXT, + style: 'information', + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { ...styles.BODY_CSS } + }).component(); const subscriptionLabel = this._view.modelBuilder.text() .withProps({ value: constants.SUBSCRIPTION, width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.LABEL_CSS, - 'margin': '0' - } + CSSStyles: { ...styles.LABEL_CSS, 'margin': '0' } }).component(); this._networkShareContainerSubscription = this._view.modelBuilder.text() .withProps({ enabled: false, width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - 'margin': '0' - } + CSSStyles: { 'margin': '0' } }).component(); const locationLabel = this._view.modelBuilder.text() .withProps({ value: constants.LOCATION, width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - ...styles.LABEL_CSS, - 'margin': '12px 0 0' - } + CSSStyles: { ...styles.LABEL_CSS, 'margin': '12px 0 0' } }).component(); this._networkShareContainerLocation = this._view.modelBuilder.text() .withProps({ enabled: false, width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { - 'margin': '0' - } + CSSStyles: { 'margin': '0' } }).component(); const resourceGroupLabel = this._view.modelBuilder.text() @@ -671,9 +592,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { value: constants.RESOURCE_GROUP, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, - CSSStyles: { - ...styles.LABEL_CSS - } + CSSStyles: { ...styles.LABEL_CSS } }).component(); this._networkShareStorageAccountResourceGroupDropdown = this._view.modelBuilder.dropDown() .withProps({ @@ -682,30 +601,27 @@ export class DatabaseBackupPage extends MigrationWizardPage { width: WIZARD_INPUT_COMPONENT_WIDTH, editable: true, fireOnTextChange: true, - CSSStyles: { - 'margin-top': '-1em' - }, + CSSStyles: { 'margin-top': '-1em' }, }).component(); - this._disposables.push(this._networkShareStorageAccountResourceGroupDropdown.onValueChanged(async (value) => { - if (value && value !== 'undefined') { - const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value); - if (selectedResourceGroup) { - for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) { - this.migrationStateModel._databaseBackup.networkShares[i].resourceGroup = selectedResourceGroup; + this._disposables.push( + this._networkShareStorageAccountResourceGroupDropdown.onValueChanged(async (value) => { + if (value && value !== 'undefined') { + const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value); + if (selectedResourceGroup) { + for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) { + this.migrationStateModel._databaseBackup.networkShares[i].resourceGroup = selectedResourceGroup; + } + this.loadNetworkShareStorageDropdown(); } - await this.loadNetworkShareStorageDropdown(); } - } - })); + })); const storageAccountLabel = this._view.modelBuilder.text() .withProps({ value: constants.STORAGE_ACCOUNT, width: WIZARD_INPUT_COMPONENT_WIDTH, requiredIndicator: true, - CSSStyles: { - ...styles.LABEL_CSS - } + CSSStyles: { ...styles.LABEL_CSS } }).component(); this._networkShareContainerStorageAccountDropdown = this._view.modelBuilder.dropDown() .withProps({ @@ -715,80 +631,110 @@ export class DatabaseBackupPage extends MigrationWizardPage { editable: true, fireOnTextChange: true, }).component(); - this._disposables.push(this._networkShareContainerStorageAccountDropdown.onValueChanged((value) => { - if (value && value !== 'undefined') { - const selectedStorageAccount = this.migrationStateModel._storageAccounts.find(sa => sa.name === value); - if (selectedStorageAccount) { - for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) { - this.migrationStateModel._databaseBackup.networkShares[i].storageAccount = selectedStorageAccount; + this._disposables.push( + this._networkShareContainerStorageAccountDropdown.onValueChanged((value) => { + if (value && value !== 'undefined') { + const selectedStorageAccount = this.migrationStateModel._storageAccounts.find(sa => sa.name === value); + if (selectedStorageAccount) { + for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) { + this.migrationStateModel._databaseBackup.networkShares[i].storageAccount = selectedStorageAccount; + } } } - } - })); + })); - this._networkShareContainerStorageAccountRefreshButton = this._view.modelBuilder.button().withProps({ - iconPath: IconPathHelper.refresh, - iconHeight: 18, - iconWidth: 18, - height: 25, - ariaLabel: constants.REFRESH, - }).component(); - - this._disposables.push(this._networkShareContainerStorageAccountRefreshButton.onDidClick(async (value) => { - await this.loadNetworkShareStorageDropdown(); - })); - - const storageAccountContainer = this._view.modelBuilder.flexContainer() + this._networkShareContainerStorageAccountRefreshButton = this._view.modelBuilder.button() .withProps({ - CSSStyles: { - 'margin-top': '-1em' - } + iconPath: IconPathHelper.refresh, + iconHeight: 18, + iconWidth: 18, + height: 25, + ariaLabel: constants.REFRESH, }).component(); - storageAccountContainer.addItem(this._networkShareContainerStorageAccountDropdown, { - flex: '0 0 auto' - }); + this._disposables.push( + this._networkShareContainerStorageAccountRefreshButton.onDidClick( + value => this.loadNetworkShareStorageDropdown())); - storageAccountContainer.addItem(this._networkShareContainerStorageAccountRefreshButton, { - flex: '0 0 auto', - CSSStyles: { - 'margin-left': '5px' - } - }); + const storageAccountContainer = this._view.modelBuilder.flexContainer() + .withProps({ CSSStyles: { 'margin-top': '-1em' } }) + .component(); - const container = this._view.modelBuilder.flexContainer().withLayout({ - flexFlow: 'column', - }).withItems([ - azureAccountHeader, - azureAccountHelpText, - azureStoragePrivateEndpointInfoBox, - subscriptionLabel, - this._networkShareContainerSubscription, - locationLabel, - this._networkShareContainerLocation, - resourceGroupLabel, - this._networkShareStorageAccountResourceGroupDropdown, - storageAccountLabel, - storageAccountContainer, - ]).withProps({ - display: 'none' - }).component(); + storageAccountContainer.addItem( + this._networkShareContainerStorageAccountDropdown, + { flex: '0 0 auto' }); - return container; + storageAccountContainer.addItem( + this._networkShareContainerStorageAccountRefreshButton, + { flex: '0 0 auto', CSSStyles: { 'margin-left': '5px' } }); + + return this._view.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'column' }) + .withItems([ + azureAccountHeader, + azureAccountHelpText, + azureStoragePrivateEndpointInfoBox, + subscriptionLabel, + this._networkShareContainerSubscription, + locationLabel, + this._networkShareContainerLocation, + resourceGroupLabel, + this._networkShareStorageAccountResourceGroupDropdown, + storageAccountLabel, + storageAccountContainer]) + .withProps({ display: 'none' }) + .component(); + } + + private async _updatePageControlsVisibility(containerType: NetworkContainerType): Promise { + const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB; + const isNetworkShare = containerType === NetworkContainerType.NETWORK_SHARE; + const isBlobContainer = containerType === NetworkContainerType.BLOB_CONTAINER; + + await utils.updateControlDisplay(this._sourceConnectionContainer, isSqlDbTarget || isNetworkShare); + await utils.updateControlDisplay(this._migrationTableSection, isSqlDbTarget); + await utils.updateControlDisplay(this._radioButtonContainer, !isSqlDbTarget); + await utils.updateControlDisplay(this._networkDetailsContainer, !isSqlDbTarget); + await utils.updateControlDisplay(this._targetDatabaseContainer, !isSqlDbTarget); + await utils.updateControlDisplay(this._networkShareStorageAccountDetails, !isSqlDbTarget); + + await utils.updateControlDisplay(this._networkShareContainer, isNetworkShare); + await utils.updateControlDisplay(this._networkShareStorageAccountDetails, isNetworkShare); + await utils.updateControlDisplay(this._networkTableContainer, isNetworkShare); + await utils.updateControlDisplay(this._blobContainer, isBlobContainer); + await utils.updateControlDisplay(this._blobTableContainer, isBlobContainer); + + await this._windowsUserAccountText.updateProperties({ required: isNetworkShare }); + await this._passwordText.updateProperties({ required: isNetworkShare }); + await this._sqlSourceUsernameInput.updateProperties({ required: isNetworkShare || isSqlDbTarget }); + await this._sqlSourcePassword.updateProperties({ required: isNetworkShare || isSqlDbTarget }); } public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { + if (pageChangeInfo.newPage < pageChangeInfo.lastPage) { + return; + } if (this.migrationStateModel.refreshDatabaseBackupPage) { + this._networkShareButton.checked = this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE; + this._blobContainerButton.checked = this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER; + await this._updatePageControlsVisibility(this.migrationStateModel._databaseBackup.networkContainerType); + + const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB; + if (isSqlDbTarget) { + this.wizardPage.title = constants.DATABASE_TABLE_SELECTION_LABEL; + this.wizardPage.description = constants.DATABASE_TABLE_SELECTION_LABEL; + await this._loadTableData(); + } try { const isOfflineMigration = this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE; const lastBackupFileColumnIndex = this._blobContainerTargetDatabaseNamesTable.columns.length - 1; this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden = !isOfflineMigration; this._blobContainerTargetDatabaseNamesTable.columns.forEach(column => { - column.width = isOfflineMigration ? WIZARD_TABLE_COLUMN_WIDTH_SMALL : WIZARD_TABLE_COLUMN_WIDTH; + column.width = isOfflineMigration + ? WIZARD_TABLE_COLUMN_WIDTH_SMALL + : WIZARD_TABLE_COLUMN_WIDTH; }); - await this.switchNetworkContainerFields(this.migrationStateModel._databaseBackup.networkContainerType); - const connectionProfile = await this.migrationStateModel.getSourceConnectionProfile(); const queryProvider = azdata.dataprotocol.getProvider( (await this.migrationStateModel.getSourceConnectionProfile()).providerId, @@ -800,12 +746,15 @@ export class DatabaseBackupPage extends MigrationWizardPage { this.migrationStateModel.sourceConnectionId)), query); const username = results.rows[0][0].displayValue; - this.migrationStateModel._authenticationType = connectionProfile.authenticationType === 'SqlLogin' + this.migrationStateModel._authenticationType = connectionProfile.authenticationType === AuthenticationType.SqlLogin ? MigrationSourceAuthenticationType.Sql - : connectionProfile.authenticationType === 'Integrated' + : connectionProfile.authenticationType === AuthenticationType.Integrated ? MigrationSourceAuthenticationType.Integrated : undefined!; - this._sourceHelpText.value = constants.SQL_SOURCE_DETAILS(this.migrationStateModel._authenticationType, connectionProfile.serverName); + this._sourceHelpText.value = constants.SQL_SOURCE_DETAILS( + this.migrationStateModel._authenticationType, + connectionProfile.serverName); + this._sqlSourceUsernameInput.value = username; this._sqlSourcePassword.value = (await azdata.connection.getCredentials(this.migrationStateModel.sourceConnectionId)).password; this._windowsUserAccountText.value = this.migrationStateModel.savedInfo?.networkShares @@ -824,22 +773,28 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._existingDatabases = await this.migrationStateModel.getManagedDatabases(); } - let originalTargetDatabaseNames = this.migrationStateModel._targetDatabaseNames; - let originalNetworkShares = this.migrationStateModel._databaseBackup.networkShares || []; - let originalBlobs = this.migrationStateModel._databaseBackup.blobs; - if (this.migrationStateModel._didUpdateDatabasesForMigration) { + const originalTargetDatabaseNames = this.migrationStateModel._targetDatabaseNames; + const originalNetworkShares = this.migrationStateModel._databaseBackup.networkShares || []; + const originalBlobs = this.migrationStateModel._databaseBackup.blobs; + if (this.migrationStateModel._didUpdateDatabasesForMigration || + this.migrationStateModel._didDatabaseMappingChange) { + this.migrationStateModel._targetDatabaseNames = []; this.migrationStateModel._databaseBackup.networkShares = []; this.migrationStateModel._databaseBackup.blobs = []; } - this.migrationStateModel._databasesForMigration.forEach((db, index) => { - let targetDatabaseName = db; + this.migrationStateModel._databasesForMigration.forEach((sourceDatabaseName, index) => { + let targetDatabaseName = isSqlDbTarget + ? this.migrationStateModel._sourceTargetMapping.get(sourceDatabaseName)?.databaseName ?? sourceDatabaseName + : sourceDatabaseName; let networkShare = {}; let blob = {}; - if (this.migrationStateModel._didUpdateDatabasesForMigration) { - const dbIndex = this.migrationStateModel._sourceDatabaseNames?.indexOf(db); + if (this.migrationStateModel._didUpdateDatabasesForMigration || + this.migrationStateModel._didDatabaseMappingChange) { + + const dbIndex = this.migrationStateModel._sourceDatabaseNames?.indexOf(sourceDatabaseName); if (dbIndex > -1) { targetDatabaseName = originalTargetDatabaseNames[dbIndex] ?? targetDatabaseName; networkShare = originalNetworkShares[dbIndex] ?? networkShare; @@ -862,213 +817,209 @@ export class DatabaseBackupPage extends MigrationWizardPage { this.migrationStateModel._databaseBackup.networkShares[index] = networkShare; this.migrationStateModel._databaseBackup.blobs[index] = blob; - const targetDatabaseInput = this._view.modelBuilder.inputBox().withProps({ - required: true, - value: targetDatabaseName, - width: WIZARD_TABLE_COLUMN_WIDTH - }).withValidation(c => { - if (this._networkShareTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values. - c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR; - return false; - } - if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { // Making sure if database with same name is not present on the target Azure SQL - c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name); - return false; - } - if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) { - c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR; - return false; - } - return true; - }).component(); - this._disposables.push(targetDatabaseInput.onTextChanged(async (value) => { - this.migrationStateModel._targetDatabaseNames[index] = value.trim(); - await this.validateFields(); - })); + const targetDatabaseInput = this._view.modelBuilder.inputBox() + .withProps({ + required: true, + value: targetDatabaseName, + width: WIZARD_TABLE_COLUMN_WIDTH + }).withValidation(c => { + //Making sure no databases have duplicate values. + if (this._networkShareTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { + c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR; + return false; + } + // Making sure if database with same name is not present on the target Azure SQL + if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { + c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name); + return false; + } + if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) { + c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR; + return false; + } + return true; + }).component(); + this._disposables.push( + targetDatabaseInput.onTextChanged(async (value) => { + this.migrationStateModel._targetDatabaseNames[index] = value.trim(); + await this.validateFields(); + })); targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index]; this._networkShareTargetDatabaseNames.push(targetDatabaseInput); - const networkShareLocationInput = this._view.modelBuilder.inputBox().withProps({ - required: true, - placeHolder: constants.NETWORK_SHARE_PATH_FORMAT, - validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION, - width: '300px' - }).withValidation(c => { - if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { - if (c.value) { - if (!/^[\\\/]{2,}[^\\\/]+[\\\/]+[^\\\/]+/.test(c.value)) { - return false; - } - } - } - return true; - }).component(); - this._disposables.push(networkShareLocationInput.onTextChanged(async (value) => { - this.migrationStateModel._databaseBackup.networkShares[index].networkShareLocation = value.trim(); - await this.validateFields(); - })); - networkShareLocationInput.value = this.migrationStateModel._databaseBackup.networkShares[index]?.networkShareLocation; - this._networkShareLocations.push(networkShareLocationInput); - - const blobTargetDatabaseInput = this._view.modelBuilder.inputBox().withProps({ - required: true, - value: targetDatabaseName, - }).withValidation(c => { - if (this._blobContainerTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values. - c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR; - return false; - } - if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { // Making sure if database with same name is not present on the target Azure SQL - c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name); - return false; - } - if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) { - c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR; - return false; - } - return true; - }).component(); - this._disposables.push(blobTargetDatabaseInput.onTextChanged((value) => { - this.migrationStateModel._targetDatabaseNames[index] = value.trim(); - })); - targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index]; - this._blobContainerTargetDatabaseNames.push(blobTargetDatabaseInput); - - const blobContainerResourceDropdown = this._view.modelBuilder.dropDown().withProps({ - ariaLabel: constants.BLOB_CONTAINER_RESOURCE_GROUP, - editable: true, - fireOnTextChange: true, - required: true, - }).component(); - - const blobContainerStorageAccountDropdown = this._view.modelBuilder.dropDown().withProps({ - ariaLabel: constants.BLOB_CONTAINER_STORAGE_ACCOUNT, - editable: true, - fireOnTextChange: true, - required: true, - enabled: false, - }).component(); - - const blobContainerDropdown = this._view.modelBuilder.dropDown().withProps({ - ariaLabel: constants.BLOB_CONTAINER, - editable: true, - fireOnTextChange: true, - required: true, - enabled: false, - }).component(); - - const blobContainerLastBackupFileDropdown = this._view.modelBuilder.dropDown().withProps({ - ariaLabel: constants.BLOB_CONTAINER_LAST_BACKUP_FILE, - editable: true, - fireOnTextChange: true, - required: true, - enabled: false, - }).component(); - - this._disposables.push(blobContainerResourceDropdown.onValueChanged(async (value) => { - if (value && value !== 'undefined' && this.migrationStateModel._resourceGroups) { - const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value); - if (selectedResourceGroup && !blobResourceGroupErrorStrings.includes(value)) { - this.migrationStateModel._databaseBackup.blobs[index].resourceGroup = selectedResourceGroup; - await this.loadBlobStorageDropdown(index); - await blobContainerStorageAccountDropdown.updateProperties({ enabled: true }); - } else { - await this.disableBlobTableDropdowns(index, constants.RESOURCE_GROUP); - } - } - })); - this._blobContainerResourceGroupDropdowns.push(blobContainerResourceDropdown); - - this._disposables.push(blobContainerStorageAccountDropdown.onValueChanged(async (value) => { - if (value && value !== 'undefined') { - const selectedStorageAccount = this.migrationStateModel._storageAccounts.find(sa => sa.name === value); - if (selectedStorageAccount && !blobStorageAccountErrorStrings.includes(value)) { - this.migrationStateModel._databaseBackup.blobs[index].storageAccount = selectedStorageAccount; - await this.loadBlobContainerDropdown(index); - await blobContainerDropdown.updateProperties({ enabled: true }); - } else { - await this.disableBlobTableDropdowns(index, constants.STORAGE_ACCOUNT); - } - } - })); - this._blobContainerStorageAccountDropdowns.push(blobContainerStorageAccountDropdown); - - this._disposables.push(blobContainerDropdown.onValueChanged(async (value) => { - if (value && value !== 'undefined' && this.migrationStateModel._blobContainers) { - const selectedBlobContainer = this.migrationStateModel._blobContainers.find(blob => blob.name === value); - if (selectedBlobContainer && !blobContainerErrorStrings.includes(value)) { - this.migrationStateModel._databaseBackup.blobs[index].blobContainer = selectedBlobContainer; - if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { - await this.loadBlobLastBackupFileDropdown(index); - await blobContainerLastBackupFileDropdown.updateProperties({ enabled: true }); - } - } else { - await this.disableBlobTableDropdowns(index, constants.BLOB_CONTAINER); - } - } - })); - this._blobContainerDropdowns.push(blobContainerDropdown); - - if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { - this._disposables.push(blobContainerLastBackupFileDropdown.onValueChanged(value => { - if (value && value !== 'undefined') { - if (this.migrationStateModel._lastFileNames) { - const selectedLastBackupFile = this.migrationStateModel._lastFileNames.find(fileName => fileName.name === value); - if (selectedLastBackupFile && !blobFileErrorStrings.includes(value)) { - this.migrationStateModel._databaseBackup.blobs[index].lastBackupFile = selectedLastBackupFile.name; + const networkShareLocationInput = this._view.modelBuilder.inputBox() + .withProps({ + required: true, + placeHolder: constants.NETWORK_SHARE_PATH_FORMAT, + validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION, + width: '300px' + }).withValidation(c => { + if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { + if (c.value) { + if (!/^[\\\/]{2,}[^\\\/]+[\\\/]+[^\\\/]+/.test(c.value)) { + return false; } } } + return true; + }).component(); + this._disposables.push( + networkShareLocationInput.onTextChanged(async (value) => { + this.migrationStateModel._databaseBackup.networkShares[index].networkShareLocation = value.trim(); + await this.validateFields(); })); + networkShareLocationInput.value = this.migrationStateModel._databaseBackup.networkShares[index]?.networkShareLocation; + this._networkShareLocations.push(networkShareLocationInput); + + const blobTargetDatabaseInput = this._view.modelBuilder.inputBox() + .withProps({ + required: true, + value: targetDatabaseName, + }).withValidation(c => { + //Making sure no databases have duplicate values. + if (this._blobContainerTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { + c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR; + return false; + } + // Making sure if database with same name is not present on the target Azure SQL + if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { + c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name); + return false; + } + if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) { + c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR; + return false; + } + return true; + }).component(); + this._disposables.push( + blobTargetDatabaseInput.onTextChanged( + (value) => { this.migrationStateModel._targetDatabaseNames[index] = value.trim(); })); + + targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index]; + this._blobContainerTargetDatabaseNames.push(blobTargetDatabaseInput); + + const blobContainerResourceDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.BLOB_CONTAINER_RESOURCE_GROUP, + editable: true, + fireOnTextChange: true, + required: true, + }).component(); + + const blobContainerStorageAccountDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.BLOB_CONTAINER_STORAGE_ACCOUNT, + editable: true, + fireOnTextChange: true, + required: true, + enabled: false, + }).component(); + + const blobContainerDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.BLOB_CONTAINER, + editable: true, + fireOnTextChange: true, + required: true, + enabled: false, + }).component(); + + const blobContainerLastBackupFileDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.BLOB_CONTAINER_LAST_BACKUP_FILE, + editable: true, + fireOnTextChange: true, + required: true, + enabled: false, + }).component(); + + this._disposables.push( + blobContainerResourceDropdown.onValueChanged(async (value) => { + if (value && value !== 'undefined' && this.migrationStateModel._resourceGroups) { + const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value); + if (selectedResourceGroup && !blobResourceGroupErrorStrings.includes(value)) { + this.migrationStateModel._databaseBackup.blobs[index].resourceGroup = selectedResourceGroup; + this.loadBlobStorageDropdown(index); + await blobContainerStorageAccountDropdown.updateProperties({ enabled: true }); + } else { + await this.disableBlobTableDropdowns(index, constants.RESOURCE_GROUP); + } + } + })); + this._blobContainerResourceGroupDropdowns.push(blobContainerResourceDropdown); + + this._disposables.push( + blobContainerStorageAccountDropdown.onValueChanged(async (value) => { + if (value && value !== 'undefined') { + const selectedStorageAccount = this.migrationStateModel._storageAccounts.find(sa => sa.name === value); + if (selectedStorageAccount && !blobStorageAccountErrorStrings.includes(value)) { + this.migrationStateModel._databaseBackup.blobs[index].storageAccount = selectedStorageAccount; + await this.loadBlobContainerDropdown(index); + await blobContainerDropdown.updateProperties({ enabled: true }); + } else { + await this.disableBlobTableDropdowns(index, constants.STORAGE_ACCOUNT); + } + } + })); + this._blobContainerStorageAccountDropdowns.push(blobContainerStorageAccountDropdown); + + this._disposables.push( + blobContainerDropdown.onValueChanged(async (value) => { + if (value && value !== 'undefined' && this.migrationStateModel._blobContainers) { + const selectedBlobContainer = this.migrationStateModel._blobContainers.find(blob => blob.name === value); + if (selectedBlobContainer && !blobContainerErrorStrings.includes(value)) { + this.migrationStateModel._databaseBackup.blobs[index].blobContainer = selectedBlobContainer; + if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { + await this.loadBlobLastBackupFileDropdown(index); + await blobContainerLastBackupFileDropdown.updateProperties({ enabled: true }); + } + } else { + await this.disableBlobTableDropdowns(index, constants.BLOB_CONTAINER); + } + } + })); + this._blobContainerDropdowns.push(blobContainerDropdown); + + if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { + this._disposables.push( + blobContainerLastBackupFileDropdown.onValueChanged(value => { + if (value && value !== 'undefined') { + if (this.migrationStateModel._lastFileNames) { + const selectedLastBackupFile = this.migrationStateModel._lastFileNames.find(fileName => fileName.name === value); + if (selectedLastBackupFile && !blobFileErrorStrings.includes(value)) { + this.migrationStateModel._databaseBackup.blobs[index].lastBackupFile = selectedLastBackupFile.name; + } + } + } + })); this._blobContainerLastBackupFileDropdowns.push(blobContainerLastBackupFileDropdown); } }); this.migrationStateModel._sourceDatabaseNames = this.migrationStateModel._databasesForMigration; + const networkShareTargetData = this.migrationStateModel._databasesForMigration + .map((db, index) => [ + { value: db }, + { value: this._networkShareTargetDatabaseNames[index] }, + { value: this._networkShareLocations[index] }]); + await this._networkShareTargetDatabaseNamesTable.setDataValues(networkShareTargetData); - let data: azdata.DeclarativeTableCellValue[][] = []; - this.migrationStateModel._databasesForMigration.forEach((db, index) => { - const targetRow: azdata.DeclarativeTableCellValue[] = []; - targetRow.push({ - value: db - }); - targetRow.push({ - value: this._networkShareTargetDatabaseNames[index] - }); - targetRow.push({ - value: this._networkShareLocations[index] - }); - data.push(targetRow); - }); - await this._networkShareTargetDatabaseNamesTable.setDataValues(data); - - data = []; - this.migrationStateModel._databasesForMigration.forEach((db, index) => { - const targetRow: azdata.DeclarativeTableCellValue[] = []; - targetRow.push({ - value: db - }); - targetRow.push({ - value: this._blobContainerTargetDatabaseNames[index] - }); - targetRow.push({ - value: this._blobContainerResourceGroupDropdowns[index] - }); - targetRow.push({ - value: this._blobContainerStorageAccountDropdowns[index] - }); - targetRow.push({ - value: this._blobContainerDropdowns[index] - }); - targetRow.push({ - value: this._blobContainerLastBackupFileDropdowns[index] - }); - data.push(targetRow); - }); - await this._blobContainerTargetDatabaseNamesTable.setDataValues(data); + const blobContainerTargetData = this.migrationStateModel._databasesForMigration + .map((db, index) => [ + { value: db }, + { value: this._blobContainerTargetDatabaseNames[index] }, + { value: this._blobContainerResourceGroupDropdowns[index] }, + { value: this._blobContainerStorageAccountDropdowns[index] }, + { value: this._blobContainerDropdowns[index] }, + { value: this._blobContainerLastBackupFileDropdowns[index] }]); + await this._blobContainerTargetDatabaseNamesTable.setDataValues(blobContainerTargetData); await this.getSubscriptionValues(); + // clear change tracking flags this.migrationStateModel.refreshDatabaseBackupPage = false; + this.migrationStateModel._didUpdateDatabasesForMigration = false; + this.migrationStateModel._didDatabaseMappingChange = false; } catch (error) { console.log(error); let errorText = error?.message; @@ -1089,9 +1040,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { } const errors: string[] = []; - switch (this.migrationStateModel._databaseBackup.networkContainerType) { - case NetworkContainerType.NETWORK_SHARE: { + case NetworkContainerType.NETWORK_SHARE: if ((this._networkShareStorageAccountResourceGroupDropdown.value)?.displayName === constants.RESOURCE_GROUP_NOT_FOUND) { errors.push(constants.INVALID_RESOURCE_GROUP_ERROR); } @@ -1099,9 +1049,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR); } break; - } - - case NetworkContainerType.BLOB_CONTAINER: { + case NetworkContainerType.BLOB_CONTAINER: this._blobContainerResourceGroupDropdowns.forEach((v, index) => { if (this.shouldDisplayBlobDropdownError(v, [constants.RESOURCE_GROUP_NOT_FOUND])) { errors.push(constants.INVALID_BLOB_RESOURCE_GROUP_ERROR(this.migrationStateModel._databasesForMigration[index])); @@ -1144,17 +1092,25 @@ export class DatabaseBackupPage extends MigrationWizardPage { }); } break; - } - default: + const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB; + if (isSqlDbTarget) { + if (!this._validateTableSelection()) { + errors.push(constants.DATABASE_TABLE_VALIDATE_SELECTION_MESSAGE); + } + break; + } return false; } - this.migrationStateModel._targetDatabaseNames.forEach(t => { - if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(t)) { // Making sure if database with same name is not present on the target Azure SQL - errors.push(constants.DATABASE_ALREADY_EXISTS_MI(t, this.migrationStateModel._targetServerInstance.name)); - } - }); + if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI) { + this.migrationStateModel._targetDatabaseNames.forEach(t => { + // Making sure if database with same name is not present on the target Azure SQL + if (this._existingDatabases.includes(t)) { + errors.push(constants.DATABASE_ALREADY_EXISTS_MI(t, this.migrationStateModel._targetServerInstance.name)); + } + }); + } this.wizard.message = { text: errors.join(EOL), @@ -1194,9 +1150,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { } } } finally { - this.wizard.registerNavigationValidator((pageChangeInfo) => { - return true; - }); + this.wizard.registerNavigationValidator((pageChangeInfo) => true); } } @@ -1211,44 +1165,23 @@ export class DatabaseBackupPage extends MigrationWizardPage { this.wizard.nextButton.enabled = true; this.migrationStateModel._databaseBackup.networkContainerType = containerType; - await this._targetDatabaseContainer.updateCssStyles({ 'display': 'inline' }); + await this._updatePageControlsVisibility(containerType); + await this.validateFields(); + } - switch (containerType) { - case NetworkContainerType.NETWORK_SHARE: { - await this._networkShareContainer.updateCssStyles({ 'display': 'inline' }); - await this._networkShareStorageAccountDetails.updateCssStyles({ 'display': 'inline' }); - await this._networkTableContainer.updateCssStyles({ 'display': 'inline' }); - - await this._blobContainer.updateCssStyles({ 'display': 'none' }); - await this._blobTableContainer.updateCssStyles({ 'display': 'none' }); - - break; - } - case NetworkContainerType.BLOB_CONTAINER: { - await this._networkShareContainer.updateCssStyles({ 'display': 'none' }); - await this._networkShareStorageAccountDetails.updateCssStyles({ 'display': 'none' }); - await this._networkTableContainer.updateCssStyles({ 'display': 'none' }); - - await this._blobContainer.updateCssStyles({ 'display': 'inline' }); - await this._blobTableContainer.updateCssStyles({ 'display': 'inline' }); - - break; + private _validateTableSelection(): boolean { + for (const targetDatabaseInfo of this.migrationStateModel._sourceTargetMapping) { + const databaseInfo = targetDatabaseInfo[1]; + if (databaseInfo) { + for (const sourceTable of databaseInfo.sourceTables) { + const tableInfo = sourceTable[1]; + if (tableInfo.selectedForMigration === true) { + return true; + } + } } } - - await this._windowsUserAccountText.updateProperties({ - required: containerType === NetworkContainerType.NETWORK_SHARE - }); - await this._passwordText.updateProperties({ - required: containerType === NetworkContainerType.NETWORK_SHARE - }); - await this._sqlSourceUsernameInput.updateProperties({ - required: containerType === NetworkContainerType.NETWORK_SHARE - }); - await this._sqlSourcePassword.updateProperties({ - required: containerType === NetworkContainerType.NETWORK_SHARE - }); - await this.validateFields(); + return false; } private async validateFields(): Promise { @@ -1288,25 +1221,41 @@ export class DatabaseBackupPage extends MigrationWizardPage { } private async loadNetworkStorageResourceGroup(): Promise { - this._networkShareStorageAccountResourceGroupDropdown.loading = true; try { - this.migrationStateModel._storageAccounts = await utils.getStorageAccounts(this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription); - this.migrationStateModel._resourceGroups = await utils.getStorageAccountResourceGroups(this.migrationStateModel._storageAccounts, this.migrationStateModel._location); - this._networkShareStorageAccountResourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this.migrationStateModel._resourceGroups); - utils.selectDefaultDropdownValue(this._networkShareStorageAccountResourceGroupDropdown, this.migrationStateModel._databaseBackup?.networkShares[0]?.resourceGroup?.id, false); + this._networkShareStorageAccountResourceGroupDropdown.loading = true; + this.migrationStateModel._storageAccounts = await utils.getStorageAccounts( + this.migrationStateModel._azureAccount, + this.migrationStateModel._databaseBackup.subscription); + this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation( + this.migrationStateModel._storageAccounts, + this.migrationStateModel._location); + this._networkShareStorageAccountResourceGroupDropdown.values = utils.getResourceDropdownValues( + this.migrationStateModel._resourceGroups, + constants.RESOURCE_GROUP_NOT_FOUND); + + utils.selectDefaultDropdownValue( + this._networkShareStorageAccountResourceGroupDropdown, + this.migrationStateModel._databaseBackup?.networkShares[0]?.resourceGroup?.id, + false); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingNetworkStorageResourceGroup', error); } finally { this._networkShareStorageAccountResourceGroupDropdown.loading = false; - await this.loadNetworkShareStorageDropdown(); + this.loadNetworkShareStorageDropdown(); } } - private async loadNetworkShareStorageDropdown(): Promise { - this._networkShareContainerStorageAccountDropdown.loading = true; - this._networkShareStorageAccountResourceGroupDropdown.loading = true; + private loadNetworkShareStorageDropdown(): void { try { - this._networkShareContainerStorageAccountDropdown.values = await utils.getStorageAccountsDropdownValues(this.migrationStateModel._storageAccounts, this.migrationStateModel._location, this.migrationStateModel._databaseBackup.networkShares[0]?.resourceGroup); + this._networkShareContainerStorageAccountDropdown.loading = true; + this._networkShareStorageAccountResourceGroupDropdown.loading = true; + + this._networkShareContainerStorageAccountDropdown.values = utils.getAzureResourceDropdownValues( + this.migrationStateModel._storageAccounts, + this.migrationStateModel._location, + this.migrationStateModel._databaseBackup?.networkShares[0]?.resourceGroup?.name, + constants.NO_STORAGE_ACCOUNT_FOUND); + utils.selectDefaultDropdownValue(this._networkShareContainerStorageAccountDropdown, this.migrationStateModel?._databaseBackup?.networkShares[0]?.storageAccount?.id, false); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingNetworkShareStorageDropdown', error); @@ -1317,11 +1266,18 @@ export class DatabaseBackupPage extends MigrationWizardPage { } private async loadBlobResourceGroup(): Promise { - this._blobContainerResourceGroupDropdowns.forEach(v => v.loading = true); try { - this.migrationStateModel._storageAccounts = await utils.getStorageAccounts(this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription); - this.migrationStateModel._resourceGroups = await utils.getStorageAccountResourceGroups(this.migrationStateModel._storageAccounts, this.migrationStateModel._location); - const resourceGroupValues = await utils.getAzureResourceGroupsDropdownValues(this.migrationStateModel._resourceGroups); + this._blobContainerResourceGroupDropdowns.forEach(v => v.loading = true); + this.migrationStateModel._storageAccounts = await utils.getStorageAccounts( + this.migrationStateModel._azureAccount, + this.migrationStateModel._databaseBackup.subscription); + this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation( + this.migrationStateModel._storageAccounts, + this.migrationStateModel._location); + const resourceGroupValues = utils.getResourceDropdownValues( + this.migrationStateModel._resourceGroups, + constants.RESOURCE_GROUP_NOT_FOUND); + this._blobContainerResourceGroupDropdowns.forEach((dropDown, index) => { dropDown.values = resourceGroupValues; utils.selectDefaultDropdownValue(dropDown, this.migrationStateModel._databaseBackup?.blobs[index]?.resourceGroup?.id, false); @@ -1333,11 +1289,19 @@ export class DatabaseBackupPage extends MigrationWizardPage { } } - private async loadBlobStorageDropdown(index: number): Promise { - this._blobContainerStorageAccountDropdowns[index].loading = true; + private loadBlobStorageDropdown(index: number): void { try { - this._blobContainerStorageAccountDropdowns[index].values = await utils.getStorageAccountsDropdownValues(this.migrationStateModel._storageAccounts, this.migrationStateModel._location, this.migrationStateModel._databaseBackup.blobs[index]?.resourceGroup); - utils.selectDefaultDropdownValue(this._blobContainerStorageAccountDropdowns[index], this.migrationStateModel._databaseBackup?.blobs[index]?.storageAccount?.id, false); + this._blobContainerStorageAccountDropdowns[index].loading = true; + this._blobContainerStorageAccountDropdowns[index].values = utils.getAzureResourceDropdownValues( + this.migrationStateModel._storageAccounts, + this.migrationStateModel._location, + this.migrationStateModel._databaseBackup.blobs[index]?.resourceGroup?.name, + constants.NO_STORAGE_ACCOUNT_FOUND); + + utils.selectDefaultDropdownValue( + this._blobContainerStorageAccountDropdowns[index], + this.migrationStateModel._databaseBackup?.blobs[index]?.storageAccount?.id, + false); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobStorageDropdown', error); } finally { @@ -1346,11 +1310,21 @@ export class DatabaseBackupPage extends MigrationWizardPage { } private async loadBlobContainerDropdown(index: number): Promise { - this._blobContainerDropdowns[index].loading = true; try { - this.migrationStateModel._blobContainers = await utils.getBlobContainer(this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount); - this._blobContainerDropdowns[index].values = await utils.getBlobContainersValues(this.migrationStateModel._blobContainers); - utils.selectDefaultDropdownValue(this._blobContainerDropdowns[index], this.migrationStateModel._databaseBackup?.blobs[index]?.blobContainer?.id, false); + this._blobContainerDropdowns[index].loading = true; + this.migrationStateModel._blobContainers = await utils.getBlobContainer( + this.migrationStateModel._azureAccount, + this.migrationStateModel._databaseBackup.subscription, + this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount); + + this._blobContainerDropdowns[index].values = utils.getResourceDropdownValues( + this.migrationStateModel._blobContainers, + constants.NO_BLOBCONTAINERS_FOUND); + + utils.selectDefaultDropdownValue( + this._blobContainerDropdowns[index], + this.migrationStateModel._databaseBackup?.blobs[index]?.blobContainer?.id, + false); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobContainers', error); } finally { @@ -1359,11 +1333,19 @@ export class DatabaseBackupPage extends MigrationWizardPage { } private async loadBlobLastBackupFileDropdown(index: number): Promise { - this._blobContainerLastBackupFileDropdowns[index].loading = true; try { - this.migrationStateModel._lastFileNames = await utils.getBlobLastBackupFileNames(this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount, this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer); - this._blobContainerLastBackupFileDropdowns[index].values = await utils.getBlobLastBackupFileNamesValues(this.migrationStateModel._lastFileNames); - utils.selectDefaultDropdownValue(this._blobContainerLastBackupFileDropdowns[index], this.migrationStateModel._databaseBackup?.blobs[index]?.lastBackupFile, false); + this._blobContainerLastBackupFileDropdowns[index].loading = true; + this.migrationStateModel._lastFileNames = await utils.getBlobLastBackupFileNames( + this.migrationStateModel._azureAccount, + this.migrationStateModel._databaseBackup.subscription, + this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount, + this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer); + this._blobContainerLastBackupFileDropdowns[index].values = await utils.getBlobLastBackupFileNamesValues( + this.migrationStateModel._lastFileNames); + utils.selectDefaultDropdownValue( + this._blobContainerLastBackupFileDropdowns[index], + this.migrationStateModel._databaseBackup?.blobs[index]?.lastBackupFile, + false); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobLastBackupFiles', error); } finally { @@ -1395,4 +1377,133 @@ export class DatabaseBackupPage extends MigrationWizardPage { utils.selectDropDownIndex(this._blobContainerStorageAccountDropdowns[rowIndex], 0); await this._blobContainerStorageAccountDropdowns[rowIndex].updateProperties(dropdownProps); } + + private _migrationTableSelectionContainer(): azdata.FlexContainer { + this._refreshButton = this._view.modelBuilder.button() + .withProps({ + buttonType: azdata.ButtonType.Normal, + iconHeight: 16, + iconWidth: 16, + iconPath: IconPathHelper.refresh, + label: constants.DATABASE_TABLE_REFRESH_LABEL, + width: 70, + CSSStyles: { 'margin': '15px 0 0 0' }, + }) + .component(); + this._disposables.push( + this._refreshButton.onDidClick( + async e => await this._loadTableData())); + + this._refreshLoading = this._view.modelBuilder.loadingComponent() + .withItem(this._refreshButton) + .withProps({ loading: false }) + .component(); + + const cssClass = undefined; + this._databaseTable = this._view.modelBuilder.table() + .withProps({ + forceFitColumns: azdata.ColumnSizingMode.AutoFit, + height: '600px', + CSSStyles: { 'margin': '15px 0 0 0' }, + data: [], + columns: [ + { + name: constants.DATABASE_TABLE_SOURCE_DATABASE_COLUMN_LABEL, + value: 'sourceDatabase', + width: 200, + type: azdata.ColumnType.text, + cssClass: cssClass, + headerCssClass: cssClass, + }, + { + name: constants.DATABASE_TABLE_TARGET_DATABASE_COLUMN_LABEL, + value: 'targetDataase', + width: 200, + type: azdata.ColumnType.text, + cssClass: cssClass, + headerCssClass: cssClass, + }, + { + name: constants.DATABASE_TABLE_SELECTED_TABLES_COLUMN_LABEL, + value: 'selectedTables', + width: 160, + type: azdata.ColumnType.hyperlink, + cssClass: cssClass, + headerCssClass: cssClass, + icon: IconPathHelper.edit, + }, + ], + }) + .component(); + + this._disposables.push( + this._databaseTable.onCellAction!( + async (rowState: azdata.ICellActionEventArgs) => { + const buttonState = rowState; + if (buttonState?.column === 2) { + // open table selection dialog + const sourceDatabaseName = this._databaseTable.data[rowState.row][0]; + const targetDatabaseInfo = this.migrationStateModel._sourceTargetMapping.get(sourceDatabaseName); + const targetDatabaseName = targetDatabaseInfo?.databaseName; + if (sourceDatabaseName && targetDatabaseName) { + await this._openTableSelectionDialog( + sourceDatabaseName, + targetDatabaseName, + () => this._loadTableData()); + } + } + })); + + return this._view.modelBuilder.flexContainer() + .withItems([ + this._refreshLoading, + this._databaseTable]) + .withLayout({ flexFlow: 'column' }) + .component(); + } + + private async _openTableSelectionDialog( + sourceDatabaseName: string, + targetDatabaseName: string, + onSaveCallback: () => Promise): Promise { + const dialog = new TableMigrationSelectionDialog( + this.migrationStateModel, + sourceDatabaseName, + onSaveCallback); + await dialog.openDialog( + constants.SELECT_DATABASE_TABLES_TITLE(targetDatabaseName)); + } + + private async _loadTableData(): Promise { + this._refreshLoading.loading = true; + const data: any[][] = []; + + this.migrationStateModel._sourceTargetMapping.forEach((targetDatabaseInfo, sourceDatabaseName) => { + if (sourceDatabaseName) { + const tableCount = targetDatabaseInfo?.sourceTables.size ?? 0; + const hasTables = tableCount > 0; + + let selectedCount = 0; + targetDatabaseInfo?.sourceTables.forEach( + tableInfo => selectedCount += tableInfo.selectedForMigration ? 1 : 0); + + const hasSelectedTables = hasTables && selectedCount > 0; + data.push([ + sourceDatabaseName, + targetDatabaseInfo?.databaseName, + { // table selection + icon: hasSelectedTables + ? IconPathHelper.completedMigration + : IconPathHelper.edit, + title: hasTables + ? constants.TABLE_SELECTION_COUNT(selectedCount, tableCount) + : constants.TABLE_SELECTION_EDIT, + }]); + } + }); + + await this._databaseTable.updateProperty('data', data); + + this._refreshLoading.loading = false; + } } diff --git a/extensions/sql-migration/src/wizard/databaseSelectorPage.ts b/extensions/sql-migration/src/wizard/databaseSelectorPage.ts index 93e756a079..8209165aba 100644 --- a/extensions/sql-migration/src/wizard/databaseSelectorPage.ts +++ b/extensions/sql-migration/src/wizard/databaseSelectorPage.ts @@ -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); diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts index dd8e098711..384d144faf 100644 --- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts +++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts @@ -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 { + 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 { - this.wizard.registerNavigationValidator((pageChangeInfo) => { - return true; - }); + this.wizard.registerNavigationValidator((pageChangeInfo) => true); } protected async handleStateChange(e: StateChangeEvent): Promise { } 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, (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, + (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(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(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(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(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 { - 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 { - 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 { - this._statusLoadingComponent.loading = true; - try { - await this.loadStatus(); - } catch (error) { - logError(TelemetryViews.MigrationWizardIntegrationRuntimePage, 'ErrorLoadingMigrationServiceStatus', error); - } finally { - this._statusLoadingComponent.loading = false; - } - } - private async loadStatus(): Promise { 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({ - 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({ - 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; } diff --git a/extensions/sql-migration/src/wizard/migrationModePage.ts b/extensions/sql-migration/src/wizard/migrationModePage.ts index aa45394cea..6c4a2e326e 100644 --- a/extensions/sql-migration/src/wizard/migrationModePage.ts +++ b/extensions/sql-migration/src/wizard/migrationModePage.ts @@ -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 { - this.originalMigrationMode = this.migrationStateModel._databaseBackup.migrationMode; - this.wizard.registerNavigationValidator((e) => { - return true; - }); - } - public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { - 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 { + if (this._originalMigrationMode !== this.migrationStateModel._databaseBackup.migrationMode) { + this.migrationStateModel.refreshDatabaseBackupPage = true; + } + this.wizard.registerNavigationValidator((e) => true); + } + protected async handleStateChange(e: StateChangeEvent): Promise { } 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 }; } } diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index 2f6c4bbbc7..80c6cbd411 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -5,6 +5,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; +import * as utils from '../api/utils'; import * as mssql from 'mssql'; import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationStateModel, MigrationTargetType, PerformanceDataSourceOptions, StateChangeEvent } from '../models/stateMachine'; @@ -83,11 +84,11 @@ export class SKURecommendationPage extends MigrationWizardPage { name: constants.SKU_RECOMMENDATION_VM_CARD_TEXT, icon: IconPathHelper.sqlVmLogo }, - // { - // type: MigrationTargetType.SQLDB, - // name: constants.SKU_RECOMMENDATION_DB_CARD_TEXT, - // icon: IconPathHelper.sqlDatabaseLogo - // } + { + type: MigrationTargetType.SQLDB, + name: constants.SKU_RECOMMENDATION_SQLDB_CARD_TEXT, + icon: IconPathHelper.sqlDatabaseLogo + } ]; constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { @@ -97,60 +98,58 @@ export class SKURecommendationPage extends MigrationWizardPage { protected async registerContent(view: azdata.ModelView) { this._view = view; this._igComponent = this.createStatusComponent(view); // The first component giving basic information - this._assessmentStatusIcon = this._view.modelBuilder.image().withProps({ - iconPath: IconPathHelper.completedMigration, - iconHeight: 17, - iconWidth: 17, - width: 20, - height: 20 - }).component(); - const igContainer = this._view.modelBuilder.flexContainer().withProps({ - CSSStyles: { - 'align-items': 'center' - } - }).component(); - igContainer.addItem(this._assessmentStatusIcon, { - flex: '0 0 auto' - }); - igContainer.addItem(this._igComponent, { - flex: '0 0 auto' - }); + this._assessmentStatusIcon = this._view.modelBuilder.image() + .withProps({ + iconPath: IconPathHelper.completedMigration, + iconHeight: 17, + iconWidth: 17, + width: 20, + height: 20 + }).component(); + const igContainer = this._view.modelBuilder.flexContainer() + .withProps({ CSSStyles: { 'align-items': 'center' } }) + .component(); + igContainer.addItem(this._assessmentStatusIcon, { flex: '0 0 auto' }); + igContainer.addItem(this._igComponent, { flex: '0 0 auto' }); this._detailsComponent = this.createDetailsComponent(view); // The details of what can be moved - this._skipAssessmentCheckbox = view.modelBuilder.checkBox().withProps({ - label: constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR_BYPASS, - checked: false, - CSSStyles: { - ...styles.SECTION_HEADER_CSS, - 'margin': '10px 0 0 0', - 'display': 'none' - }, - }).component(); - this._skipAssessmentSubText = view.modelBuilder.text().withProps({ - value: constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR_DETAIL, - CSSStyles: { - 'margin': '0 0 0 15px', - 'font-size': '13px', - 'color': 'red', - 'width': '590px', - 'display': 'none' - }, - }).component(); + this._skipAssessmentCheckbox = view.modelBuilder.checkBox() + .withProps({ + label: constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR_BYPASS, + checked: false, + CSSStyles: { + ...styles.SECTION_HEADER_CSS, + 'margin': '10px 0 0 0', + 'display': 'none' + }, + }).component(); + this._skipAssessmentSubText = view.modelBuilder.text() + .withProps({ + value: constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR_DETAIL, + CSSStyles: { + 'margin': '0 0 0 15px', + 'font-size': '13px', + 'color': 'red', + 'width': '590px', + 'display': 'none' + }, + }).component(); - this._disposables.push(this._skipAssessmentCheckbox.onChanged(async (value) => { - await this._setAssessmentState(false, true); - })); + this._disposables.push( + this._skipAssessmentCheckbox.onChanged( + async (value) => await this._setAssessmentState(false, true))); - const refreshAssessmentButton = this._view.modelBuilder.button().withProps({ - iconPath: IconPathHelper.refresh, - label: constants.REFRESH_ASSESSMENT_BUTTON_LABEL, - width: 160, - height: 24, - CSSStyles: { - ...styles.BODY_CSS, - 'margin': '12px 0 4px 0' - } - }).component(); + const refreshAssessmentButton = this._view.modelBuilder.button() + .withProps({ + iconPath: IconPathHelper.refresh, + label: constants.REFRESH_ASSESSMENT_BUTTON_LABEL, + width: 160, + height: 24, + CSSStyles: { + ...styles.BODY_CSS, + 'margin': '12px 0 4px 0' + } + }).component(); this._disposables.push(refreshAssessmentButton.onDidClick(async () => { await this.startCardLoading(); @@ -158,97 +157,73 @@ export class SKURecommendationPage extends MigrationWizardPage { await this.constructDetails(); })); - const statusContainer = this._view.modelBuilder.flexContainer().withLayout({ - flexFlow: 'column', - }).withItems( - [ + const statusContainer = this._view.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'column' }) + .withItems([ igContainer, this._detailsComponent, refreshAssessmentButton, this._skipAssessmentCheckbox, - this._skipAssessmentSubText, - ] - ).withProps({ - CSSStyles: { - 'margin': '0' - } - }).component(); + this._skipAssessmentSubText]) + .withProps({ CSSStyles: { 'margin': '0' } }) + .component(); this._chooseTargetComponent = await this.createChooseTargetComponent(view); const _azureRecommendationsContainer = this.createAzureRecommendationContainer(view); this.assessmentGroupContainer = await this.createViewAssessmentsContainer(); - this._formContainer = view.modelBuilder.formContainer().withFormItems( - [ - { - title: '', - component: statusContainer - }, - { - component: this._chooseTargetComponent - }, - { - component: _azureRecommendationsContainer - }, - { - component: this.assessmentGroupContainer - }, - ] - ).withProps({ - CSSStyles: { - 'display': 'none', - 'padding-top': '0', - } - }); + this._formContainer = view.modelBuilder.formContainer() + .withFormItems([ + { component: statusContainer, title: '' }, + { component: this._chooseTargetComponent }, + { component: _azureRecommendationsContainer }, + { component: this.assessmentGroupContainer }]) + .withProps({ + CSSStyles: { + 'display': 'none', + 'padding-top': '0', + } + }); - this._assessmentComponent = this._view.modelBuilder.flexContainer().withLayout({ - height: '100%', - flexFlow: 'column' - }).withProps({ - CSSStyles: { - 'margin-left': '30px' - } - }).component(); + this._assessmentComponent = this._view.modelBuilder.flexContainer() + .withLayout({ height: '100%', flexFlow: 'column' }) + .withProps({ CSSStyles: { 'margin-left': '30px' } }) + .component(); this._assessmentComponent.addItem(this.createAssessmentProgress(), { flex: '0 0 auto' }); this._assessmentComponent.addItem(await this.createAssessmentInfo(), { flex: '0 0 auto' }); - this._rootContainer = this._view.modelBuilder.flexContainer().withLayout({ - height: '100%', - flexFlow: 'column' - }).withProps({ - ariaLive: 'polite', - }).component(); + this._rootContainer = this._view.modelBuilder.flexContainer() + .withLayout({ height: '100%', flexFlow: 'column' }) + .withProps({ ariaLive: 'polite' }) + .component(); this._rootContainer.addItem(this._assessmentComponent, { flex: '0 0 auto' }); this._rootContainer.addItem(this._formContainer.component(), { flex: '0 0 auto' }); - 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 this._view.initializeModel(this._rootContainer); } private createStatusComponent(view: azdata.ModelView): azdata.TextComponent { - const component = view.modelBuilder.text().withProps({ - CSSStyles: { - ...styles.SECTION_HEADER_CSS, - 'margin-left': '8px' - } - }).component(); + const component = view.modelBuilder.text() + .withProps({ + CSSStyles: { + ...styles.SECTION_HEADER_CSS, + 'margin-left': '8px' + } + }).component(); return component; } private createDetailsComponent(view: azdata.ModelView): azdata.TextComponent { - const component = view.modelBuilder.text().withProps({ - CSSStyles: { - ...styles.BODY_CSS - } - }).component(); + const component = view.modelBuilder.text() + .withProps({ CSSStyles: { ...styles.BODY_CSS } }) + .component(); return component; } private async createChooseTargetComponent(view: azdata.ModelView): Promise { - const chooseYourTargetText = this._view.modelBuilder.text().withProps({ value: constants.SKU_RECOMMENDATION_CHOOSE_A_TARGET, CSSStyles: { @@ -339,88 +314,89 @@ export class SKURecommendationPage extends MigrationWizardPage { ] }); - this._disposables.push(this._rbg.onLinkClick(async (e: azdata.RadioCardLinkClickEvent) => { - if (this.hasRecommendations()) { - const skuRecommendationResultsDialog = new SkuRecommendationResultsDialog(this.migrationStateModel, product.type); - if (e.cardId === skuRecommendationResultsDialog._targetType) { - await skuRecommendationResultsDialog.openDialog(e.cardId, this.migrationStateModel._skuRecommendationResults.recommendations); + this._disposables.push( + this._rbg.onLinkClick(async (e: azdata.RadioCardLinkClickEvent) => { + if (this.hasRecommendations()) { + if (e.cardId === product.type) { + const skuRecommendationResultsDialog = new SkuRecommendationResultsDialog(this.migrationStateModel, product.type); + await skuRecommendationResultsDialog.openDialog( + e.cardId, + this.migrationStateModel._skuRecommendationResults.recommendations); + } } - } - })); + })); }); this._disposables.push(this._rbg.onSelectionChanged(async (value) => { if (value) { this.assessmentGroupContainer.display = 'inline'; - await this.changeTargetType(value.cardId); + this.changeTargetType(value.cardId); } })); - this._rbgLoader = this._view.modelBuilder.loadingComponent().withItem( - this._rbg - ).component(); + this._rbgLoader = this._view.modelBuilder.loadingComponent() + .withItem(this._rbg) + .component(); - const component = this._view.modelBuilder.divContainer().withItems( - [ - chooseYourTargetText, - this._rbgLoader - ] - ).component(); + const component = this._view.modelBuilder.divContainer() + .withItems([chooseYourTargetText, this._rbgLoader]) + .component(); return component; } private async createViewAssessmentsContainer(): Promise { this._viewAssessmentsHelperText = this._view.modelBuilder.text().withProps({ value: constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI, - CSSStyles: { - ...styles.SECTION_HEADER_CSS - }, + CSSStyles: { ...styles.SECTION_HEADER_CSS }, width: WIZARD_INPUT_COMPONENT_WIDTH }).component(); const button = this._view.modelBuilder.button().withProps({ label: constants.VIEW_SELECT_BUTTON_LABEL, width: 100, - CSSStyles: { - 'margin': '12px 0' - } + CSSStyles: { 'margin': '12px 0' } }).component(); - let serverName = this.migrationStateModel.serverName || (await this.migrationStateModel.getSourceConnectionProfile()).serverName; + const serverName = this.migrationStateModel.serverName || (await this.migrationStateModel.getSourceConnectionProfile()).serverName; - let miDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLMI); - let vmDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLVM); + const miDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLMI); + const vmDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLVM); + const dbDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLDB); this._disposables.push(button.onDidClick(async (e) => { - if (this._rbg.selectedCardId === MigrationTargetType.SQLVM) { - this._rbg.selectedCardId = MigrationTargetType.SQLVM; - await vmDialog.openDialog(); - } else if (this._rbg.selectedCardId === MigrationTargetType.SQLMI) { - this._rbg.selectedCardId = MigrationTargetType.SQLMI; - await miDialog.openDialog(); + switch (this._rbg.selectedCardId) { + case MigrationTargetType.SQLVM: + this._rbg.selectedCardId = MigrationTargetType.SQLVM; + return await vmDialog.openDialog(); + case MigrationTargetType.SQLMI: + this._rbg.selectedCardId = MigrationTargetType.SQLMI; + return await miDialog.openDialog(); + case MigrationTargetType.SQLDB: + this._rbg.selectedCardId = MigrationTargetType.SQLDB; + return await dbDialog.openDialog(); } })); - this._databaseSelectedHelperText = this._view.modelBuilder.text().withProps({ - CSSStyles: { - ...styles.BODY_CSS, - }, - ariaLive: 'polite' - }).component(); + this._databaseSelectedHelperText = this._view.modelBuilder.text() + .withProps({ + CSSStyles: { ...styles.BODY_CSS }, + ariaLive: 'polite', + }).component(); - const container = this._view.modelBuilder.flexContainer().withItems([ - this._viewAssessmentsHelperText, - button, - this._databaseSelectedHelperText - ]).withProps({ - 'display': 'none' - }).component(); + const container = this._view.modelBuilder.flexContainer() + .withItems([ + this._viewAssessmentsHelperText, + button, + this._databaseSelectedHelperText + ]).withProps({ + 'display': 'none' + }).component(); return container; } - private async changeTargetType(newTargetType: string) { + private changeTargetType(newTargetType: string): void { switch (newTargetType) { - case MigrationTargetType.SQLMI: { + case MigrationTargetType.SQLMI: const miDbs = this.migrationStateModel._miDbs.filter( db => this.migrationStateModel._databasesForAssessment.findIndex( dba => dba === db) >= 0); @@ -429,9 +405,7 @@ export class SKURecommendationPage extends MigrationWizardPage { this.migrationStateModel._targetType = MigrationTargetType.SQLMI; this.migrationStateModel._databasesForMigration = miDbs; break; - } - - case MigrationTargetType.SQLVM: { + case MigrationTargetType.SQLVM: const vmDbs = this.migrationStateModel._vmDbs.filter( db => this.migrationStateModel._databasesForAssessment.findIndex( dba => dba === db) >= 0); @@ -440,10 +414,20 @@ export class SKURecommendationPage extends MigrationWizardPage { this.migrationStateModel._targetType = MigrationTargetType.SQLVM; this.migrationStateModel._databasesForMigration = vmDbs; break; - } + case MigrationTargetType.SQLDB: + const dbDbs = this.migrationStateModel._sqldbDbs.filter( + db => this.migrationStateModel._databasesForAssessment.findIndex( + dba => dba === db) >= 0); + + this._viewAssessmentsHelperText.value = constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_SQLDB; + this.migrationStateModel._targetType = MigrationTargetType.SQLDB; + this.migrationStateModel._databasesForMigration = dbDbs; + break; } - this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED(this.migrationStateModel._databasesForMigration.length, this.migrationStateModel._databasesForAssessment.length); + this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED( + this.migrationStateModel._databasesForMigration.length, + this.migrationStateModel._databasesForAssessment.length); this.migrationStateModel.refreshDatabaseBackupPage = true; } @@ -459,7 +443,7 @@ export class SKURecommendationPage extends MigrationWizardPage { const errors: string[] = []; await this._setAssessmentState(true, false); try { - await this.migrationStateModel.getDatabaseAssessments(MigrationTargetType.SQLMI); + await this.migrationStateModel.getDatabaseAssessments([MigrationTargetType.SQLMI, MigrationTargetType.SQLDB]); const assessmentError = this.migrationStateModel._assessmentResults?.assessmentError; if (assessmentError) { errors.push(`message: ${assessmentError.message}${EOL}stack: ${assessmentError.stack}`); @@ -485,14 +469,16 @@ export class SKURecommendationPage extends MigrationWizardPage { } else { this._assessmentStatusIcon.iconPath = IconPathHelper.completedMigration; this._igComponent.value = constants.ASSESSMENT_COMPLETED(serverName); - this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL(this.migrationStateModel._assessmentResults?.databaseAssessments?.length); + this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL( + this.migrationStateModel._assessmentResults?.databaseAssessments?.length); } } } else { // use prior assessment results this._assessmentStatusIcon.iconPath = IconPathHelper.completedMigration; this._igComponent.value = constants.ASSESSMENT_COMPLETED(serverName); - this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL(this.migrationStateModel._assessmentResults?.databaseAssessments?.length); + this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL( + this.migrationStateModel._assessmentResults?.databaseAssessments?.length); } if (this.migrationStateModel.savedInfo?.migrationTargetType) { @@ -543,46 +529,36 @@ export class SKURecommendationPage extends MigrationWizardPage { } private async _setAssessmentState(assessing: boolean, failedAssessment: boolean): Promise { - let display: azdata.DisplayType = assessing ? 'block' : 'none'; - await this._assessmentComponent.updateCssStyles({ 'display': display }); - this._assessmentComponent.display = display; + await utils.updateControlDisplay(this._assessmentComponent, assessing); + await utils.updateControlDisplay(this._skipAssessmentCheckbox, !assessing && failedAssessment); + await utils.updateControlDisplay( + this._skipAssessmentSubText, + !assessing && failedAssessment, + 'block'); + await utils.updateControlDisplay(this._formContainer.component(), !assessing); + await utils.updateControlDisplay( + this._chooseTargetComponent, + !failedAssessment || this._skipAssessmentCheckbox.checked === true); - display = !assessing && failedAssessment ? 'block' : 'none'; - await this._skipAssessmentCheckbox.updateCssStyles({ 'display': display }); - this._skipAssessmentCheckbox.display = display; - await this._skipAssessmentSubText.updateCssStyles({ 'display': display }); - this._skipAssessmentSubText.display = display; - - await this._formContainer.component().updateCssStyles({ 'display': !assessing ? 'block' : 'none' }); - - display = failedAssessment && !this._skipAssessmentCheckbox.checked ? 'none' : 'block'; - await this._chooseTargetComponent.updateCssStyles({ 'display': display }); - this._chooseTargetComponent.display = display; - - display = !this._rbg.selectedCardId || failedAssessment && !this._skipAssessmentCheckbox.checked ? 'none' : 'inline'; - await this.assessmentGroupContainer.updateCssStyles({ 'display': display }); - this.assessmentGroupContainer.display = display; - - display = (this._rbg.selectedCardId - && (!failedAssessment || this._skipAssessmentCheckbox.checked) - && this.migrationStateModel._databasesForMigration?.length > 0) - ? 'inline' - : 'none'; + await utils.updateControlDisplay( + this.assessmentGroupContainer, + this._rbg.selectedCardId !== undefined && (!failedAssessment || this._skipAssessmentCheckbox.checked === true)); this._assessmentLoader.loading = assessing; } public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { + if (pageChangeInfo.newPage < pageChangeInfo.lastPage) { + return; + } + this.wizard.registerNavigationValidator((pageChangeInfo) => { - const errors: string[] = []; - this.wizard.message = { - text: '', - level: azdata.window.MessageLevel.Error - }; + this.wizard.message = { text: '' }; if (pageChangeInfo.newPage < pageChangeInfo.lastPage) { return true; } + const errors: string[] = []; if (this._rbg.selectedCardId === undefined || this._rbg.selectedCardId === '') { errors.push(constants.SELECT_TARGET_TO_CONTINUE); } @@ -620,10 +596,13 @@ export class SKURecommendationPage extends MigrationWizardPage { public async refreshCardText(showLoadingIcon: boolean = true): Promise { this._rbgLoader.loading = showLoadingIcon && true; - if (this._rbg.selectedCardId === MigrationTargetType.SQLMI) { - this.migrationStateModel._databasesForMigration = this.migrationStateModel._miDbs; - } else { - this.migrationStateModel._databasesForMigration = this.migrationStateModel._vmDbs; + switch (this._rbg.selectedCardId) { + case MigrationTargetType.SQLMI: + this.migrationStateModel._databasesForMigration = this.migrationStateModel._miDbs; + case MigrationTargetType.SQLDB: + this.migrationStateModel._databasesForMigration = this.migrationStateModel._sqldbDbs; + case MigrationTargetType.SQLVM: + this.migrationStateModel._databasesForMigration = this.migrationStateModel._vmDbs; } const dbCount = this.migrationStateModel._assessmentResults?.databaseAssessments?.length; @@ -650,21 +629,24 @@ export class SKURecommendationPage extends MigrationWizardPage { this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.AZURE_RECOMMENDATION_CARD_IN_PROGRESS; } else { - this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.AZURE_RECOMMENDATION_CARD_NOT_ENABLED; + this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = + constants.AZURE_RECOMMENDATION_CARD_NOT_ENABLED; } } let recommendation; switch (product.type) { case MigrationTargetType.SQLMI: - this._rbg.cards[index].descriptions[CardDescriptionIndex.ASSESSMENT_STATUS].textValue = constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount); + this._rbg.cards[index].descriptions[CardDescriptionIndex.ASSESSMENT_STATUS].textValue = + constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount); if (this.hasRecommendations()) { recommendation = this.migrationStateModel._skuRecommendationResults.recommendations.sqlMiRecommendationResults[0]; // result returned but no SKU recommended - if (!recommendation.targetSku) { - this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.SKU_RECOMMENDATION_NO_RECOMMENDATION; + if (!recommendation?.targetSku) { + this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = + constants.SKU_RECOMMENDATION_NO_RECOMMENDATION; } else { const serviceTier = recommendation.targetSku.category?.sqlServiceTier === mssql.AzureSqlPaaSServiceTier.GeneralPurpose @@ -675,41 +657,62 @@ export class SKURecommendationPage extends MigrationWizardPage { : recommendation.targetSku.category?.hardwareType === mssql.AzureSqlPaaSHardwareType.PremiumSeries ? constants.PREMIUM_SERIES : constants.PREMIUM_SERIES_MEMORY_OPTIMIZED; - this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.MI_CONFIGURATION_PREVIEW(hardwareType, serviceTier, recommendation.targetSku.computeSize!, recommendation.targetSku.storageMaxSizeInMb! / 1024); + this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = + constants.MI_CONFIGURATION_PREVIEW( + hardwareType, + serviceTier, + recommendation.targetSku.computeSize!, + recommendation.targetSku.storageMaxSizeInMb! / 1024); } } break; case MigrationTargetType.SQLVM: - this._rbg.cards[index].descriptions[CardDescriptionIndex.ASSESSMENT_STATUS].textValue = constants.CAN_BE_MIGRATED(dbCount, dbCount); + this._rbg.cards[index].descriptions[CardDescriptionIndex.ASSESSMENT_STATUS].textValue = + constants.CAN_BE_MIGRATED(dbCount, dbCount); if (this.hasRecommendations()) { recommendation = this.migrationStateModel._skuRecommendationResults.recommendations.sqlVmRecommendationResults[0]; // result returned but no SKU recommended - if (!recommendation.targetSku) { - this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.SKU_RECOMMENDATION_NO_RECOMMENDATION; + if (!recommendation?.targetSku) { + this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = + constants.SKU_RECOMMENDATION_NO_RECOMMENDATION; this._rbg.cards[index].descriptions[CardDescriptionIndex.VM_CONFIGURATIONS].textValue = ''; } else { - this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.VM_CONFIGURATION(recommendation.targetSku.virtualMachineSize!.sizeName, recommendation.targetSku.virtualMachineSize!.vCPUsAvailable); + this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = + constants.VM_CONFIGURATION( + recommendation.targetSku.virtualMachineSize!.sizeName, + recommendation.targetSku.virtualMachineSize!.vCPUsAvailable); - const dataDisk = constants.STORAGE_CONFIGURATION(recommendation.targetSku.dataDiskSizes![0].size, recommendation.targetSku.dataDiskSizes!.length); - const storageDisk = constants.STORAGE_CONFIGURATION(recommendation.targetSku.logDiskSizes![0].size, recommendation.targetSku.logDiskSizes!.length); + const dataDisk = constants.STORAGE_CONFIGURATION( + recommendation.targetSku.dataDiskSizes![0].size, + recommendation.targetSku.dataDiskSizes!.length); + const storageDisk = constants.STORAGE_CONFIGURATION( + recommendation.targetSku.logDiskSizes![0].size, + recommendation.targetSku.logDiskSizes!.length); const tempDb = recommendation.targetSku.tempDbDiskSizes!.length > 0 - ? constants.STORAGE_CONFIGURATION(recommendation.targetSku.logDiskSizes![0].size, recommendation.targetSku.logDiskSizes!.length) + ? constants.STORAGE_CONFIGURATION( + recommendation.targetSku.logDiskSizes![0].size, + recommendation.targetSku.logDiskSizes!.length) : constants.LOCAL_SSD; - this._rbg.cards[index].descriptions[CardDescriptionIndex.VM_CONFIGURATIONS].textValue = constants.VM_CONFIGURATION_PREVIEW(dataDisk, storageDisk, tempDb); + this._rbg.cards[index].descriptions[CardDescriptionIndex.VM_CONFIGURATIONS].textValue = + constants.VM_CONFIGURATION_PREVIEW(dataDisk, storageDisk, tempDb); } } break; case MigrationTargetType.SQLDB: - this._rbg.cards[index].descriptions[CardDescriptionIndex.ASSESSMENT_STATUS].textValue = constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount); + this._rbg.cards[index].descriptions[CardDescriptionIndex.ASSESSMENT_STATUS].textValue = + constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount); if (this.hasRecommendations()) { - const successfulRecommendationsCount = this.migrationStateModel._skuRecommendationResults.recommendations.sqlDbRecommendationResults.filter(r => r.targetSku !== null).length; - this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.RECOMMENDATIONS_AVAILABLE(successfulRecommendationsCount); + const successfulRecommendationsCount = + this.migrationStateModel._skuRecommendationResults.recommendations.sqlDbRecommendationResults + .filter(r => r.targetSku !== null).length; + this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = + constants.RECOMMENDATIONS_AVAILABLE(successfulRecommendationsCount); } break; } @@ -719,10 +722,10 @@ export class SKURecommendationPage extends MigrationWizardPage { await this._rbg.updateProperties({ cards: this._rbg.cards }); if (this._rbg.selectedCardId) { - await this.changeTargetType(this._rbg.selectedCardId); + this.changeTargetType(this._rbg.selectedCardId); } - this._rbgLoader.loading = showLoadingIcon && false; + this._rbgLoader.loading = false; } public async startCardLoading(): Promise { @@ -730,9 +733,7 @@ export class SKURecommendationPage extends MigrationWizardPage { // but updating the card text will do for now this._supportedProducts.forEach((product, index) => { this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.LOADING_RECOMMENDATIONS; - this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textStyles = { - ...styles.BODY_CSS, - }; + this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textStyles = { ...styles.BODY_CSS }; this._rbg.cards[index].descriptions[CardDescriptionIndex.VM_CONFIGURATIONS].textValue = ''; }); @@ -740,22 +741,24 @@ export class SKURecommendationPage extends MigrationWizardPage { } private createAssessmentProgress(): azdata.FlexContainer { + this._assessmentLoader = this._view.modelBuilder.loadingComponent() + .component(); - this._assessmentLoader = this._view.modelBuilder.loadingComponent().component(); + this._assessmentProgress = this._view.modelBuilder.text() + .withProps({ + value: constants.ASSESSMENT_IN_PROGRESS, + CSSStyles: { + ...styles.PAGE_TITLE_CSS, + 'margin-right': '20px' + } + }).component(); - this._assessmentProgress = this._view.modelBuilder.text().withProps({ - value: constants.ASSESSMENT_IN_PROGRESS, - CSSStyles: { - ...styles.PAGE_TITLE_CSS, - 'margin-right': '20px' - } - }).component(); - - this._progressContainer = this._view.modelBuilder.flexContainer().withLayout({ - height: '100%', - flexFlow: 'row', - alignItems: 'center' - }).component(); + this._progressContainer = this._view.modelBuilder.flexContainer() + .withLayout({ + height: '100%', + flexFlow: 'row', + alignItems: 'center' + }).component(); this._progressContainer.addItem(this._assessmentProgress, { flex: '0 0 auto' }); this._progressContainer.addItem(this._assessmentLoader, { flex: '0 0 auto' }); @@ -763,45 +766,50 @@ export class SKURecommendationPage extends MigrationWizardPage { } private async createAssessmentInfo(): Promise { - this._assessmentInfo = this._view.modelBuilder.text().withProps({ - value: constants.ASSESSMENT_IN_PROGRESS_CONTENT((await this.migrationStateModel.getSourceConnectionProfile()).serverName), - CSSStyles: { - ...styles.BODY_CSS, - 'width': '660px' - } - }).component(); + this._assessmentInfo = this._view.modelBuilder.text() + .withProps({ + value: constants.ASSESSMENT_IN_PROGRESS_CONTENT((await this.migrationStateModel.getSourceConnectionProfile()).serverName), + CSSStyles: { + ...styles.BODY_CSS, + 'width': '660px' + } + }).component(); return this._assessmentInfo; } private createAzureRecommendationContainer(_view: azdata.ModelView): azdata.FlexContainer { - const container = _view.modelBuilder.flexContainer().withProps({ - CSSStyles: { - 'flex-direction': 'column', - 'max-width': '700px', - 'margin-bottom': '1em', - } - }).component(); - this._azureRecommendationSectionText = _view.modelBuilder.text().withProps({ - value: constants.AZURE_RECOMMENDATION, - description: '', - CSSStyles: { - ...styles.SECTION_HEADER_CSS, - 'margin': '12px 0 8px', - } - }).component(); - this._azureRecommendationInfoText = _view.modelBuilder.text().withProps({ - value: constants.AZURE_RECOMMENDATION_STATUS_NOT_ENABLED, - CSSStyles: { - ...styles.BODY_CSS, - 'margin': '0', - } - }).component(); - const learnMoreLink = _view.modelBuilder.hyperlink().withProps({ - label: constants.LEARN_MORE, - ariaLabel: constants.LEARN_MORE, - url: 'https://aka.ms/ads-sql-sku-recommend', - showLinkIcon: true, - }).component(); + const container = _view.modelBuilder.flexContainer() + .withProps({ + CSSStyles: { + 'flex-direction': 'column', + 'max-width': '700px', + 'margin-bottom': '1em', + } + }).component(); + this._azureRecommendationSectionText = _view.modelBuilder.text() + .withProps({ + value: constants.AZURE_RECOMMENDATION, + description: '', + CSSStyles: { + ...styles.SECTION_HEADER_CSS, + 'margin': '12px 0 8px', + } + }).component(); + this._azureRecommendationInfoText = _view.modelBuilder.text() + .withProps({ + value: constants.AZURE_RECOMMENDATION_STATUS_NOT_ENABLED, + CSSStyles: { + ...styles.BODY_CSS, + 'margin': '0', + } + }).component(); + const learnMoreLink = _view.modelBuilder.hyperlink() + .withProps({ + label: constants.LEARN_MORE, + ariaLabel: constants.LEARN_MORE, + url: 'https://aka.ms/ads-sql-sku-recommend', + showLinkIcon: true, + }).component(); const azureRecommendationsInfoContainer = _view.modelBuilder.flexContainer() .withItems([ this._azureRecommendationInfoText, @@ -822,19 +830,16 @@ export class SKURecommendationPage extends MigrationWizardPage { } }).component(); const getAzureRecommendationDialog = new GetAzureRecommendationDialog(this, this.wizard, this.migrationStateModel); - this._disposables.push(this._getAzureRecommendationButton.onDidClick(async (e) => { - await getAzureRecommendationDialog.openDialog(); - })); + this._disposables.push(this._getAzureRecommendationButton.onDidClick( + async (e) => await getAzureRecommendationDialog.openDialog())); + + this._skuGetRecommendationContainer = _view.modelBuilder.flexContainer() + .withProps({ CSSStyles: { 'flex-direction': 'column', } }) + .component(); - this._skuGetRecommendationContainer = _view.modelBuilder.flexContainer().withProps({ - CSSStyles: { - 'flex-direction': 'column', - } - }).component(); this._skuGetRecommendationContainer.addItems([ azureRecommendationsInfoContainer, - this._getAzureRecommendationButton, - ]); + this._getAzureRecommendationButton]); this._skuDataCollectionStatusContainer = this.createPerformanceCollectionStatusContainer(_view); this._skuEditParametersContainer = this.createSkuEditParameters(_view); @@ -848,30 +853,28 @@ export class SKURecommendationPage extends MigrationWizardPage { } private createPerformanceCollectionStatusContainer(_view: azdata.ModelView): azdata.FlexContainer { - const container = _view.modelBuilder.flexContainer().withProps({ - CSSStyles: { - 'flex-direction': 'column', - 'display': this.migrationStateModel.performanceCollectionNotStarted() ? 'none' : 'block', - } - }).component(); + const container = _view.modelBuilder.flexContainer() + .withProps({ + CSSStyles: { + 'flex-direction': 'column', + 'display': this.migrationStateModel.performanceCollectionNotStarted() ? 'none' : 'block', + } + }).component(); - this._skuDataCollectionStatusIcon = _view.modelBuilder.image().withProps({ - iconPath: IconPathHelper.inProgressMigration, - iconHeight: 16, - iconWidth: 16, - width: 16, - height: 16, - CSSStyles: { - 'margin-right': '4px', - } - }).component(); - this._skuDataCollectionStatusText = _view.modelBuilder.text().withProps({ - value: '', - CSSStyles: { - ...styles.BODY_CSS, - 'margin': '0' - } - }).component(); + this._skuDataCollectionStatusIcon = _view.modelBuilder.image() + .withProps({ + iconPath: IconPathHelper.inProgressMigration, + iconHeight: 16, + iconWidth: 16, + width: 16, + height: 16, + CSSStyles: { 'margin-right': '4px' } + }).component(); + this._skuDataCollectionStatusText = _view.modelBuilder.text() + .withProps({ + value: '', + CSSStyles: { ...styles.BODY_CSS, 'margin': '0' } + }).component(); const statusIconTextContainer = _view.modelBuilder.flexContainer() .withItems([ @@ -887,25 +890,27 @@ export class SKURecommendationPage extends MigrationWizardPage { } }).component(); - this._skuDataCollectionTimerText = _view.modelBuilder.text().withProps({ - value: '', - CSSStyles: { - ...styles.LIGHT_LABEL_CSS, - 'margin': '0 0 8px 20px', - } - }).component(); + this._skuDataCollectionTimerText = _view.modelBuilder.text() + .withProps({ + value: '', + CSSStyles: { + ...styles.LIGHT_LABEL_CSS, + 'margin': '0 0 8px 20px', + } + }).component(); - this._skuStopDataCollectionButton = this._view.modelBuilder.button().withProps({ - iconPath: IconPathHelper.cancel, - label: constants.STOP_PERFORMANCE_COLLECTION, - width: 150, - height: 24, - CSSStyles: { - ...styles.BODY_CSS, - 'margin': '0', - 'display': this.migrationStateModel.performanceCollectionInProgress() ? 'block' : 'none', - } - }).component(); + this._skuStopDataCollectionButton = this._view.modelBuilder.button() + .withProps({ + iconPath: IconPathHelper.cancel, + label: constants.STOP_PERFORMANCE_COLLECTION, + width: 150, + height: 24, + CSSStyles: { + ...styles.BODY_CSS, + 'margin': '0', + 'display': this.migrationStateModel.performanceCollectionInProgress() ? 'block' : 'none', + } + }).component(); this._disposables.push(this._skuStopDataCollectionButton.onDidClick(async (e) => { await this.migrationStateModel.stopPerfDataCollection(); await this.refreshAzureRecommendation(); @@ -922,16 +927,16 @@ export class SKURecommendationPage extends MigrationWizardPage { 'display': this.migrationStateModel.performanceCollectionStopped() ? 'block' : 'none', } }).component(); - this._disposables.push(this._skuRestartDataCollectionButton.onDidClick(async (e) => { - await this.migrationStateModel.startPerfDataCollection( - this.migrationStateModel._skuRecommendationPerformanceLocation, - this.migrationStateModel._performanceDataQueryIntervalInSeconds, - this.migrationStateModel._staticDataQueryIntervalInSeconds, - this.migrationStateModel._numberOfPerformanceDataQueryIterations, - this - ); - await this.refreshSkuRecommendationComponents(); - })); + this._disposables.push( + this._skuRestartDataCollectionButton.onDidClick(async (e) => { + await this.migrationStateModel.startPerfDataCollection( + this.migrationStateModel._skuRecommendationPerformanceLocation, + this.migrationStateModel._performanceDataQueryIntervalInSeconds, + this.migrationStateModel._staticDataQueryIntervalInSeconds, + this.migrationStateModel._numberOfPerformanceDataQueryIterations, + this); + await this.refreshSkuRecommendationComponents(); + })); this._refreshAzureRecommendationButton = this._view.modelBuilder.button().withProps({ iconPath: IconPathHelper.refresh, @@ -943,16 +948,18 @@ export class SKURecommendationPage extends MigrationWizardPage { 'margin': '0 0 0 12px', } }).component(); - this._disposables.push(this._refreshAzureRecommendationButton.onDidClick(async (e) => { - await this.refreshAzureRecommendation(); - })); - this._skuLastRefreshTimeText = this._view.modelBuilder.text().withProps({ - value: constants.LAST_REFRESHED_TIME(), - CSSStyles: { - ...styles.SMALL_NOTE_CSS, - 'margin': '0 0 4px 4px', - }, - }).component(); + this._disposables.push( + this._refreshAzureRecommendationButton.onDidClick( + async (e) => await this.refreshAzureRecommendation())); + + this._skuLastRefreshTimeText = this._view.modelBuilder.text() + .withProps({ + value: constants.LAST_REFRESHED_TIME(), + CSSStyles: { + ...styles.SMALL_NOTE_CSS, + 'margin': '0 0 4px 4px', + }, + }).component(); this._skuControlButtonsContainer = _view.modelBuilder.flexContainer() .withProps({ CSSStyles: { @@ -966,14 +973,13 @@ export class SKURecommendationPage extends MigrationWizardPage { this._skuStopDataCollectionButton, this._skuRestartDataCollectionButton, this._refreshAzureRecommendationButton, - this._skuLastRefreshTimeText, - ]); + this._skuLastRefreshTimeText]); container.addItems([ this._skuControlButtonsContainer, statusIconTextContainer, - this._skuDataCollectionTimerText, - ]); + this._skuDataCollectionTimerText]); + return container; } @@ -984,83 +990,94 @@ export class SKURecommendationPage extends MigrationWizardPage { 'display': this.migrationStateModel.performanceCollectionNotStarted() ? 'none' : 'block', } }).component(); - const recommendationParametersSection = _view.modelBuilder.text().withProps({ - value: constants.RECOMMENDATION_PARAMETERS, - CSSStyles: { - ...styles.BODY_CSS, - 'margin': '16px 0 8px' - } - }).component(); + const recommendationParametersSection = _view.modelBuilder.text() + .withProps({ + value: constants.RECOMMENDATION_PARAMETERS, + CSSStyles: { + ...styles.BODY_CSS, + 'margin': '16px 0 8px' + } + }).component(); - const editParametersButton = this._view.modelBuilder.button().withProps({ - iconPath: IconPathHelper.edit, - label: constants.EDIT_PARAMETERS, - width: 130, - height: 24, - CSSStyles: { - ...styles.BODY_CSS, - 'margin': '0', - 'width': 'fit-content', - } - }).component(); + const editParametersButton = this._view.modelBuilder.button() + .withProps({ + iconPath: IconPathHelper.edit, + label: constants.EDIT_PARAMETERS, + width: 130, + height: 24, + CSSStyles: { + ...styles.BODY_CSS, + 'margin': '0', + 'width': 'fit-content', + } + }).component(); let skuEditParametersDialog = new SkuEditParametersDialog(this, this.migrationStateModel); - this._disposables.push(editParametersButton.onDidClick(async () => { - await skuEditParametersDialog.openDialog(); - })); + this._disposables.push( + editParametersButton.onDidClick( + async () => await skuEditParametersDialog.openDialog())); const createParameterGroup = (label: string, value: string): { flexContainer: azdata.FlexContainer, text: azdata.TextComponent, } => { - const parameterGroup = this._view.modelBuilder.flexContainer().withProps({ - CSSStyles: { - 'flex-direction': 'row', - 'align-content': 'left', - 'width': 'fit-content', - 'margin-right': '24px', - } - }).component(); - const labelText = this._view.modelBuilder.text().withProps({ - value: label + ':', - CSSStyles: { - ...styles.LIGHT_LABEL_CSS, - 'width': 'fit-content', - 'margin-right': '4px', - } - }).component(); - const valueText = this._view.modelBuilder.text().withProps({ - value: value, - CSSStyles: { - ...styles.BODY_CSS, - 'width': 'fit-content,', - } - }).component(); + const parameterGroup = this._view.modelBuilder.flexContainer() + .withProps({ + CSSStyles: { + 'flex-direction': 'row', + 'align-content': 'left', + 'width': 'fit-content', + 'margin-right': '24px', + } + }).component(); + const labelText = this._view.modelBuilder.text() + .withProps({ + value: label + ':', + CSSStyles: { + ...styles.LIGHT_LABEL_CSS, + 'width': 'fit-content', + 'margin-right': '4px', + } + }).component(); + const valueText = this._view.modelBuilder.text() + .withProps({ + value: value, + CSSStyles: { + ...styles.BODY_CSS, + 'width': 'fit-content,', + } + }).component(); parameterGroup.addItems([ labelText, - valueText, - ]); + valueText]); return { flexContainer: parameterGroup, text: valueText, }; }; - const scaleFactorParameterGroup = createParameterGroup(constants.SCALE_FACTOR, this.migrationStateModel._skuScalingFactor.toString()); + const scaleFactorParameterGroup = createParameterGroup( + constants.SCALE_FACTOR, + this.migrationStateModel._skuScalingFactor.toString()); this._skuScaleFactorText = scaleFactorParameterGroup.text; - const skuTargetPercentileParameterGroup = createParameterGroup(constants.PERCENTAGE_UTILIZATION, constants.PERCENTAGE(this.migrationStateModel._skuTargetPercentile)); + const skuTargetPercentileParameterGroup = createParameterGroup( + constants.PERCENTAGE_UTILIZATION, + constants.PERCENTAGE(this.migrationStateModel._skuTargetPercentile)); this._skuTargetPercentileText = skuTargetPercentileParameterGroup.text; - const skuEnablePreviewParameterGroup = createParameterGroup(constants.ENABLE_PREVIEW_SKU, this.migrationStateModel._skuEnablePreview ? constants.YES : constants.NO); + const skuEnablePreviewParameterGroup = createParameterGroup( + constants.ENABLE_PREVIEW_SKU, + this.migrationStateModel._skuEnablePreview ? constants.YES : constants.NO); this._skuEnablePreviewSkuText = skuEnablePreviewParameterGroup.text; - const parametersContainer = _view.modelBuilder.flexContainer().withProps({ - CSSStyles: { - 'margin': '8px 0', - 'flex-direction': 'row', - 'width': 'fit-content', - } - }).component(); + const parametersContainer = _view.modelBuilder.flexContainer() + .withProps({ + CSSStyles: { + 'margin': '8px 0', + 'flex-direction': 'row', + 'width': 'fit-content', + } + }).component(); parametersContainer.addItems([ scaleFactorParameterGroup.flexContainer, skuTargetPercentileParameterGroup.flexContainer, @@ -1172,7 +1189,10 @@ export class SKURecommendationPage extends MigrationWizardPage { } private hasRecommendations(): boolean { - return this.migrationStateModel._skuRecommendationResults?.recommendations && !this.migrationStateModel._skuRecommendationResults?.recommendationError ? true : false; + return this.migrationStateModel._skuRecommendationResults?.recommendations + && !this.migrationStateModel._skuRecommendationResults?.recommendationError + ? true + : false; } } diff --git a/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts b/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts index 878dfbc9e6..9faf7e8385 100644 --- a/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts +++ b/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts @@ -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, diff --git a/extensions/sql-migration/src/wizard/summaryPage.ts b/extensions/sql-migration/src/wizard/summaryPage.ts index 3cdf09345e..f6c75de05e 100644 --- a/extensions/sql-migration/src/wizard/summaryPage.ts +++ b/extensions/sql-migration/src/wizard/summaryPage.ts @@ -24,134 +24,204 @@ export class SummaryPage extends MigrationWizardPage { protected async registerContent(view: azdata.ModelView): Promise { 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 { 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 { this._flexContainer.clearItems(); - this.wizard.registerNavigationValidator(async (pageChangeInfo) => { - return true; - }); + this.wizard.registerNavigationValidator(async (pageChangeInfo) => true); } protected async handleStateChange(e: StateChangeEvent): Promise { } private async createNetworkContainerRows(): Promise { - 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; } diff --git a/extensions/sql-migration/src/wizard/targetSelectionPage.ts b/extensions/sql-migration/src/wizard/targetSelectionPage.ts index 0960bd3aa0..e3d510e0ee 100644 --- a/extensions/sql-migration/src/wizard/targetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/targetSelectionPage.ts @@ -13,7 +13,8 @@ import * as styles from '../constants/styles'; import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; import * as utils from '../api/utils'; import { azureResource } from 'azurecore'; -import { SqlVMServer } from '../api/azure'; +import { AzureSqlDatabaseServer, SqlVMServer } from '../api/azure'; +import { collectTargetDatabaseInfo, TargetDatabaseInfo } from '../api/sqlUtils'; export class TargetSelectionPage extends MigrationWizardPage { private _view!: azdata.ModelView; @@ -25,92 +26,111 @@ export class TargetSelectionPage extends MigrationWizardPage { private _accountTenantFlexContainer!: azdata.FlexContainer; private _azureSubscriptionDropdown!: azdata.DropDownComponent; private _azureLocationDropdown!: azdata.DropDownComponent; + private _azureResourceGroupLabel!: azdata.TextComponent; private _azureResourceGroupDropdown!: azdata.DropDownComponent; private _azureResourceDropdownLabel!: azdata.TextComponent; private _azureResourceDropdown!: azdata.DropDownComponent; - + private _azureResourceTable!: azdata.DeclarativeTableComponent; + private _resourceSelectionContainer!: azdata.FlexContainer; + private _resourceAuthenticationContainer!: azdata.FlexContainer; + private _targetUserNameInputBox!: azdata.InputBoxComponent; + private _targetPasswordInputBox!: azdata.InputBoxComponent; + private _testConectionButton!: azdata.ButtonComponent; + private _connectionResultsInfoBox!: azdata.InfoBoxComponent; private _migrationTargetPlatform!: MigrationTargetType; - constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { - super(wizard, azdata.window.createWizardPage(constants.AZURE_SQL_TARGET_PAGE_TITLE), migrationStateModel); + constructor( + wizard: azdata.window.Wizard, + migrationStateModel: MigrationStateModel) { + super( + wizard, + azdata.window.createWizardPage(constants.AZURE_SQL_TARGET_PAGE_TITLE), + migrationStateModel); } protected async registerContent(view: azdata.ModelView): Promise { this._view = view; - this._pageDescription = this._view.modelBuilder.text().withProps({ - value: constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(), - CSSStyles: { - ...styles.BODY_CSS, - 'margin': '0' - } - }).component(); - - const form = this._view.modelBuilder.formContainer() - .withFormItems( - [ - { - component: this._pageDescription - }, - { - component: this.createAzureAccountsDropdown() - }, - { - component: this.createAzureTenantContainer() - }, - { - component: this.createTargetDropdownContainer() - } - ] - ).withProps({ - CSSStyles: { - 'padding-top': '0' - } + this._pageDescription = this._view.modelBuilder.text() + .withProps({ + value: constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(), + CSSStyles: { ...styles.BODY_CSS, 'margin': '0' } }).component(); - this._disposables.push(this._view.onClosed(e => { - this._disposables.forEach( - d => { try { d.dispose(); } catch { } }); - })); + const form = this._view.modelBuilder.formContainer() + .withFormItems([ + { component: this._pageDescription }, + { component: this.createAzureAccountsDropdown() }, + { component: this.createAzureTenantContainer() }, + { component: this.createTargetDropdownContainer() } + ]).withProps({ + CSSStyles: { 'padding-top': '0' } + }).component(); + + this._disposables.push( + this._view.onClosed(e => { + this._disposables.forEach( + d => { try { d.dispose(); } catch { } }); + })); await this._view.initializeModel(form); } public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { + if (pageChangeInfo.newPage < pageChangeInfo.lastPage) { + return; + } switch (this.migrationStateModel._targetType) { case MigrationTargetType.SQLMI: this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_MI_CARD_TEXT); this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE; this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE; break; - case MigrationTargetType.SQLVM: this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_VM_CARD_TEXT); this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE; this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE; break; + case MigrationTargetType.SQLDB: + this._pageDescription.value = constants.AZURE_SQL_TARGET_PAGE_DESCRIPTION(constants.SKU_RECOMMENDATION_SQLDB_CARD_TEXT); + this._azureResourceDropdownLabel.value = constants.AZURE_SQL_DATABASE; + this._azureResourceDropdown.ariaLabel = constants.AZURE_SQL_DATABASE; + this._updateConnectionButtonState(); + break; } + const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB; + await this._targetUserNameInputBox.updateProperty('required', isSqlDbTarget); + await this._targetPasswordInputBox.updateProperty('required', isSqlDbTarget); + await utils.updateControlDisplay(this._resourceAuthenticationContainer, isSqlDbTarget); await this.populateAzureAccountsDropdown(); + if (this._migrationTargetPlatform !== this.migrationStateModel._targetType) { // if the user had previously selected values on this page, then went back to change the migration target platform // and came back, forcibly reload the location/resource group/resource values since they will now be different this._migrationTargetPlatform = this.migrationStateModel._targetType; + + await this._azureResourceTable.setDataValues([]); + this._targetPasswordInputBox.value = ''; + this.migrationStateModel._sqlMigrationServices = undefined!; + this.migrationStateModel._targetServerInstance = undefined!; + this.migrationStateModel._resourceGroup = undefined!; + this.migrationStateModel._location = undefined!; await this.populateLocationDropdown(); - await this.populateResourceGroupDropdown(); - await this.populateResourceInstanceDropdown(); + } + + if (this.migrationStateModel._didUpdateDatabasesForMigration) { + await this._azureResourceTable.setDataValues([]); + this._initializeSourceTargetDatabaseMap(); + this._updateConnectionButtonState(); } this.wizard.registerNavigationValidator((pageChangeInfo) => { - const errors: string[] = []; - this.wizard.message = { - text: '', - level: azdata.window.MessageLevel.Error - }; - + this.wizard.message = { text: '' }; if (pageChangeInfo.newPage < pageChangeInfo.lastPage) { return true; } + const errors: string[] = []; if (!this.migrationStateModel._azureAccount) { errors.push(constants.INVALID_ACCOUNT_ERROR); } @@ -130,26 +150,49 @@ export class TargetSelectionPage extends MigrationWizardPage { const resourceDropdownValue = (this._azureResourceDropdown.value)?.displayName; switch (this.migrationStateModel._targetType) { - case MigrationTargetType.SQLMI: { + case MigrationTargetType.SQLMI: const targetMi = this.migrationStateModel._targetServerInstance as azureResource.AzureSqlManagedInstance; if (!targetMi || resourceDropdownValue === constants.NO_MANAGED_INSTANCE_FOUND) { errors.push(constants.INVALID_MANAGED_INSTANCE_ERROR); - break; } if (targetMi.properties.state !== 'Ready') { errors.push(constants.MI_NOT_READY_ERROR(targetMi.name, targetMi.properties.state)); - break; } break; - } - case MigrationTargetType.SQLVM: { + case MigrationTargetType.SQLVM: const targetVm = this.migrationStateModel._targetServerInstance as SqlVMServer; if (!targetVm || resourceDropdownValue === constants.NO_VIRTUAL_MACHINE_FOUND) { errors.push(constants.INVALID_VIRTUAL_MACHINE_ERROR); - break; } break; - } + case MigrationTargetType.SQLDB: + const targetSqlDB = this.migrationStateModel._targetServerInstance as AzureSqlDatabaseServer; + if (!targetSqlDB || resourceDropdownValue === constants.NO_SQL_DATABASE_FOUND) { + errors.push(constants.INVALID_SQL_DATABASE_ERROR); + } + // TODO: verify what state check is needed/possible? + if (targetSqlDB.properties.state !== 'Ready') { + errors.push(constants.SQLDB_NOT_READY_ERROR(targetSqlDB.name, targetSqlDB.properties.state)); + } + + // validate target sqldb username exists + const targetUsernameValue = this._targetUserNameInputBox.value ?? ''; + if (targetUsernameValue.length < 1) { + errors.push(constants.MISSING_TARGET_USERNAME_ERROR); + } + + // validate target sqldb password exists + const targetPasswordValue = this._targetPasswordInputBox.value ?? ''; + if (targetPasswordValue.length < 1) { + errors.push(constants.MISSING_TARGET_PASSWORD_ERROR); + } + + // validate source and target database mapping + const mappingErrors = this._getSourceTargetMappingErrors(); + if (mappingErrors.length > 0) { + errors.push(...mappingErrors); + } + break; } if (errors.length > 0) { @@ -164,417 +207,808 @@ export class TargetSelectionPage extends MigrationWizardPage { } public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { - this.wizard.registerNavigationValidator((e) => { - return true; - }); + this.wizard.registerNavigationValidator(async (pageChangeInfo) => true); } protected async handleStateChange(e: StateChangeEvent): Promise { } private createAzureAccountsDropdown(): azdata.FlexContainer { - const azureAccountLabel = this._view.modelBuilder.text().withProps({ - value: constants.ACCOUNTS_SELECTION_PAGE_TITLE, - width: WIZARD_INPUT_COMPONENT_WIDTH, - requiredIndicator: true, - CSSStyles: { - ...styles.LABEL_CSS, - 'margin-top': '-1em' - } - }).component(); - this._azureAccountsDropdown = this._view.modelBuilder.dropDown().withProps({ - ariaLabel: constants.ACCOUNTS_SELECTION_PAGE_TITLE, - width: WIZARD_INPUT_COMPONENT_WIDTH, - editable: true, - required: true, - fireOnTextChange: true, - placeholder: constants.SELECT_AN_ACCOUNT, - CSSStyles: { - 'margin-top': '-1em' - }, - }).component(); - this._disposables.push(this._azureAccountsDropdown.onValueChanged(async (value) => { - if (value && value !== 'undefined') { - const selectedAccount = this.migrationStateModel._azureAccounts.find(account => account.displayInfo.displayName === value); - this.migrationStateModel._azureAccount = (selectedAccount) - ? utils.deepClone(selectedAccount)! - : undefined!; + const azureAccountLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.ACCOUNTS_SELECTION_PAGE_TITLE, + width: WIZARD_INPUT_COMPONENT_WIDTH, + requiredIndicator: true, + CSSStyles: { ...styles.LABEL_CSS, 'margin-top': '-1em' } + }).component(); + this._azureAccountsDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.ACCOUNTS_SELECTION_PAGE_TITLE, + width: WIZARD_INPUT_COMPONENT_WIDTH, + editable: true, + required: true, + fireOnTextChange: true, + placeholder: constants.SELECT_AN_ACCOUNT, + CSSStyles: { 'margin-top': '-1em' }, + }).component(); + this._disposables.push( + this._azureAccountsDropdown.onValueChanged(async (value) => { + if (value && value !== 'undefined') { + const selectedAccount = this.migrationStateModel._azureAccounts.find(account => account.displayInfo.displayName === value); + this.migrationStateModel._azureAccount = (selectedAccount) + ? utils.deepClone(selectedAccount)! + : undefined!; + } await this.populateTenantsDropdown(); - } - })); + })); const linkAccountButton = this._view.modelBuilder.hyperlink() .withProps({ label: constants.ACCOUNT_LINK_BUTTON_LABEL, url: '', - CSSStyles: { - ...styles.BODY_CSS - } + CSSStyles: { ...styles.BODY_CSS } }) .component(); - this._disposables.push(linkAccountButton.onDidClick(async (event) => { - await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount'); - await this.populateAzureAccountsDropdown(); - this.wizard.message = { - text: '' - }; - await this._azureAccountsDropdown.validate(); - })); + this._disposables.push( + linkAccountButton.onDidClick(async (event) => { + await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount'); + await this.populateAzureAccountsDropdown(); + this.wizard.message = { text: '' }; + await this._azureAccountsDropdown.validate(); + })); - const flexContainer = this._view.modelBuilder.flexContainer() - .withLayout({ - flexFlow: 'column' - }) + return this._view.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'column' }) .withItems([ azureAccountLabel, this._azureAccountsDropdown, - linkAccountButton - ]) + linkAccountButton]) .component(); - return flexContainer; } private createAzureTenantContainer(): azdata.FlexContainer { - const azureTenantDropdownLabel = this._view.modelBuilder.text().withProps({ - value: constants.AZURE_TENANT, - CSSStyles: { - ...styles.LABEL_CSS - } - }).component(); + const azureTenantDropdownLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.AZURE_TENANT, + CSSStyles: { ...styles.LABEL_CSS } + }).component(); - this._accountTenantDropdown = this._view.modelBuilder.dropDown().withProps({ - ariaLabel: constants.AZURE_TENANT, - width: WIZARD_INPUT_COMPONENT_WIDTH, - editable: true, - fireOnTextChange: true, - placeholder: constants.SELECT_A_TENANT - }).component(); + this._accountTenantDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.AZURE_TENANT, + width: WIZARD_INPUT_COMPONENT_WIDTH, + editable: true, + fireOnTextChange: true, + placeholder: constants.SELECT_A_TENANT + }).component(); - this._disposables.push(this._accountTenantDropdown.onValueChanged(async (value) => { - if (value && value !== 'undefined') { - /** - * Replacing all the tenants in azure account with the tenant user has selected. - * All azure requests will only run on this tenant from now on - */ - const selectedTenant = this.migrationStateModel._accountTenants.find(tenant => tenant.displayName === value); - if (selectedTenant) { - this.migrationStateModel._azureTenant = utils.deepClone(selectedTenant)!; - this.migrationStateModel._azureAccount.properties.tenants = [this.migrationStateModel._azureTenant]; + this._disposables.push( + this._accountTenantDropdown.onValueChanged(async (value) => { + if (value && value !== 'undefined') { + /** + * Replacing all the tenants in azure account with the tenant user has selected. + * All azure requests will only run on this tenant from now on + */ + const selectedTenant = this.migrationStateModel._accountTenants.find(tenant => tenant.displayName === value); + if (selectedTenant) { + this.migrationStateModel._azureTenant = utils.deepClone(selectedTenant)!; + this.migrationStateModel._azureAccount.properties.tenants = [this.migrationStateModel._azureTenant]; + } } await this.populateSubscriptionDropdown(); - } - })); + })); this._accountTenantFlexContainer = this._view.modelBuilder.flexContainer() - .withLayout({ - flexFlow: 'column' - }) + .withLayout({ flexFlow: 'column' }) .withItems([ azureTenantDropdownLabel, - this._accountTenantDropdown - ]) - .withProps({ - CSSStyles: { - 'display': 'none' - } - }) + this._accountTenantDropdown]) + .withProps({ CSSStyles: { 'display': 'none' } }) .component(); return this._accountTenantFlexContainer; } private createTargetDropdownContainer(): azdata.FlexContainer { - const subscriptionDropdownLabel = this._view.modelBuilder.text().withProps({ - value: constants.SUBSCRIPTION, - description: constants.TARGET_SUBSCRIPTION_INFO, - width: WIZARD_INPUT_COMPONENT_WIDTH, - requiredIndicator: true, - CSSStyles: { - ...styles.LABEL_CSS, - } - }).component(); - this._azureSubscriptionDropdown = this._view.modelBuilder.dropDown().withProps({ - ariaLabel: constants.SUBSCRIPTION, - width: WIZARD_INPUT_COMPONENT_WIDTH, - editable: true, - required: true, - fireOnTextChange: true, - placeholder: constants.SELECT_A_SUBSCRIPTION, - CSSStyles: { - 'margin-top': '-1em' - }, - }).component(); - this._disposables.push(this._azureSubscriptionDropdown.onValueChanged(async (value) => { - if (value && value !== 'undefined' && value !== constants.NO_SUBSCRIPTIONS_FOUND) { - const selectedSubscription = this.migrationStateModel._subscriptions.find(subscription => `${subscription.name} - ${subscription.id}` === value); - this.migrationStateModel._targetSubscription = (selectedSubscription) - ? utils.deepClone(selectedSubscription)! - : undefined!; - this.migrationStateModel.refreshDatabaseBackupPage = true; + const subscriptionDropdownLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.SUBSCRIPTION, + description: constants.TARGET_SUBSCRIPTION_INFO, + width: WIZARD_INPUT_COMPONENT_WIDTH, + requiredIndicator: true, + CSSStyles: { ...styles.LABEL_CSS, } + }).component(); + this._azureSubscriptionDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.SUBSCRIPTION, + width: WIZARD_INPUT_COMPONENT_WIDTH, + editable: true, + required: true, + fireOnTextChange: true, + placeholder: constants.SELECT_A_SUBSCRIPTION, + CSSStyles: { 'margin-top': '-1em' }, + }).component(); + this._disposables.push( + this._azureSubscriptionDropdown.onValueChanged(async (value) => { + if (value && value !== 'undefined' && value !== constants.NO_SUBSCRIPTIONS_FOUND) { + const selectedSubscription = this.migrationStateModel._subscriptions.find(subscription => `${subscription.name} - ${subscription.id}` === value); + this.migrationStateModel._targetSubscription = (selectedSubscription) + ? utils.deepClone(selectedSubscription)! + : undefined!; + this.migrationStateModel.refreshDatabaseBackupPage = true; + } await this.populateLocationDropdown(); - } - })); + })); - const azureLocationLabel = this._view.modelBuilder.text().withProps({ - value: constants.LOCATION, - description: constants.TARGET_LOCATION_INFO, - width: WIZARD_INPUT_COMPONENT_WIDTH, - requiredIndicator: true, - CSSStyles: { - ...styles.LABEL_CSS - } - }).component(); - this._azureLocationDropdown = this._view.modelBuilder.dropDown().withProps({ - ariaLabel: constants.LOCATION, - width: WIZARD_INPUT_COMPONENT_WIDTH, - editable: true, - required: true, - fireOnTextChange: true, - placeholder: constants.SELECT_A_LOCATION, - CSSStyles: { - 'margin-top': '-1em' - }, - }).component(); - this._disposables.push(this._azureLocationDropdown.onValueChanged(async (value) => { - if (value && value !== 'undefined' && value !== constants.NO_LOCATION_FOUND) { - const selectedLocation = this.migrationStateModel._locations.find(location => location.displayName === value); - this.migrationStateModel._location = (selectedLocation) - ? utils.deepClone(selectedLocation)! - : undefined!; + const azureLocationLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.LOCATION, + description: constants.TARGET_LOCATION_INFO, + width: WIZARD_INPUT_COMPONENT_WIDTH, + requiredIndicator: true, + CSSStyles: { ...styles.LABEL_CSS } + }).component(); + this._azureLocationDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.LOCATION, + width: WIZARD_INPUT_COMPONENT_WIDTH, + editable: true, + required: true, + fireOnTextChange: true, + placeholder: constants.SELECT_A_LOCATION, + CSSStyles: { 'margin-top': '-1em' }, + }).component(); + this._disposables.push( + this._azureLocationDropdown.onValueChanged(async (value) => { + if (value && value !== 'undefined' && value !== constants.NO_LOCATION_FOUND) { + const selectedLocation = this.migrationStateModel._locations.find(location => location.displayName === value); + this.migrationStateModel._location = (selectedLocation) + ? utils.deepClone(selectedLocation)! + : undefined!; + } this.migrationStateModel.refreshDatabaseBackupPage = true; await this.populateResourceGroupDropdown(); - await this.populateResourceInstanceDropdown(); - } - })); + })); - const azureResourceGroupLabel = this._view.modelBuilder.text().withProps({ - value: constants.RESOURCE_GROUP, - description: constants.TARGET_RESOURCE_GROUP_INFO, - width: WIZARD_INPUT_COMPONENT_WIDTH, - requiredIndicator: true, - CSSStyles: { - ...styles.LABEL_CSS - } - }).component(); - this._azureResourceGroupDropdown = this._view.modelBuilder.dropDown().withProps({ - ariaLabel: constants.RESOURCE_GROUP, - width: WIZARD_INPUT_COMPONENT_WIDTH, - editable: true, - required: true, - fireOnTextChange: true, - placeholder: constants.SELECT_A_RESOURCE_GROUP, - CSSStyles: { - 'margin-top': '-1em' - }, - }).component(); - this._disposables.push(this._azureResourceGroupDropdown.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._resourceGroup = (selectedResourceGroup) - ? utils.deepClone(selectedResourceGroup)! - : undefined!; - await this.populateResourceInstanceDropdown(); - } - })); + this._resourceSelectionContainer = this._createResourceDropdowns(); + this._resourceAuthenticationContainer = this._createResourceAuthenticationContainer(); - this._azureResourceDropdownLabel = this._view.modelBuilder.text().withProps({ - value: constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE, - description: constants.TARGET_RESOURCE_INFO, - width: WIZARD_INPUT_COMPONENT_WIDTH, - requiredIndicator: true, - CSSStyles: { - ...styles.LABEL_CSS - } - }).component(); - this._azureResourceDropdown = this._view.modelBuilder.dropDown().withProps({ - ariaLabel: constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE, - width: WIZARD_INPUT_COMPONENT_WIDTH, - editable: true, - required: true, - fireOnTextChange: true, - placeholder: constants.SELECT_A_SERVICE, - CSSStyles: { - 'margin-top': '-1em' - }, - }).component(); - this._disposables.push(this._azureResourceDropdown.onValueChanged(async (value) => { - if (value && value !== 'undefined' && value !== constants.NO_MANAGED_INSTANCE_FOUND && value !== constants.NO_VIRTUAL_MACHINE_FOUND) { - this.migrationStateModel._sqlMigrationServices = undefined!; - - switch (this.migrationStateModel._targetType) { - case MigrationTargetType.SQLVM: - const selectedVm = this.migrationStateModel._targetSqlVirtualMachines.find(vm => vm.name === value); - if (selectedVm) { - this.migrationStateModel._targetServerInstance = utils.deepClone(selectedVm)! as SqlVMServer; - } - break; - - case MigrationTargetType.SQLMI: - const selectedMi = this.migrationStateModel._targetManagedInstances.find(mi => mi.name === value || constants.UNAVAILABLE_TARGET_PREFIX(mi.name) === value); - if (selectedMi) { - this.migrationStateModel._targetServerInstance = utils.deepClone(selectedMi)! as azureResource.AzureSqlManagedInstance; - - if (this.migrationStateModel._targetServerInstance.properties.state !== 'Ready') { - this.wizard.message = { - text: constants.MI_NOT_READY_ERROR(this.migrationStateModel._targetServerInstance.name, this.migrationStateModel._targetServerInstance.properties.state), - level: azdata.window.MessageLevel.Error - }; - } else { - this.wizard.message = { - text: '', - level: azdata.window.MessageLevel.Error - }; - } - } - break; - } - } - })); - - return this._view.modelBuilder.flexContainer().withItems( - [ + return this._view.modelBuilder.flexContainer() + .withItems([ subscriptionDropdownLabel, this._azureSubscriptionDropdown, azureLocationLabel, this._azureLocationDropdown, - azureResourceGroupLabel, + this._resourceSelectionContainer, + this._resourceAuthenticationContainer]) + .withLayout({ flexFlow: 'column' }) + .component(); + } + + private _createResourceAuthenticationContainer(): azdata.FlexContainer { + // target user name + const targetUserNameLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.TARGET_USERNAME_LAbEL, + requiredIndicator: true, + CSSStyles: { ...styles.LABEL_CSS, 'margin-top': '-1em' } + }).component(); + this._targetUserNameInputBox = this._view.modelBuilder.inputBox() + .withProps({ + width: '300px', + inputType: 'text', + placeHolder: constants.TARGET_USERNAME_PLACEHOLDER, + required: false, + CSSStyles: { 'margin-top': '-1em' }, + }).component(); + + this._disposables.push( + this._targetUserNameInputBox.onTextChanged( + (value: string) => this.migrationStateModel._targetUserName = value ?? '')); + + this._disposables.push( + this._targetUserNameInputBox.onValidityChanged( + valid => this._updateConnectionButtonState())); + + // target password + const targetPasswordLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.TARGET_PASSWORD_LAbEL, + requiredIndicator: true, + title: '', + CSSStyles: { ...styles.LABEL_CSS, 'margin-top': '-1em' } + }).component(); + this._targetPasswordInputBox = this._view.modelBuilder.inputBox() + .withProps({ + width: '300px', + inputType: 'password', + placeHolder: constants.TARGET_PASSWORD_PLACEHOLDER, + required: false, + CSSStyles: { 'margin-top': '-1em' }, + }).component(); + this._disposables.push( + this._targetPasswordInputBox.onTextChanged( + (value: string) => { this.migrationStateModel._targetPassword = value ?? ''; })); + + this._disposables.push( + this._targetPasswordInputBox.onValidityChanged( + valid => this._updateConnectionButtonState())); + + // test connection button + this._testConectionButton = this._view.modelBuilder.button() + .withProps({ + enabled: false, + label: constants.TARGET_CONNECTION_LABEL, + width: '80px', + }).component(); + + this._connectionResultsInfoBox = this._view.modelBuilder.infoBox() + .withProps({ + style: 'success', + text: '', + announceText: true, + CSSStyles: { 'display': 'none' }, + }) + .component(); + + const connectionButtonLoadingContainer = this._view.modelBuilder.loadingComponent() + .withItem(this._testConectionButton) + .withProps({ loading: false }) + .component(); + + this._disposables.push( + this._testConectionButton.onDidClick(async (value) => { + this.wizard.message = { text: '' }; + + const targetDatabaseServer = this.migrationStateModel._targetServerInstance as AzureSqlDatabaseServer; + const userName = this.migrationStateModel._targetUserName; + const password = this.migrationStateModel._targetPassword; + const targetDatabases: TargetDatabaseInfo[] = []; + if (targetDatabaseServer && userName && password) { + try { + connectionButtonLoadingContainer.loading = true; + await utils.updateControlDisplay(this._connectionResultsInfoBox, false); + targetDatabases.push( + ...await collectTargetDatabaseInfo( + targetDatabaseServer, + userName, + password)); + await this._showConnectionResults(targetDatabases); + } catch (error) { + this.wizard.message = { + level: azdata.window.MessageLevel.Error, + text: constants.AZURE_SQL_TARGET_CONNECTION_ERROR_TITLE, + description: constants.SQL_TARGET_CONNECTION_ERROR(error.message), + }; + await this._showConnectionResults( + targetDatabases, + constants.AZURE_SQL_TARGET_CONNECTION_ERROR_TITLE); + } + finally { + connectionButtonLoadingContainer.loading = false; + } + } + await this._populateResourceMappingTable(targetDatabases); + })); + + const connectionContainer = this._view.modelBuilder.flexContainer() + .withItems([ + connectionButtonLoadingContainer, + this._connectionResultsInfoBox], + { flex: '0 0 auto' }) + .withLayout({ + flexFlow: 'row', + alignContent: 'center', + alignItems: 'center', + }) + .withProps({ CSSStyles: { 'margin': '15px 0 0 0' } }) + .component(); + + const mapSourceHeading = this._view.modelBuilder.text() + .withProps({ + value: constants.MAP_SOURCE_TARGET_HEADING, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-weight': '600', + 'font-size': '16px', + 'margin': '15px 0 0 0', + }, + }) + .component(); + const mapSourceDetails = this._view.modelBuilder.text() + .withProps({ + value: constants.MAP_SOURCE_TARGET_DESCRIPTION, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + 'margin': '15px 0 15px 0', + }, + }) + .component(); + this._azureResourceTable = this._createResourceTable(); + + return this._view.modelBuilder.flexContainer() + .withItems([ + targetUserNameLabel, + this._targetUserNameInputBox, + targetPasswordLabel, + this._targetPasswordInputBox, + connectionContainer, + mapSourceHeading, + mapSourceDetails, + this._azureResourceTable]) + .withLayout({ flexFlow: 'column' }) + .withProps({ CSSStyles: { 'margin': '15px 0 0 0' } }) + .component(); + } + + private async _showConnectionResults( + databases: TargetDatabaseInfo[], + errorMessage?: string): Promise { + + const hasError = errorMessage !== undefined; + const hasDatabases = databases.length > 0; + this._connectionResultsInfoBox.style = hasError + ? 'error' + : hasDatabases + ? 'success' + : 'warning'; + this._connectionResultsInfoBox.text = hasError + ? constants.SQL_TARGET_CONNECTION_ERROR(errorMessage) + : hasDatabases + ? constants.SQL_TARGET_CONNECTION_SUCCESS(databases.length.toLocaleString()) + : constants.SQL_TARGET_MISSING_SOURCE_DATABASES; + await utils.updateControlDisplay(this._connectionResultsInfoBox, true); + } + + private _createResourceDropdowns(): azdata.FlexContainer { + this._azureResourceGroupLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.RESOURCE_GROUP, + description: constants.TARGET_RESOURCE_GROUP_INFO, + width: WIZARD_INPUT_COMPONENT_WIDTH, + requiredIndicator: true, + CSSStyles: { ...styles.LABEL_CSS } + }).component(); + this._azureResourceGroupDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.RESOURCE_GROUP, + width: WIZARD_INPUT_COMPONENT_WIDTH, + editable: true, + required: true, + fireOnTextChange: true, + placeholder: constants.SELECT_A_RESOURCE_GROUP, + CSSStyles: { 'margin-top': '-1em' }, + }).component(); + this._disposables.push( + this._azureResourceGroupDropdown.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._resourceGroup = (selectedResourceGroup) + ? utils.deepClone(selectedResourceGroup)! + : undefined!; + } + await this.populateResourceInstanceDropdown(); + })); + + this._azureResourceDropdownLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE, + description: constants.TARGET_RESOURCE_INFO, + width: WIZARD_INPUT_COMPONENT_WIDTH, + requiredIndicator: true, + CSSStyles: { ...styles.LABEL_CSS } + }).component(); + this._azureResourceDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE, + width: WIZARD_INPUT_COMPONENT_WIDTH, + editable: true, + required: true, + fireOnTextChange: true, + placeholder: constants.SELECT_SERVICE_PLACEHOLDER, + CSSStyles: { 'margin-top': '-1em' }, + loading: false, + }).component(); + this._disposables.push( + this._azureResourceDropdown.onValueChanged(async (value) => { + const isSqlDbTarget = this.migrationStateModel._targetType === MigrationTargetType.SQLDB; + if (value && value !== 'undefined' && + value !== constants.NO_MANAGED_INSTANCE_FOUND && + value !== constants.NO_SQL_DATABASE_SERVER_FOUND && + value !== constants.NO_VIRTUAL_MACHINE_FOUND) { + + switch (this.migrationStateModel._targetType) { + case MigrationTargetType.SQLVM: + const selectedVm = this.migrationStateModel._targetSqlVirtualMachines.find(vm => vm.name === value); + if (selectedVm) { + this.migrationStateModel._targetServerInstance = utils.deepClone(selectedVm)! as SqlVMServer; + } + break; + case MigrationTargetType.SQLMI: + const selectedMi = this.migrationStateModel._targetManagedInstances + .find(mi => mi.name === value + || constants.UNAVAILABLE_TARGET_PREFIX(mi.name) === value); + + if (selectedMi) { + this.migrationStateModel._targetServerInstance = utils.deepClone(selectedMi)! as azureResource.AzureSqlManagedInstance; + + this.wizard.message = { text: '' }; + if (this.migrationStateModel._targetServerInstance.properties.state !== 'Ready') { + this.wizard.message = { + text: constants.MI_NOT_READY_ERROR( + this.migrationStateModel._targetServerInstance.name, + this.migrationStateModel._targetServerInstance.properties.state), + level: azdata.window.MessageLevel.Error + }; + } + } + break; + case MigrationTargetType.SQLDB: + const sqlDatabaseServer = this.migrationStateModel._targetSqlDatabaseServers.find( + sqldb => sqldb.name === value || constants.UNAVAILABLE_TARGET_PREFIX(sqldb.name) === value); + + if (sqlDatabaseServer) { + this.migrationStateModel._targetServerInstance = utils.deepClone(sqlDatabaseServer)! as AzureSqlDatabaseServer; + this.wizard.message = { text: '' }; + if (this.migrationStateModel._targetServerInstance.properties.state === 'Ready') { + this._targetUserNameInputBox.value = this.migrationStateModel._targetServerInstance.properties.administratorLogin; + } else { + this.wizard.message = { + text: constants.SQLDB_NOT_READY_ERROR( + this.migrationStateModel._targetServerInstance.name, + this.migrationStateModel._targetServerInstance.properties.state), + level: azdata.window.MessageLevel.Error + }; + } + } + break; + } + } else { + this.migrationStateModel._targetServerInstance = undefined!; + if (isSqlDbTarget) { + this._targetUserNameInputBox.value = ''; + } + } + + this.migrationStateModel._sqlMigrationServices = undefined!; + if (isSqlDbTarget) { + await this._azureResourceTable.setDataValues([]); + this._targetPasswordInputBox.value = ''; + this._initializeSourceTargetDatabaseMap(); + this._updateConnectionButtonState(); + } + })); + + return this._view.modelBuilder.flexContainer() + .withItems([ + this._azureResourceGroupLabel, this._azureResourceGroupDropdown, this._azureResourceDropdownLabel, - this._azureResourceDropdown - ] - ).withLayout({ - flexFlow: 'column', - }).component(); + this._azureResourceDropdown]) + .withLayout({ flexFlow: 'column' }) + .component(); + } + + private _initializeSourceTargetDatabaseMap(): void { + // initialize source / target database map + this.migrationStateModel._sourceTargetMapping = new Map(); + this.migrationStateModel._targetDatabaseNames = []; + this.migrationStateModel._databasesForMigration.forEach( + sourceDatabaseName => this.migrationStateModel._sourceTargetMapping.set( + sourceDatabaseName, undefined)); + } + + private _updateConnectionButtonState(): void { + const targetDatabaseServer = (this._azureResourceDropdown.value as azdata.CategoryValue)?.name ?? ''; + const userName = this._targetUserNameInputBox.value ?? ''; + const password = this._targetPasswordInputBox.value ?? ''; + this._testConectionButton.enabled = targetDatabaseServer.length > 0 + && userName.length > 0 + && password.length > 0; + } + + private _createResourceTable(): azdata.DeclarativeTableComponent { + const columWidth = '50%'; + const headerStyles = { + 'padding': '0px', + 'border-style': 'none', + 'text-align': 'left', + 'font-weight': '600', + }; + const rowStyles = { + 'padding': '1px 0px', + 'border-style': 'none', + 'text-align': 'left', + 'white-space': 'nowrap', + 'text-overflow': 'ellipsis', + 'overflow': 'hidden', + }; + + return this._view.modelBuilder.declarativeTable() + .withProps({ + ariaLabel: constants.MAP_SOURCE_TARGET_HEADING, + width: WIZARD_INPUT_COMPONENT_WIDTH, + data: [], + display: 'inline-block', + CSSStyles: { 'padding': '0px' }, + columns: [ + { + displayName: constants.MAP_SOURCE_COLUMN, + valueType: azdata.DeclarativeDataType.string, + width: columWidth, + isReadOnly: true, + rowCssStyles: rowStyles, + headerCssStyles: headerStyles, + }, + { + displayName: constants.MAP_TARGET_COLUMN, + valueType: azdata.DeclarativeDataType.component, + width: columWidth, + isReadOnly: false, + rowCssStyles: rowStyles, + headerCssStyles: headerStyles, + }, + ], + }) + .withValidation(table => table.dataValues !== undefined && table.dataValues.length > 0) + .component(); } private async populateAzureAccountsDropdown(): Promise { try { - this.updateDropdownLoadingStatus(TargetDropDowns.AzureAccount, true); + this._azureAccountsDropdown.loading = true; this.migrationStateModel._azureAccounts = await utils.getAzureAccounts(); this._azureAccountsDropdown.values = await utils.getAzureAccountsDropdownValues(this.migrationStateModel._azureAccounts); - utils.selectDefaultDropdownValue(this._azureAccountsDropdown, this.migrationStateModel._azureAccount?.displayInfo?.userId, false); } finally { - this.updateDropdownLoadingStatus(TargetDropDowns.AzureAccount, false); + this._azureAccountsDropdown.loading = false; + utils.selectDefaultDropdownValue( + this._azureAccountsDropdown, + this.migrationStateModel._azureAccount?.displayInfo?.userId, + false); } } private async populateTenantsDropdown(): Promise { try { - this.updateDropdownLoadingStatus(TargetDropDowns.Tenant, true); + this._accountTenantDropdown.loading = true; if (this.migrationStateModel._azureAccount && this.migrationStateModel._azureAccount.isStale === false && this.migrationStateModel._azureAccount.properties.tenants.length > 0) { this.migrationStateModel._accountTenants = utils.getAzureTenants(this.migrationStateModel._azureAccount); this._accountTenantDropdown.values = await utils.getAzureTenantsDropdownValues(this.migrationStateModel._accountTenants); - utils.selectDefaultDropdownValue(this._accountTenantDropdown, this.migrationStateModel._azureTenant?.id, true); } + utils.selectDefaultDropdownValue( + this._accountTenantDropdown, + this.migrationStateModel._azureTenant?.id, + true); await this._accountTenantFlexContainer.updateCssStyles(this.migrationStateModel._azureAccount.properties.tenants.length > 1 ? { 'display': 'inline' } : { 'display': 'none' } ); await this._azureAccountsDropdown.validate(); } finally { - this.updateDropdownLoadingStatus(TargetDropDowns.Tenant, false); - await this.populateSubscriptionDropdown(); + this._accountTenantDropdown.loading = false; } } - private async populateSubscriptionDropdown(): Promise { try { - this.updateDropdownLoadingStatus(TargetDropDowns.Subscription, true); + this._azureSubscriptionDropdown.loading = true; this.migrationStateModel._subscriptions = await utils.getAzureSubscriptions(this.migrationStateModel._azureAccount); this._azureSubscriptionDropdown.values = await utils.getAzureSubscriptionsDropdownValues(this.migrationStateModel._subscriptions); - utils.selectDefaultDropdownValue(this._azureSubscriptionDropdown, this.migrationStateModel._targetSubscription?.id, false); } catch (e) { console.log(e); } finally { - this.updateDropdownLoadingStatus(TargetDropDowns.Subscription, false); + this._azureSubscriptionDropdown.loading = false; + utils.selectDefaultDropdownValue( + this._azureSubscriptionDropdown, + this.migrationStateModel._targetSubscription?.id, + false); } } public async populateLocationDropdown(): Promise { try { - this.updateDropdownLoadingStatus(TargetDropDowns.Location, true); + this._azureLocationDropdown.loading = true; switch (this.migrationStateModel._targetType) { case MigrationTargetType.SQLMI: - this.migrationStateModel._targetManagedInstances = await utils.getManagedInstances(this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription); - this.migrationStateModel._locations = await utils.getSqlManagedInstanceLocations(this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription, this.migrationStateModel._targetManagedInstances); + this.migrationStateModel._targetManagedInstances = await utils.getManagedInstances( + this.migrationStateModel._azureAccount, + this.migrationStateModel._targetSubscription); + this.migrationStateModel._locations = await utils.getResourceLocations( + this.migrationStateModel._azureAccount, + this.migrationStateModel._targetSubscription, + this.migrationStateModel._targetManagedInstances); break; case MigrationTargetType.SQLVM: - this.migrationStateModel._targetSqlVirtualMachines = await utils.getVirtualMachines(this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription); - this.migrationStateModel._locations = await utils.getSqlVirtualMachineLocations(this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription, this.migrationStateModel._targetSqlVirtualMachines); + this.migrationStateModel._targetSqlVirtualMachines = await utils.getVirtualMachines( + this.migrationStateModel._azureAccount, + this.migrationStateModel._targetSubscription); + this.migrationStateModel._locations = await utils.getResourceLocations( + this.migrationStateModel._azureAccount, + this.migrationStateModel._targetSubscription, + this.migrationStateModel._targetSqlVirtualMachines); + break; + case MigrationTargetType.SQLDB: + this.migrationStateModel._targetSqlDatabaseServers = await utils.getAzureSqlDatabaseServers( + this.migrationStateModel._azureAccount, + this.migrationStateModel._targetSubscription); + this.migrationStateModel._locations = await utils.getResourceLocations( + this.migrationStateModel._azureAccount, + this.migrationStateModel._targetSubscription, + this.migrationStateModel._targetSqlDatabaseServers); break; } this._azureLocationDropdown.values = await utils.getAzureLocationsDropdownValues(this.migrationStateModel._locations); - utils.selectDefaultDropdownValue(this._azureLocationDropdown, this.migrationStateModel._location?.displayName, true); } catch (e) { console.log(e); } finally { - this.updateDropdownLoadingStatus(TargetDropDowns.Location, false); + this._azureLocationDropdown.loading = false; + utils.selectDefaultDropdownValue( + this._azureLocationDropdown, + this.migrationStateModel._location?.displayName, + true); } } public async populateResourceGroupDropdown(): Promise { try { - this.updateDropdownLoadingStatus(TargetDropDowns.ResourceGroup, true); + this._azureResourceGroupDropdown.loading = true; switch (this.migrationStateModel._targetType) { case MigrationTargetType.SQLMI: - this.migrationStateModel._resourceGroups = await utils.getSqlManagedInstanceResourceGroups(this.migrationStateModel._targetManagedInstances, this.migrationStateModel._location); + this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation( + this.migrationStateModel._targetManagedInstances, + this.migrationStateModel._location); break; case MigrationTargetType.SQLVM: - this.migrationStateModel._resourceGroups = await utils.getSqlVirtualMachineResourceGroups(this.migrationStateModel._targetSqlVirtualMachines, this.migrationStateModel._location); + this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation( + this.migrationStateModel._targetSqlVirtualMachines, + this.migrationStateModel._location); + break; + case MigrationTargetType.SQLDB: + this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation( + this.migrationStateModel._targetSqlDatabaseServers, + this.migrationStateModel._location); break; } - this._azureResourceGroupDropdown.values = await utils.getAzureResourceGroupsDropdownValues(this.migrationStateModel._resourceGroups); - utils.selectDefaultDropdownValue(this._azureResourceGroupDropdown, this.migrationStateModel._resourceGroup?.id, false); + this._azureResourceGroupDropdown.values = utils.getResourceDropdownValues( + this.migrationStateModel._resourceGroups, + constants.RESOURCE_GROUP_NOT_FOUND); } catch (e) { console.log(e); } finally { - this.updateDropdownLoadingStatus(TargetDropDowns.ResourceGroup, false); + this._azureResourceGroupDropdown.loading = false; + utils.selectDefaultDropdownValue( + this._azureResourceGroupDropdown, + this.migrationStateModel._resourceGroup?.id, + false); } } private async populateResourceInstanceDropdown(): Promise { try { - this.updateDropdownLoadingStatus(TargetDropDowns.ResourceInstance, true); + this._azureResourceDropdown.loading = true; switch (this.migrationStateModel._targetType) { - case MigrationTargetType.SQLMI: { - this._azureResourceDropdown.values = await utils.getManagedInstancesDropdownValues(this.migrationStateModel._targetManagedInstances, this.migrationStateModel._location, this.migrationStateModel._resourceGroup); + case MigrationTargetType.SQLMI: + this._azureResourceDropdown.values = await utils.getManagedInstancesDropdownValues( + this.migrationStateModel._targetManagedInstances, + this.migrationStateModel._location, + this.migrationStateModel._resourceGroup); break; + case MigrationTargetType.SQLVM: + this._azureResourceDropdown.values = utils.getAzureResourceDropdownValues( + this.migrationStateModel._targetSqlVirtualMachines, + this.migrationStateModel._location, + this.migrationStateModel._resourceGroup?.name, + constants.NO_VIRTUAL_MACHINE_FOUND); + break; + case MigrationTargetType.SQLDB: + this._azureResourceDropdown.values = utils.getAzureResourceDropdownValues( + this.migrationStateModel._targetSqlDatabaseServers, + this.migrationStateModel._location, + this.migrationStateModel._resourceGroup?.name, + constants.NO_SQL_DATABASE_SERVER_FOUND); + break; + } + } finally { + this._azureResourceDropdown.loading = false; + utils.selectDefaultDropdownValue( + this._azureResourceDropdown, + this.migrationStateModel._targetServerInstance?.name, + true); + } + } + + private async _populateResourceMappingTable(targetDatabases: TargetDatabaseInfo[]): Promise { + // populate target database list + const databaseValues = this._getTargetDatabaseDropdownValues( + targetDatabases, + constants.NO_SQL_DATABASE_FOUND); + + const data: azdata.DeclarativeTableCellValue[][] = this.migrationStateModel._databasesForMigration + .map(sourceDatabase => { + // target database dropdown + const targetDatabaseDropDown = this._view.modelBuilder.dropDown() + .withProps({ + width: '100%', + required: true, + editable: true, + fireOnTextChange: true, + values: databaseValues, + placeholder: constants.MAP_TARGET_PLACEHOLDER, + }) + .component(); + this._disposables.push( + targetDatabaseDropDown.onValueChanged((targetDatabaseName: string) => { + const targetDatabaseInfo = targetDatabases.find( + targetDb => targetDb.databaseName === targetDatabaseName); + this.migrationStateModel._sourceTargetMapping.set( + sourceDatabase, + targetDatabaseInfo); + this.migrationStateModel._didDatabaseMappingChange = true; + })); + + const targetDatabaseName = this.migrationStateModel._sourceTargetMapping.get(sourceDatabase)?.databaseName ?? ''; + if (targetDatabaseName.length > 0) { + utils.selectDefaultDropdownValue( + targetDatabaseDropDown, + targetDatabaseName); } - case MigrationTargetType.SQLVM: { - this._azureResourceDropdown.values = await utils.getVirtualMachinesDropdownValues(this.migrationStateModel._targetSqlVirtualMachines, this.migrationStateModel._location, this.migrationStateModel._resourceGroup); - break; + + return [ + { value: sourceDatabase }, + { value: targetDatabaseDropDown }, + ]; + }) || []; + + await this._azureResourceTable.setDataValues(data); + } + + private _getTargetDatabaseDropdownValues( + databases: TargetDatabaseInfo[], + resourceNotFoundMessage: string): azdata.CategoryValue[] { + + if (databases?.length > 0) { + return databases.map(database => { + const databaseName = database.databaseName; + return { name: databaseName, displayName: databaseName }; + }); + } + + return [{ name: '', displayName: resourceNotFoundMessage }]; + } + + private _getSourceTargetMappingErrors(): string[] { + // Validate source/target database mappings: + const errors: string[] = []; + const targetDatabaseKeys = new Map(); + const migrationDatabaseCount = this._azureResourceTable.dataValues?.length ?? 0; + this.migrationStateModel._targetDatabaseNames = []; + if (migrationDatabaseCount === 0) { + errors.push(constants.SQL_TARGET_MAPPING_ERROR_MISSING_TARGET); + } else { + for (let i = 0; i < this.migrationStateModel._databasesForMigration.length; i++) { + const sourceDatabaseName = this.migrationStateModel._databasesForMigration[i]; + const targetDatabaseInfo = this.migrationStateModel._sourceTargetMapping.get(sourceDatabaseName); + const targetDatabaseName = targetDatabaseInfo?.databaseName; + if (targetDatabaseName && targetDatabaseName.length > 0) { + if (!targetDatabaseKeys.has(targetDatabaseName)) { + targetDatabaseKeys.set(targetDatabaseName, sourceDatabaseName); + this.migrationStateModel._targetDatabaseNames.push(targetDatabaseName); + } else { + const mappedSourceDatabaseName = targetDatabaseKeys.get(targetDatabaseName) ?? ''; + // target mapped only once + errors.push( + constants.SQL_TARGET_CONNECTION_DUPLICATE_TARGET_MAPPING( + targetDatabaseName, + sourceDatabaseName, + mappedSourceDatabaseName)); + } + } else { + // source/target has mapping + errors.push(constants.SQL_TARGET_CONNECTION_SOURCE_NOT_MAPPED(sourceDatabaseName)); } } - utils.selectDefaultDropdownValue(this._azureResourceDropdown, this.migrationStateModel._targetServerInstance?.name, true); - } catch (e) { - console.log(e); - } finally { - this.updateDropdownLoadingStatus(TargetDropDowns.ResourceInstance, false); - } - } - - private updateDropdownLoadingStatus(dropdown: TargetDropDowns, loading: boolean): void { - switch (dropdown) { - case TargetDropDowns.AzureAccount: - this._azureAccountsDropdown.loading = loading; - case TargetDropDowns.Subscription: - this._azureSubscriptionDropdown.loading = loading; - case TargetDropDowns.Location: - this._azureLocationDropdown.loading = loading; - case TargetDropDowns.ResourceGroup: - this._azureResourceGroupDropdown.loading = loading; - case TargetDropDowns.ResourceInstance: - this._azureResourceDropdown.loading = loading; } + return errors; } } - -export enum TargetDropDowns { - AzureAccount, - Tenant, - Subscription, - Location, - ResourceGroup, - ResourceInstance, -} diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index eff34c0a58..44001593eb 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -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) { + private readonly _serviceContextChangedEvent: vscode.EventEmitter) { } public async openWizard(connectionId: string): Promise { @@ -40,7 +41,11 @@ export class WizardController { private async createWizard(stateModel: MigrationStateModel): Promise { 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 { + private async updateServiceContext( + stateModel: MigrationStateModel, + serviceContextChangedEvent: vscode.EventEmitter): Promise { + 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 { +export async function createHeadingTextComponent( + view: azdata.ModelView, + value: string, + firstElement: boolean = false): Promise { + 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(); }