diff --git a/extensions/sql-migration/README.md b/extensions/sql-migration/README.md index 0802965491..b8373cb408 100644 --- a/extensions/sql-migration/README.md +++ b/extensions/sql-migration/README.md @@ -1,5 +1,5 @@ # Azure SQL Migration -The Azure SQL Migration extension in Azure Data Studio brings together a simplified assessment and migration experience that delivers the following capabilities: +The Azure SQL Migration extension in Azure Data Studio brings together a simplified assessment, recommendation and migration experience that delivers the following capabilities: - A responsive user interface that provides an easy-to-navigate step-by-step wizard to deliver an integrated assessment, Azure recommendation and migration experience. - An enhanced assessment engine that can assess SQL Server instances and identify databases that are ready for migration to Azure SQL Managed Instance or SQL Server on Azure Virtual Machines. - SKU recommender to collect performance data from the source SQL Server instance to generate right-sized Azure SQL recommendation. diff --git a/extensions/sql-migration/images/sqlMigrationService.svg b/extensions/sql-migration/images/sqlMigrationService.svg new file mode 100644 index 0000000000..b719591f1a --- /dev/null +++ b/extensions/sql-migration/images/sqlMigrationService.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/sql-migration/images/view.svg b/extensions/sql-migration/images/view.svg new file mode 100644 index 0000000000..67438f588a --- /dev/null +++ b/extensions/sql-migration/images/view.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index 10d62e2f24..d22330dbf3 100644 --- a/extensions/sql-migration/package.json +++ b/extensions/sql-migration/package.json @@ -2,9 +2,9 @@ "name": "sql-migration", "displayName": "%displayName%", "description": "%description%", - "version": "0.1.14", + "version": "1.0.0", "publisher": "Microsoft", - "preview": true, + "preview": false, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", "icon": "images/extension.png", "aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e", diff --git a/extensions/sql-migration/package.nls.json b/extensions/sql-migration/package.nls.json index e38506c24a..be46808da0 100644 --- a/extensions/sql-migration/package.nls.json +++ b/extensions/sql-migration/package.nls.json @@ -8,6 +8,7 @@ "start-migration-command": "Migrate to Azure SQL", "send-feedback-command": "Feedback", "new-support-request-command": "New support request", + "refresh-migrations-command": "Refresh", "migration-context-menu-category": "Migration Context Menu", "complete-cutover-menu": "Complete cutover", "database-details-menu": "Database details", diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index 315714c11c..8af861998e 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -142,10 +142,15 @@ export async function getBlobs(account: azdata.Account, subscription: Subscripti return blobNames!; } -export async function getSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise { +export async function getSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string): Promise { + const sqlMigrationServiceId = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}`; + return await getSqlMigrationServiceById(account, subscription, sqlMigrationServiceId); +} + +export async function getSqlMigrationServiceById(account: azdata.Account, subscription: Subscription, sqlMigrationServiceId: string): Promise { const api = await getAzureCoreAPI(); - const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=2020-09-01-preview`); - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); + const path = encodeURI(`${sqlMigrationServiceId}?api-version=2022-01-30-preview`); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } @@ -153,6 +158,20 @@ export async function getSqlMigrationService(account: azdata.Account, subscripti return response.response.data; } +export async function getSqlMigrationServicesByResourceGroup(account: azdata.Account, subscription: Subscription, resouceGroupName: string): Promise { + const api = await getAzureCoreAPI(); + const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resouceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices?api-version=2022-01-30-preview`); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true); + if (response.errors.length > 0) { + throw new Error(response.errors.toString()); + } + sortResourceArrayByName(response.response.data.value); + response.response.data.value.forEach((sms: SqlMigrationService) => { + sms.properties.resourceGroup = getResourceGroupFromId(sms.id); + }); + return response.response.data.value; +} + export async function getSqlMigrationServices(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); const path = encodeURI(`/subscriptions/${subscription.id}/providers/Microsoft.DataMigration/sqlMigrationServices?api-version=2022-01-30-preview`); @@ -169,7 +188,7 @@ 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=2020-09-01-preview`); + const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=2022-01-30-preview`); const requestBody = { 'location': regionName }; @@ -181,7 +200,7 @@ export async function createSqlMigrationService(account: azdata.Account, subscri const maxRetry = 24; let i = 0; for (i = 0; i < maxRetry; i++) { - const asyncResponse = await api.makeAzureRestRequest(account, subscription, asyncUrl.replace('https://management.azure.com/', ''), azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); + const asyncResponse = await api.makeAzureRestRequest(account, subscription, asyncUrl.replace('https://management.azure.com/', ''), azurecore.HttpRequestMethod.GET, undefined, true); const creationStatus = asyncResponse.response.data.status; if (creationStatus === ProvisioningState.Succeeded) { break; @@ -196,10 +215,10 @@ export async function createSqlMigrationService(account: azdata.Account, subscri return response.response.data; } -export async function getSqlMigrationServiceAuthKeys(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise { +export async function getSqlMigrationServiceAuthKeys(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string): Promise { const api = await getAzureCoreAPI(); - const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/ListAuthKeys?api-version=2020-09-01-preview`); - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, getSessionIdHeader(sessionId)); + const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/ListAuthKeys?api-version=2022-01-30-preview`); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } @@ -209,15 +228,15 @@ export async function getSqlMigrationServiceAuthKeys(account: azdata.Account, su }; } -export async function regenerateSqlMigrationServiceAuthKey(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, keyName: string, sessionId: string): Promise { +export async function regenerateSqlMigrationServiceAuthKey(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, keyName: string): Promise { const api = await getAzureCoreAPI(); - const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/regenerateAuthKeys?api-version=2020-09-01-preview`); + const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/regenerateAuthKeys?api-version=2022-01-30-preview`); const requestBody = { 'location': regionName, 'keyName': keyName, }; - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, requestBody, true, undefined, getSessionIdHeader(sessionId)); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, requestBody, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } @@ -239,10 +258,10 @@ export async function getStorageAccountAccessKeys(account: azdata.Account, subsc }; } -export async function getSqlMigrationServiceMonitoringData(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationService: string, sessionId: string): Promise { +export async function getSqlMigrationServiceMonitoringData(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationService: string): Promise { const api = await getAzureCoreAPI(); - const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationService}/monitoringData?api-version=2020-09-01-preview`); - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); + const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationService}/monitoringData?api-version=2022-01-30-preview`); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } @@ -251,7 +270,7 @@ export async function getSqlMigrationServiceMonitoringData(account: azdata.Accou export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, regionName: string, targetServer: SqlManagedInstance | SqlVMServer, targetDatabaseName: string, requestBody: StartDatabaseMigrationRequest, sessionId: string): Promise { const api = await getAzureCoreAPI(); - const path = encodeURI(`${targetServer.id}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=2020-09-01-preview`); + const path = encodeURI(`${targetServer.id}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=2022-01-30-preview`); const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, undefined, getSessionIdHeader(sessionId)); if (response.errors.length > 0) { throw new Error(response.errors.toString()); @@ -264,70 +283,72 @@ export async function startDatabaseMigration(account: azdata.Account, subscripti }; } -export async function getMigrationStatus(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration, sessionId: string): Promise { - if (!migration.id) { - throw new Error('NullMigrationId'); - } - - const migrationOperationId = migration.properties?.migrationOperationId; - if (migrationOperationId === undefined && - migration.properties.provisioningState === ProvisioningState.Failed) { - return migration; - } +export async function getMigrationDetails(account: azdata.Account, subscription: Subscription, migrationId: string, migrationOperationId?: string): Promise { const path = migrationOperationId === undefined - ? encodeURI(`${migration.id}?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`) - : encodeURI(`${migration.id}?migrationOperationId=${migrationOperationId}&$expand=MigrationStatusDetails&api-version=2020-09-01-preview`); + ? encodeURI(`${migrationId}?$expand=MigrationStatusDetails&api-version=2022-01-30-preview`) + : encodeURI(`${migrationId}?migrationOperationId=${migrationOperationId}&$expand=MigrationStatusDetails&api-version=2022-01-30-preview`); const api = await getAzureCoreAPI(); - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, undefined); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } - const migrationUpdate: DatabaseMigration = response.response.data; - if (migration.properties) { - migrationUpdate.properties.sourceDatabaseName = migration.properties.sourceDatabaseName; - migrationUpdate.properties.backupConfiguration = migration.properties.backupConfiguration; - migrationUpdate.properties.offlineConfiguration = migration.properties.offlineConfiguration; - } - - return migrationUpdate; + return response.response.data; } -export async function getMigrationAsyncOperationDetails(account: azdata.Account, subscription: Subscription, url: string, sessionId: string): Promise { +export async function getServiceMigrations(account: azdata.Account, subscription: Subscription, resourceId: string): Promise { + const path = encodeURI(`${resourceId}/listMigrations?&api-version=2022-01-30-preview`); const api = await getAzureCoreAPI(); - const response = await api.makeAzureRestRequest(account, subscription, url.replace('https://management.azure.com/', ''), azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); + const response = await api.makeAzureRestRequest( + account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, undefined); + if (response.errors.length > 0) { + throw new Error(response.errors.toString()); + } + + return response.response.data.value; +} + +export async function getMigrationTargetInstance(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise { + const targetServerId = getMigrationTargetId(migration); + const path = encodeURI(`${targetServerId}?api-version=2021-11-01-preview`); + const api = await getAzureCoreAPI(); + const response = await api.makeAzureRestRequest( + account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, undefined); + if (response.errors.length > 0) { + throw new Error(response.errors.toString()); + } + + return response.response.data; + + + return {}; +} + +export async function getMigrationAsyncOperationDetails(account: azdata.Account, subscription: Subscription, url: string): Promise { + const api = await getAzureCoreAPI(); + const response = await api.makeAzureRestRequest(account, subscription, url.replace('https://management.azure.com/', ''), azurecore.HttpRequestMethod.GET, undefined, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } return response.response.data; } -export async function listMigrationsBySqlMigrationService(account: azdata.Account, subscription: Subscription, sqlMigrationService: SqlMigrationService, sessionId: string): Promise { +export async function startMigrationCutover(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise { const api = await getAzureCoreAPI(); - const path = encodeURI(`${sqlMigrationService.id}/listMigrations?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`); - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); + const path = encodeURI(`${migration.id}/operations/${migration.properties.migrationOperationId}/cutover?api-version=2022-01-30-preview`); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, undefined); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } return response.response.data.value; } -export async function startMigrationCutover(account: azdata.Account, subscription: Subscription, migrationStatus: DatabaseMigration, sessionId: string): Promise { +export async function stopMigration(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise { const api = await getAzureCoreAPI(); - const path = encodeURI(`${migrationStatus.id}/operations/${migrationStatus.properties.migrationOperationId}/cutover?api-version=2020-09-01-preview`); - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, getSessionIdHeader(sessionId)); - if (response.errors.length > 0) { - throw new Error(response.errors.toString()); - } - return response.response.data.value; -} - -export async function stopMigration(account: azdata.Account, subscription: Subscription, migrationStatus: DatabaseMigration, sessionId: string): Promise { - const api = await getAzureCoreAPI(); - const path = encodeURI(`${migrationStatus.id}/operations/${migrationStatus.properties.migrationOperationId}/cancel?api-version=2020-09-01-preview`); - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, getSessionIdHeader(sessionId)); + const path = encodeURI(`${migration.id}/operations/${migration.properties.migrationOperationId}/cancel?api-version=2022-01-30-preview`); + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, undefined); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } @@ -354,6 +375,17 @@ export function sortResourceArrayByName(resourceArray: SortableAzureResources[]) }); } +export function getMigrationTargetId(migration: DatabaseMigration): string { + // `${targetServerId}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=2022-01-30-preview` + const paths = migration.id.split('/providers/Microsoft.DataMigration/', 1); + return paths[0]; +} + +export function getMigrationTargetName(migration: DatabaseMigration): string { + const targetServerId = getMigrationTargetId(migration); + return getResourceName(targetServerId); +} + export function getResourceGroupFromId(id: string): string { return id.replace(RegExp('^(.*?)/resourceGroups/'), '').replace(RegExp('/providers/.*'), '').toLowerCase(); } @@ -452,6 +484,7 @@ export interface DatabaseMigration { name: string; type: string; } + export interface DatabaseMigrationProperties { scope: string; provisioningState: 'Succeeded' | 'Failed' | 'Creating'; @@ -460,8 +493,8 @@ export interface DatabaseMigrationProperties { migrationStatusDetails?: MigrationStatusDetails; startedOn: string; endedOn: string; - sourceSqlConnection: SqlConnectionInfo; sourceDatabaseName: string; + sourceServerName: string; targetDatabaseCollation: string; migrationService: string; migrationOperationId: string; @@ -469,6 +502,7 @@ export interface DatabaseMigrationProperties { offlineConfiguration: OfflineConfiguration; migrationFailureError: ErrorInfo; } + export interface MigrationStatusDetails { migrationState: string; startedOn: string; @@ -528,6 +562,7 @@ export interface BackupSetInfo { export interface SourceLocation { fileShare?: DatabaseMigrationFileShare; azureBlob?: DatabaseMigrationAzureBlob; + fileStorageType: 'FileShare' | 'AzureBlob' | 'None'; } export interface TargetLocation { diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts index 0f907f0539..ece16679a2 100644 --- a/extensions/sql-migration/src/api/utils.ts +++ b/extensions/sql-migration/src/api/utils.ts @@ -7,8 +7,9 @@ import { window, CategoryValue, DropDownComponent, IconPath } from 'azdata'; import { IconPathHelper } from '../constants/iconPathHelper'; import { DAYS, HRS, MINUTE, SEC } from '../constants/strings'; import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel'; -import { MigrationStatus, MigrationContext, ProvisioningState } from '../models/migrationLocalStorage'; +import { MigrationStatus, ProvisioningState } from '../models/migrationLocalStorage'; import * as crypto from 'crypto'; +import { DatabaseMigration } from './azure'; export function deepClone(obj: T): T { if (!obj || typeof obj !== 'object') { @@ -89,40 +90,34 @@ export function convertTimeDifferenceToDuration(startTime: Date, endTime: Date): } } -export function filterMigrations(databaseMigrations: MigrationContext[], statusFilter: string, databaseNameFilter?: string): MigrationContext[] { - let filteredMigration: MigrationContext[] = []; +export function filterMigrations(databaseMigrations: DatabaseMigration[], statusFilter: string, databaseNameFilter?: string): DatabaseMigration[] { + let filteredMigration: DatabaseMigration[] = []; if (statusFilter === AdsMigrationStatus.ALL) { filteredMigration = databaseMigrations; } else if (statusFilter === AdsMigrationStatus.ONGOING) { - filteredMigration = databaseMigrations.filter((value) => { - const status = value.migrationContext.properties?.migrationStatus; - const provisioning = value.migrationContext.properties?.provisioningState; - return status === MigrationStatus.InProgress - || status === MigrationStatus.Creating - || provisioning === MigrationStatus.Creating; - }); + filteredMigration = databaseMigrations.filter( + value => { + const status = value.properties?.migrationStatus; + return status === MigrationStatus.InProgress + || status === MigrationStatus.Creating + || value.properties?.provisioningState === MigrationStatus.Creating; + }); } else if (statusFilter === AdsMigrationStatus.SUCCEEDED) { - filteredMigration = databaseMigrations.filter((value) => { - const status = value.migrationContext.properties?.migrationStatus; - return status === MigrationStatus.Succeeded; - }); + filteredMigration = databaseMigrations.filter( + value => value.properties?.migrationStatus === MigrationStatus.Succeeded); } else if (statusFilter === AdsMigrationStatus.FAILED) { - filteredMigration = databaseMigrations.filter((value) => { - const status = value.migrationContext.properties?.migrationStatus; - const provisioning = value.migrationContext.properties?.provisioningState; - return status === MigrationStatus.Failed - || provisioning === ProvisioningState.Failed; - }); + filteredMigration = databaseMigrations.filter( + value => + value.properties?.migrationStatus === MigrationStatus.Failed || + value.properties?.provisioningState === ProvisioningState.Failed); } else if (statusFilter === AdsMigrationStatus.COMPLETING) { - filteredMigration = databaseMigrations.filter((value) => { - const status = value.migrationContext.properties?.migrationStatus; - return status === MigrationStatus.Completing; - }); + filteredMigration = databaseMigrations.filter( + value => value.properties?.migrationStatus === MigrationStatus.Completing); } if (databaseNameFilter) { - filteredMigration = filteredMigration.filter((value) => { - return value.migrationContext.name.toLowerCase().includes(databaseNameFilter.toLowerCase()); - }); + const filter = databaseNameFilter.toLowerCase(); + filteredMigration = filteredMigration.filter( + migration => migration.name?.toLowerCase().includes(filter)); } return filteredMigration; } @@ -144,35 +139,36 @@ export function convertIsoTimeToLocalTime(isoTime: string): Date { return new Date(isoDate.getTime() + (isoDate.getTimezoneOffset() * 60000)); } -export type SupportedAutoRefreshIntervals = -1 | 15000 | 30000 | 60000 | 180000 | 300000; - export function selectDefaultDropdownValue(dropDown: DropDownComponent, value?: string, useDisplayName: boolean = true): void { - const selectedIndex = value ? findDropDownItemIndex(dropDown, value, useDisplayName) : -1; - if (selectedIndex > -1) { - selectDropDownIndex(dropDown, selectedIndex); - } else { - selectDropDownIndex(dropDown, 0); + if (dropDown.values && dropDown.values.length > 0) { + const selectedIndex = value ? findDropDownItemIndex(dropDown, value, useDisplayName) : -1; + if (selectedIndex > -1) { + selectDropDownIndex(dropDown, selectedIndex); + } else { + selectDropDownIndex(dropDown, 0); + } } } export function selectDropDownIndex(dropDown: DropDownComponent, index: number): void { - if (index >= 0 && dropDown.values && index <= dropDown.values.length - 1) { - const value = dropDown.values[index]; - dropDown.value = value as CategoryValue; + if (dropDown.values && dropDown.values.length > 0) { + if (index >= 0 && index <= dropDown.values.length - 1) { + dropDown.value = dropDown.values[index] as CategoryValue; + return; + } } + dropDown.value = undefined; } export function findDropDownItemIndex(dropDown: DropDownComponent, value: string, useDisplayName: boolean = true): number { - if (dropDown.values) { - if (useDisplayName) { - return dropDown.values.findIndex((v: any) => - (v as CategoryValue)?.displayName?.toLowerCase() === value?.toLowerCase()); - } else { - return dropDown.values.findIndex((v: any) => - (v as CategoryValue)?.name?.toLowerCase() === value?.toLowerCase()); - } + if (value && dropDown.values && dropDown.values.length > 0) { + const searachValue = value?.toLowerCase(); + return useDisplayName + ? dropDown.values.findIndex((v: any) => + (v as CategoryValue)?.displayName?.toLowerCase() === searachValue) + : dropDown.values.findIndex((v: any) => + (v as CategoryValue)?.name?.toLowerCase() === searachValue); } - return -1; } diff --git a/extensions/sql-migration/src/constants/helper.ts b/extensions/sql-migration/src/constants/helper.ts index 9497e2fc65..1c20d22848 100644 --- a/extensions/sql-migration/src/constants/helper.ts +++ b/extensions/sql-migration/src/constants/helper.ts @@ -4,45 +4,70 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; -import { MigrationContext, MigrationStatus } from '../models/migrationLocalStorage'; -import { MigrationMode, MigrationTargetType } from '../models/stateMachine'; +import { DatabaseMigration } from '../api/azure'; +import { MigrationStatus } from '../models/migrationLocalStorage'; +import { FileStorageType, MigrationMode, MigrationTargetType } from '../models/stateMachine'; import * as loc from './strings'; export enum SQLTargetAssetType { SQLMI = 'microsoft.sql/managedinstances', SQLVM = 'Microsoft.SqlVirtualMachine/sqlVirtualMachines', + SQLDB = 'Microsoft.Sql/servers', } -export function getMigrationTargetType(migration: MigrationContext): string { - switch (migration.targetManagedInstance.type) { - case SQLTargetAssetType.SQLMI: - return loc.SQL_MANAGED_INSTANCE; - case SQLTargetAssetType.SQLVM: - return loc.SQL_VIRTUAL_MACHINE; - default: - return ''; +export function getMigrationTargetType(migration: DatabaseMigration): string { + const id = migration.id?.toLowerCase(); + if (id?.indexOf(SQLTargetAssetType.SQLMI.toLowerCase()) > -1) { + return loc.SQL_MANAGED_INSTANCE; } + else if (id?.indexOf(SQLTargetAssetType.SQLVM.toLowerCase()) > -1) { + return loc.SQL_VIRTUAL_MACHINE; + } + else if (id?.indexOf(SQLTargetAssetType.SQLDB.toLowerCase()) > -1) { + return loc.SQL_DATABASE; + } + return ''; } -export function getMigrationTargetTypeEnum(migration: MigrationContext): MigrationTargetType | undefined { - switch (migration.targetManagedInstance.type) { +export function getMigrationTargetTypeEnum(migration: DatabaseMigration): MigrationTargetType | undefined { + switch (migration.type) { case SQLTargetAssetType.SQLMI: return MigrationTargetType.SQLMI; case SQLTargetAssetType.SQLVM: return MigrationTargetType.SQLVM; + case SQLTargetAssetType.SQLDB: + return MigrationTargetType.SQLDB; default: return undefined; } } -export function getMigrationMode(migration: MigrationContext): string { - return migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? loc.OFFLINE : loc.ONLINE; +export function getMigrationMode(migration: DatabaseMigration): string { + return isOfflineMigation(migration) + ? loc.OFFLINE + : loc.ONLINE; } -export function getMigrationModeEnum(migration: MigrationContext): MigrationMode { - return migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? MigrationMode.OFFLINE : MigrationMode.ONLINE; +export function getMigrationModeEnum(migration: DatabaseMigration): MigrationMode { + return isOfflineMigation(migration) + ? MigrationMode.OFFLINE + : MigrationMode.ONLINE; } +export function isOfflineMigation(migration: DatabaseMigration): boolean { + return migration.properties.offlineConfiguration?.offline === true; +} + +export function isBlobMigration(migration: DatabaseMigration): boolean { + return migration?.properties?.backupConfiguration?.sourceLocation?.fileStorageType === FileStorageType.AzureBlob; +} + +export function getMigrationStatus(migration: DatabaseMigration): string { + return migration.properties.migrationStatus + ?? migration.properties.provisioningState; +} + + export function canRetryMigration(status: string | undefined): boolean { return status === undefined || status === MigrationStatus.Failed || @@ -50,15 +75,14 @@ export function canRetryMigration(status: string | undefined): boolean { status === MigrationStatus.Canceled; } - -const TABLE_CHECKBOX_INDEX = 0; -const TABLE_DB_NAME_INDEX = 1; export function selectDatabasesFromList(selectedDbs: string[], databaseTableValues: azdata.DeclarativeTableCellValue[][]): azdata.DeclarativeTableCellValue[][] { + const TABLE_CHECKBOX_INDEX = 0; + const TABLE_DB_NAME_INDEX = 1; const sourceDatabaseNames = selectedDbs?.map(dbName => dbName.toLocaleLowerCase()) || []; if (sourceDatabaseNames?.length > 0) { for (let i in databaseTableValues) { const row = databaseTableValues[i]; - const dbName = (row[TABLE_DB_NAME_INDEX].value as string).toLocaleLowerCase(); + const dbName = (row[TABLE_DB_NAME_INDEX].value as string)?.toLocaleLowerCase(); if (sourceDatabaseNames.indexOf(dbName) > -1) { row[TABLE_CHECKBOX_INDEX].value = true; } diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts index ddc9e4dae1..5b2d5d8b94 100644 --- a/extensions/sql-migration/src/constants/iconPathHelper.ts +++ b/extensions/sql-migration/src/constants/iconPathHelper.ts @@ -43,6 +43,8 @@ export class IconPathHelper { public static edit: IconPath; public static restartDataCollection: IconPath; public static stop: IconPath; + public static view: IconPath; + public static sqlMigrationService: IconPath; public static setExtensionContext(context: vscode.ExtensionContext) { IconPathHelper.copy = { @@ -173,5 +175,13 @@ export class IconPathHelper { light: context.asAbsolutePath('images/stop.svg'), dark: context.asAbsolutePath('images/stop.svg') }; + IconPathHelper.view = { + light: context.asAbsolutePath('images/view.svg'), + dark: context.asAbsolutePath('images/view.svg') + }; + IconPathHelper.sqlMigrationService = { + light: context.asAbsolutePath('images/sqlMigrationService.svg'), + dark: context.asAbsolutePath('images/sqlMigrationService.svg'), + }; } } diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 96e188832b..3ae9d46d99 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -65,7 +65,6 @@ export const SKU_RECOMMENDATION_ASSESSMENT_ERROR_DETAIL = localize('sql.migratio export const REFRESH_ASSESSMENT_BUTTON_LABEL = localize('sql.migration.refresh.assessment.button.label', "Refresh assessment"); 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_VM_CARD_TEXT = localize('sql.migration.sku.vm.card.title', "SQL Server on Azure Virtual Machine"); @@ -299,6 +298,25 @@ 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 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"); +export const SELECT_A_LOCATION = localize('sql.migration.select.service.select.a.location', "Select a location"); +export const SELECT_A_RESOURCE_GROUP = localize('sql.migration.select.service.select.a.resource.group', "Select a resource group"); +export const SELECT_A_SERVICE = localize('sql.migration.select.service.select.a.service', "Select a Database Migration Service"); +export const SELECT_ACCOUNT_ERROR = localize('sql.migration.select.service.select.account.error', "An error occurred while loading available Azure accounts."); +export const SELECT_TENANT_ERROR = localize('sql.migration.select.service.select.tenant.error', "An error occurred while loading available Azure account tenants."); +export const SELECT_SUBSCRIPTION_ERROR = localize('sql.migration.select.service.select.subscription.error', "An error occurred while loading account subscriptions. Please check your Azure connection and try again."); +export const SELECT_LOCATION_ERROR = localize('sql.migration.select.service.select.location.error', "An error occurred while loading locations. Please check your Azure connection and try again."); +export const SELECT_RESOURCE_GROUP_ERROR = localize('sql.migration.select.service.select.resource.group.error', "An error occurred while loading available resource groups. Please check your Azure connection and try again."); +export const SELECT_SERVICE_ERROR = localize('sql.migration.select.service.select.service.error', "An error occurred while loading available database migration services. Please check your Azure connection and try again."); +export function ACCOUNT_CREDENTIALS_REFRESH(accountName: string): string { + return localize( + 'sql.migration.account.credentials.refresh.required', + "{0} (requires credentials refresh)", + accountName); +} + // database backup page export const DATABASE_BACKUP_PAGE_TITLE = localize('sql.migration.database.page.title', "Database backup"); export const DATABASE_BACKUP_PAGE_DESCRIPTION = localize('sql.migration.database.page.description', "Select the location of the database backups to use during migration."); @@ -453,7 +471,7 @@ export const CREATE = localize('sql.migration.create', "Create"); export const CANCEL = localize('sql.migration.cancel', "Cancel"); export const TYPE = localize('sql.migration.type', "Type"); export const USER_ACCOUNT = localize('sql.migration.path.user.account', "User account"); -export const VIEW_ALL = localize('sql.migration.view.all', "View all"); +export const VIEW_ALL = localize('sql.migration.view.all', "All database migrations"); export const TARGET = localize('sql.migration.target', "Target"); export const AZURE_SQL = localize('sql.migration.azure.sql', "Azure SQL"); export const CLOSE = localize('sql.migration.close', "Close"); @@ -494,6 +512,9 @@ export const NOTEBOOK_SQL_MIGRATION_ASSESSMENT_TITLE = localize('sql.migration.s export const NOTEBOOK_OPEN_ERROR = localize('sql.migration.notebook.open.error', "Failed to open the migration notebook."); // Dashboard +export function DASHBOARD_REFRESH_MIGRATIONS(error: string): string { + return localize('sql.migration.refresh.migrations.error', "An error occurred while refreshing the migrations list: '{0}'. Please check your linked Azure connection and click refresh to try again.", error); +} export const DASHBOARD_TITLE = localize('sql.migration.dashboard.title', "Azure SQL Migration"); export const DASHBOARD_DESCRIPTION = localize('sql.migration.dashboard.description', "Determine the migration readiness of your SQL Server instances, identify a recommended Azure SQL target, and complete the migration of your SQL Server instance to Azure SQL Managed Instance or SQL Server on Azure Virtual Machines."); export const DASHBOARD_MIGRATE_TASK_BUTTON_TITLE = localize('sql.migration.dashboard.migrate.task.button', "Migrate to Azure SQL"); @@ -505,10 +526,9 @@ export const PRE_REQ_1 = localize('sql.migration.pre.req.1', "Azure account deta export const PRE_REQ_2 = localize('sql.migration.pre.req.2', "Azure SQL Managed Instance or SQL Server on Azure Virtual Machine"); export const PRE_REQ_3 = localize('sql.migration.pre.req.3', "Backup location details"); export const MIGRATION_IN_PROGRESS = localize('sql.migration.migration.in.progress', "Database migrations in progress"); -export const MIGRATION_FAILED = localize('sql.migration.failed', "Migrations failed"); -export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Migrations completed"); -export const MIGRATION_CUTOVER_CARD = localize('sql.migration.cutover.card', "Completing cutover"); -export const MIGRATION_NOT_STARTED = localize('sql.migration.migration.not.started', "Migrations not started"); +export const MIGRATION_FAILED = localize('sql.migration.failed', "Database migrations failed"); +export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Database migrations completed"); +export const MIGRATION_CUTOVER_CARD = localize('sql.migration.cutover.card', "Database migrations completing cutover"); export const SHOW_STATUS = localize('sql.migration.show.status', "Show status"); export function MIGRATION_INPROGRESS_WARNING(count: number) { switch (count) { @@ -593,6 +613,7 @@ export const NO_PENDING_BACKUPS = localize('sql.migration.no.pending.backups', " //Migration status dialog export const ADD_ACCOUNT = localize('sql.migration.status.add.account', "Add account"); export const ADD_ACCOUNT_MESSAGE = localize('sql.migration.status.add.account.MESSAGE', "Add your Azure account to view existing migrations and their status."); +export const SELECT_SERVICE_MESSAGE = localize('sql.migration.status.select.service.MESSAGE', "Select a Database Migration Service to monitor migrations."); export const STATUS_ALL = localize('sql.migration.status.dropdown.all', "Status: All"); export const STATUS_ONGOING = localize('sql.migration.status.dropdown.ongoing', "Status: Ongoing"); export const STATUS_COMPLETING = localize('sql.migration.status.dropdown.completing', "Status: Completing"); @@ -602,11 +623,13 @@ export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migratio export const ONLINE = localize('sql.migration.online', "Online"); export const OFFLINE = localize('sql.migration.offline', "Offline"); export const DATABASE = localize('sql.migration.database', "Database"); +export const STATUS_COLUMN = localize('sql.migration.database.status.column', "Status"); export const DATABASE_MIGRATION_SERVICE = localize('sql.migration.database.migration.service', "Database Migration Service"); export const DURATION = localize('sql.migration.duration', "Duration"); export const AZURE_SQL_TARGET = localize('sql.migration.azure.sql.target', "Target type"); export const SQL_MANAGED_INSTANCE = localize('sql.migration.sql.managed.instance', "SQL Managed Instance"); export const SQL_VIRTUAL_MACHINE = localize('sql.migration.sql.virtual.machine', "SQL Virtual Machine"); +export const SQL_DATABASE = localize('sql.migration.sql.database', "SQL Database"); export const TARGET_AZURE_SQL_INSTANCE_NAME = localize('sql.migration.target.azure.sql.instance.name', "Target name"); export const MIGRATION_MODE = localize('sql.migration.cutover.type', "Migration mode"); export const START_TIME = localize('sql.migration.start.time', "Start time"); @@ -745,3 +768,15 @@ export const MIGRATION_RETRY_ERROR = localize('sql.migration.retry.migration.err export const INVALID_OWNER_URI = localize('sql.migration.invalid.owner.uri.error', 'Cannot connect to the database due to invalid OwnerUri (Parameter \'OwnerUri\')'); export const DATABASE_BACKUP_PAGE_LOAD_ERROR = localize('sql.migration.database.backup.load.error', 'An error occurred while accessing database details.'); + +// Migration Service Section Dialog +export const MIGRATION_SERVICE_SELECT_TITLE = localize('sql.migration.select.service.title', 'Select Database Migration Service'); +export const MIGRATION_SERVICE_SELECT_APPLY_LABEL = localize('sql.migration.select.service.apply.label', 'Apply'); +export const MIGRATION_SERVICE_CLEAR = localize('sql.migration.select.service.delete.label', 'Clear'); +export const MIGRATION_SERVICE_SELECT_HEADING = localize('sql.migration.select.service.heading', 'Filter the migration list by Database Migration Service'); +export const MIGRATION_SERVICE_SELECT_SERVICE_LABEL = localize('sql.migration.select.service.service.label', 'Azure Database Migration Service'); +export const MIGRATION_SERVICE_SELECT_SERVICE_PROMPT = localize('sql.migration.select.service.prompt', 'Select a Database Migration Service'); +export function MIGRATION_SERVICE_SERVICE_PROMPT(serviceName: string): string { + return localize('sql.migration.service.prompt', '{0} (change)', serviceName); +} +export const MIGRATION_SERVICE_DESCRIPTION = localize('sql.migration.select.service.description', 'Azure Database Migration Service'); diff --git a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts index 70758528a3..20507936f0 100644 --- a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts +++ b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts @@ -5,15 +5,17 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; -import { MigrationContext, MigrationLocalStorage } from '../models/migrationLocalStorage'; import { logError, TelemetryViews } from '../telemtery'; import * as loc from '../constants/strings'; import { IconPath, IconPathHelper } from '../constants/iconPathHelper'; import { MigrationStatusDialog } from '../dialog/migrationStatus/migrationStatusDialog'; import { AdsMigrationStatus } from '../dialog/migrationStatus/migrationStatusDialogModel'; -import { filterMigrations, SupportedAutoRefreshIntervals } from '../api/utils'; +import { filterMigrations } from '../api/utils'; import * as styles from '../constants/styles'; import * as nls from 'vscode-nls'; +import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog'; +import { DatabaseMigration } from '../api/azure'; +import { getCurrentMigrations, getSelectedServiceStatus, isServiceContextValid, MigrationLocalStorage } from '../models/migrationLocalStorage'; const localize = nls.loadMessageBundle(); interface IActionMetadata { @@ -25,7 +27,12 @@ interface IActionMetadata { } const maxWidth = 800; -const refreshFrequency: SupportedAutoRefreshIntervals = 180000; +const BUTTON_CSS = { + 'font-size': '13px', + 'line-height': '18px', + 'margin': '4px 0', + 'text-align': 'left', +}; interface StatusCard { container: azdata.DivContainer; @@ -37,39 +44,33 @@ interface StatusCard { export class DashboardWidget { private _context: vscode.ExtensionContext; - private _migrationStatusCardsContainer!: azdata.FlexContainer; private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent; private _view!: azdata.ModelView; - private _inProgressMigrationButton!: StatusCard; private _inProgressWarningMigrationButton!: StatusCard; + private _allMigrationButton!: StatusCard; private _successfulMigrationButton!: StatusCard; private _failedMigrationButton!: StatusCard; private _completingMigrationButton!: StatusCard; - private _notStartedMigrationCard!: StatusCard; - private _migrationStatusMap: Map = new Map(); - private _viewAllMigrationsButton!: azdata.ButtonComponent; + private _selectServiceText!: azdata.TextComponent; + private _serviceContextButton!: azdata.ButtonComponent; + private _refreshButton!: azdata.ButtonComponent; - private _autoRefreshHandle!: NodeJS.Timeout; private _disposables: vscode.Disposable[] = []; - private isRefreshing: boolean = false; + public onDialogClosed = async (): Promise => { + const label = await getSelectedServiceStatus(); + this._serviceContextButton.label = label; + this._serviceContextButton.title = label; + await this.refreshMigrations(); + }; + constructor(context: vscode.ExtensionContext) { this._context = context; } - private async getCurrentMigrations(): Promise { - const connectionId = (await azdata.connection.getCurrentConnection()).connectionId; - return this._migrationStatusMap.get(connectionId)!; - } - - private async setCurrentMigrations(migrations: MigrationContext[]): Promise { - const connectionId = (await azdata.connection.getCurrentConnection()).connectionId; - this._migrationStatusMap.set(connectionId, migrations); - } - public register(): void { azdata.ui.registerModelViewProvider('migration.dashboard', async (view) => { this._view = view; @@ -82,7 +83,10 @@ export class DashboardWidget { const header = this.createHeader(view); // Files need to have the vscode-file scheme to be loaded by ADS - const watermarkUri = vscode.Uri.file(IconPathHelper.migrationDashboardHeaderBackground.light).with({ scheme: 'vscode-file' }); + const watermarkUri = vscode.Uri + .file(IconPathHelper.migrationDashboardHeaderBackground.light) + .with({ scheme: 'vscode-file' }); + container.addItem(header, { CSSStyles: { 'background-image': ` @@ -107,11 +111,11 @@ export class DashboardWidget { 'margin': '0 24px' } }); - this._disposables.push(this._view.onClosed(e => { - clearInterval(this._autoRefreshHandle); - 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(container); await this.refreshMigrations(); @@ -119,8 +123,6 @@ export class DashboardWidget { } private createHeader(view: azdata.ModelView): azdata.FlexContainer { - this.setAutoRefresh(refreshFrequency); - const header = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: maxWidth, @@ -229,95 +231,71 @@ export class DashboardWidget { 'transition': 'all .5s ease', } }).component(); - this._disposables.push(buttonContainer.onDidClick(async () => { - if (taskMetaData.command) { - await vscode.commands.executeCommand(taskMetaData.command); - } - })); + this._disposables.push( + buttonContainer.onDidClick(async () => { + if (taskMetaData.command) { + await vscode.commands.executeCommand(taskMetaData.command); + } + })); return view.modelBuilder.divContainer().withItems([buttonContainer]).component(); } - private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void { - const classVariable = this; - clearInterval(this._autoRefreshHandle); - if (interval !== -1) { - this._autoRefreshHandle = setInterval(async function () { await classVariable.refreshMigrations(); }, interval); - } - } - - private async refreshMigrations(): Promise { + public async refreshMigrations(): Promise { if (this.isRefreshing) { return; } this.isRefreshing = true; - this._viewAllMigrationsButton.enabled = false; this._migrationStatusCardLoadingContainer.loading = true; + let migrations: DatabaseMigration[] = []; try { - await this.setCurrentMigrations(await this.getMigrations()); - const migrations = await this.getCurrentMigrations(); - const inProgressMigrations = filterMigrations(migrations, AdsMigrationStatus.ONGOING); - let warningCount = 0; - for (let i = 0; i < inProgressMigrations.length; i++) { - if ( - inProgressMigrations[i].asyncOperationResult?.error?.message || - inProgressMigrations[i].migrationContext.properties.migrationFailureError?.message || - inProgressMigrations[i].migrationContext.properties.migrationStatusDetails?.fileUploadBlockingErrors || - inProgressMigrations[i].migrationContext.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(); - - const successfulMigration = filterMigrations(migrations, AdsMigrationStatus.SUCCEEDED); - - this._successfulMigrationButton.count.value = successfulMigration.length.toString(); - - const failedMigrations = filterMigrations(migrations, AdsMigrationStatus.FAILED); - const failedCount = failedMigrations.length; - if (failedCount > 0) { - this._failedMigrationButton.container.display = ''; - this._failedMigrationButton.count.value = failedCount.toString(); - } else { - this._failedMigrationButton.container.display = 'none'; - } - - const completingCutoverMigrations = filterMigrations(migrations, AdsMigrationStatus.COMPLETING); - const cutoverCount = completingCutoverMigrations.length; - if (cutoverCount > 0) { - this._completingMigrationButton.container.display = ''; - this._completingMigrationButton.count.value = cutoverCount.toString(); - } else { - this._completingMigrationButton.container.display = 'none'; - } - - } catch (error) { - logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', error); - - } finally { - this.isRefreshing = false; - this._migrationStatusCardLoadingContainer.loading = false; - this._viewAllMigrationsButton.enabled = true; + migrations = await getCurrentMigrations(); + } catch (e) { + logError(TelemetryViews.SqlServerDashboard, 'RefreshgMigrationFailed', e); + void vscode.window.showErrorMessage(loc.DASHBOARD_REFRESH_MIGRATIONS(e.message)); } + 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; } - private async getMigrations(): Promise { - const currentConnection = (await azdata.connection.getCurrentConnection()); - return await MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection, true); + private _updateStatusCard( + migrations: DatabaseMigration[], + card: StatusCard, + status: AdsMigrationStatus, + show?: boolean): void { + const list = filterMigrations(migrations, status); + const count = list?.length || 0; + card.container.display = count > 0 || show ? '' : 'none'; + card.count.value = count.toString(); } - private createStatusCard( cardIconPath: IconPath, cardTitle: string, @@ -334,26 +312,27 @@ export class DashboardWidget { } }).component(); - const statusIcon = this._view.modelBuilder.image().withProps({ - iconPath: cardIconPath!.light, - iconHeight: 24, - iconWidth: 24, - height: 32, - CSSStyles: { - 'margin': '0 8px' - } - }).component(); + const statusIcon = this._view.modelBuilder.image() + .withProps({ + iconPath: cardIconPath!.light, + iconHeight: 24, + iconWidth: 24, + height: 32, + CSSStyles: { 'margin': '0 8px' } + }).component(); - const textContainer = this._view.modelBuilder.flexContainer().withLayout({ - flexFlow: 'column' - }).component(); + const textContainer = this._view.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'column' }) + .component(); - const cardTitleText = this._view.modelBuilder.text().withProps({ value: cardTitle }).withProps({ - CSSStyles: { - ...styles.SECTION_HEADER_CSS, - 'width': '240px' - } - }).component(); + const cardTitleText = this._view.modelBuilder.text() + .withProps({ value: cardTitle }) + .withProps({ + CSSStyles: { + ...styles.SECTION_HEADER_CSS, + 'width': '240px', + } + }).component(); textContainer.addItem(cardTitleText); const cardCount = this._view.modelBuilder.text().withProps({ @@ -368,32 +347,31 @@ export class DashboardWidget { let warningContainer; let warningText; if (hasSubtext) { - const warningIcon = this._view.modelBuilder.image().withProps({ - iconPath: IconPathHelper.warning, - iconWidth: 12, - iconHeight: 12, - width: 12, - height: 18 - }).component(); + const warningIcon = this._view.modelBuilder.image() + .withProps({ + iconPath: IconPathHelper.warning, + iconWidth: 12, + iconHeight: 12, + width: 12, + height: 18, + }).component(); const warningDescription = ''; - warningText = this._view.modelBuilder.text().withProps({ value: warningDescription }).withProps({ - CSSStyles: { - ...styles.BODY_CSS, - 'padding-left': '8px', - } - }).component(); + warningText = this._view.modelBuilder.text().withProps({ value: warningDescription }) + .withProps({ + CSSStyles: { + ...styles.BODY_CSS, + 'padding-left': '8px', + } + }).component(); - warningContainer = this._view.modelBuilder.flexContainer().withItems([ - warningIcon, - warningText - ], { - flex: '0 0 auto' - }).withProps({ - CSSStyles: { - 'align-items': 'center' - } - }).component(); + warningContainer = this._view.modelBuilder.flexContainer() + .withItems( + [warningIcon, warningText], + { flex: '0 0 auto' }) + .withProps({ + CSSStyles: { 'align-items': 'center' } + }).component(); textContainer.addItem(warningContainer); } @@ -452,255 +430,243 @@ export class DashboardWidget { const statusContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '400px', - height: '360px', + height: '385px', justifyContent: 'flex-start', }).withProps({ CSSStyles: { 'border': '1px solid rgba(0, 0, 0, 0.1)', - 'padding': '16px' + 'padding': '10px', } }).component(); - const statusContainerTitle = view.modelBuilder.text().withProps({ - value: loc.DATABASE_MIGRATION_STATUS, - CSSStyles: { - ...styles.SECTION_HEADER_CSS - } - }).component(); + const statusContainerTitle = view.modelBuilder.text() + .withProps({ + value: loc.DATABASE_MIGRATION_STATUS, + width: '100%', + CSSStyles: { ...styles.SECTION_HEADER_CSS } + }).component(); - this._viewAllMigrationsButton = view.modelBuilder.hyperlink().withProps({ - label: loc.VIEW_ALL, - url: '', - CSSStyles: { - ...styles.BODY_CSS - } - }).component(); + this._refreshButton = view.modelBuilder.button() + .withProps({ + label: loc.REFRESH, + iconPath: IconPathHelper.refresh, + iconHeight: 16, + iconWidth: 16, + width: 70, + CSSStyles: { 'float': 'right' } + }).component(); - this._disposables.push(this._viewAllMigrationsButton.onDidClick(async (e) => { - const migrationStatus = await this.getCurrentMigrations(); - new MigrationStatusDialog(this._context, migrationStatus ? migrationStatus : await this.getMigrations(), AdsMigrationStatus.ALL).initialize(); - })); - - const refreshButton = view.modelBuilder.hyperlink().withProps({ - label: loc.REFRESH, - url: '', - ariaRole: 'button', - CSSStyles: { - ...styles.BODY_CSS, - 'text-align': 'right', - } - }).component(); - - this._disposables.push(refreshButton.onDidClick(async (e) => { - refreshButton.enabled = false; - await this.refreshMigrations(); - refreshButton.enabled = true; - })); - - const buttonContainer = view.modelBuilder.flexContainer().withLayout({ - justifyContent: 'flex-end', - }).component(); - - buttonContainer.addItem(this._viewAllMigrationsButton, { - CSSStyles: { - 'padding-right': '8px', - 'border-right': '1px solid', - } - }); - - buttonContainer.addItem(refreshButton, { - CSSStyles: { - 'padding-left': '8px', - } - }); - - const addAccountImage = view.modelBuilder.image().withProps({ - iconPath: IconPathHelper.addAzureAccount, - iconHeight: 100, - iconWidth: 100, - width: 96, - height: 96, - CSSStyles: { - 'opacity': '50%', - 'margin': '15% auto 10% auto', - 'filter': 'drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25))', - 'display': 'none' - } - }).component(); - - const addAccountText = view.modelBuilder.text().withProps({ - value: loc.ADD_ACCOUNT_MESSAGE, - width: 198, - height: 34, - CSSStyles: { - ...styles.NOTE_CSS, - 'margin': 'auto', - 'text-align': 'center', - 'display': 'none' - } - }).component(); - - const addAccountButton = view.modelBuilder.button().withProps({ - label: loc.ADD_ACCOUNT, - width: '100px', - enabled: true, - CSSStyles: { - 'margin': '5% 40%', - 'display': 'none' - } - }).component(); - - this._disposables.push(addAccountButton.onDidClick(async (e) => { - await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount'); - addAccountButton.enabled = false; - let accounts = await azdata.accounts.getAllAccounts(); - - if (accounts.length !== 0) { - await addAccountImage.updateCssStyles({ - 'display': 'none' - }); - await addAccountText.updateCssStyles({ - 'display': 'none' - }); - await addAccountButton.updateCssStyles({ - 'display': 'none' - }); - await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': 'visible' }); - await this._viewAllMigrationsButton.updateCssStyles({ 'visibility': 'visible' }); - } - await this.refreshMigrations(); - })); - - const header = view.modelBuilder.flexContainer().withItems( - [ + const statusHeadingContainer = view.modelBuilder.flexContainer() + .withItems([ statusContainerTitle, - buttonContainer - ] - ).withLayout({ - flexFlow: 'row', - alignItems: 'center' - }).component(); + this._refreshButton, + ]).withLayout({ + alignContent: 'center', + alignItems: 'center', + flexFlow: 'row', + }).component(); - this._migrationStatusCardsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + this._disposables.push( + this._refreshButton.onDidClick(async (e) => { + this._refreshButton.enabled = false; + await this.refreshMigrations(); + this._refreshButton.enabled = true; + })); - let accounts = await azdata.accounts.getAllAccounts(); + const buttonContainer = view.modelBuilder.flexContainer() + .withProps({ + CSSStyles: { + 'justify-content': 'left', + 'align-iems': 'center', + }, + }) + .component(); - if (accounts.length === 0) { - await addAccountImage.updateCssStyles({ - 'display': 'block' - }); - await addAccountText.updateCssStyles({ - 'display': 'block' - }); - await addAccountButton.updateCssStyles({ - 'display': 'block' - }); - await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': 'hidden' }); - await this._viewAllMigrationsButton.updateCssStyles({ 'visibility': 'hidden' }); - } + buttonContainer.addItem( + await this.createServiceSelector(this._view)); + this._selectServiceText = view.modelBuilder.text() + .withProps({ + value: loc.SELECT_SERVICE_MESSAGE, + CSSStyles: { + 'font-size': '12px', + 'margin': '10px', + 'font-weight': '350', + 'text-align': 'center', + 'display': 'none' + } + }).component(); + + const header = view.modelBuilder.flexContainer() + .withItems([statusHeadingContainer, buttonContainer]) + .withLayout({ flexFlow: 'column', }) + .component(); + + this._migrationStatusCardsContainer = view.modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'column', + height: '272px', + }) + .withProps({ CSSStyles: { 'overflow': 'hidden auto' } }) + .component(); + + await this._updateSummaryStatus(); + + // in progress this._inProgressMigrationButton = this.createStatusCard( IconPathHelper.inProgressMigration, - loc.MIGRATION_IN_PROGRESS - ); - this._disposables.push(this._inProgressMigrationButton.container.onDidClick(async (e) => { - const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING); - dialog.initialize(); - })); + loc.MIGRATION_IN_PROGRESS); + this._disposables.push( + this._inProgressMigrationButton.container.onDidClick(async (e) => { + const dialog = new MigrationStatusDialog( + this._context, + AdsMigrationStatus.ONGOING, + this.onDialogClosed); + await dialog.initialize(); + })); this._migrationStatusCardsContainer.addItem( - this._inProgressMigrationButton.container - ); + this._inProgressMigrationButton.container, + { flex: '0 0 auto' }); + // in progress warning this._inProgressWarningMigrationButton = this.createStatusCard( IconPathHelper.inProgressMigration, loc.MIGRATION_IN_PROGRESS, - true - ); - this._disposables.push(this._inProgressWarningMigrationButton.container.onDidClick(async (e) => { - const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING); - dialog.initialize(); - })); + true); + this._disposables.push( + this._inProgressWarningMigrationButton.container.onDidClick(async (e) => { + const dialog = new MigrationStatusDialog( + this._context, + AdsMigrationStatus.ONGOING, + this.onDialogClosed); + await dialog.initialize(); + })); this._migrationStatusCardsContainer.addItem( - this._inProgressWarningMigrationButton.container - ); + this._inProgressWarningMigrationButton.container, + { flex: '0 0 auto' }); + // successful this._successfulMigrationButton = this.createStatusCard( IconPathHelper.completedMigration, - loc.MIGRATION_COMPLETED - ); - this._disposables.push(this._successfulMigrationButton.container.onDidClick(async (e) => { - const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.SUCCEEDED); - dialog.initialize(); - })); + loc.MIGRATION_COMPLETED); + this._disposables.push( + this._successfulMigrationButton.container.onDidClick(async (e) => { + const dialog = new MigrationStatusDialog( + this._context, + AdsMigrationStatus.SUCCEEDED, + this.onDialogClosed); + await dialog.initialize(); + })); this._migrationStatusCardsContainer.addItem( - this._successfulMigrationButton.container - ); - + this._successfulMigrationButton.container, + { flex: '0 0 auto' }); + // completing this._completingMigrationButton = this.createStatusCard( IconPathHelper.completingCutover, - loc.MIGRATION_CUTOVER_CARD - ); - this._disposables.push(this._completingMigrationButton.container.onDidClick(async (e) => { - const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.COMPLETING); - dialog.initialize(); - })); + loc.MIGRATION_CUTOVER_CARD); + this._disposables.push( + this._completingMigrationButton.container.onDidClick(async (e) => { + const dialog = new MigrationStatusDialog( + this._context, + AdsMigrationStatus.COMPLETING, + this.onDialogClosed); + await dialog.initialize(); + })); this._migrationStatusCardsContainer.addItem( - this._completingMigrationButton.container - ); + this._completingMigrationButton.container, + { flex: '0 0 auto' }); + // failed this._failedMigrationButton = this.createStatusCard( IconPathHelper.error, - loc.MIGRATION_FAILED - ); - this._disposables.push(this._failedMigrationButton.container.onDidClick(async (e) => { - const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.FAILED); - dialog.initialize(); - })); + loc.MIGRATION_FAILED); + this._disposables.push( + this._failedMigrationButton.container.onDidClick(async (e) => { + const dialog = new MigrationStatusDialog( + this._context, + AdsMigrationStatus.FAILED, + this.onDialogClosed); + await dialog.initialize(); + })); this._migrationStatusCardsContainer.addItem( - this._failedMigrationButton.container - ); + this._failedMigrationButton.container, + { flex: '0 0 auto' }); - this._notStartedMigrationCard = this.createStatusCard( - IconPathHelper.notStartedMigration, - loc.MIGRATION_NOT_STARTED - ); - this._disposables.push(this._notStartedMigrationCard.container.onDidClick((e) => { - void vscode.window.showInformationMessage('Feature coming soon'); - })); + // all migrations + this._allMigrationButton = this.createStatusCard( + IconPathHelper.view, + loc.VIEW_ALL); + this._disposables.push( + this._allMigrationButton.container.onDidClick(async (e) => { + const dialog = new MigrationStatusDialog( + this._context, + AdsMigrationStatus.ALL, + this.onDialogClosed); + await dialog.initialize(); + })); + this._migrationStatusCardsContainer.addItem( + this._allMigrationButton.container, + { flex: '0 0 auto' }); - this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent().withItem(this._migrationStatusCardsContainer).component(); - - statusContainer.addItem( - header, { - CSSStyles: { - 'margin-bottom': '16px' - } - } - ); - - statusContainer.addItem(addAccountImage, {}); - statusContainer.addItem(addAccountText, {}); - statusContainer.addItem(addAccountButton, {}); + this._migrationStatusCardLoadingContainer = view.modelBuilder.loadingComponent() + .withItem(this._migrationStatusCardsContainer) + .component(); + statusContainer.addItem(header, { CSSStyles: { 'margin-bottom': '10px' } }); + statusContainer.addItem(this._selectServiceText, {}); statusContainer.addItem(this._migrationStatusCardLoadingContainer, {}); return statusContainer; } + private async _updateSummaryStatus(): Promise { + const serviceContext = await MigrationLocalStorage.getMigrationServiceContext(); + const isContextValid = isServiceContextValid(serviceContext); + await this._selectServiceText.updateCssStyles({ 'display': isContextValid ? 'none' : 'block' }); + await this._migrationStatusCardsContainer.updateCssStyles({ 'visibility': isContextValid ? 'visible' : 'hidden' }); + this._refreshButton.enabled = isContextValid; + } + + private async createServiceSelector(view: azdata.ModelView): Promise { + const serviceContextLabel = await getSelectedServiceStatus(); + this._serviceContextButton = view.modelBuilder.button() + .withProps({ + iconPath: IconPathHelper.sqlMigrationService, + iconHeight: 22, + iconWidth: 22, + label: serviceContextLabel, + title: serviceContextLabel, + description: loc.MIGRATION_SERVICE_DESCRIPTION, + buttonType: azdata.ButtonType.Informational, + width: 375, + CSSStyles: { ...BUTTON_CSS }, + }) + .component(); + + this._disposables.push( + this._serviceContextButton.onDidClick(async () => { + const dialog = new SelectMigrationServiceDialog(this.onDialogClosed); + await dialog.initialize(); + })); + + return this._serviceContextButton; + } + private createVideoLinks(view: azdata.ModelView): azdata.Component { - const linksContainer = view.modelBuilder.flexContainer().withLayout({ - flexFlow: 'column', - width: '400px', - height: '360px', - justifyContent: 'flex-start', - }).withProps({ - CSSStyles: { - 'border': '1px solid rgba(0, 0, 0, 0.1)', - 'padding': '16px', - 'overflow': 'scroll', - } - }).component(); + const linksContainer = view.modelBuilder.flexContainer() + .withLayout({ + flexFlow: 'column', + width: '440px', + height: '385px', + justifyContent: 'flex-start', + }).withProps({ + CSSStyles: { + 'border': '1px solid rgba(0, 0, 0, 0.1)', + 'padding': '10px', + 'overflow': 'scroll', + } + }).component(); const titleComponent = view.modelBuilder.text().withProps({ value: loc.HELP_TITLE, CSSStyles: { @@ -809,11 +775,12 @@ export class DashboardWidget { ...styles.BODY_CSS } }).component(); - this._disposables.push(video1Container.onDidClick(async () => { - if (linkMetaData.link) { - await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link)); - } - })); + this._disposables.push( + video1Container.onDidClick(async () => { + if (linkMetaData.link) { + await vscode.env.openExternal(vscode.Uri.parse(linkMetaData.link)); + } + })); videosContainer.addItem(video1Container, { CSSStyles: { 'background-image': `url(${vscode.Uri.file(linkMetaData.iconPath?.light)})`, diff --git a/extensions/sql-migration/src/dialog/assessmentResults/savedAssessmentDialog.ts b/extensions/sql-migration/src/dialog/assessmentResults/savedAssessmentDialog.ts index 65a3697837..5ac98026c9 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/savedAssessmentDialog.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/savedAssessmentDialog.ts @@ -22,7 +22,10 @@ export class SavedAssessmentDialog { private context: vscode.ExtensionContext; private _disposables: vscode.Disposable[] = []; - constructor(context: vscode.ExtensionContext, stateModel: MigrationStateModel) { + constructor( + context: vscode.ExtensionContext, + stateModel: MigrationStateModel, + private readonly _onClosedCallback: () => Promise) { this.stateModel = stateModel; this.context = context; } @@ -53,7 +56,7 @@ export class SavedAssessmentDialog { dialog.registerCloseValidator(async () => { if (this.stateModel.resumeAssessment) { - if (!this.stateModel.loadSavedInfo()) { + if (await !this.stateModel.loadSavedInfo()) { void vscode.window.showInformationMessage(constants.OPEN_SAVED_INFO_ERROR); return false; } @@ -77,7 +80,11 @@ export class SavedAssessmentDialog { } protected async execute() { - const wizardController = new WizardController(this.context, this.stateModel); + const wizardController = new WizardController( + this.context, + this.stateModel, + this._onClosedCallback); + await wizardController.openWizard(this.stateModel.sourceConnectionId); this._isOpen = false; } @@ -103,11 +110,11 @@ export class SavedAssessmentDialog { checked: true }).component(); - radioStart.onDidChangeCheckedState((e) => { + this._disposables.push(radioStart.onDidChangeCheckedState((e) => { if (e) { this.stateModel.resumeAssessment = false; } - }); + })); const radioContinue = view.modelBuilder.radioButton().withProps({ label: constants.RESUME_SESSION, name: buttonGroup, @@ -117,11 +124,11 @@ export class SavedAssessmentDialog { checked: false }).component(); - radioContinue.onDidChangeCheckedState((e) => { + this._disposables.push(radioContinue.onDidChangeCheckedState((e) => { if (e) { this.stateModel.resumeAssessment = true; } - }); + })); const flex = view.modelBuilder.flexContainer() .withLayout({ diff --git a/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts b/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts index 782593c653..1674dbbd2b 100644 --- a/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts +++ b/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts @@ -95,7 +95,14 @@ export class CreateSqlMigrationServiceDialog { try { clearDialogMessage(this._dialogObject); this._selectedResourceGroup = resourceGroup; - this._createdMigrationService = await createSqlMigrationService(this._model._azureAccount, subscription, resourceGroup, location, serviceName!, this._model._sessionId); + this._createdMigrationService = await createSqlMigrationService( + this._model._azureAccount, + subscription, + resourceGroup, + location, + serviceName!, + this._model._sessionId); + if (this._createdMigrationService.error) { this.setDialogMessage(`${this._createdMigrationService.error.code} : ${this._createdMigrationService.error.message}`); this._statusLoadingComponent.loading = false; @@ -490,7 +497,12 @@ export class CreateSqlMigrationServiceDialog { for (let i = 0; i < maxRetries; i++) { try { clearDialogMessage(this._dialogObject); - migrationServiceStatus = await getSqlMigrationService(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService.name, this._model._sessionId); + migrationServiceStatus = await getSqlMigrationService( + this._model._azureAccount, + subscription, + resourceGroup, + location, + this._createdMigrationService.name); break; } catch (e) { this._dialogObject.message = { @@ -502,7 +514,13 @@ export class CreateSqlMigrationServiceDialog { } await new Promise(r => setTimeout(r, 5000)); } - const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService!.name, this._model._sessionId); + const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData( + this._model._azureAccount, + subscription, + resourceGroup, + location, + this._createdMigrationService!.name); + this.irNodes = migrationServiceMonitoringStatus.nodes.map((node) => { return node.nodeName; }); @@ -536,7 +554,12 @@ export class CreateSqlMigrationServiceDialog { const subscription = this._model._targetSubscription; const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name; const location = this._model._targetServerInstance.location; - const keys = await getSqlMigrationServiceAuthKeys(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService!.name, this._model._sessionId); + const keys = await getSqlMigrationServiceAuthKeys( + this._model._azureAccount, + subscription, + resourceGroup, + location, + this._createdMigrationService!.name); this._copyKey1Button = this._view.modelBuilder.button().withProps({ title: constants.COPY_KEY1, diff --git a/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts index a11a8c0c92..bfc91795e9 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/confirmCutoverDialog.ts @@ -7,10 +7,11 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel'; import * as constants from '../../constants/strings'; -import { SqlManagedInstance } from '../../api/azure'; +import { getMigrationTargetInstance, SqlManagedInstance } from '../../api/azure'; import { IconPathHelper } from '../../constants/iconPathHelper'; import { convertByteSizeToReadableUnit, get12HourTime } from '../../api/utils'; import * as styles from '../../constants/styles'; +import { isBlobMigration } from '../../constants/helper'; export class ConfirmCutoverDialog { private _dialogObject!: azdata.window.Dialog; private _view!: azdata.ModelView; @@ -21,20 +22,17 @@ export class ConfirmCutoverDialog { } async initialize(): Promise { - - let tab = azdata.window.createTab(''); + const tab = azdata.window.createTab(''); tab.registerContent(async (view: azdata.ModelView) => { this._view = view; const completeCutoverText = view.modelBuilder.text().withProps({ value: constants.COMPLETE_CUTOVER, - CSSStyles: { - ...styles.PAGE_TITLE_CSS - } + CSSStyles: { ...styles.PAGE_TITLE_CSS } }).component(); const sourceDatabaseText = view.modelBuilder.text().withProps({ - value: this.migrationCutoverModel._migration.migrationContext.properties.sourceDatabaseName, + value: this.migrationCutoverModel._migration.properties.sourceDatabaseName, CSSStyles: { ...styles.SMALL_NOTE_CSS, 'margin': '4px 0px 8px' @@ -42,12 +40,9 @@ export class ConfirmCutoverDialog { }).component(); const separator = this._view.modelBuilder.separator().withProps({ width: '800px' }).component(); - const helpMainText = this._view.modelBuilder.text().withProps({ value: constants.CUTOVER_HELP_MAIN, - CSSStyles: { - ...styles.BODY_CSS - } + CSSStyles: { ...styles.BODY_CSS } }).component(); const helpStepsText = this._view.modelBuilder.text().withProps({ @@ -58,8 +53,9 @@ export class ConfirmCutoverDialog { } }).component(); - - const fileContainer = this.migrationCutoverModel.isBlobMigration() ? this.createBlobFileContainer() : this.createNetworkShareFileContainer(); + const fileContainer = isBlobMigration(this.migrationCutoverModel.migrationStatus) + ? this.createBlobFileContainer() + : this.createNetworkShareFileContainer(); const confirmCheckbox = this._view.modelBuilder.checkBox().withProps({ CSSStyles: { @@ -76,16 +72,19 @@ export class ConfirmCutoverDialog { const cutoverWarning = this._view.modelBuilder.infoBox().withProps({ text: constants.COMPLETING_CUTOVER_WARNING, style: 'warning', - CSSStyles: { - ...styles.BODY_CSS - } + CSSStyles: { ...styles.BODY_CSS } }).component(); - let infoDisplay = 'none'; - if (this.migrationCutoverModel._migration.targetManagedInstance.id.toLocaleLowerCase().includes('managedinstances') - && (this.migrationCutoverModel._migration.targetManagedInstance)?.sku?.tier === 'BusinessCritical') { - infoDisplay = 'inline'; + if (this.migrationCutoverModel._migration.id.toLocaleLowerCase().includes('managedinstances')) { + const targetInstance = await getMigrationTargetInstance( + this.migrationCutoverModel._serviceConstext.azureAccount!, + this.migrationCutoverModel._serviceConstext.subscription!, + this.migrationCutoverModel._migration); + + if ((targetInstance)?.sku?.tier === 'BusinessCritical') { + infoDisplay = 'inline'; + } } const businessCriticalInfoBox = this._view.modelBuilder.infoBox().withProps({ @@ -111,23 +110,18 @@ export class ConfirmCutoverDialog { businessCriticalInfoBox ]).component(); - this._dialogObject.okButton.enabled = false; this._dialogObject.okButton.label = constants.COMPLETE_CUTOVER; this._disposables.push(this._dialogObject.okButton.onClick(async (e) => { await this.migrationCutoverModel.startCutover(); - void vscode.window.showInformationMessage(constants.CUTOVER_IN_PROGRESS(this.migrationCutoverModel._migration.migrationContext.properties.sourceDatabaseName)); + void vscode.window.showInformationMessage( + constants.CUTOVER_IN_PROGRESS( + this.migrationCutoverModel._migration.properties.sourceDatabaseName)); })); const formBuilder = view.modelBuilder.formContainer().withFormItems( - [ - { - component: container - } - ], - { - horizontal: false - } + [{ component: container }], + { horizontal: false } ); const form = formBuilder.withLayout({ width: '100%' }).component(); @@ -144,18 +138,14 @@ export class ConfirmCutoverDialog { private createBlobFileContainer(): azdata.FlexContainer { const container = this._view.modelBuilder.flexContainer().withProps({ - CSSStyles: { - 'margin': '8px 0' - } + CSSStyles: { 'margin': '8px 0' } }).component(); - const containerHeading = this._view.modelBuilder.text().withProps({ value: constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0), width: 250, - CSSStyles: { - ...styles.LABEL_CSS - } + CSSStyles: { ...styles.LABEL_CSS } }).component(); + container.addItem(containerHeading, { flex: '0' }); const refreshButton = this._view.modelBuilder.button().withProps({ iconPath: IconPathHelper.refresh, @@ -165,13 +155,7 @@ export class ConfirmCutoverDialog { height: 20, label: constants.REFRESH, }).component(); - - - container.addItem(containerHeading, { - flex: '0' - }); - - refreshButton.onDidClick(async e => { + this._disposables.push(refreshButton.onDidClick(async e => { refreshLoader.loading = true; try { await this.migrationCutoverModel.fetchStatus(); @@ -184,11 +168,8 @@ export class ConfirmCutoverDialog { } finally { refreshLoader.loading = false; } - }); - - container.addItem(refreshButton, { - flex: '0' - }); + })); + container.addItem(refreshButton, { flex: '0' }); const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({ loading: false, @@ -197,10 +178,8 @@ export class ConfirmCutoverDialog { 'margin-left': '8px' } }).component(); + container.addItem(refreshLoader, { flex: '0' }); - container.addItem(refreshLoader, { - flex: '0' - }); return container; } @@ -227,23 +206,18 @@ export class ConfirmCutoverDialog { } }).component(); - containerHeading.onDidClick(async e => { + this._disposables.push(containerHeading.onDidClick(async e => { if (expanded) { containerHeading.iconPath = IconPathHelper.expandButtonClosed; containerHeading.iconHeight = 12; - await fileTable.updateCssStyles({ - 'display': 'none' - }); - + await fileTable.updateCssStyles({ 'display': 'none' }); } else { containerHeading.iconPath = IconPathHelper.expandButtonOpen; containerHeading.iconHeight = 8; - await fileTable.updateCssStyles({ - 'display': 'inline' - }); + await fileTable.updateCssStyles({ 'display': 'inline' }); } expanded = !expanded; - }); + })); const refreshButton = this._view.modelBuilder.button().withProps({ iconPath: IconPathHelper.refresh, @@ -252,16 +226,12 @@ export class ConfirmCutoverDialog { width: 70, height: 20, label: constants.REFRESH, - CSSStyles: { - 'margin-top': '13px' - } + CSSStyles: { 'margin-top': '13px' } }).component(); - headingRow.addItem(containerHeading, { - flex: '0' - }); + headingRow.addItem(containerHeading, { flex: '0' }); - refreshButton.onDidClick(async e => { + this._disposables.push(refreshButton.onDidClick(async e => { refreshLoader.loading = true; try { await this.migrationCutoverModel.fetchStatus(); @@ -276,11 +246,8 @@ export class ConfirmCutoverDialog { } finally { refreshLoader.loading = false; } - }); - - headingRow.addItem(refreshButton, { - flex: '0' - }); + })); + headingRow.addItem(refreshButton, { flex: '0' }); const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({ loading: false, @@ -290,20 +257,13 @@ export class ConfirmCutoverDialog { 'height': '13px' } }).component(); - - headingRow.addItem(refreshLoader, { - flex: '0' - }); - + headingRow.addItem(refreshLoader, { flex: '0' }); container.addItem(headingRow); const lastScanCompleted = this._view.modelBuilder.text().withProps({ value: constants.LAST_SCAN_COMPLETED(get12HourTime(new Date())), - CSSStyles: { - ...styles.NOTE_CSS - } + CSSStyles: { ...styles.NOTE_CSS } }).component(); - container.addItem(lastScanCompleted); const fileTable = this._view.modelBuilder.table().withProps({ @@ -327,9 +287,7 @@ export class ConfirmCutoverDialog { data: [], width: 400, height: 150, - CSSStyles: { - 'display': 'none' - } + CSSStyles: { 'display': 'none' } }).component(); container.addItem(fileTable); this.refreshFileTable(fileTable); @@ -347,9 +305,7 @@ export class ConfirmCutoverDialog { ]; }); } else { - fileTable.data = [ - [constants.NO_PENDING_BACKUPS] - ]; + fileTable.data = [[constants.NO_PENDING_BACKUPS]]; } } diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts index 677b0d95c7..6b622f39ba 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts @@ -6,26 +6,24 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { IconPathHelper } from '../../constants/iconPathHelper'; -import { BackupFileInfoStatus, MigrationContext, MigrationStatus } from '../../models/migrationLocalStorage'; +import { BackupFileInfoStatus, MigrationServiceContext, MigrationStatus } from '../../models/migrationLocalStorage'; import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel'; import * as loc from '../../constants/strings'; -import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage, SupportedAutoRefreshIntervals, clearDialogMessage, displayDialogErrorMessage } from '../../api/utils'; +import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage, clearDialogMessage, displayDialogErrorMessage } from '../../api/utils'; import { EOL } from 'os'; import { ConfirmCutoverDialog } from './confirmCutoverDialog'; import { logError, TelemetryViews } from '../../telemtery'; import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog'; import * as styles from '../../constants/styles'; -import { canRetryMigration } from '../../constants/helper'; +import { canRetryMigration, getMigrationStatus, isBlobMigration, isOfflineMigation } from '../../constants/helper'; +import { DatabaseMigration, getResourceName } from '../../api/azure'; -const refreshFrequency: SupportedAutoRefreshIntervals = 30000; const statusImageSize: number = 14; export class MigrationCutoverDialog { - private _context: vscode.ExtensionContext; private _dialogObject!: azdata.window.Dialog; private _view!: azdata.ModelView; private _model: MigrationCutoverDialogModel; - private _migration: MigrationContext; private _databaseTitleName!: azdata.TextComponent; private _cutoverButton!: azdata.ButtonComponent; @@ -52,7 +50,6 @@ export class MigrationCutoverDialog { private _fileCount!: azdata.TextComponent; private _fileTable!: azdata.DeclarativeTableComponent; - private _autoRefreshHandle!: any; private _disposables: vscode.Disposable[] = []; private _emptyTableFill!: azdata.FlexContainer; @@ -60,10 +57,13 @@ export class MigrationCutoverDialog { readonly _infoFieldWidth: string = '250px'; - constructor(context: vscode.ExtensionContext, migration: MigrationContext) { - this._context = context; - this._migration = migration; - this._model = new MigrationCutoverDialogModel(migration); + constructor( + private readonly _context: vscode.ExtensionContext, + private readonly _serviceContext: MigrationServiceContext, + private readonly _migration: DatabaseMigration, + private readonly _onClosedCallback: () => Promise) { + + this._model = new MigrationCutoverDialogModel(_serviceContext, _migration); this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 'wide'); } @@ -224,15 +224,14 @@ export class MigrationCutoverDialog { ); const form = formBuilder.withLayout({ width: '100%' }).component(); - this._disposables.push(this._view.onClosed(e => { - clearInterval(this._autoRefreshHandle); - this._disposables.forEach( - d => { try { d.dispose(); } catch { } }); - })); + this._disposables.push( + this._view.onClosed(e => { + this._disposables.forEach( + d => { try { d.dispose(); } catch { } }); + })); - return view.initializeModel(form).then(async (value) => { - await this.refreshStatus(); - }); + await view.initializeModel(form); + await this.refreshStatus(); } catch (e) { logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e); } @@ -242,9 +241,6 @@ export class MigrationCutoverDialog { this._dialogObject.cancelButton.hidden = true; this._dialogObject.okButton.label = loc.CLOSE; - this._disposables.push(this._dialogObject.okButton.onClick(e => { - clearInterval(this._autoRefreshHandle); - })); azdata.window.openDialog(this._dialogObject); } @@ -262,7 +258,7 @@ export class MigrationCutoverDialog { ...styles.PAGE_TITLE_CSS }, width: 950, - value: this._model._migration.migrationContext.properties.sourceDatabaseName + value: this._model._migration.properties.sourceDatabaseName }).component(); const databaseSubTitle = this._view.modelBuilder.text().withProps({ @@ -282,8 +278,6 @@ export class MigrationCutoverDialog { width: 950 }).component(); - this.setAutoRefresh(refreshFrequency); - const titleLogoContainer = this._view.modelBuilder.flexContainer().withProps({ width: 1000 }).component(); @@ -314,7 +308,7 @@ export class MigrationCutoverDialog { enabled: false, CSSStyles: { ...styles.BODY_CSS, - 'display': this._isOnlineMigration() ? 'block' : 'none' + 'display': isOfflineMigation(this._model._migration) ? 'none' : 'block' } }).component(); @@ -322,16 +316,13 @@ export class MigrationCutoverDialog { await this.refreshStatus(); const dialog = new ConfirmCutoverDialog(this._model); await dialog.initialize(); - await this.refreshStatus(); if (this._model.CutoverError) { displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CUTOVER_ERROR, this._model.CutoverError); } })); - headerActions.addItem(this._cutoverButton, { - flex: '0' - }); + headerActions.addItem(this._cutoverButton, { flex: '0' }); this._cancelButton = this._view.modelBuilder.button().withProps({ iconPath: IconPathHelper.cancel, @@ -377,7 +368,11 @@ export class MigrationCutoverDialog { this._disposables.push(this._retryButton.onDidClick( async (e) => { await this.refreshStatus(); - let retryMigrationDialog = new RetryMigrationDialog(this._context, this._migration); + const retryMigrationDialog = new RetryMigrationDialog( + this._context, + this._serviceContext, + this._migration, + this._onClosedCallback); await retryMigrationDialog.openDialog(); } )); @@ -397,12 +392,14 @@ export class MigrationCutoverDialog { } }).component(); - this._disposables.push(this._refreshButton.onDidClick( - async (e) => await this.refreshStatus())); + this._disposables.push( + this._refreshButton.onDidClick(async (e) => { + this._refreshButton.enabled = false; + await this.refreshStatus(); + this._refreshButton.enabled = true; + })); - headerActions.addItem(this._refreshButton, { - flex: '0', - }); + headerActions.addItem(this._refreshButton, { flex: '0' }); this._copyDatabaseMigrationDetails = this._view.modelBuilder.button().withProps({ iconPath: IconPathHelper.copy, @@ -425,9 +422,7 @@ export class MigrationCutoverDialog { headerActions.addItem(this._copyDatabaseMigrationDetails, { flex: '0', - CSSStyles: { - 'margin-left': '5px' - } + CSSStyles: { 'margin-left': '5px' } }); // create new support request button. Hiding button until sql migration support has been setup. @@ -443,11 +438,11 @@ export class MigrationCutoverDialog { } }).component(); - this._newSupportRequest.onDidClick(async (e) => { - const serviceId = this._model._migration.controller.id; + this._disposables.push(this._newSupportRequest.onDidClick(async (e) => { + const serviceId = this._model._migration.properties.migrationService; const supportUrl = `https://portal.azure.com/#resource${serviceId}/supportrequest`; await vscode.env.openExternal(vscode.Uri.parse(supportUrl)); - }); + })); headerActions.addItem(this._newSupportRequest, { flex: '0', @@ -519,12 +514,12 @@ export class MigrationCutoverDialog { addInfoFieldToContainer(this._targetServerInfoField, flexTarget); addInfoFieldToContainer(this._targetVersionInfoField, flexTarget); - const isBlobMigration = this._model.isBlobMigration(); + const _isBlobMigration = isBlobMigration(this._model._migration); const flexStatus = this._view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' '); - this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', isBlobMigration); + this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', _isBlobMigration); this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, ''); addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus); addInfoFieldToContainer(this._fullBackupFileOnInfoField, flexStatus); @@ -533,10 +528,10 @@ export class MigrationCutoverDialog { const flexFile = this._view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); - this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', isBlobMigration); + this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', _isBlobMigration); this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, ''); - this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', isBlobMigration); - this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', !isBlobMigration); + this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', _isBlobMigration); + this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', !_isBlobMigration); addInfoFieldToContainer(this._lastLSNInfoField, flexFile); addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile); addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile); @@ -561,33 +556,8 @@ export class MigrationCutoverDialog { return flexInfo; } - private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void { - const shouldRefresh = (status: string | undefined) => !status - || status === MigrationStatus.InProgress - || status === MigrationStatus.Creating - || status === MigrationStatus.Completing - || status === MigrationStatus.Canceling; - - if (shouldRefresh(this.getMigrationStatus())) { - const classVariable = this; - clearInterval(this._autoRefreshHandle); - if (interval !== -1) { - this._autoRefreshHandle = setInterval(async function () { await classVariable.refreshStatus(); }, interval); - } - } - } - private getMigrationDetails(): string { - if (this._model.migrationOpStatus) { - return (JSON.stringify( - { - 'async-operation-details': this._model.migrationOpStatus, - 'details': this._model.migrationStatus - } - , undefined, 2)); - } else { - return (JSON.stringify(this._model.migrationStatus, undefined, 2)); - } + return JSON.stringify(this._model.migrationStatus, undefined, 2); } private async refreshStatus(): Promise { @@ -598,18 +568,13 @@ export class MigrationCutoverDialog { try { clearDialogMessage(this._dialogObject); - if (this._isOnlineMigration()) { - await this._cutoverButton.updateCssStyles({ - 'display': 'block' - }); - } + await this._cutoverButton.updateCssStyles( + { 'display': isOfflineMigation(this._model._migration) ? 'none' : 'block' }); this.isRefreshing = true; this._refreshLoader.loading = true; await this._model.fetchStatus(); const errors = []; - errors.push(this._model.migrationOpStatus.error?.message); - errors.push(this._model._migration.asyncOperationResult?.error?.message); errors.push(this._model.migrationStatus.properties.provisioningError); errors.push(this._model.migrationStatus.properties.migrationFailureError?.message); errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []); @@ -626,12 +591,12 @@ export class MigrationCutoverDialog { description: this.getMigrationDetails() }; const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId); - const sqlServerName = this._model._migration.sourceConnectionProfile.serverName; - const sourceDatabaseName = this._model._migration.migrationContext.properties.sourceDatabaseName; + const sqlServerName = this._model._migration.properties.sourceServerName; + const sourceDatabaseName = this._model._migration.properties.sourceDatabaseName; const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!); const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion; - const targetDatabaseName = this._model._migration.migrationContext.name; - const targetServerName = this._model._migration.targetManagedInstance.name; + const targetDatabaseName = this._model._migration.name; + const targetServerName = getResourceName(this._model._migration.id); let targetServerVersion; if (this._model.migrationStatus.id.includes('managedInstances')) { targetServerVersion = loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE; @@ -644,30 +609,30 @@ export class MigrationCutoverDialog { const tableData: ActiveBackupFileSchema[] = []; - this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach((activeBackupSet) => { + this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach( + (activeBackupSet) => { + if (this._shouldDisplayBackupFileTable()) { + 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) : '-', + backupStartTime: activeBackupSet.backupStartDate, + firstLSN: activeBackupSet.firstLSN, + lastLSN: activeBackupSet.lastLSN + }; + }) + ); + } - if (this._shouldDisplayBackupFileTable()) { - 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) : '-', - backupStartTime: activeBackupSet.backupStartDate, - firstLSN: activeBackupSet.firstLSN, - lastLSN: activeBackupSet.lastLSN - }; - }) - ); - } - - if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) { - lastAppliedSSN = activeBackupSet.lastLSN; - lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate; - } - }); + if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) { + lastAppliedSSN = activeBackupSet.lastLSN; + lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate; + } + }); this._sourceDatabaseInfoField.text.value = sourceDatabaseName; this._sourceDetailsInfoField.text.value = sqlServerName; @@ -677,21 +642,23 @@ export class MigrationCutoverDialog { this._targetServerInfoField.text.value = targetServerName; this._targetVersionInfoField.text.value = targetServerVersion; - const migrationStatusTextValue = this.getMigrationStatus(); + const migrationStatusTextValue = this._getMigrationStatus(); this._migrationStatusInfoField.text.value = migrationStatusTextValue ?? '-'; this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migrationStatusTextValue); this._fullBackupFileOnInfoField.text.value = this._model.migrationStatus?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? '-'; let backupLocation; - const isBlobMigration = this._model.isBlobMigration(); + const _isBlobMigration = isBlobMigration(this._model._migration); // Displaying storage accounts and blob container for azure blob backups. - if (isBlobMigration) { - const storageAccountResourceId = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId; - const blobContainerName = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName; - backupLocation = `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`; + if (_isBlobMigration) { + const storageAccountResourceId = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId; + const blobContainerName = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName; + backupLocation = storageAccountResourceId && blobContainerName + ? `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}` + : undefined; } else { - const fileShare = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.fileShare; + const fileShare = this._model._migration.properties.backupConfiguration?.sourceLocation?.fileShare; backupLocation = fileShare?.path! ?? '-'; } this._backupLocationInfoField.text.value = backupLocation ?? '-'; @@ -700,7 +667,7 @@ export class MigrationCutoverDialog { this._lastAppliedBackupInfoField.text.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename ?? '-'; this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : '-'; - if (isBlobMigration) { + if (_isBlobMigration) { if (!this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) { this._currentRestoringFileInfoField.text.value = '-'; } else if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename === this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) { @@ -752,7 +719,7 @@ export class MigrationCutoverDialog { this._cutoverButton.enabled = false; if (migrationStatusTextValue === MigrationStatus.InProgress) { - if (isBlobMigration) { + if (_isBlobMigration) { if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) { this._cutoverButton.enabled = true; } @@ -856,21 +823,14 @@ export class MigrationCutoverDialog { }; } - private _isOnlineMigration(): boolean { - return this._model._migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? false : true; - } - private _shouldDisplayBackupFileTable(): boolean { - return !this._model.isBlobMigration(); + return !isBlobMigration(this._model._migration); } - private getMigrationStatus(): string { - if (this._model.migrationStatus) { - return this._model.migrationStatus.properties.migrationStatus - ?? this._model.migrationStatus.properties.provisioningState; - } - return this._model._migration.migrationContext.properties.migrationStatus - ?? this._model._migration.migrationContext.properties.provisioningState; + private _getMigrationStatus(): string { + return this._model.migrationStatus + ? getMigrationStatus(this._model.migrationStatus) + : getMigrationStatus(this._model._migration); } } diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts index 09cfcce295..196b04b3a1 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialogModel.ts @@ -3,43 +3,36 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getMigrationStatus, DatabaseMigration, startMigrationCutover, stopMigration, getMigrationAsyncOperationDetails, AzureAsyncOperationResource, BackupFileInfo, getResourceGroupFromId } from '../../api/azure'; -import { BackupFileInfoStatus, MigrationContext } from '../../models/migrationLocalStorage'; +import { DatabaseMigration, startMigrationCutover, stopMigration, BackupFileInfo, getResourceGroupFromId, getMigrationDetails, getMigrationTargetName } from '../../api/azure'; +import { BackupFileInfoStatus, MigrationServiceContext } from '../../models/migrationLocalStorage'; import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery'; import * as constants from '../../constants/strings'; import { EOL } from 'os'; -import { getMigrationTargetType, getMigrationMode } from '../../constants/helper'; +import { getMigrationTargetType, getMigrationMode, isBlobMigration } from '../../constants/helper'; export class MigrationCutoverDialogModel { public CutoverError?: Error; public CancelMigrationError?: Error; - public migrationStatus!: DatabaseMigration; - public migrationOpStatus!: AzureAsyncOperationResource; - constructor(public _migration: MigrationContext) { + + constructor( + public _serviceConstext: MigrationServiceContext, + public _migration: DatabaseMigration + ) { } public async fetchStatus(): Promise { - if (this._migration.asyncUrl) { - this.migrationOpStatus = await getMigrationAsyncOperationDetails( - this._migration.azureAccount, - this._migration.subscription, - this._migration.asyncUrl, - this._migration.sessionId!); - } - - this.migrationStatus = await getMigrationStatus( - this._migration.azureAccount, - this._migration.subscription, - this._migration.migrationContext, - this._migration.sessionId!); + this.migrationStatus = await getMigrationDetails( + this._serviceConstext.azureAccount!, + this._serviceConstext.subscription!, + this._migration.id, + this._migration.properties?.migrationOperationId); sendSqlMigrationActionEvent( TelemetryViews.MigrationCutoverDialog, TelemetryAction.MigrationStatus, { - 'sessionId': this._migration.sessionId!, 'migrationStatus': this.migrationStatus.properties?.migrationStatus }, {} @@ -51,18 +44,16 @@ export class MigrationCutoverDialogModel { public async startCutover(): Promise { try { this.CutoverError = undefined; - if (this.migrationStatus) { + if (this._migration) { const cutover = await startMigrationCutover( - this._migration.azureAccount, - this._migration.subscription, - this.migrationStatus, - this._migration.sessionId! - ); + this._serviceConstext.azureAccount!, + this._serviceConstext.subscription!, + this._migration!); sendSqlMigrationActionEvent( TelemetryViews.MigrationCutoverDialog, TelemetryAction.CutoverMigration, { - ...this.getTelemetryProps(this._migration), + ...this.getTelemetryProps(this._serviceConstext, this._migration), 'migrationEndTime': new Date().toString(), }, {} @@ -79,8 +70,6 @@ export class MigrationCutoverDialogModel { public async fetchErrors(): Promise { const errors = []; await this.fetchStatus(); - errors.push(this.migrationOpStatus.error?.message); - errors.push(this._migration.asyncOperationResult?.error?.message); errors.push(this.migrationStatus.properties.migrationFailureError?.message); return errors .filter((e, i, arr) => e !== undefined && i === arr.indexOf(e)) @@ -93,18 +82,16 @@ export class MigrationCutoverDialogModel { if (this.migrationStatus) { const cutoverStartTime = new Date().toString(); await stopMigration( - this._migration.azureAccount, - this._migration.subscription, - this.migrationStatus, - this._migration.sessionId! - ); + this._serviceConstext.azureAccount!, + this._serviceConstext.subscription!, + this.migrationStatus); sendSqlMigrationActionEvent( TelemetryViews.MigrationCutoverDialog, TelemetryAction.CancelMigration, { - ...this.getTelemetryProps(this._migration), + ...this.getTelemetryProps(this._serviceConstext, this._migration), 'migrationMode': getMigrationMode(this._migration), - 'cutoverStartTime': cutoverStartTime + 'cutoverStartTime': cutoverStartTime, }, {} ); @@ -116,12 +103,8 @@ export class MigrationCutoverDialogModel { return undefined!; } - public isBlobMigration(): boolean { - return this._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.azureBlob !== undefined; - } - public confirmCutoverStepsString(): string { - if (this.isBlobMigration()) { + if (isBlobMigration(this.migrationStatus)) { return `${constants.CUTOVER_HELP_STEP1} ${constants.CUTOVER_HELP_STEP2_BLOB_CONTAINER} ${constants.CUTOVER_HELP_STEP3_BLOB_CONTAINER}`; @@ -152,16 +135,15 @@ export class MigrationCutoverDialogModel { return files; } - private getTelemetryProps(migration: MigrationContext) { + private getTelemetryProps(serviceContext: MigrationServiceContext, migration: DatabaseMigration) { return { - 'sessionId': migration.sessionId!, - 'subscriptionId': migration.subscription.id, - 'resourceGroup': getResourceGroupFromId(migration.targetManagedInstance.id), - 'sqlServerName': migration.sourceConnectionProfile.serverName, - 'sourceDatabaseName': migration.migrationContext.properties.sourceDatabaseName, + 'subscriptionId': serviceContext.subscription!.id, + 'resourceGroup': getResourceGroupFromId(migration.id), + 'sqlServerName': migration.properties.sourceServerName, + 'sourceDatabaseName': migration.properties.sourceDatabaseName, 'targetType': getMigrationTargetType(migration), - 'targetDatabaseName': migration.migrationContext.name, - 'targetServerName': migration.targetManagedInstance.name, + 'targetDatabaseName': migration.name, + 'targetServerName': getMigrationTargetName(migration), }; } } diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts index e4eb74540b..5f0cbaae06 100644 --- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts @@ -6,22 +6,19 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { IconPathHelper } from '../../constants/iconPathHelper'; -import { MigrationContext, MigrationLocalStorage, MigrationStatus } from '../../models/migrationLocalStorage'; +import { getCurrentMigrations, getSelectedServiceStatus, MigrationLocalStorage, MigrationStatus } from '../../models/migrationLocalStorage'; import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog'; import { AdsMigrationStatus, MigrationStatusDialogModel } from './migrationStatusDialogModel'; import * as loc from '../../constants/strings'; -import { clearDialogMessage, convertTimeDifferenceToDuration, displayDialogErrorMessage, filterMigrations, getMigrationStatusImage, SupportedAutoRefreshIntervals } from '../../api/utils'; +import { clearDialogMessage, convertTimeDifferenceToDuration, displayDialogErrorMessage, filterMigrations, getMigrationStatusImage } from '../../api/utils'; import { SqlMigrationServiceDetailsDialog } from '../sqlMigrationService/sqlMigrationServiceDetailsDialog'; import { ConfirmCutoverDialog } from '../migrationCutover/confirmCutoverDialog'; import { MigrationCutoverDialogModel } from '../migrationCutover/migrationCutoverDialogModel'; import { getMigrationTargetType, getMigrationMode, canRetryMigration } from '../../constants/helper'; import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog'; - -const refreshFrequency: SupportedAutoRefreshIntervals = 180000; - -const statusImageSize: number = 14; -const imageCellStyles: azdata.CssStyles = { 'margin': '3px 3px 0 0', 'padding': '0' }; -const statusCellStyles: azdata.CssStyles = { 'margin': '0', 'padding': '0' }; +import { DatabaseMigration, getResourceName } from '../../api/azure'; +import { logError, TelemetryViews } from '../../telemtery'; +import { SelectMigrationServiceDialog } from '../selectMigrationService/selectMigrationServiceDialog'; const MenuCommands = { Cutover: 'sqlmigration.cutover', @@ -40,53 +37,56 @@ export class MigrationStatusDialog { private _view!: azdata.ModelView; private _searchBox!: azdata.InputBoxComponent; private _refresh!: azdata.ButtonComponent; + private _serviceContextButton!: azdata.ButtonComponent; private _statusDropdown!: azdata.DropDownComponent; - private _statusTable!: azdata.DeclarativeTableComponent; + private _statusTable!: azdata.TableComponent; private _refreshLoader!: azdata.LoadingComponent; - private _autoRefreshHandle!: NodeJS.Timeout; private _disposables: vscode.Disposable[] = []; + private _filteredMigrations: DatabaseMigration[] = []; private isRefreshing = false; - constructor(context: vscode.ExtensionContext, migrations: MigrationContext[], private _filter: AdsMigrationStatus) { + constructor( + context: vscode.ExtensionContext, + private _filter: AdsMigrationStatus, + private _onClosedCallback: () => Promise) { + this._context = context; - this._model = new MigrationStatusDialogModel(migrations); - this._dialogObject = azdata.window.createModelViewDialog(loc.MIGRATION_STATUS, 'MigrationControllerDialog', 'wide'); + this._model = new MigrationStatusDialogModel([]); + this._dialogObject = azdata.window.createModelViewDialog( + loc.MIGRATION_STATUS, + 'MigrationControllerDialog', + 'wide'); } - initialize() { + async initialize() { let tab = azdata.window.createTab(''); tab.registerContent(async (view: azdata.ModelView) => { this._view = view; this.registerCommands(); - const formBuilder = view.modelBuilder.formContainer().withFormItems( - [ - { - component: this.createSearchAndRefreshContainer() - }, - { - component: this.createStatusTable() - } - ], - { - horizontal: false - } - ); - const form = formBuilder.withLayout({ width: '100%' }).component(); - this._disposables.push(this._view.onClosed(e => { - clearInterval(this._autoRefreshHandle); - this._disposables.forEach( - d => { try { d.dispose(); } catch { } }); - })); + const form = view.modelBuilder.formContainer() + .withFormItems( + [ + { component: await this.createSearchAndRefreshContainer() }, + { component: this.createStatusTable() } + ], + { horizontal: false } + ).withLayout({ width: '100%' }) + .component(); + this._disposables.push( + this._view.onClosed(async e => { + this._disposables.forEach( + d => { try { d.dispose(); } catch { } }); - return view.initializeModel(form); + await this._onClosedCallback(); + })); + + await view.initializeModel(form); + return await this.refreshTable(); }); this._dialogObject.content = [tab]; this._dialogObject.cancelButton.hidden = true; this._dialogObject.okButton.label = loc.CLOSE; - this._disposables.push(this._dialogObject.okButton.onClick(e => { - clearInterval(this._autoRefreshHandle); - })); azdata.window.openDialog(this._dialogObject); } @@ -100,111 +100,124 @@ export class MigrationStatusDialog { private canCutoverMigration = (status: string | undefined) => status === MigrationStatus.InProgress; - private createSearchAndRefreshContainer(): azdata.FlexContainer { - this._searchBox = this._view.modelBuilder.inputBox().withProps({ - stopEnterPropagation: true, - placeHolder: loc.SEARCH_FOR_MIGRATIONS, - width: '360px' - }).component(); - - this._disposables.push(this._searchBox.onTextChanged(async (value) => { - await this.populateMigrationTable(); - })); - - this._refresh = this._view.modelBuilder.button().withProps({ - iconPath: IconPathHelper.refresh, - iconHeight: '16px', - iconWidth: '20px', - height: '30px', - label: loc.REFRESH_BUTTON_LABEL, - }).component(); + private async createSearchAndRefreshContainer(): Promise { + this._searchBox = this._view.modelBuilder.inputBox() + .withProps({ + stopEnterPropagation: true, + placeHolder: loc.SEARCH_FOR_MIGRATIONS, + width: '360px' + }).component(); + this._disposables.push( + this._searchBox.onTextChanged( + async (value) => await this.populateMigrationTable())); + this._refresh = this._view.modelBuilder.button() + .withProps({ + iconPath: IconPathHelper.refresh, + iconHeight: '16px', + iconWidth: '20px', + label: loc.REFRESH_BUTTON_LABEL, + }).component(); this._disposables.push( this._refresh.onDidClick( - async (e) => { await this.refreshTable(); })); + async (e) => await this.refreshTable())); - const flexContainer = this._view.modelBuilder.flexContainer().withProps({ - width: 900, - CSSStyles: { - 'justify-content': 'left' - }, - }).component(); - - flexContainer.addItem(this._searchBox, { - flex: '0' - }); - - this._statusDropdown = this._view.modelBuilder.dropDown().withProps({ - ariaLabel: loc.MIGRATION_STATUS_FILTER, - values: this._model.statusDropdownValues, - width: '220px' - }).component(); - - this._disposables.push(this._statusDropdown.onValueChanged(async (value) => { - await this.populateMigrationTable(); - })); + this._statusDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: loc.MIGRATION_STATUS_FILTER, + values: this._model.statusDropdownValues, + width: '220px' + }).component(); + this._disposables.push( + this._statusDropdown.onValueChanged( + async (value) => await this.populateMigrationTable())); if (this._filter) { - this._statusDropdown.value = (this._statusDropdown.values).find((value) => { - return value.name === this._filter; - }); + this._statusDropdown.value = + (this._statusDropdown.values) + .find(value => value.name === this._filter); } - flexContainer.addItem(this._statusDropdown, { - flex: '0', - CSSStyles: { - 'margin-left': '20px' - } - }); + this._refreshLoader = this._view.modelBuilder.loadingComponent() + .withProps({ loading: false }) + .component(); - flexContainer.addItem(this._refresh, { - flex: '0', - CSSStyles: { - 'margin-left': '20px' - } - }); + const searchLabel = this._view.modelBuilder.text() + .withProps({ + value: 'Status', + CSSStyles: { + 'font-size': '13px', + 'font-weight': '600', + 'margin': '3px 0 0 0', + }, + }).component(); - this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({ - loading: false, - height: '55px' - }).component(); + const serviceContextLabel = await getSelectedServiceStatus(); + this._serviceContextButton = this._view.modelBuilder.button() + .withProps({ + iconPath: IconPathHelper.sqlMigrationService, + iconHeight: 22, + iconWidth: 22, + label: serviceContextLabel, + title: serviceContextLabel, + description: loc.MIGRATION_SERVICE_DESCRIPTION, + buttonType: azdata.ButtonType.Informational, + width: 270, + }).component(); - flexContainer.addItem(this._refreshLoader, { - flex: '0 0 auto', - CSSStyles: { - 'margin-left': '20px' - } - }); - this.setAutoRefresh(refreshFrequency); - const container = this._view.modelBuilder.flexContainer().withProps({ - width: 1000 - }).component(); - container.addItem(flexContainer, { - flex: '0 0 auto', - CSSStyles: { - 'width': '980px' - } - }); + const onDialogClosed = async (): Promise => { + const label = await getSelectedServiceStatus(); + this._serviceContextButton.label = label; + this._serviceContextButton.title = label; + await this.refreshTable(); + }; + + this._disposables.push( + this._serviceContextButton.onDidClick( + async () => { + const dialog = new SelectMigrationServiceDialog(onDialogClosed); + await dialog.initialize(); + })); + + const flexContainer = this._view.modelBuilder.flexContainer() + .withProps({ + width: '100%', + CSSStyles: { + 'justify-content': 'left', + 'align-items': 'center', + 'padding': '0px', + 'display': 'flex', + 'flex-direction': 'row', + }, + }).component(); + + flexContainer.addItem(this._searchBox, { flex: '0' }); + flexContainer.addItem(this._serviceContextButton, { flex: '0', CSSStyles: { 'margin-left': '20px' } }); + flexContainer.addItem(searchLabel, { flex: '0', CSSStyles: { 'margin-left': '20px' } }); + flexContainer.addItem(this._statusDropdown, { flex: '0', CSSStyles: { 'margin-left': '5px' } }); + flexContainer.addItem(this._refresh, { flex: '0', CSSStyles: { 'margin-left': '20px' } }); + flexContainer.addItem(this._refreshLoader, { flex: '0 0 auto', CSSStyles: { 'margin-left': '20px' } }); + + const container = this._view.modelBuilder.flexContainer() + .withProps({ width: 1245 }) + .component(); + container.addItem(flexContainer, { flex: '0 0 auto', }); return container; } - private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void { - const classVariable = this; - clearInterval(this._autoRefreshHandle); - if (interval !== -1) { - this._autoRefreshHandle = setInterval(async function () { await classVariable.refreshTable(); }, interval); - } - } - private registerCommands(): void { this._disposables.push(vscode.commands.registerCommand( MenuCommands.Cutover, async (migrationId: string) => { try { clearDialogMessage(this._dialogObject); - const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); - if (this.canCutoverMigration(migration?.migrationContext.properties.migrationStatus)) { - const cutoverDialogModel = new MigrationCutoverDialogModel(migration!); + const migration = this._model._migrations.find( + migration => migration.id === migrationId); + + if (this.canCutoverMigration(migration?.properties.migrationStatus)) { + const cutoverDialogModel = new MigrationCutoverDialogModel( + await MigrationLocalStorage.getMigrationServiceContext(), + migration!); await cutoverDialogModel.fetchStatus(); const dialog = new ConfirmCutoverDialog(cutoverDialogModel); await dialog.initialize(); @@ -224,8 +237,12 @@ export class MigrationStatusDialog { MenuCommands.ViewDatabase, async (migrationId: string) => { try { - const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); - const dialog = new MigrationCutoverDialog(this._context, migration!); + const migration = this._model._migrations.find(migration => migration.id === migrationId); + const dialog = new MigrationCutoverDialog( + this._context, + await MigrationLocalStorage.getMigrationServiceContext(), + migration!, + this._onClosedCallback); await dialog.initialize(); } catch (e) { console.log(e); @@ -236,8 +253,8 @@ export class MigrationStatusDialog { MenuCommands.ViewTarget, async (migrationId: string) => { try { - const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); - const url = 'https://portal.azure.com/#resource/' + migration!.targetManagedInstance.id; + const migration = this._model._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) { console.log(e); @@ -248,8 +265,10 @@ export class MigrationStatusDialog { MenuCommands.ViewService, async (migrationId: string) => { try { - const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); - const dialog = new SqlMigrationServiceDetailsDialog(migration!); + const migration = this._model._migrations.find(migration => migration.id === migrationId); + const dialog = new SqlMigrationServiceDetailsDialog( + await MigrationLocalStorage.getMigrationServiceContext(), + migration!); await dialog.initialize(); } catch (e) { console.log(e); @@ -261,17 +280,12 @@ export class MigrationStatusDialog { async (migrationId: string) => { try { clearDialogMessage(this._dialogObject); - const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); - const cutoverDialogModel = new MigrationCutoverDialogModel(migration!); + const migration = this._model._migrations.find(migration => migration.id === migrationId); + const cutoverDialogModel = new MigrationCutoverDialogModel( + await MigrationLocalStorage.getMigrationServiceContext(), + migration!); await cutoverDialogModel.fetchStatus(); - if (cutoverDialogModel.migrationOpStatus) { - await vscode.env.clipboard.writeText(JSON.stringify({ - 'async-operation-details': cutoverDialogModel.migrationOpStatus, - 'details': cutoverDialogModel.migrationStatus - }, undefined, 2)); - } else { - await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migrationStatus, undefined, 2)); - } + await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migrationStatus, undefined, 2)); await vscode.window.showInformationMessage(loc.DETAILS_COPIED); } catch (e) { @@ -285,11 +299,13 @@ export class MigrationStatusDialog { async (migrationId: string) => { try { clearDialogMessage(this._dialogObject); - const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); - if (this.canCancelMigration(migration?.migrationContext.properties.migrationStatus)) { + const migration = this._model._migrations.find(migration => migration.id === migrationId); + if (this.canCancelMigration(migration?.properties.migrationStatus)) { void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => { if (v === loc.YES) { - const cutoverDialogModel = new MigrationCutoverDialogModel(migration!); + const cutoverDialogModel = new MigrationCutoverDialogModel( + await MigrationLocalStorage.getMigrationServiceContext(), + migration!); await cutoverDialogModel.fetchStatus(); await cutoverDialogModel.cancelMigration(); @@ -312,9 +328,13 @@ export class MigrationStatusDialog { async (migrationId: string) => { try { clearDialogMessage(this._dialogObject); - const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); - if (canRetryMigration(migration?.migrationContext.properties.migrationStatus)) { - let retryMigrationDialog = new RetryMigrationDialog(this._context, migration!); + const migration = this._model._migrations.find(migration => migration.id === migrationId); + if (canRetryMigration(migration?.properties.migrationStatus)) { + let retryMigrationDialog = new RetryMigrationDialog( + this._context, + await MigrationLocalStorage.getMigrationServiceContext(), + migration!, + this._onClosedCallback); await retryMigrationDialog.openDialog(); } else { @@ -329,75 +349,43 @@ export class MigrationStatusDialog { private async populateMigrationTable(): Promise { try { - const migrations = filterMigrations( + this._filteredMigrations = filterMigrations( this._model._migrations, (this._statusDropdown.value).name, this._searchBox.value!); - migrations.sort((m1, m2) => { - return new Date(m1.migrationContext.properties?.startedOn) > new Date(m2.migrationContext.properties?.startedOn) ? -1 : 1; + this._filteredMigrations.sort((m1, m2) => { + return new Date(m1.properties?.startedOn) > new Date(m2.properties?.startedOn) ? -1 : 1; }); - const data: azdata.DeclarativeTableCellValue[][] = migrations.map((migration, index) => { + const data: any[] = this._filteredMigrations.map((migration, index) => { return [ - { value: this._getDatabaserHyperLink(migration) }, - { value: this._getMigrationStatus(migration) }, - { value: getMigrationMode(migration) }, - { value: getMigrationTargetType(migration) }, - { value: migration.targetManagedInstance.name }, - { value: migration.controller.name }, - { - value: this._getMigrationDuration( - migration.migrationContext.properties.startedOn, - migration.migrationContext.properties.endedOn) - }, - { value: this._getMigrationTime(migration.migrationContext.properties.startedOn) }, - { value: this._getMigrationTime(migration.migrationContext.properties.endedOn) }, - { - value: { - commands: this._getMenuCommands(migration), - context: migration.migrationContext.id - }, - } + { + icon: IconPathHelper.sqlDatabaseLogo, + title: migration.properties.sourceDatabaseName ?? '-', + }, // database + { + icon: getMigrationStatusImage(migration.properties.migrationStatus), + title: this._getMigrationStatus(migration), + }, // statue + getMigrationMode(migration), // mode + getMigrationTargetType(migration), // targetType + getResourceName(migration.id), // targetName + getResourceName(migration.properties.migrationService), // migrationService + this._getMigrationDuration( + migration.properties.startedOn, + migration.properties.endedOn), // duration + this._getMigrationTime(migration.properties.startedOn), // startTime + this._getMigrationTime(migration.properties.endedOn), // endTime ]; }); - await this._statusTable.setDataValues(data); + await this._statusTable.updateProperty('data', data); } catch (e) { - console.log(e); + logError(TelemetryViews.MigrationStatusDialog, 'Error populating migrations list page', e); } } - private _getDatabaserHyperLink(migration: MigrationContext): azdata.FlexContainer { - const imageControl = this._view.modelBuilder.image() - .withProps({ - iconPath: IconPathHelper.sqlDatabaseLogo, - iconHeight: statusImageSize, - iconWidth: statusImageSize, - height: statusImageSize, - width: statusImageSize, - CSSStyles: imageCellStyles - }) - .component(); - - const databaseHyperLink = this._view.modelBuilder - .hyperlink() - .withProps({ - label: migration.migrationContext.properties.sourceDatabaseName, - url: '', - CSSStyles: statusCellStyles - }).component(); - - this._disposables.push(databaseHyperLink.onDidClick( - async (e) => await (new MigrationCutoverDialog(this._context, migration)).initialize())); - - return this._view.modelBuilder - .flexContainer() - .withItems([imageControl, databaseHyperLink]) - .withProps({ CSSStyles: statusCellStyles, display: 'inline-flex' }) - .component(); - } - private _getMigrationTime(migrationTime: string): string { return migrationTime ? new Date(migrationTime).toLocaleString() @@ -420,39 +408,11 @@ export class MigrationStatusDialog { return '---'; } - private _getMenuCommands(migration: MigrationContext): string[] { - const menuCommands: string[] = []; - const migrationStatus = migration?.migrationContext?.properties?.migrationStatus; - - if (getMigrationMode(migration) === loc.ONLINE && - this.canCutoverMigration(migrationStatus)) { - menuCommands.push(MenuCommands.Cutover); - } - - menuCommands.push(...[ - MenuCommands.ViewDatabase, - MenuCommands.ViewTarget, - MenuCommands.ViewService, - MenuCommands.CopyMigration]); - - if (this.canCancelMigration(migrationStatus)) { - menuCommands.push(MenuCommands.CancelMigration); - } - - if (canRetryMigration(migrationStatus)) { - menuCommands.push(MenuCommands.RetryMigration); - } - - return menuCommands; - } - - private _getMigrationStatus(migration: MigrationContext): azdata.FlexContainer { - const properties = migration.migrationContext.properties; + private _getMigrationStatus(migration: DatabaseMigration): string { + const properties = migration.properties; const migrationStatus = properties.migrationStatus ?? properties.provisioningState; let warningCount = 0; - if (migration.asyncOperationResult?.error?.message) { - warningCount++; - } + if (properties.migrationFailureError?.message) { warningCount++; } @@ -463,17 +423,16 @@ export class MigrationStatusDialog { warningCount++; } - return this._getStatusControl(migrationStatus, warningCount, migration); + return loc.STATUS_VALUE(migrationStatus, warningCount) + (loc.STATUS_WARNING_COUNT(migrationStatus, warningCount) ?? ''); } public openCalloutDialog(dialogHeading: string, dialogName?: string, calloutMessageText?: string): void { - const dialog = azdata.window.createModelViewDialog(dialogHeading, dialogName, 288, 'callout', 'left', true, false, - { - xPos: 0, - yPos: 0, - width: 20, - height: 20 - }); + const dialog = azdata.window.createModelViewDialog(dialogHeading, dialogName, 288, 'callout', 'left', true, false, { + xPos: 0, + yPos: 0, + width: 20, + height: 20 + }); const tab: azdata.window.DialogTab = azdata.window.createTab(''); tab.registerContent(async view => { const warningContentContainer = view.modelBuilder.divContainer().component(); @@ -499,73 +458,6 @@ export class MigrationStatusDialog { azdata.window.openDialog(dialog); } - private _getStatusControl(status: string, count: number, migration: MigrationContext): azdata.DivContainer { - const control = this._view.modelBuilder - .divContainer() - .withItems([ - // migration status icon - this._view.modelBuilder.image() - .withProps({ - iconPath: getMigrationStatusImage(status), - iconHeight: statusImageSize, - iconWidth: statusImageSize, - height: statusImageSize, - width: statusImageSize, - CSSStyles: imageCellStyles - }) - .component(), - // migration status text - this._view.modelBuilder.text().withProps({ - value: loc.STATUS_VALUE(status, count), - height: statusImageSize, - CSSStyles: statusCellStyles, - }).component() - ]) - .withProps({ CSSStyles: statusCellStyles, display: 'inline-flex' }) - .component(); - - if (count > 0) { - const migrationWarningImage = this._view.modelBuilder.image() - .withProps({ - iconPath: this._statusInfoMap(status), - iconHeight: statusImageSize, - iconWidth: statusImageSize, - height: statusImageSize, - width: statusImageSize, - CSSStyles: imageCellStyles - }).component(); - - const migrationWarningCount = this._view.modelBuilder.hyperlink() - .withProps({ - label: loc.STATUS_WARNING_COUNT(status, count) ?? '', - ariaLabel: loc.ERROR, - url: '', - height: statusImageSize, - CSSStyles: statusCellStyles, - }).component(); - - control.addItems([ - migrationWarningImage, - migrationWarningCount - ]); - - this._disposables.push(migrationWarningCount.onDidClick(async () => { - const cutoverDialogModel = new MigrationCutoverDialogModel(migration!); - const errors = await cutoverDialogModel.fetchErrors(); - this.openCalloutDialog( - status === MigrationStatus.InProgress - || status === MigrationStatus.Completing - ? loc.WARNING - : loc.ERROR, - 'input-table-row-dialog', - errors - ); - })); - } - - return control; - } - private async refreshTable(): Promise { if (this.isRefreshing) { return; @@ -575,8 +467,7 @@ export class MigrationStatusDialog { try { clearDialogMessage(this._dialogObject); this._refreshLoader.loading = true; - const currentConnection = await azdata.connection.getCurrentConnection(); - this._model._migrations = await MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection, true); + this._model._migrations = await getCurrentMigrations(); await this.populateMigrationTable(); } catch (e) { displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e); @@ -587,115 +478,111 @@ export class MigrationStatusDialog { } } - private createStatusTable(): azdata.DeclarativeTableComponent { - const rowCssStyle: azdata.CssStyles = { - 'border': 'none', - 'text-align': 'left', - 'border-bottom': '1px solid', - }; + private createStatusTable(): azdata.TableComponent { + const headerCssStyles = undefined; + const rowCssStyles = undefined; - const headerCssStyles: azdata.CssStyles = { - 'border': 'none', - 'text-align': 'left', - 'border-bottom': '1px solid', - 'font-weight': 'bold', - 'padding-left': '0px', - 'padding-right': '0px' - }; - - this._statusTable = this._view.modelBuilder.declarativeTable().withProps({ + this._statusTable = this._view.modelBuilder.table().withProps({ ariaLabel: loc.MIGRATION_STATUS, + data: [], + forceFitColumns: azdata.ColumnSizingMode.ForceFit, + height: '600px', + width: '1095px', + display: 'grid', columns: [ - { - displayName: loc.DATABASE, - valueType: azdata.DeclarativeDataType.component, - width: '90px', - isReadOnly: true, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles + { + cssClass: rowCssStyles, + headerCssClass: headerCssStyles, + name: loc.DATABASE, + value: 'database', + width: 190, + type: azdata.ColumnType.hyperlink, + icon: IconPathHelper.sqlDatabaseLogo, + showText: true, + }, + { + cssClass: rowCssStyles, + headerCssClass: headerCssStyles, + name: loc.STATUS_COLUMN, + value: 'status', + width: 120, + type: azdata.ColumnType.hyperlink, }, { - displayName: loc.MIGRATION_STATUS, - valueType: azdata.DeclarativeDataType.component, - width: '170px', - isReadOnly: true, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles + cssClass: rowCssStyles, + headerCssClass: headerCssStyles, + name: loc.MIGRATION_MODE, + value: 'mode', + width: 85, + type: azdata.ColumnType.text, }, { - displayName: loc.MIGRATION_MODE, - valueType: azdata.DeclarativeDataType.string, - width: '90px', - isReadOnly: true, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles + cssClass: rowCssStyles, + headerCssClass: headerCssStyles, + name: loc.AZURE_SQL_TARGET, + value: 'targetType', + width: 120, + type: azdata.ColumnType.text, }, { - displayName: loc.AZURE_SQL_TARGET, - valueType: azdata.DeclarativeDataType.string, - width: '130px', - isReadOnly: true, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles + cssClass: rowCssStyles, + headerCssClass: headerCssStyles, + name: loc.TARGET_AZURE_SQL_INSTANCE_NAME, + value: 'targetName', + width: 125, + type: azdata.ColumnType.text, }, { - displayName: loc.TARGET_AZURE_SQL_INSTANCE_NAME, - valueType: azdata.DeclarativeDataType.string, - width: '130px', - isReadOnly: true, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles + cssClass: rowCssStyles, + headerCssClass: headerCssStyles, + name: loc.DATABASE_MIGRATION_SERVICE, + value: 'migrationService', + width: 140, + type: azdata.ColumnType.text, }, { - displayName: loc.DATABASE_MIGRATION_SERVICE, - valueType: azdata.DeclarativeDataType.string, - width: '150px', - isReadOnly: true, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles + cssClass: rowCssStyles, + headerCssClass: headerCssStyles, + name: loc.DURATION, + value: 'duration', + width: 50, + type: azdata.ColumnType.text, }, { - displayName: loc.DURATION, - valueType: azdata.DeclarativeDataType.string, - width: '55px', - isReadOnly: true, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles + cssClass: rowCssStyles, + headerCssClass: headerCssStyles, + name: loc.START_TIME, + value: 'startTime', + width: 115, + type: azdata.ColumnType.text, }, { - displayName: loc.START_TIME, - valueType: azdata.DeclarativeDataType.string, - width: '140px', - isReadOnly: true, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles + cssClass: rowCssStyles, + headerCssClass: headerCssStyles, + name: loc.FINISH_TIME, + value: 'finishTime', + width: 115, + type: azdata.ColumnType.text, }, - { - displayName: loc.FINISH_TIME, - valueType: azdata.DeclarativeDataType.string, - width: '140px', - isReadOnly: true, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles - }, - { - displayName: '', - valueType: azdata.DeclarativeDataType.menu, - width: '20px', - isReadOnly: true, - rowCssStyles: rowCssStyle, - headerCssStyles: headerCssStyles, - } ] }).component(); + + this._disposables.push(this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => { + const buttonState = rowState; + switch (buttonState?.column) { + case 0: + case 1: + const migration = this._filteredMigrations[rowState.row]; + const dialog = new MigrationCutoverDialog( + this._context, + await MigrationLocalStorage.getMigrationServiceContext(), + migration, + this._onClosedCallback); + await dialog.initialize(); + break; + } + })); + return this._statusTable; } - - private _statusInfoMap(status: string): azdata.IconPath { - return status === MigrationStatus.InProgress - || status === MigrationStatus.Creating - || status === MigrationStatus.Completing - ? IconPathHelper.warning - : IconPathHelper.error; - } } diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts index b28f56f2b7..f01e971254 100644 --- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts +++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import { DatabaseMigration } from '../../api/azure'; import * as loc from '../../constants/strings'; -import { MigrationContext } from '../../models/migrationLocalStorage'; export class MigrationStatusDialogModel { public statusDropdownValues: azdata.CategoryValue[] = [ @@ -27,7 +27,7 @@ export class MigrationStatusDialogModel { } ]; - constructor(public _migrations: MigrationContext[]) { + constructor(public _migrations: DatabaseMigration[]) { } } diff --git a/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts b/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts index e7e877225e..be0fad765d 100644 --- a/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts +++ b/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts @@ -7,26 +7,26 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as mssql from 'mssql'; import { azureResource } from 'azureResource'; -import { getLocations, getResourceGroupFromId, getBlobContainerId, getFullResourceGroupFromId, getResourceName } from '../../api/azure'; +import { getLocations, getResourceGroupFromId, getBlobContainerId, getFullResourceGroupFromId, getResourceName, DatabaseMigration, getMigrationTargetInstance } from '../../api/azure'; import { MigrationMode, MigrationStateModel, NetworkContainerType, SavedInfo } from '../../models/stateMachine'; -import { MigrationContext } from '../../models/migrationLocalStorage'; +import { MigrationServiceContext } from '../../models/migrationLocalStorage'; import { WizardController } from '../../wizard/wizardController'; import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper'; import * as constants from '../../constants/strings'; export class RetryMigrationDialog { - private _context: vscode.ExtensionContext; - private _migration: MigrationContext; - constructor(context: vscode.ExtensionContext, migration: MigrationContext) { - this._context = context; - this._migration = migration; + constructor( + private readonly _context: vscode.ExtensionContext, + private readonly _serviceContext: MigrationServiceContext, + private readonly _migration: DatabaseMigration, + private readonly _onClosedCallback: () => Promise) { } - private createMigrationStateModel(migration: MigrationContext, connectionId: string, serverName: string, api: mssql.IExtension, location: azureResource.AzureLocation): MigrationStateModel { + 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); - const sourceDatabaseName = migration.migrationContext.properties.sourceDatabaseName; + const sourceDatabaseName = migration.properties.sourceDatabaseName; let savedInfo: SavedInfo; savedInfo = { closedPage: 0, @@ -41,53 +41,56 @@ export class RetryMigrationDialog { migrationTargetType: getMigrationTargetTypeEnum(migration)!, // TargetSelection - azureAccount: migration.azureAccount, - azureTenant: migration.azureAccount.properties.tenants[0], - subscription: migration.subscription, + azureAccount: serviceContext.azureAccount!, + azureTenant: serviceContext.azureAccount!.properties.tenants[0]!, + subscription: serviceContext.subscription!, location: location, resourceGroup: { - id: getFullResourceGroupFromId(migration.targetManagedInstance.id), - name: getResourceGroupFromId(migration.targetManagedInstance.id), - subscription: migration.subscription + id: getFullResourceGroupFromId(migration.id), + name: getResourceGroupFromId(migration.id), + subscription: serviceContext.subscription!, }, - targetServerInstance: migration.targetManagedInstance, + targetServerInstance: await getMigrationTargetInstance( + serviceContext.azureAccount!, + serviceContext.subscription!, + migration), // MigrationMode migrationMode: getMigrationModeEnum(migration), // DatabaseBackup - targetDatabaseNames: [migration.migrationContext.name], + targetDatabaseNames: [migration.name], networkContainerType: null, networkShares: [], blobs: [], // Integration Runtime - sqlMigrationService: migration.controller, + sqlMigrationService: serviceContext.migrationService, }; - const getStorageAccountResourceGroup = (storageAccountResourceId: string) => { + const getStorageAccountResourceGroup = (storageAccountResourceId: string): azureResource.AzureResourceResourceGroup => { return { id: getFullResourceGroupFromId(storageAccountResourceId!), name: getResourceGroupFromId(storageAccountResourceId!), - subscription: migration.subscription + subscription: this._serviceContext.subscription! }; }; - const getStorageAccount = (storageAccountResourceId: string) => { + const getStorageAccount = (storageAccountResourceId: string): azureResource.AzureGraphResource => { const storageAccountName = getResourceName(storageAccountResourceId); return { type: 'microsoft.storage/storageaccounts', id: storageAccountResourceId!, tenantId: savedInfo.azureTenant?.id!, - subscriptionId: migration.subscription.id, + subscriptionId: this._serviceContext.subscription?.id!, name: storageAccountName, location: savedInfo.location!.name, }; }; - const sourceLocation = migration.migrationContext.properties.backupConfiguration.sourceLocation; + const sourceLocation = migration.properties.backupConfiguration?.sourceLocation; if (sourceLocation?.fileShare) { savedInfo.networkContainerType = NetworkContainerType.NETWORK_SHARE; - const storageAccountResourceId = migration.migrationContext.properties.backupConfiguration.targetLocation?.storageAccountResourceId!; + const storageAccountResourceId = migration.properties.backupConfiguration?.targetLocation?.storageAccountResourceId!; savedInfo.networkShares = [ { password: '', @@ -106,9 +109,9 @@ export class RetryMigrationDialog { blobContainer: { id: getBlobContainerId(getFullResourceGroupFromId(storageAccountResourceId!), getResourceName(storageAccountResourceId!), sourceLocation?.azureBlob.blobContainerName), name: sourceLocation?.azureBlob.blobContainerName, - subscription: migration.subscription + subscription: this._serviceContext.subscription! }, - lastBackupFile: getMigrationModeEnum(migration) === MigrationMode.OFFLINE ? migration.migrationContext.properties.offlineConfiguration.lastBackupName! : undefined, + lastBackupFile: getMigrationModeEnum(migration) === MigrationMode.OFFLINE ? migration.properties.offlineConfiguration?.lastBackupName! : undefined, storageAccount: getStorageAccount(storageAccountResourceId!), resourceGroup: getStorageAccountResourceGroup(storageAccountResourceId!), storageKey: '' @@ -123,10 +126,18 @@ export class RetryMigrationDialog { } public async openDialog(dialogName?: string) { - const locations = await getLocations(this._migration.azureAccount, this._migration.subscription); + const locations = await getLocations( + this._serviceContext.azureAccount!, + this._serviceContext.subscription!); + + const targetInstance = await getMigrationTargetInstance( + this._serviceContext.azureAccount!, + this._serviceContext.subscription!, + this._migration); + let location: azureResource.AzureLocation; locations.forEach(azureLocation => { - if (azureLocation.name === this._migration.targetManagedInstance.location) { + if (azureLocation.name === targetInstance.location) { location = azureLocation; } }); @@ -146,10 +157,13 @@ export class RetryMigrationDialog { } const api = (await vscode.extensions.getExtension(mssql.extension.name)?.activate()) as mssql.IExtension; - const stateModel = this.createMigrationStateModel(this._migration, connectionId, serverName, api, location!); + const stateModel = await this.createMigrationStateModel(this._serviceContext, this._migration, connectionId, serverName, api, location!); - if (stateModel.loadSavedInfo()) { - const wizardController = new WizardController(this._context, stateModel); + if (await stateModel.loadSavedInfo()) { + const wizardController = new WizardController( + this._context, + stateModel, + this._onClosedCallback); 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 new file mode 100644 index 0000000000..f7a20d3e33 --- /dev/null +++ b/extensions/sql-migration/src/dialog/selectMigrationService/selectMigrationServiceDialog.ts @@ -0,0 +1,597 @@ +/*--------------------------------------------------------------------------------------------- + * 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 azurecore from 'azurecore'; +import { MigrationLocalStorage, MigrationServiceContext } from '../../models/migrationLocalStorage'; +import { azureResource } from 'azureResource'; +import * as styles from '../../constants/styles'; +import * as constants from '../../constants/strings'; +import { findDropDownItemIndex, selectDefaultDropdownValue, deepClone } from '../../api/utils'; +import { getFullResourceGroupFromId, getLocations, getSqlMigrationServices, getSubscriptions, SqlMigrationService } from '../../api/azure'; +import { logError, TelemetryViews } from '../../telemtery'; + +const CONTROL_MARGIN = '20px'; +const INPUT_COMPONENT_WIDTH = '100%'; +const STYLE_HIDE = { 'display': 'none' }; +const STYLE_ShOW = { 'display': 'inline' }; +export const BODY_CSS = { + 'font-size': '13px', + 'line-height': '18px', + 'margin': '4px 0', +}; +const LABEL_CSS = { + ...styles.LABEL_CSS, + 'margin': '0 0 0 0', + 'font-weight': '600', +}; +const DROPDOWN_CSS = { + 'margin': '-1em 0 0 0', +}; +const TENANT_DROPDOWN_CSS = { + 'margin': '1em 0 0 0', +}; + +export class SelectMigrationServiceDialog { + private _dialog: azdata.window.Dialog; + private _view!: azdata.ModelView; + private _disposables: vscode.Disposable[] = []; + private _serviceContext!: MigrationServiceContext; + private _azureAccounts!: azdata.Account[]; + private _accountTenants!: azurecore.Tenant[]; + private _subscriptions!: azureResource.AzureResourceSubscription[]; + private _locations!: azureResource.AzureLocation[]; + private _resourceGroups!: azureResource.AzureResourceResourceGroup[]; + private _sqlMigrationServices!: SqlMigrationService[]; + private _azureAccountsDropdown!: azdata.DropDownComponent; + private _accountTenantDropdown!: azdata.DropDownComponent; + private _accountTenantFlexContainer!: azdata.FlexContainer; + private _azureSubscriptionDropdown!: azdata.DropDownComponent; + private _azureLocationDropdown!: azdata.DropDownComponent; + private _azureResourceGroupDropdown!: azdata.DropDownComponent; + private _azureServiceDropdownLabel!: azdata.TextComponent; + private _azureServiceDropdown!: azdata.DropDownComponent; + private _deleteButton!: azdata.window.Button; + + constructor( + private readonly _onClosedCallback: () => Promise) { + this._dialog = azdata.window.createModelViewDialog( + constants.MIGRATION_SERVICE_SELECT_TITLE, + 'SelectMigraitonServiceDialog', + 460, + 'normal'); + } + + async initialize(): Promise { + this._serviceContext = await MigrationLocalStorage.getMigrationServiceContext(); + + await this._dialog.registerContent(async (view: azdata.ModelView) => { + this._disposables.push( + view.onClosed(e => { + this._disposables.forEach( + d => { try { d.dispose(); } catch { } }); + })); + await this.registerContent(view); + }); + + this._dialog.okButton.label = constants.MIGRATION_SERVICE_SELECT_APPLY_LABEL; + this._dialog.okButton.position = 'left'; + this._dialog.cancelButton.position = 'right'; + + this._deleteButton = azdata.window.createButton( + constants.MIGRATION_SERVICE_CLEAR, + 'right'); + this._disposables.push( + this._deleteButton.onClick(async (value) => { + await MigrationLocalStorage.saveMigrationServiceContext({}); + await this._onClosedCallback(); + azdata.window.closeDialog(this._dialog); + })); + this._dialog.customButtons = [this._deleteButton]; + + azdata.window.openDialog(this._dialog); + } + + protected async registerContent(view: azdata.ModelView): Promise { + this._view = view; + + const flexContainer = this._view.modelBuilder + .flexContainer() + .withItems([ + this._createHeading(), + this._createAzureAccountsDropdown(), + this._createAzureTenantContainer(), + this._createServiceSelectionContainer(), + ]) + .withLayout({ flexFlow: 'column' }) + .withProps({ CSSStyles: { 'padding': CONTROL_MARGIN } }) + .component(); + + await this._view.initializeModel(flexContainer); + await this._populateAzureAccountsDropdown(); + } + + private _createHeading(): azdata.TextComponent { + return this._view.modelBuilder.text() + .withProps({ + value: constants.MIGRATION_SERVICE_SELECT_HEADING, + CSSStyles: { ...styles.BODY_CSS } + }).component(); + } + + private _createAzureAccountsDropdown(): azdata.FlexContainer { + const azureAccountLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.ACCOUNTS_SELECTION_PAGE_TITLE, + requiredIndicator: true, + CSSStyles: { ...LABEL_CSS } + }).component(); + this._azureAccountsDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.ACCOUNTS_SELECTION_PAGE_TITLE, + width: INPUT_COMPONENT_WIDTH, + editable: true, + required: true, + fireOnTextChange: true, + placeholder: constants.SELECT_AN_ACCOUNT, + CSSStyles: { ...DROPDOWN_CSS }, + }).component(); + this._disposables.push( + this._azureAccountsDropdown.onValueChanged(async (value) => { + const selectedIndex = findDropDownItemIndex(this._azureAccountsDropdown, value); + this._serviceContext.azureAccount = (selectedIndex > -1) + ? deepClone(this._azureAccounts[selectedIndex]) + : undefined!; + await this._populateTentantsDropdown(); + })); + + const linkAccountButton = this._view.modelBuilder.hyperlink() + .withProps({ + label: constants.ACCOUNT_LINK_BUTTON_LABEL, + url: '', + CSSStyles: { ...styles.BODY_CSS }, + }).component(); + + this._disposables.push( + linkAccountButton.onDidClick(async (event) => { + await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount'); + await this._populateAzureAccountsDropdown(); + })); + + return this._view.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'column' }) + .withItems([ + azureAccountLabel, + this._azureAccountsDropdown, + linkAccountButton, + ]).component(); + } + + private _createAzureTenantContainer(): azdata.FlexContainer { + const azureTenantDropdownLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.AZURE_TENANT, + CSSStyles: { ...LABEL_CSS, ...TENANT_DROPDOWN_CSS }, + }).component(); + this._accountTenantDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.AZURE_TENANT, + width: INPUT_COMPONENT_WIDTH, + editable: true, + fireOnTextChange: true, + placeholder: constants.SELECT_A_TENANT, + }).component(); + this._disposables.push( + this._accountTenantDropdown.onValueChanged(async value => { + const selectedIndex = findDropDownItemIndex(this._accountTenantDropdown, value); + this._serviceContext.tenant = (selectedIndex > -1) + ? deepClone(this._accountTenants[selectedIndex]) + : undefined!; + await this._populateSubscriptionDropdown(); + })); + + this._accountTenantFlexContainer = this._view.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'column' }) + .withItems([ + azureTenantDropdownLabel, + this._accountTenantDropdown, + ]) + .withProps({ CSSStyles: { ...STYLE_HIDE, } }) + .component(); + return this._accountTenantFlexContainer; + } + + private _createServiceSelectionContainer(): azdata.FlexContainer { + const subscriptionDropdownLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.SUBSCRIPTION, + description: constants.TARGET_SUBSCRIPTION_INFO, + requiredIndicator: true, + CSSStyles: { ...LABEL_CSS } + }).component(); + this._azureSubscriptionDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.SUBSCRIPTION, + width: INPUT_COMPONENT_WIDTH, + editable: true, + required: true, + fireOnTextChange: true, + placeholder: constants.SELECT_A_SUBSCRIPTION, + CSSStyles: { ...DROPDOWN_CSS }, + }).component(); + this._disposables.push( + this._azureSubscriptionDropdown.onValueChanged(async (value) => { + const selectedIndex = findDropDownItemIndex(this._azureSubscriptionDropdown, value); + this._serviceContext.subscription = (selectedIndex > -1) + ? deepClone(this._subscriptions[selectedIndex]) + : undefined!; + await this._populateLocationDropdown(); + })); + + const azureLocationLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.LOCATION, + description: constants.TARGET_LOCATION_INFO, + requiredIndicator: true, + CSSStyles: { ...LABEL_CSS } + }).component(); + this._azureLocationDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.LOCATION, + width: INPUT_COMPONENT_WIDTH, + editable: true, + required: true, + fireOnTextChange: true, + placeholder: constants.SELECT_A_LOCATION, + CSSStyles: { ...DROPDOWN_CSS }, + }).component(); + this._disposables.push( + this._azureLocationDropdown.onValueChanged(async (value) => { + const selectedIndex = findDropDownItemIndex(this._azureLocationDropdown, value); + this._serviceContext.location = (selectedIndex > -1) + ? deepClone(this._locations[selectedIndex]) + : undefined!; + await this._populateResourceGroupDropdown(); + })); + + const azureResourceGroupLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.RESOURCE_GROUP, + description: constants.TARGET_RESOURCE_GROUP_INFO, + requiredIndicator: true, + CSSStyles: { ...LABEL_CSS } + }).component(); + this._azureResourceGroupDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.RESOURCE_GROUP, + width: INPUT_COMPONENT_WIDTH, + editable: true, + required: true, + fireOnTextChange: true, + placeholder: constants.SELECT_A_RESOURCE_GROUP, + CSSStyles: { ...DROPDOWN_CSS }, + }).component(); + this._disposables.push( + this._azureResourceGroupDropdown.onValueChanged(async (value) => { + const selectedIndex = findDropDownItemIndex(this._azureResourceGroupDropdown, value); + this._serviceContext.resourceGroup = (selectedIndex > -1) + ? deepClone(this._resourceGroups[selectedIndex]) + : undefined!; + await this._populateMigrationServiceDropdown(); + })); + + this._azureServiceDropdownLabel = this._view.modelBuilder.text() + .withProps({ + value: constants.MIGRATION_SERVICE_SELECT_SERVICE_LABEL, + description: constants.TARGET_RESOURCE_INFO, + requiredIndicator: true, + CSSStyles: { ...LABEL_CSS } + }).component(); + this._azureServiceDropdown = this._view.modelBuilder.dropDown() + .withProps({ + ariaLabel: constants.MIGRATION_SERVICE_SELECT_SERVICE_LABEL, + width: INPUT_COMPONENT_WIDTH, + editable: true, + required: true, + fireOnTextChange: true, + placeholder: constants.SELECT_A_SERVICE, + CSSStyles: { ...DROPDOWN_CSS }, + }).component(); + this._disposables.push( + this._azureServiceDropdown.onValueChanged(async (value) => { + const selectedIndex = findDropDownItemIndex(this._azureServiceDropdown, value, true); + this._serviceContext.migrationService = (selectedIndex > -1) + ? deepClone(this._sqlMigrationServices.find(service => service.name === value)) + : undefined!; + await this._updateButtonState(); + })); + + this._disposables.push( + this._dialog.okButton.onClick(async (value) => { + await MigrationLocalStorage.saveMigrationServiceContext(this._serviceContext); + await this._onClosedCallback(); + })); + + return this._view.modelBuilder.flexContainer() + .withItems([ + subscriptionDropdownLabel, + this._azureSubscriptionDropdown, + azureLocationLabel, + this._azureLocationDropdown, + azureResourceGroupLabel, + this._azureResourceGroupDropdown, + this._azureServiceDropdownLabel, + this._azureServiceDropdown, + ]).withLayout({ flexFlow: 'column' }) + .component(); + } + + private async _updateButtonState(): Promise { + this._dialog.okButton.enabled = this._serviceContext.migrationService !== undefined; + } + + private async _populateAzureAccountsDropdown(): Promise { + try { + this._azureAccountsDropdown.loading = true; + this._azureAccountsDropdown.values = await this._getAccountDropdownValues(); + if (this._azureAccountsDropdown.values.length > 0) { + selectDefaultDropdownValue( + this._azureAccountsDropdown, + this._serviceContext.azureAccount?.displayInfo?.userId, + false); + this._azureAccountsDropdown.loading = false; + } + } catch (error) { + logError(TelemetryViews.SelectMigrationServiceDialog, '_populateAzureAccountsDropdown', error); + void vscode.window.showErrorMessage( + constants.SELECT_ACCOUNT_ERROR, + error.message); + } finally { + this._azureAccountsDropdown.loading = false; + } + } + + private async _populateTentantsDropdown(): Promise { + try { + this._accountTenantDropdown.loading = true; + this._accountTenantDropdown.values = this._getTenantDropdownValues( + this._serviceContext.azureAccount); + await this._accountTenantFlexContainer.updateCssStyles( + this._accountTenants.length > 1 + ? STYLE_ShOW + : STYLE_HIDE); + if (this._accountTenantDropdown.values.length > 0) { + selectDefaultDropdownValue( + this._accountTenantDropdown, + this._serviceContext.tenant?.id, + false); + this._accountTenantDropdown.loading = false; + } + } catch (error) { + logError(TelemetryViews.SelectMigrationServiceDialog, '_populateTentantsDropdown', error); + void vscode.window.showErrorMessage( + constants.SELECT_TENANT_ERROR, + error.message); + } finally { + this._accountTenantDropdown.loading = false; + } + } + + private async _populateSubscriptionDropdown(): Promise { + try { + this._azureSubscriptionDropdown.loading = true; + this._azureSubscriptionDropdown.values = await this._getSubscriptionDropdownValues( + this._serviceContext.azureAccount); + if (this._azureSubscriptionDropdown.values.length > 0) { + selectDefaultDropdownValue( + this._azureSubscriptionDropdown, + this._serviceContext.subscription?.id, + false); + this._azureSubscriptionDropdown.loading = false; + } + } catch (error) { + logError(TelemetryViews.SelectMigrationServiceDialog, '_populateSubscriptionDropdown', error); + void vscode.window.showErrorMessage( + constants.SELECT_SUBSCRIPTION_ERROR, + error.message); + } finally { + this._azureSubscriptionDropdown.loading = false; + } + } + + private async _populateLocationDropdown(): Promise { + try { + this._azureLocationDropdown.loading = true; + this._azureLocationDropdown.values = await this._getAzureLocationDropdownValues( + this._serviceContext.azureAccount, + this._serviceContext.subscription); + if (this._azureLocationDropdown.values.length > 0) { + selectDefaultDropdownValue( + this._azureLocationDropdown, + this._serviceContext.location?.displayName, + true); + this._azureLocationDropdown.loading = false; + } + } catch (error) { + logError(TelemetryViews.SelectMigrationServiceDialog, '_populateLocationDropdown', error); + void vscode.window.showErrorMessage( + constants.SELECT_LOCATION_ERROR, + error.message); + } finally { + this._azureLocationDropdown.loading = false; + } + } + + private async _populateResourceGroupDropdown(): Promise { + try { + this._azureResourceGroupDropdown.loading = true; + this._azureResourceGroupDropdown.values = await this._getAzureResourceGroupDropdownValues( + this._serviceContext.location); + if (this._azureResourceGroupDropdown.values.length > 0) { + selectDefaultDropdownValue( + this._azureResourceGroupDropdown, + this._serviceContext.resourceGroup?.id, + false); + this._azureResourceGroupDropdown.loading = false; + } + } catch (error) { + logError(TelemetryViews.SelectMigrationServiceDialog, '_populateResourceGroupDropdown', error); + void vscode.window.showErrorMessage( + constants.SELECT_RESOURCE_GROUP_ERROR, + error.message); + } finally { + this._azureResourceGroupDropdown.loading = false; + } + } + + private async _populateMigrationServiceDropdown(): Promise { + try { + this._azureServiceDropdown.loading = true; + this._azureServiceDropdown.values = await this._getMigrationServiceDropdownValues( + this._serviceContext.azureAccount, + this._serviceContext.subscription, + this._serviceContext.location, + this._serviceContext.resourceGroup); + + if (this._azureServiceDropdown.values.length > 0) { + selectDefaultDropdownValue( + this._azureServiceDropdown, + this._serviceContext?.migrationService?.id, + false); + } + } catch (error) { + logError(TelemetryViews.SelectMigrationServiceDialog, '_populateMigrationServiceDropdown', error); + void vscode.window.showErrorMessage( + constants.SELECT_SERVICE_ERROR, + error.message); + } finally { + this._azureServiceDropdown.loading = false; + } + } + + private async _getAccountDropdownValues(): Promise { + this._azureAccounts = await azdata.accounts.getAllAccounts() || []; + return this._azureAccounts.map(account => { + return { + name: account.displayInfo.userId, + displayName: account.isStale + ? constants.ACCOUNT_CREDENTIALS_REFRESH(account.displayInfo.displayName) + : account.displayInfo.displayName, + }; + }); + } + + private async _getSubscriptionDropdownValues(account?: azdata.Account): Promise { + this._subscriptions = []; + if (account?.isStale === false) { + try { + this._subscriptions = await getSubscriptions(account); + this._subscriptions.sort((a, b) => a.name.localeCompare(b.name)); + } catch (error) { + logError(TelemetryViews.SelectMigrationServiceDialog, '_getSubscriptionDropdownValues', error); + void vscode.window.showErrorMessage( + constants.SELECT_SUBSCRIPTION_ERROR, + error.message); + } + } + + return this._subscriptions.map(subscription => { + return { + name: subscription.id, + displayName: `${subscription.name} - ${subscription.id}`, + }; + }); + } + + private _getTenantDropdownValues(account?: azdata.Account): azdata.CategoryValue[] { + this._accountTenants = account?.isStale === false + ? account?.properties?.tenants ?? [] + : []; + + return this._accountTenants.map(tenant => { + return { + name: tenant.id, + displayName: tenant.displayName, + }; + }); + } + + private async _getAzureLocationDropdownValues( + account?: azdata.Account, + subscription?: azureResource.AzureResourceSubscription): Promise { + let locations: azureResource.AzureLocation[] = []; + if (account && subscription) { + // get all available locations + locations = await getLocations(account, subscription); + this._sqlMigrationServices = await getSqlMigrationServices( + account, + subscription) || []; + this._sqlMigrationServices.sort((a, b) => a.name.localeCompare(b.name)); + } else { + this._sqlMigrationServices = []; + } + + // keep locaitons with services only + this._locations = locations.filter( + (loc, i) => this._sqlMigrationServices.some(service => service.location === loc.name)); + this._locations.sort((a, b) => a.name.localeCompare(b.name)); + return this._locations.map(loc => { + return { + name: loc.name, + displayName: loc.displayName, + }; + }); + } + + private async _getAzureResourceGroupDropdownValues(location?: azureResource.AzureLocation): Promise { + this._resourceGroups = location + ? this._getMigrationServicesResourceGroups(location) + : []; + this._resourceGroups.sort((a, b) => a.name.localeCompare(b.name)); + return this._resourceGroups.map(rg => { + return { + name: rg.id, + displayName: rg.name, + }; + }); + } + + private _getMigrationServicesResourceGroups(location?: azureResource.AzureLocation): azureResource.AzureResourceResourceGroup[] { + const resourceGroups = this._sqlMigrationServices + .filter(service => service.location === location?.name) + .map(service => service.properties.resourceGroup); + + return resourceGroups + .filter((rg, i, arr) => arr.indexOf(rg) === i) + .map(rg => { + return { + id: getFullResourceGroupFromId(rg), + name: rg, + }; + }); + } + + private async _getMigrationServiceDropdownValues( + account?: azdata.Account, + subscription?: azureResource.AzureResourceSubscription, + location?: azureResource.AzureLocation, + resourceGroup?: azureResource.AzureResourceResourceGroup): Promise { + + const locationName = location?.name?.toLowerCase(); + const resourceGroupName = resourceGroup?.name?.toLowerCase(); + + return this._sqlMigrationServices + .filter(service => + service.location?.toLowerCase() === locationName && + service.properties?.resourceGroup?.toLowerCase() === resourceGroupName) + .map(service => { + return ({ + name: service.id, + displayName: `${service.name}`, + }); + }); + } +} diff --git a/extensions/sql-migration/src/dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog.ts b/extensions/sql-migration/src/dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog.ts index 5975ad86f3..6860beffb6 100644 --- a/extensions/sql-migration/src/dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog.ts +++ b/extensions/sql-migration/src/dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog.ts @@ -5,10 +5,10 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; -import { getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, regenerateSqlMigrationServiceAuthKey } from '../../api/azure'; +import { DatabaseMigration, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, regenerateSqlMigrationServiceAuthKey } from '../../api/azure'; import { IconPathHelper } from '../../constants/iconPathHelper'; import * as constants from '../../constants/strings'; -import { MigrationContext } from '../../models/migrationLocalStorage'; +import { MigrationServiceContext } from '../../models/migrationLocalStorage'; import * as styles from '../../constants/styles'; const CONTROL_MARGIN = '10px'; @@ -28,7 +28,10 @@ export class SqlMigrationServiceDetailsDialog { private _migrationServiceAuthKeyTable!: azdata.DeclarativeTableComponent; private _disposables: vscode.Disposable[] = []; - constructor(private migrationContext: MigrationContext) { + constructor( + private _serviceContext: MigrationServiceContext, + private _migration: DatabaseMigration) { + this._dialog = azdata.window.createModelViewDialog( '', 'SqlMigrationServiceDetailsDialog', @@ -46,7 +49,8 @@ export class SqlMigrationServiceDetailsDialog { await this.createServiceContent( view, - this.migrationContext); + this._serviceContext, + this._migration); }); this._dialog.okButton.label = constants.SQL_MIGRATION_SERVICE_DETAILS_BUTTON_LABEL; @@ -55,33 +59,31 @@ export class SqlMigrationServiceDetailsDialog { azdata.window.openDialog(this._dialog); } - private async createServiceContent(view: azdata.ModelView, migrationContext: MigrationContext): Promise { + private async createServiceContent(view: azdata.ModelView, serviceContext: MigrationServiceContext, migration: DatabaseMigration): Promise { this._migrationServiceAuthKeyTable = this._createIrTable(view); const serviceNode = (await getSqlMigrationServiceMonitoringData( - migrationContext.azureAccount, - migrationContext.subscription, - migrationContext.controller.properties.resourceGroup, - migrationContext.controller.location, - migrationContext.controller.name, - this.migrationContext.sessionId! - )); + serviceContext.azureAccount!, + serviceContext.subscription!, + serviceContext.migrationService?.properties.resourceGroup!, + serviceContext.migrationService?.location!, + serviceContext.migrationService?.name!)); const serviceNodeName = serviceNode.nodes?.map(node => node.nodeName).join(', ') || constants.SQL_MIGRATION_SERVICE_DETAILS_STATUS_UNAVAILABLE; const flexContainer = view.modelBuilder .flexContainer() .withItems([ - this._createHeading(view, migrationContext), + this._createHeading(view, this._migration), view.modelBuilder .separator() .withProps({ width: STRETCH_WIDTH }) .component(), this._createTextItem(view, constants.SUBSCRIPTION, LABEL_MARGIN), - this._createTextItem(view, migrationContext.subscription.name, VALUE_MARGIN), + this._createTextItem(view, serviceContext.subscription?.name!, VALUE_MARGIN), this._createTextItem(view, constants.LOCATION, LABEL_MARGIN), - this._createTextItem(view, migrationContext.controller.location.toUpperCase(), VALUE_MARGIN), + this._createTextItem(view, serviceContext.migrationService?.location?.toUpperCase()!, VALUE_MARGIN), this._createTextItem(view, constants.RESOURCE_GROUP, LABEL_MARGIN), - this._createTextItem(view, migrationContext.controller.properties.resourceGroup, VALUE_MARGIN), + this._createTextItem(view, serviceContext.migrationService?.properties.resourceGroup!, VALUE_MARGIN), this._createTextItem(view, constants.SQL_MIGRATION_SERVICE_DETAILS_IR_LABEL, LABEL_MARGIN), this._createTextItem(view, serviceNodeName, VALUE_MARGIN), this._createTextItem( @@ -96,10 +98,10 @@ export class SqlMigrationServiceDetailsDialog { .component(); await view.initializeModel(flexContainer); - return await this._refreshAuthTable(view, migrationContext); + return await this._refreshAuthTable(view, serviceContext, migration); } - private _createHeading(view: azdata.ModelView, migrationContext: MigrationContext): azdata.FlexContainer { + private _createHeading(view: azdata.ModelView, migration: DatabaseMigration): azdata.FlexContainer { return view.modelBuilder .flexContainer() .withItems([ @@ -120,19 +122,15 @@ export class SqlMigrationServiceDetailsDialog { view.modelBuilder .text() .withProps({ - value: migrationContext.controller.name, - CSSStyles: { - ...styles.SECTION_HEADER_CSS - } + value: this._serviceContext.migrationService?.name, + CSSStyles: { ...styles.SECTION_HEADER_CSS } }) .component(), view.modelBuilder .text() .withProps({ value: constants.SQL_MIGRATION_SERVICE_DETAILS_SUB_TITLE, - CSSStyles: { - ...styles.SMALL_NOTE_CSS - } + CSSStyles: { ...styles.SMALL_NOTE_CSS } }) .component(), ]) @@ -197,15 +195,14 @@ export class SqlMigrationServiceDetailsDialog { }; } - private async _regenerateAuthKey(view: azdata.ModelView, migrationContext: MigrationContext, keyName: string): Promise { + private async _regenerateAuthKey(view: azdata.ModelView, serviceContext: MigrationServiceContext, migration: DatabaseMigration, keyName: string): Promise { const keys = await regenerateSqlMigrationServiceAuthKey( - migrationContext.azureAccount, - migrationContext.subscription, - migrationContext.controller.properties.resourceGroup, - migrationContext.controller.location.toUpperCase(), - migrationContext.controller.name, - keyName, - migrationContext.sessionId!); + serviceContext.azureAccount!, + serviceContext.subscription!, + serviceContext.migrationService?.properties.resourceGroup!, + serviceContext.migrationService?.properties.location?.toUpperCase()!, + serviceContext.migrationService?.name!, + keyName); if (keys?.authKey1 && keyName === AUTH_KEY1) { await this._updateTableCell(this._migrationServiceAuthKeyTable, 0, 1, keys.authKey1, constants.SERVICE_KEY1_LABEL); @@ -223,14 +220,13 @@ export class SqlMigrationServiceDetailsDialog { await vscode.window.showInformationMessage(constants.AUTH_KEY_REFRESHED(keyName)); } - private async _refreshAuthTable(view: azdata.ModelView, migrationContext: MigrationContext): Promise { + private async _refreshAuthTable(view: azdata.ModelView, serviceContext: MigrationServiceContext, migration: DatabaseMigration): Promise { const keys = await getSqlMigrationServiceAuthKeys( - migrationContext.azureAccount, - migrationContext.subscription, - migrationContext.controller.properties.resourceGroup, - migrationContext.controller.location.toUpperCase(), - migrationContext.controller.name, - migrationContext.sessionId!); + serviceContext.azureAccount!, + serviceContext.subscription!, + serviceContext.migrationService?.properties.resourceGroup!, + serviceContext.migrationService?.location.toUpperCase()!, + serviceContext.migrationService?.name!); const copyKey1Button = view.modelBuilder .button() @@ -275,7 +271,7 @@ export class SqlMigrationServiceDetailsDialog { }) .component(); this._disposables.push(refreshKey1Button.onDidClick( - async (e) => await this._regenerateAuthKey(view, migrationContext, AUTH_KEY1))); + async (e) => await this._regenerateAuthKey(view, serviceContext, migration, AUTH_KEY1))); const refreshKey2Button = view.modelBuilder .button() @@ -288,7 +284,7 @@ export class SqlMigrationServiceDetailsDialog { }) .component(); this._disposables.push(refreshKey2Button.onDidClick( - async (e) => await this._regenerateAuthKey(view, migrationContext, AUTH_KEY2))); + async (e) => await this._regenerateAuthKey(view, serviceContext, migration, AUTH_KEY2))); await this._migrationServiceAuthKeyTable.updateProperties({ dataValues: [ diff --git a/extensions/sql-migration/src/main.ts b/extensions/sql-migration/src/main.ts index ef4c18988f..b7475d5b18 100644 --- a/extensions/sql-migration/src/main.ts +++ b/extensions/sql-migration/src/main.ts @@ -32,47 +32,54 @@ class SQLMigration { 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; + 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(); + 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()}`); + 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.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); - }), + 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?.refreshMigrations()), ]; this.context.subscriptions.push(...commandDisposables); @@ -97,14 +104,20 @@ class SQLMigration { if (api) { this.stateModel = new MigrationStateModel(this.context, connectionId, api.sqlMigration); this.context.subscriptions.push(this.stateModel); - let savedInfo = this.checkSavedInfo(serverName); + const savedInfo = this.checkSavedInfo(serverName); if (savedInfo) { this.stateModel.savedInfo = savedInfo; this.stateModel.serverName = serverName; - let savedAssessmentDialog = new SavedAssessmentDialog(this.context, this.stateModel); + const savedAssessmentDialog = new SavedAssessmentDialog( + this.context, + this.stateModel, + async () => await widget?.onDialogClosed()); await savedAssessmentDialog.openDialog(); } else { - const wizardController = new WizardController(this.context, this.stateModel); + const wizardController = new WizardController( + this.context, + this.stateModel, + async () => await widget?.onDialogClosed()); await wizardController.openWizard(connectionId); } } @@ -131,10 +144,11 @@ class SQLMigration { } let sqlMigration: SQLMigration; +let widget: DashboardWidget; export async function activate(context: vscode.ExtensionContext) { sqlMigration = new SQLMigration(context); await sqlMigration.registerCommands(); - let widget = new DashboardWidget(context); + widget = new DashboardWidget(context); widget.register(); } diff --git a/extensions/sql-migration/src/models/migrationLocalStorage.ts b/extensions/sql-migration/src/models/migrationLocalStorage.ts index 69aece0025..90003501a8 100644 --- a/extensions/sql-migration/src/models/migrationLocalStorage.ts +++ b/extensions/sql-migration/src/models/migrationLocalStorage.ts @@ -2,11 +2,13 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; -import { azureResource } from 'azureResource'; -import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../telemtery'; -import { DatabaseMigration, SqlMigrationService, SqlManagedInstance, getMigrationStatus, AzureAsyncOperationResource, getMigrationAsyncOperationDetails, SqlVMServer, getSubscriptions } from '../api/azure'; import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import * as azurecore from 'azurecore'; +import { azureResource } from 'azureResource'; +import { DatabaseMigration, SqlMigrationService, getSubscriptions, getServiceMigrations } from '../api/azure'; +import { deepClone } from '../api/utils'; +import * as loc from '../constants/strings'; export class MigrationLocalStorage { private static context: vscode.ExtensionContext; @@ -16,161 +18,75 @@ export class MigrationLocalStorage { MigrationLocalStorage.context = context; } - public static async getMigrationsBySourceConnections(connectionProfile: azdata.connection.ConnectionProfile, refreshStatus?: boolean): Promise { - const undefinedSessionId = '{undefined}'; - const result: MigrationContext[] = []; - const validMigrations: MigrationContext[] = []; - const startTime = new Date().toString(); - // fetch saved migrations - const migrationMementos: MigrationContext[] = this.context.globalState.get(this.mementoToken) || []; - for (let i = 0; i < migrationMementos.length; i++) { - const migration = migrationMementos[i]; - migration.migrationContext = this.removeMigrationSecrets(migration.migrationContext); - migration.sessionId = migration.sessionId ?? undefinedSessionId; - if (migration.sourceConnectionProfile.serverName === connectionProfile.serverName) { - // refresh migration status - if (refreshStatus) { - try { - await this.refreshMigrationAzureAccount(migration); - - if (migration.asyncUrl) { - migration.asyncOperationResult = await getMigrationAsyncOperationDetails( - migration.azureAccount, - migration.subscription, - migration.asyncUrl, - migration.sessionId!); - } - - migration.migrationContext = await getMigrationStatus( - migration.azureAccount, - migration.subscription, - migration.migrationContext, - migration.sessionId!); - } - catch (e) { - // Keeping only valid migrations in cache. Clearing all the migrations which return ResourceDoesNotExit error. - switch (e.message) { - case 'ResourceDoesNotExist': - case 'NullMigrationId': - continue; - default: - logError(TelemetryViews.MigrationLocalStorage, 'MigrationBySourceConnectionError', e); - } - } - } - result.push(migration); - } - validMigrations.push(migration); + public static async getMigrationServiceContext(): Promise { + const connectionProfile = await azdata.connection.getCurrentConnection(); + if (connectionProfile) { + const serverContextKey = `${this.mementoToken}.${connectionProfile.serverName}.serviceContext`; + return deepClone(await this.context.globalState.get(serverContextKey)) || {}; } - - await this.context.globalState.update(this.mementoToken, validMigrations); - - sendSqlMigrationActionEvent( - TelemetryViews.MigrationLocalStorage, - TelemetryAction.Done, - { - 'startTime': startTime, - 'endTime': new Date().toString() - }, - { - 'migrationCount': migrationMementos.length - } - ); - - // only save updated migration context - if (refreshStatus) { - const migrations: MigrationContext[] = this.context.globalState.get(this.mementoToken) || []; - validMigrations.forEach(migration => { - const idx = migrations.findIndex(m => m.migrationContext.id === migration.migrationContext.id); - if (idx > -1) { - migrations[idx] = migration; - } - }); - - // check global state for migrations count mismatch, avoid saving - // state if the count has changed when a migration may have been added - const current: MigrationContext[] = this.context.globalState.get(this.mementoToken) || []; - if (current.length === migrations.length) { - await this.context.globalState.update(this.mementoToken, migrations); - } - } - return result; + return {}; } - public static async refreshMigrationAzureAccount(migration: MigrationContext): Promise { - if (migration.azureAccount.isStale) { + public static async saveMigrationServiceContext(serviceContext: MigrationServiceContext): 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)); + } + } + + public static async refreshMigrationAzureAccount(serviceContext: MigrationServiceContext, migration: DatabaseMigration): Promise { + if (serviceContext.azureAccount?.isStale) { const accounts = await azdata.accounts.getAllAccounts(); - const account = accounts.find(a => !a.isStale && a.key.accountId === migration.azureAccount.key.accountId); + const account = accounts.find(a => !a.isStale && a.key.accountId === serviceContext.azureAccount?.key.accountId); if (account) { const subscriptions = await getSubscriptions(account); - const subscription = subscriptions.find(s => s.id === migration.subscription.id); + const subscription = subscriptions.find(s => s.id === serviceContext.subscription?.id); if (subscription) { - migration.azureAccount = account; + serviceContext.azureAccount = account; + await this.saveMigrationServiceContext(serviceContext); } } } } - - public static async saveMigration( - connectionProfile: azdata.connection.ConnectionProfile, - migrationContext: DatabaseMigration, - targetMI: SqlManagedInstance | SqlVMServer, - azureAccount: azdata.Account, - subscription: azureResource.AzureResourceSubscription, - controller: SqlMigrationService, - asyncURL: string, - sessionId: string): Promise { - try { - let migrationMementos: MigrationContext[] = this.context.globalState.get(this.mementoToken) || []; - migrationMementos = migrationMementos.filter(m => m.migrationContext.id !== migrationContext.id); - migrationMementos.push({ - sourceConnectionProfile: connectionProfile, - migrationContext: this.removeMigrationSecrets(migrationContext), - targetManagedInstance: targetMI, - subscription: subscription, - azureAccount: azureAccount, - controller: controller, - asyncUrl: asyncURL, - sessionId: sessionId - }); - await this.context.globalState.update(this.mementoToken, migrationMementos); - } catch (e) { - logError(TelemetryViews.MigrationLocalStorage, 'CantSaveMigration', e); - } - } - - public static async clearMigrations(): Promise { - await this.context.globalState.update(this.mementoToken, ([] as MigrationContext[])); - } - - public static removeMigrationSecrets(migration: DatabaseMigration): DatabaseMigration { - // remove secrets from migration context - if (migration.properties.sourceSqlConnection?.password) { - migration.properties.sourceSqlConnection.password = ''; - } - if (migration.properties.backupConfiguration?.sourceLocation?.fileShare?.password) { - migration.properties.backupConfiguration.sourceLocation.fileShare.password = ''; - } - if (migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.accountKey) { - migration.properties.backupConfiguration.sourceLocation.azureBlob.accountKey = ''; - } - if (migration.properties.backupConfiguration?.targetLocation?.accountKey) { - migration.properties.backupConfiguration.targetLocation.accountKey = ''; - } - return migration; - } } -export interface MigrationContext { - sourceConnectionProfile: azdata.connection.ConnectionProfile, - migrationContext: DatabaseMigration, - targetManagedInstance: SqlManagedInstance | SqlVMServer, - azureAccount: azdata.Account, - subscription: azureResource.AzureResourceSubscription, - controller: SqlMigrationService, - asyncUrl: string, - asyncOperationResult?: AzureAsyncOperationResource, - sessionId?: string +export function isServiceContextValid(serviceContext: MigrationServiceContext): boolean { + return ( + serviceContext.azureAccount?.isStale === false && + serviceContext.location?.id !== undefined && + serviceContext.migrationService?.id !== undefined && + serviceContext.resourceGroup?.id !== undefined && + serviceContext.subscription?.id !== undefined && + serviceContext.tenant?.id !== undefined + ); +} + +export async function getSelectedServiceStatus(): Promise { + const serviceContext = await MigrationLocalStorage.getMigrationServiceContext(); + const serviceName = serviceContext?.migrationService?.name; + return serviceName && isServiceContextValid(serviceContext) + ? loc.MIGRATION_SERVICE_SERVICE_PROMPT(serviceName) + : loc.MIGRATION_SERVICE_SELECT_SERVICE_PROMPT; +} + +export async function getCurrentMigrations(): Promise { + const serviceContext = await MigrationLocalStorage.getMigrationServiceContext(); + return isServiceContextValid(serviceContext) + ? await getServiceMigrations( + serviceContext.azureAccount!, + serviceContext.subscription!, + serviceContext.migrationService?.id!) + : []; +} + +export interface MigrationServiceContext { + azureAccount?: azdata.Account, + tenant?: azurecore.Tenant, + subscription?: azureResource.AzureResourceSubscription, + location?: azureResource.AzureLocation, + resourceGroup?: azureResource.AzureResourceResourceGroup, + migrationService?: SqlMigrationService, } export enum MigrationStatus { diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index bb03fd8e69..2b1d837fea 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -8,9 +8,8 @@ import { azureResource } from 'azureResource'; import * as azurecore from 'azurecore'; import * as vscode from 'vscode'; import * as mssql from 'mssql'; -import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer, getLocations, getLocationDisplayName, getSqlManagedInstanceDatabases, getBlobs, sortResourceArrayByName, getFullResourceGroupFromId, getResourceGroupFromId, getResourceGroups } from '../api/azure'; +import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer, getLocations, getLocationDisplayName, getSqlManagedInstanceDatabases, getBlobs, sortResourceArrayByName, getFullResourceGroupFromId, getResourceGroupFromId, getResourceGroups, getSqlMigrationServicesByResourceGroup } from '../api/azure'; import * as constants from '../constants/strings'; -import { MigrationLocalStorage } from './migrationLocalStorage'; import * as nls from 'vscode-nls'; import { v4 as uuidv4 } from 'uuid'; import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery'; @@ -58,6 +57,12 @@ export enum NetworkContainerType { NETWORK_SHARE } +export enum FileStorageType { + FileShare = 'FileShare', + AzureBlob = 'AzureBlob', + None = 'None', +} + export enum Page { DatabaseSelector, SKURecommendation, @@ -826,8 +831,10 @@ export class MigrationStateModel implements Model, vscode.Disposable { } accountValues = this._azureAccounts.map((account): azdata.CategoryValue => { return { - displayName: account.displayInfo.displayName, - name: account.displayInfo.userId + name: account.displayInfo.userId, + displayName: account.isStale + ? constants.ACCOUNT_CREDENTIALS_REFRESH(account.displayInfo.displayName) + : account.displayInfo.displayName }; }); } catch (e) { @@ -871,7 +878,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { public async getSubscriptionsDropdownValues(): Promise { let subscriptionsValues: azdata.CategoryValue[] = []; try { - if (this._azureAccount) { + if (this._azureAccount?.isStale === false) { this._subscriptions = await getSubscriptions(this._azureAccount); } else { this._subscriptions = []; @@ -1471,7 +1478,12 @@ export class MigrationStateModel implements Model, vscode.Disposable { let sqlMigrationServiceValues: azdata.CategoryValue[] = []; try { if (this._azureAccount && subscription && resourceGroupName && this._targetServerInstance) { - this._sqlMigrationServices = (await getSqlMigrationServices(this._azureAccount, subscription)).filter(sms => sms.location.toLowerCase() === this._targetServerInstance.location.toLowerCase() && sms.properties.resourceGroup.toLowerCase() === resourceGroupName.toLowerCase()); + const services = await getSqlMigrationServicesByResourceGroup( + this._azureAccount, + subscription, + resourceGroupName?.toLowerCase()); + const targetLoc = this._targetServerInstance.location.toLowerCase(); + this._sqlMigrationServices = services.filter(sms => sms.location.toLowerCase() === targetLoc); } else { this._sqlMigrationServices = []; } @@ -1545,6 +1557,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { requestBody.properties.backupConfiguration = { targetLocation: undefined!, sourceLocation: { + fileStorageType: 'AzureBlob', azureBlob: { storageAccountResourceId: this._databaseBackup.blobs[i].storageAccount.id, accountKey: this._databaseBackup.blobs[i].storageKey, @@ -1567,6 +1580,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { accountKey: this._databaseBackup.networkShares[i].storageKey, }, sourceLocation: { + fileStorageType: 'FileShare', fileShare: { path: this._databaseBackup.networkShares[i].networkShareLocation, username: this._databaseBackup.networkShares[i].windowsUser, @@ -1584,8 +1598,8 @@ export class MigrationStateModel implements Model, vscode.Disposable { this._targetServerInstance, this._targetDatabaseNames[i], requestBody, - this._sessionId - ); + this._sessionId); + response.databaseMigration.properties.sourceDatabaseName = this._databasesForMigration[i]; response.databaseMigration.properties.backupConfiguration = requestBody.properties.backupConfiguration!; response.databaseMigration.properties.offlineConfiguration = requestBody.properties.offlineConfiguration!; @@ -1621,22 +1635,18 @@ export class MigrationStateModel implements Model, vscode.Disposable { } ); - await MigrationLocalStorage.saveMigration( - currentConnection!, - response.databaseMigration, - this._targetServerInstance, - this._azureAccount, - this._targetSubscription, - this._sqlMigrationService!, - response.asyncUrl, - this._sessionId - ); - void vscode.window.showInformationMessage(localize("sql.migration.starting.migration.message", 'Starting migration for database {0} to {1} - {2}', this._databasesForMigration[i], this._targetServerInstance.name, this._targetDatabaseNames[i])); + void vscode.window.showInformationMessage( + localize( + "sql.migration.starting.migration.message", + 'Starting migration for database {0} to {1} - {2}', + this._databasesForMigration[i], + this._targetServerInstance.name, + this._targetDatabaseNames[i])); } } catch (e) { void vscode.window.showErrorMessage( localize('sql.migration.starting.migration.error', "An error occurred while starting the migration: '{0}'", e.message)); - console.log(e); + logError(TelemetryViews.MigrationLocalStorage, 'StartMigrationFailed', e); } finally { // kill existing data collection if user start migration @@ -1718,7 +1728,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { } } - public loadSavedInfo(): Boolean { + public async loadSavedInfo(): Promise { try { this._targetType = this.savedInfo.migrationTargetType || undefined!; diff --git a/extensions/sql-migration/src/telemtery.ts b/extensions/sql-migration/src/telemtery.ts index e702dbefaf..7bd85d17a7 100644 --- a/extensions/sql-migration/src/telemtery.ts +++ b/extensions/sql-migration/src/telemtery.ts @@ -28,7 +28,8 @@ export enum TelemetryViews { SqlMigrationWizard = 'SqlMigrationWizard', MigrationLocalStorage = 'MigrationLocalStorage', SkuRecommendationWizard = 'SkuRecommendationWizard', - DataCollectionWizard = 'GetAzureRecommendationDialog' + DataCollectionWizard = 'GetAzureRecommendationDialog', + SelectMigrationServiceDialog = 'SelectMigrationServiceDialog', } export enum TelemetryAction { diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index 52598b84ab..9a4318ea3d 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -786,15 +786,27 @@ export class DatabaseBackupPage extends MigrationWizardPage { await this.switchNetworkContainerFields(this.migrationStateModel._databaseBackup.networkContainerType); const connectionProfile = await this.migrationStateModel.getSourceConnectionProfile(); - const queryProvider = azdata.dataprotocol.getProvider((await this.migrationStateModel.getSourceConnectionProfile()).providerId, azdata.DataProviderType.QueryProvider); + const queryProvider = azdata.dataprotocol.getProvider( + (await this.migrationStateModel.getSourceConnectionProfile()).providerId, + azdata.DataProviderType.QueryProvider); + const query = 'select SUSER_NAME()'; - const results = await queryProvider.runQueryAndReturn(await (azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId)), query); + 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 === 'SqlLogin' + ? MigrationSourceAuthenticationType.Sql + : connectionProfile.authenticationType === 'Integrated' + ? MigrationSourceAuthenticationType.Integrated + : undefined!; 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[0]?.windowsUser; + this._windowsUserAccountText.value = this.migrationStateModel.savedInfo?.networkShares + ? this.migrationStateModel.savedInfo?.networkShares[0]?.windowsUser + : ''; this._networkShareTargetDatabaseNames = []; this._networkShareLocations = []; @@ -809,7 +821,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { } let originalTargetDatabaseNames = this.migrationStateModel._targetDatabaseNames; - let originalNetworkShares = this.migrationStateModel._databaseBackup.networkShares; + let originalNetworkShares = this.migrationStateModel._databaseBackup.networkShares || []; let originalBlobs = this.migrationStateModel._databaseBackup.blobs; if (this.migrationStateModel._didUpdateDatabasesForMigration) { this.migrationStateModel._targetDatabaseNames = []; @@ -830,7 +842,10 @@ export class DatabaseBackupPage extends MigrationWizardPage { blob = originalBlobs[dbIndex] ?? blob; } else { // network share values are uniform for all dbs in the same migration, except for networkShareLocation - const previouslySelectedNetworkShare = originalNetworkShares[0]; + const previouslySelectedNetworkShare = originalNetworkShares.length > 0 + ? originalNetworkShares[0] + : ''; + if (previouslySelectedNetworkShare) { networkShare = { ...previouslySelectedNetworkShare, diff --git a/extensions/sql-migration/src/wizard/databaseSelectorPage.ts b/extensions/sql-migration/src/wizard/databaseSelectorPage.ts index 8570646e7f..20eb9f1602 100644 --- a/extensions/sql-migration/src/wizard/databaseSelectorPage.ts +++ b/extensions/sql-migration/src/wizard/databaseSelectorPage.ts @@ -8,43 +8,16 @@ import * as vscode from 'vscode'; import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../constants/strings'; -import { IconPath, IconPathHelper } from '../constants/iconPathHelper'; import { debounce } from '../api/utils'; import * as styles from '../constants/styles'; -import { selectDatabasesFromList } from '../constants/helper'; - -const styleLeft: azdata.CssStyles = { - 'border': 'none', - 'text-align': 'left', - 'white-space': 'nowrap', - 'text-overflow': 'ellipsis', - 'overflow': 'hidden', - 'box-shadow': '0px -1px 0px 0px rgba(243, 242, 241, 1) inset' -}; - -const styleCheckBox: azdata.CssStyles = { - 'border': 'none', - 'text-align': 'left', - 'white-space': 'nowrap', - 'text-overflow': 'ellipsis', - 'overflow': 'hidden', -}; - -const styleRight: azdata.CssStyles = { - 'border': 'none', - 'text-align': 'right', - 'white-space': 'nowrap', - 'text-overflow': 'ellipsis', - 'overflow': 'hidden', - 'box-shadow': '0px -1px 0px 0px rgba(243, 242, 241, 1) inset' -}; +import { IconPathHelper } from '../constants/iconPathHelper'; export class DatabaseSelectorPage extends MigrationWizardPage { private _view!: azdata.ModelView; - private _databaseSelectorTable!: azdata.DeclarativeTableComponent; + private _databaseSelectorTable!: azdata.TableComponent; private _dbNames!: string[]; private _dbCount!: azdata.TextComponent; - private _databaseTableValues!: azdata.DeclarativeTableCellValue[][]; + private _databaseTableValues!: any[]; private _disposables: vscode.Disposable[] = []; constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { @@ -115,7 +88,6 @@ export class DatabaseSelectorPage extends MigrationWizardPage { protected async handleStateChange(e: StateChangeEvent): Promise { } - private createSearchComponent(): azdata.DivContainer { let resourceSearchBox = this._view.modelBuilder.inputBox().withProps({ stopEnterPropagation: true, @@ -137,74 +109,36 @@ export class DatabaseSelectorPage extends MigrationWizardPage { } @debounce(500) - private _filterTableList(value: string): void { + private async _filterTableList(value: string, selectedList?: string[]): Promise { + const selectedRows: number[] = []; + const selectedDatabases = selectedList || this.selectedDbs(); + let tableRows = this._databaseTableValues; if (this._databaseTableValues && value?.length > 0) { - const filter: number[] = []; - this._databaseTableValues.forEach((row, index) => { - // undo when bug #16445 is fixed - // const flexContainer: azdata.FlexContainer = row[1]?.value as azdata.FlexContainer; - // const textComponent: azdata.TextComponent = flexContainer?.items[1] as azdata.TextComponent; - // const cellText = textComponent?.value?.toLowerCase(); - const text = row[1]?.value as string; - const cellText = text?.toLowerCase(); - const searchText: string = value?.toLowerCase(); - if (cellText?.includes(searchText)) { - filter.push(index); - } - }); - - this._databaseSelectorTable.setFilter(filter); - } else { - this._databaseSelectorTable.setFilter(undefined); + tableRows = this._databaseTableValues + .filter(row => { + const searchText = value?.toLowerCase(); + return row[2]?.toLowerCase()?.indexOf(searchText) > -1 // database name + || row[3]?.toLowerCase()?.indexOf(searchText) > -1 // state + || row[4]?.toLowerCase()?.indexOf(searchText) > -1 // size + || row[5]?.toLowerCase()?.indexOf(searchText) > -1; // last backup date + }); } + + for (let row = 0; row < tableRows.length; row++) { + const database: string = tableRows[row][2]; + if (selectedDatabases.includes(database)) { + selectedRows.push(row); + } + } + + await this._databaseSelectorTable.updateProperty('data', tableRows); + this._databaseSelectorTable.selectedRows = selectedRows; + await this.updateValuesOnSelection(); } public async createRootContainer(view: azdata.ModelView): Promise { - const providerId = (await this.migrationStateModel.getSourceConnectionProfile()).providerId; - const metaDataService = azdata.dataprotocol.getProvider(providerId, azdata.DataProviderType.MetadataProvider); - const ownerUri = await azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId); - const results = await metaDataService.getDatabases(ownerUri); - const excludeDbs: string[] = [ - 'master', - 'tempdb', - 'msdb', - 'model' - ]; - this._dbNames = []; - let finalResult = results.filter((db) => !excludeDbs.includes(db.options.name)); - finalResult.sort((a, b) => a.options.name.localeCompare(b.options.name)); - this._databaseTableValues = []; - for (let index in finalResult) { - let selectable = true; - if (constants.OFFLINE_CAPS.includes(finalResult[index].options.state)) { - selectable = false; - } - this._databaseTableValues.push([ - { - value: false, - style: styleCheckBox, - enabled: selectable - }, - { - value: this.createIconTextCell(IconPathHelper.sqlDatabaseLogo, finalResult[index].options.name), - style: styleLeft - }, - { - value: `${finalResult[index].options.state}`, - style: styleLeft - }, - { - value: `${finalResult[index].options.sizeInMB}`, - style: styleRight - }, - { - value: `${finalResult[index].options.lastBackup}`, - style: styleLeft - } - ]); - this._dbNames.push(finalResult[index].options.name); - } + await this._loadDatabaseList(this.migrationStateModel, this.migrationStateModel._assessedDatabaseList); const text = this._view.modelBuilder.text().withProps({ value: constants.DATABASE_FOR_ASSESSMENT_DESCRIPTION, @@ -214,70 +148,83 @@ export class DatabaseSelectorPage extends MigrationWizardPage { }).component(); this._dbCount = this._view.modelBuilder.text().withProps({ - value: constants.DATABASES_SELECTED(this.selectedDbs.length, this._databaseTableValues.length), + value: constants.DATABASES_SELECTED( + this.selectedDbs().length, + this._databaseTableValues.length), CSSStyles: { ...styles.BODY_CSS, 'margin-top': '8px' } }).component(); - this._databaseSelectorTable = this._view.modelBuilder.declarativeTable().withProps( - { - enableRowSelection: true, - width: '100%', - CSSStyles: { - 'border': 'none' - }, + const cssClass = 'no-borders'; + this._databaseSelectorTable = this._view.modelBuilder.table() + .withProps({ + data: [], + width: 650, + height: '100%', + forceFitColumns: azdata.ColumnSizingMode.ForceFit, columns: [ - { - displayName: '', - valueType: azdata.DeclarativeDataType.boolean, - width: 20, - isReadOnly: false, - showCheckAll: true, - headerCssStyles: styleCheckBox + { + value: '', + width: 10, + type: azdata.ColumnType.checkBox, + action: azdata.ActionOnCellCheckboxCheck.selectRow, + resizable: false, + cssClass: cssClass, + headerCssClass: cssClass, }, { - displayName: constants.DATABASE, - // undo when bug #16445 is fixed - // valueType: azdata.DeclarativeDataType.component, - valueType: azdata.DeclarativeDataType.string, - width: '100%', - isReadOnly: true, - headerCssStyles: styleLeft + value: 'databaseicon', + name: '', + width: 10, + type: azdata.ColumnType.icon, + headerCssClass: cssClass, + cssClass: cssClass, + resizable: false, }, { - displayName: constants.STATUS, - valueType: azdata.DeclarativeDataType.string, - width: 100, - isReadOnly: true, - headerCssStyles: styleLeft + name: constants.DATABASE, + value: 'database', + type: azdata.ColumnType.text, + width: 360, + cssClass: cssClass, + headerCssClass: cssClass, }, { - displayName: constants.SIZE, - valueType: azdata.DeclarativeDataType.string, - width: 125, - isReadOnly: true, - headerCssStyles: styleRight + name: constants.STATUS, + value: 'status', + type: azdata.ColumnType.text, + width: 80, + cssClass: cssClass, + headerCssClass: cssClass, }, { - displayName: constants.LAST_BACKUP, - valueType: azdata.DeclarativeDataType.string, - width: 150, - isReadOnly: true, - headerCssStyles: styleLeft - } + name: constants.SIZE, + value: 'size', + type: azdata.ColumnType.text, + width: 80, + cssClass: cssClass, + headerCssClass: cssClass, + }, + { + name: constants.LAST_BACKUP, + value: 'lastBackup', + type: azdata.ColumnType.text, + width: 130, + cssClass: cssClass, + headerCssClass: cssClass, + }, ] - } - ).component(); + }).component(); - this._databaseTableValues = selectDatabasesFromList(this.migrationStateModel._databasesForAssessment, this._databaseTableValues); - await this._databaseSelectorTable.setDataValues(this._databaseTableValues); - await this.updateValuesOnSelection(); - - this._disposables.push(this._databaseSelectorTable.onDataChanged(async () => { + 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); + const flex = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', height: '100%', @@ -293,65 +240,60 @@ export class DatabaseSelectorPage extends MigrationWizardPage { return flex; } + private async _loadDatabaseList(stateMachine: MigrationStateModel, selectedDatabases: string[]): Promise { + const providerId = (await stateMachine.getSourceConnectionProfile()).providerId; + const metaDataService = azdata.dataprotocol.getProvider( + providerId, + azdata.DataProviderType.MetadataProvider); + const ownerUri = await azdata.connection.getUriForConnection( + stateMachine.sourceConnectionId); + const excludeDbs: string[] = [ + 'master', + 'tempdb', + 'msdb', + 'model' + ]; + const databaseList = (await metaDataService + .getDatabases(ownerUri)) + .filter(database => !excludeDbs.includes(database.options.name)) + || []; + + databaseList.sort((a, b) => a.options.name.localeCompare(b.options.name)); + this._dbNames = []; + + this._databaseTableValues = databaseList.map(database => { + const databaseName = database.options.name; + this._dbNames.push(databaseName); + return [ + selectedDatabases?.indexOf(databaseName) > -1, + { + icon: IconPathHelper.sqlDatabaseLogo, + title: databaseName, + }, + databaseName, + database.options.state, + database.options.sizeInMB, + database.options.lastBackup, + ]; + }) || []; + } + public selectedDbs(): string[] { - let result: string[] = []; - this._databaseSelectorTable?.dataValues?.forEach((arr, index) => { - if (arr[0].value === true) { - result.push(this._dbNames[index]); - } - }); - return result; + const rows = this._databaseSelectorTable?.data || []; + const databases = this._databaseSelectorTable?.selectedRows || []; + return databases + .filter(row => row < rows.length) + .map(row => rows[row][2]) + || []; } private async updateValuesOnSelection() { + const selectedDatabases = this.selectedDbs() || []; await this._dbCount.updateProperties({ - 'value': constants.DATABASES_SELECTED(this.selectedDbs().length, this._databaseTableValues.length) + 'value': constants.DATABASES_SELECTED( + selectedDatabases.length, + this._databaseSelectorTable.data?.length || 0) }); - this.migrationStateModel._databasesForAssessment = this.selectedDbs(); + this.migrationStateModel._databasesForAssessment = selectedDatabases; } - - // 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': '110px' - // } - // }).component(); - - // cellContainer.addItem(textComponent, { - // CSSStyles: { - // 'width': 'auto' - // } - // }); - - // return cellContainer; - // } - // undo when bug #16445 is fixed - } diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts index c91c5effe7..822f2c5872 100644 --- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts +++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts @@ -44,27 +44,21 @@ export class IntergrationRuntimePage extends MigrationWizardPage { protected async registerContent(view: azdata.ModelView): Promise { this._view = view; - this._statusLoadingComponent = view.modelBuilder.loadingComponent().withItem(this.createDMSDetailsContainer()).component(); + this._statusLoadingComponent = view.modelBuilder.loadingComponent() + .withItem(this.createDMSDetailsContainer()) + .component(); - this._dmsInfoContainer = this._view.modelBuilder.flexContainer().withItems([ - this._statusLoadingComponent - ]).component(); + this._dmsInfoContainer = this._view.modelBuilder.flexContainer() + .withItems([this._statusLoadingComponent]) + .component(); const form = view.modelBuilder.formContainer() - .withFormItems( - [ - { - component: this.migrationServiceDropdownContainer() - }, - { - component: this._dmsInfoContainer - } - ] - ).withProps({ - CSSStyles: { - 'padding-top': '0' - } - }).component(); + .withFormItems([ + { component: this.migrationServiceDropdownContainer() }, + { component: this._dmsInfoContainer } + ]) + .withProps({ CSSStyles: { 'padding-top': '0' } }) + .component(); this._disposables.push(this._view.onClosed(e => { this._disposables.forEach( @@ -419,29 +413,26 @@ export class IntergrationRuntimePage extends MigrationWizardPage { this.migrationStateModel._targetSubscription, this.migrationStateModel._sqlMigrationService.properties.resourceGroup, this.migrationStateModel._sqlMigrationService.location, - this.migrationStateModel._sqlMigrationService.name, - this.migrationStateModel._sessionId); + this.migrationStateModel._sqlMigrationService.name); this.migrationStateModel._sqlMigrationService = migrationService; const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData( this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription, this.migrationStateModel._sqlMigrationService.properties.resourceGroup, this.migrationStateModel._sqlMigrationService.location, - this.migrationStateModel._sqlMigrationService!.name, - this.migrationStateModel._sessionId); - this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map(node => node.nodeName); + this.migrationStateModel._sqlMigrationService!.name); + this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map( + node => node.nodeName); + const migrationServiceAuthKeys = await getSqlMigrationServiceAuthKeys( this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription, this.migrationStateModel._sqlMigrationService.properties.resourceGroup, this.migrationStateModel._sqlMigrationService.location, - this.migrationStateModel._sqlMigrationService!.name, - this.migrationStateModel._sessionId - ); + this.migrationStateModel._sqlMigrationService!.name); - this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map((node) => { - return node.nodeName; - }); + this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map( + node => node.nodeName); const state = migrationService.properties.integrationRuntimeState; if (state === 'Online') { @@ -458,25 +449,21 @@ export class IntergrationRuntimePage extends MigrationWizardPage { const data = [ [ + { value: constants.SERVICE_KEY1_LABEL }, + { value: migrationServiceAuthKeys.authKey1 }, { - value: constants.SERVICE_KEY1_LABEL - }, - { - value: migrationServiceAuthKeys.authKey1 - }, - { - value: this._view.modelBuilder.flexContainer().withItems([this._copy1, this._refresh1]).component() + value: this._view.modelBuilder.flexContainer() + .withItems([this._copy1, this._refresh1]) + .component() } ], [ + { value: constants.SERVICE_KEY2_LABEL }, + { value: migrationServiceAuthKeys.authKey2 }, { - value: constants.SERVICE_KEY2_LABEL - }, - { - value: migrationServiceAuthKeys.authKey2 - }, - { - value: this._view.modelBuilder.flexContainer().withItems([this._copy2, this._refresh2]).component() + value: this._view.modelBuilder.flexContainer() + .withItems([this._copy2, this._refresh2]) + .component() } ] ]; diff --git a/extensions/sql-migration/src/wizard/targetSelectionPage.ts b/extensions/sql-migration/src/wizard/targetSelectionPage.ts index 8c61089976..f5a73a59a9 100644 --- a/extensions/sql-migration/src/wizard/targetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/targetSelectionPage.ts @@ -186,7 +186,8 @@ export class TargetSelectionPage extends MigrationWizardPage { const selectedAzureAccount = this.migrationStateModel.getAccount(selectedIndex); // Making a clone of the account object to preserve the original tenants this.migrationStateModel._azureAccount = deepClone(selectedAzureAccount); - if (this.migrationStateModel._azureAccount.properties.tenants.length > 1) { + if (selectedAzureAccount.isStale === false && + this.migrationStateModel._azureAccount.properties.tenants.length > 1) { this.migrationStateModel._accountTenants = selectedAzureAccount.properties.tenants; this._accountTenantDropdown.values = await this.migrationStateModel.getTenantValues(); selectDropDownIndex(this._accountTenantDropdown, 0); diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index 8b733bc6ba..a251a18f5a 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -17,14 +17,17 @@ import { MigrationModePage } from './migrationModePage'; import { DatabaseSelectorPage } from './databaseSelectorPage'; import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError } from '../telemtery'; import * as styles from '../constants/styles'; +import { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage'; +import { azureResource } from 'azureResource'; export const WIZARD_INPUT_COMPONENT_WIDTH = '600px'; export class WizardController { private _wizardObject!: azdata.window.Wizard; - private _model!: MigrationStateModel; private _disposables: vscode.Disposable[] = []; - constructor(private readonly extensionContext: vscode.ExtensionContext, model: MigrationStateModel) { - this._model = model; + constructor( + private readonly extensionContext: vscode.ExtensionContext, + private readonly _model: MigrationStateModel, + private readonly _onClosedCallback: () => Promise) { } public async openWizard(connectionId: string): Promise { @@ -105,13 +108,12 @@ 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._model.extensionContext.subscriptions.push( + this._wizardObject.onPageChanged( + async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => { + await pages[0].onPageEnter(pageChangeInfo); + })); - this._model.extensionContext.subscriptions.push(this._wizardObject.doneButton.onClick(async (e) => { - await stateModel.startMigration(); - })); this._disposables.push(saveAndCloseButton.onClick(async () => { await stateModel.saveInfo(serverName, this._wizardObject.currentPage); await this._wizardObject.close(); @@ -134,16 +136,65 @@ export class WizardController { this._wizardObject.doneButton.label = loc.START_MIGRATION_TEXT; - this._disposables.push(this._wizardObject.doneButton.onClick(e => { - sendSqlMigrationActionEvent( - TelemetryViews.SqlMigrationWizard, - TelemetryAction.PageButtonClick, - { - ...this.getTelemetryProps(), - 'buttonPressed': TelemetryAction.Done, - 'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title - }, {}); - })); + this._disposables.push( + this._wizardObject.doneButton.onClick(async (e) => { + await stateModel.startMigration(); + await this.updateServiceContext(stateModel); + await this._onClosedCallback(); + + sendSqlMigrationActionEvent( + TelemetryViews.SqlMigrationWizard, + TelemetryAction.PageButtonClick, + { + ...this.getTelemetryProps(), + 'buttonPressed': TelemetryAction.Done, + 'pageTitle': this._wizardObject.pages[this._wizardObject.currentPage].title + }, {}); + })); + } + + private async updateServiceContext(stateModel: MigrationStateModel): Promise { + const resourceGroup = this._getResourceGroupByName( + stateModel._resourceGroups, + stateModel._sqlMigrationService?.properties.resourceGroup); + + const subscription = this._getSubscriptionFromResourceId( + stateModel._subscriptions, + resourceGroup?.id); + + const location = this._getLocationByValue( + stateModel._locations, + stateModel._sqlMigrationService?.location); + + return await MigrationLocalStorage.saveMigrationServiceContext( + { + azureAccount: stateModel._azureAccount, + tenant: stateModel._azureTenant, + subscription: subscription, + location: location, + resourceGroup: resourceGroup, + migrationService: stateModel._sqlMigrationService, + }); + } + + 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 { + return locations.find(loc => loc.name === name); + } + + 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/'); + if (parts?.length && parts?.length > 0) { + const subscriptionId: string = parts[0]; + return subscriptions.find(sub => sub.id === subscriptionId, 1); + } + } + return undefined; } private async sendPageButtonClickEvent(pageChangeInfo: azdata.window.WizardPageChangeInfo) {