diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index 6dcf768184..3155959b16 100644 --- a/extensions/sql-migration/package.json +++ b/extensions/sql-migration/package.json @@ -88,6 +88,11 @@ "title": "%cancel-migration-menu%", "category": "%migration-context-menu-category%" }, + { + "command": "sqlmigration.delete.migration", + "title": "%delete-migration-menu%", + "category": "%migration-context-menu-category%" + }, { "command": "sqlmigration.retry.migration", "title": "%retry-migration-menu%", @@ -128,6 +133,10 @@ "command": "sqlmigration.cancel.migration", "when": "false" }, + { + "command": "sqlmigration.delete.migration", + "when": "false" + }, { "command": "sqlmigration.retry.migration", "when": "false" diff --git a/extensions/sql-migration/package.nls.json b/extensions/sql-migration/package.nls.json index be46808da0..2e123f420d 100644 --- a/extensions/sql-migration/package.nls.json +++ b/extensions/sql-migration/package.nls.json @@ -16,5 +16,6 @@ "view-service-menu": "Database Migration Service details", "copy-migration-menu": "Copy migration details", "cancel-migration-menu": "Cancel migration", + "delete-migration-menu": "Delete migration", "retry-migration-menu": "Retry migration" } diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index 5e663cc9af..e306440132 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -286,7 +286,11 @@ export async function getVMInstanceView(sqlVm: SqlVMServer, account: azdata.Acco const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host); if (response.errors.length > 0) { - throw new Error(response.errors.toString()); + const message = response.errors + .map(err => err.message) + .join(', '); + throw new Error(message); + } return response.response.data; @@ -299,7 +303,11 @@ export async function getAzureResourceGivenId(account: azdata.Account, subscript const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host); if (response.errors.length > 0) { - throw new Error(response.errors.toString()); + const message = response.errors + .map(err => err.message) + .join(', '); + throw new Error(message); + } return response.response.data; @@ -620,6 +628,19 @@ export async function stopMigration(account: azdata.Account, subscription: Subsc } } +export async function deleteMigration(account: azdata.Account, subscription: Subscription, migrationId: string): Promise { + const api = await getAzureCoreAPI(); + const path = encodeURI(`${migrationId}?api-version=${DMSV2_API_VERSION}`); + const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint; + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.DELETE, undefined, true, host); + if (response.errors.length > 0) { + const message = response.errors + .map(err => err.message) + .join(', '); + throw new Error(message); + } +} + export async function getLocationDisplayName(location: string): Promise { const api = await getAzureCoreAPI(); return api.getRegionDisplayName(location); diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts index c03e55d55a..46ebd992f8 100644 --- a/extensions/sql-migration/src/api/utils.ts +++ b/extensions/sql-migration/src/api/utils.ts @@ -26,6 +26,7 @@ export const MenuCommands = { ViewService: 'sqlmigration.view.service', CopyMigration: 'sqlmigration.copy.migration', CancelMigration: 'sqlmigration.cancel.migration', + DeleteMigration: 'sqlmigration.delete.migration', RetryMigration: 'sqlmigration.retry.migration', StartMigration: 'sqlmigration.start', StartLoginMigration: 'sqlmigration.login.start', @@ -423,7 +424,7 @@ export async function getAzureAccountsDropdownValues(accounts: Account[]): Promi accounts.forEach((account) => { accountsValues.push({ name: account.displayInfo.userId, - displayName: account.isStale + displayName: isAccountTokenStale(account) ? constants.ACCOUNT_CREDENTIALS_REFRESH(account.displayInfo.displayName) : account.displayInfo.displayName }); @@ -439,6 +440,10 @@ export async function getAzureAccountsDropdownValues(accounts: Account[]): Promi return accountsValues; } +export function isAccountTokenStale(account: Account | undefined): boolean { + return account === undefined || account?.isStale === true; +} + export function getAzureTenants(account?: Account): Tenant[] { return account?.properties.tenants || []; } @@ -446,9 +451,9 @@ export function getAzureTenants(account?: Account): Tenant[] { export async function getAzureSubscriptions(account?: Account): Promise { let subscriptions: azureResource.AzureResourceSubscription[] = []; try { - if (account) { - subscriptions = !account.isStale ? await azure.getSubscriptions(account) : []; - } + subscriptions = account && !isAccountTokenStale(account) + ? await azure.getSubscriptions(account) + : []; } catch (e) { logError(TelemetryViews.Utils, 'utils.getAzureSubscriptions', e); } diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 0c3672341f..05c79df924 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -991,12 +991,15 @@ export const ALL_BACKUPS_RESTORED = localize('sql.migration.all.backups.restored export const ACTIVE_BACKUP_FILES = localize('sql.migration.active.backup.files', "Active backup files"); export const MIGRATION_STATUS_REFRESH_ERROR = localize('sql.migration.cutover.status.refresh.error', 'An error occurred while refreshing the migration status.'); export const MIGRATION_CANCELLATION_ERROR = localize('sql.migration.cancel.error', 'An error occurred while canceling the migration.'); +export const MIGRATION_DELETE_ERROR = localize('sql.migration.delete.error', 'An error occurred while deleting the migration.'); + export const STATUS = localize('sql.migration.status', "Status"); export const BACKUP_START_TIME = localize('sql.migration.backup.start.time', "Backup start time"); export const FIRST_LSN = localize('sql.migration.first.lsn', "First LSN"); export const LAST_LSN = localize('sql.migration.last.LSN', "Last LSN"); export const CANNOT_START_CUTOVER_ERROR = localize('sql.migration.cannot.start.cutover.error', "The cutover process cannot start until all the migrations are done. To return the latest file status, refresh your browser window."); export const CANCEL_MIGRATION = localize('sql.migration.cancel.migration', "Cancel migration"); +export const DELETE_MIGRATION = localize('sql.migration.delete.migration', "Delete migration"); export function ACTIVE_BACKUP_FILES_ITEMS(fileCount: number) { if (fileCount === 1) { return localize('sql.migration.active.backup.files.items', "Active backup files (1 item)"); @@ -1007,6 +1010,8 @@ export function ACTIVE_BACKUP_FILES_ITEMS(fileCount: number) { export const COPY_MIGRATION_DETAILS = localize('sql.migration.copy.migration.details', "Copy migration details"); export const DETAILS_COPIED = localize('sql.migration.details.copied', "Details copied"); export const CANCEL_MIGRATION_CONFIRMATION = localize('sql.cancel.migration.confirmation', "Are you sure you want to cancel this migration?"); +export const DELETE_MIGRATION_CONFIRMATION = localize('sql.delete.migration.confirmation', "Are you sure you want to delete this migration?"); + export const YES = localize('sql.migration.yes', "Yes"); export const NO = localize('sql.migration.no', "No"); export const NA = localize('sql.migration.na', "N/A"); @@ -1047,6 +1052,7 @@ export function CUTOVER_IN_PROGRESS(dbName: string): string { return localize('sql.migration.cutover.in.progress', "Cutover in progress for database '{0}'", dbName); } export const MIGRATION_CANNOT_CANCEL = localize('sql.migration.cannot.cancel', 'Migration is not in progress and cannot be canceled.'); +export const MIGRATION_CANNOT_DELETE = localize('sql.migration.cannot.delete', 'Migration is currently in progress and cannot be deleted.'); export const MIGRATION_CANNOT_CUTOVER = localize('sql.migration.cannot.cutover', 'Migration is not in progress and cannot be cutover.'); export const FILE_NAME = localize('sql.migration.file.name', "File name"); export const SIZE_COLUMN_HEADER = localize('sql.migration.size.column.header', "Size"); diff --git a/extensions/sql-migration/src/dashboard/migrationDetailsBlobContainerTab.ts b/extensions/sql-migration/src/dashboard/migrationDetailsBlobContainerTab.ts index 864d1a8a86..b019a7d92a 100644 --- a/extensions/sql-migration/src/dashboard/migrationDetailsBlobContainerTab.ts +++ b/extensions/sql-migration/src/dashboard/migrationDetailsBlobContainerTab.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import * as loc from '../constants/strings'; import { getSqlServerName, getMigrationStatusImage } from '../api/utils'; import { logError, TelemetryViews } from '../telemetry'; -import { canCancelMigration, canCutoverMigration, canRetryMigration, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper'; +import { canCancelMigration, canCutoverMigration, canDeleteMigration, canRetryMigration, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation } from '../constants/helper'; import { getResourceName } from '../api/azure'; import { InfoFieldSchema, infoFieldWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase'; import { EmptySettingValue } from './tabBase'; @@ -37,7 +37,7 @@ export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase Promise, + openMigrationsListFcn: (refresh?: boolean) => Promise, statusBar: DashboardStatusBar, ): Promise { @@ -115,6 +115,7 @@ export class MigrationDetailsBlobContainerTab extends MigrationDetailsTabBase Promise, + openMigrationsListFcn: (refresh?: boolean) => Promise, statusBar: DashboardStatusBar): Promise { this.view = view; @@ -182,6 +182,7 @@ export class MigrationDetailsFileShareTab extends MigrationDetailsTabBase extends TabBase { protected model!: MigrationCutoverDialogModel; protected databaseLabel!: azdata.TextComponent; protected serviceContext!: MigrationServiceContext; - protected openMigrationsListFcn!: () => Promise; + protected openMigrationsListFcn!: (refresh?: boolean) => Promise; protected cutoverButton!: azdata.ButtonComponent; protected refreshButton!: azdata.ButtonComponent; protected cancelButton!: azdata.ButtonComponent; + protected deleteButton!: azdata.ButtonComponent; protected refreshLoader!: azdata.LoadingComponent; protected copyDatabaseMigrationDetails!: azdata.ButtonComponent; protected newSupportRequest!: azdata.ButtonComponent; @@ -51,7 +55,7 @@ export abstract class MigrationDetailsTabBase extends TabBase { public abstract create( context: vscode.ExtensionContext, view: azdata.ModelView, - openMigrationsListFcn: () => Promise, + openMigrationsListFcn: (refresh?: boolean) => Promise, statusBar: DashboardStatusBar): Promise; protected abstract migrationInfoGrid(): Promise; @@ -186,6 +190,46 @@ export abstract class MigrationDetailsTabBase extends TabBase { }); })); + this.deleteButton = this.view.modelBuilder.button() + .withProps({ + iconPath: IconPathHelper.discard, + iconHeight: '16px', + iconWidth: '16px', + label: loc.DELETE_MIGRATION, + height: buttonHeight, + enabled: false, + }).component(); + + this.disposables.push( + this.deleteButton.onDidClick( + async (e) => { + await this.statusBar.clearError(); + try { + if (canDeleteMigration(this.model.migration)) { + const response = await vscode.window.showInformationMessage( + loc.DELETE_MIGRATION_CONFIRMATION, + { modal: true }, + loc.YES, + loc.NO); + if (response === loc.YES) { + await deleteMigration( + this.serviceContext.azureAccount!, + this.serviceContext.subscription!, + this.model.migration.id); + await this.openMigrationsListFcn(true); + } + } else { + await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_DELETE); + logError(TelemetryViews.MigrationDetailsTab, MenuCommands.DeleteMigration, "cannot delete migration"); + } + } catch (e) { + await this.statusBar.showError( + loc.MIGRATION_DELETE_ERROR, + loc.MIGRATION_DELETE_ERROR, + e.message); + logError(TelemetryViews.MigrationDetailsTab, MenuCommands.DeleteMigration, e); + } + })); this.retryButton = this.view.modelBuilder.button() .withProps({ @@ -266,6 +310,7 @@ export abstract class MigrationDetailsTabBase extends TabBase { toolbarContainer.addToolbarItems([ { component: this.cutoverButton }, { component: this.cancelButton }, + { component: this.deleteButton }, { component: this.retryButton }, { component: this.copyDatabaseMigrationDetails, toolbarSeparatorAfter: true }, { component: this.newSupportRequest, toolbarSeparatorAfter: true }, diff --git a/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts b/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts index 8196f53287..53467ae1d0 100644 --- a/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts +++ b/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import * as loc from '../constants/strings'; import { getSqlServerName, getMigrationStatusImage, getPipelineStatusImage, debounce } from '../api/utils'; import { logError, TelemetryViews } from '../telemetry'; -import { canCancelMigration, canCutoverMigration, canRetryMigration, formatDateTimeString, formatNumber, formatSizeBytes, formatSizeKb, formatTime, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation, PipelineStatusCodes } from '../constants/helper'; +import { canCancelMigration, canCutoverMigration, canDeleteMigration, canRetryMigration, formatDateTimeString, formatNumber, formatSizeBytes, formatSizeKb, formatTime, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation, PipelineStatusCodes } from '../constants/helper'; import { CopyProgressDetail, getResourceName } from '../api/azure'; import { InfoFieldSchema, infoFieldLgWidth, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase'; import { IconPathHelper } from '../constants/iconPathHelper'; @@ -63,7 +63,7 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase Promise, + openMigrationsListFcn: (refresh?: boolean) => Promise, statusBar: DashboardStatusBar): Promise { this.view = view; @@ -162,6 +162,7 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase { menuCommands.push(MenuCommands.CancelMigration); } + if (canDeleteMigration(migration)) { + menuCommands.push(MenuCommands.DeleteMigration); + } + return menuCommands; } diff --git a/extensions/sql-migration/src/dashboard/migrationsTab.ts b/extensions/sql-migration/src/dashboard/migrationsTab.ts index 93fe871dbc..19e96e0651 100644 --- a/extensions/sql-migration/src/dashboard/migrationsTab.ts +++ b/extensions/sql-migration/src/dashboard/migrationsTab.ts @@ -85,9 +85,12 @@ export class MigrationsTab extends TabBase { this.statusBar); this.disposables.push(this._migrationsListTab); - const openMigrationsListTab = async (): Promise => { + const openMigrationsListTab = async (refresh?: boolean): Promise => { await this.statusBar.clearError(); await this._openTab(this._migrationsListTab); + if (refresh) { + await this._migrationsListTab.refresh(); + } }; this._migrationDetailsBlobTab = await new MigrationDetailsBlobContainerTab().create( diff --git a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts index a67ebfa973..3c8d7e1cd6 100644 --- a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts +++ b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts @@ -6,9 +6,9 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { promises as fs } from 'fs'; -import { DatabaseMigration, getMigrationDetails } from '../api/azure'; +import { DatabaseMigration, deleteMigration, getMigrationDetails } from '../api/azure'; import { MenuCommands, SqlMigrationExtensionId } from '../api/utils'; -import { canCancelMigration, canCutoverMigration, canRetryMigration } from '../constants/helper'; +import { canCancelMigration, canCutoverMigration, canDeleteMigration, canRetryMigration } from '../constants/helper'; import { IconPathHelper } from '../constants/iconPathHelper'; import { MigrationNotebookInfo, NotebookPathHelper } from '../constants/notebookPathHelper'; import * as loc from '../constants/strings'; @@ -41,6 +41,7 @@ export class DashboardWidget { private readonly _onServiceContextChanged: vscode.EventEmitter; private readonly _migrationDetailsEvent: vscode.EventEmitter; private readonly _errorEvent: vscode.EventEmitter; + private _migrationsTab!: MigrationsTab; constructor(context: vscode.ExtensionContext) { this._context = context; @@ -94,11 +95,11 @@ export class DashboardWidget { if (!migrationsTabInitialized) { migrationsTabInitialized = true; tabs.selectTab(MigrationsTabId); - await migrationsTab.setMigrationFilter(AdsMigrationStatus.ALL); - await migrationsTab.refresh(); - await migrationsTab.setMigrationFilter(filter); + await this._migrationsTab.setMigrationFilter(AdsMigrationStatus.ALL); + await this._migrationsTab.refresh(); + await this._migrationsTab.setMigrationFilter(filter); } else { - const promise = migrationsTab.setMigrationFilter(filter); + const promise = this._migrationsTab.setMigrationFilter(filter); tabs.selectTab(MigrationsTabId); await promise; } @@ -111,16 +112,16 @@ export class DashboardWidget { statusBar); disposables.push(dashboardTab); - const migrationsTab = await new MigrationsTab().create( + this._migrationsTab = await new MigrationsTab().create( this._context, view, this._onServiceContextChanged, this._migrationDetailsEvent, statusBar); - disposables.push(migrationsTab); + disposables.push(this._migrationsTab); const tabs = view.modelBuilder.tabbedPanel() - .withTabs([dashboardTab, migrationsTab]) + .withTabs([dashboardTab, this._migrationsTab]) .withLayout({ alwaysShowTabs: true, orientation: azdata.TabOrientation.Horizontal }) .withProps({ CSSStyles: { @@ -137,7 +138,7 @@ export class DashboardWidget { await this.clearError(await getSourceConnectionId()); if (tabId === MigrationsTabId && !migrationsTabInitialized) { migrationsTabInitialized = true; - await migrationsTab.refresh(); + await this._migrationsTab.refresh(); } })); @@ -210,8 +211,10 @@ export class DashboardWidget { async (args: MenuCommandArgs) => { try { const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId); - const url = 'https://portal.azure.com/#resource/' + migration!.properties.scope; - await vscode.env.openExternal(vscode.Uri.parse(url)); + if (migration) { + const url = 'https://portal.azure.com/#resource/' + migration.properties.scope; + await vscode.env.openExternal(vscode.Uri.parse(url)); + } } catch (e) { await this.showError( args.connectionId, @@ -229,10 +232,13 @@ export class DashboardWidget { try { await this.clearError(args.connectionId); const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId); - const dialog = new SqlMigrationServiceDetailsDialog( - await MigrationLocalStorage.getMigrationServiceContext(), - migration!); - await dialog.initialize(); + const serviceContext = await MigrationLocalStorage.getMigrationServiceContext(); + if (migration && serviceContext) { + const dialog = new SqlMigrationServiceDetailsDialog( + serviceContext, + migration); + await dialog.initialize(); + } } catch (e) { await this.showError( args.connectionId, @@ -269,40 +275,81 @@ export class DashboardWidget { } })); - this._context.subscriptions.push(vscode.commands.registerCommand( - MenuCommands.CancelMigration, - async (args: MenuCommandArgs) => { - try { - await this.clearError(args.connectionId); - const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId); - if (canCancelMigration(migration)) { - void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO) - .then(async (v) => { - if (v === loc.YES) { - const cutoverDialogModel = new MigrationCutoverDialogModel( - await MigrationLocalStorage.getMigrationServiceContext(), - migration!); - await cutoverDialogModel.fetchStatus(); - await cutoverDialogModel.cancelMigration(); + this._context.subscriptions.push( + vscode.commands.registerCommand( + MenuCommands.CancelMigration, + async (args: MenuCommandArgs) => { + try { + await this.clearError(args.connectionId); + const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId); + if (migration && canCancelMigration(migration)) { + void vscode.window + .showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO) + .then(async (v) => { + if (v === loc.YES) { + const cutoverDialogModel = new MigrationCutoverDialogModel( + await MigrationLocalStorage.getMigrationServiceContext(), + migration!); + await cutoverDialogModel.fetchStatus(); + await cutoverDialogModel.cancelMigration(); - if (cutoverDialogModel.CancelMigrationError) { - void vscode.window.showErrorMessage(loc.MIGRATION_CANNOT_CANCEL); - logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, cutoverDialogModel.CancelMigrationError); + if (cutoverDialogModel.CancelMigrationError) { + void vscode.window.showErrorMessage(loc.MIGRATION_CANNOT_CANCEL); + logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, cutoverDialogModel.CancelMigrationError); + } } - } - }); - } else { - await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CANCEL); + }); + } else { + await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CANCEL); + } + } catch (e) { + await this.showError( + args.connectionId, + loc.MIGRATION_CANCELLATION_ERROR, + loc.MIGRATION_CANCELLATION_ERROR, + e.message); + logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, e); } - } catch (e) { - await this.showError( - args.connectionId, - loc.MIGRATION_CANCELLATION_ERROR, - loc.MIGRATION_CANCELLATION_ERROR, - e.message); - logError(TelemetryViews.MigrationsTab, MenuCommands.CancelMigration, e); - } - })); + })); + + this._context.subscriptions.push( + vscode.commands.registerCommand( + MenuCommands.DeleteMigration, + async (args: MenuCommandArgs) => { + await this.clearError(args.connectionId); + try { + const service = await MigrationLocalStorage.getMigrationServiceContext(); + const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId); + // get migration details can return undefined when the migration has been auto-cleaned + // however, since the migration is still returned in getlist, we make a best effort to delete by id. + if (service && ( + (migration && canDeleteMigration(migration)) || + (migration === undefined && args.migrationId?.length > 0))) { + const response = await vscode.window.showInformationMessage( + loc.DELETE_MIGRATION_CONFIRMATION, + { modal: true }, + loc.YES, + loc.NO); + if (response === loc.YES) { + await deleteMigration( + service.azureAccount!, + service.subscription!, + args.migrationId); + await this._migrationsTab.refresh(); + } + } else { + await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_DELETE); + logError(TelemetryViews.MigrationsTab, MenuCommands.DeleteMigration, "cannot delete migration"); + } + } catch (e) { + await this.showError( + args.connectionId, + loc.MIGRATION_DELETE_ERROR, + loc.MIGRATION_DELETE_ERROR, + e.message); + logError(TelemetryViews.MigrationsTab, MenuCommands.DeleteMigration, e); + } + })); this._context.subscriptions.push( vscode.commands.registerCommand( @@ -311,11 +358,11 @@ export class DashboardWidget { try { await this.clearError(args.connectionId); const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId); - if (canRetryMigration(migration)) { + if (migration && canRetryMigration(migration)) { const retryMigrationDialog = new RetryMigrationDialog( this._context, await MigrationLocalStorage.getMigrationServiceContext(), - migration!, + migration, this._onServiceContextChanged); await retryMigrationDialog.openDialog(); } @@ -418,11 +465,16 @@ export class DashboardWidget { private async _getMigrationById(migrationId: string, migrationOperationId: string): Promise { const context = await MigrationLocalStorage.getMigrationServiceContext(); if (context.azureAccount && context.subscription) { - return getMigrationDetails( - context.azureAccount, - context.subscription, - migrationId, - migrationOperationId); + try { + const migration = getMigrationDetails( + context.azureAccount, + context.subscription, + migrationId, + migrationOperationId); + return migration; + } catch (error) { + logError(TelemetryViews.MigrationsTab, "_getMigrationById", error); + } } return undefined; } diff --git a/extensions/sql-migration/src/models/migrationLocalStorage.ts b/extensions/sql-migration/src/models/migrationLocalStorage.ts index 6bfe9ce4cb..fe78122134 100644 --- a/extensions/sql-migration/src/models/migrationLocalStorage.ts +++ b/extensions/sql-migration/src/models/migrationLocalStorage.ts @@ -6,7 +6,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as azurecore from 'azurecore'; import { DatabaseMigration, SqlMigrationService, getSubscriptions, getServiceMigrations } from '../api/azure'; -import { deepClone } from '../api/utils'; +import { deepClone, isAccountTokenStale } from '../api/utils'; import * as loc from '../constants/strings'; import { ServiceContextChangeEvent } from '../dashboard/tabBase'; import { getSourceConnectionProfile } from '../api/sqlUtils'; @@ -38,9 +38,9 @@ export class MigrationLocalStorage { } public static async refreshMigrationAzureAccount(serviceContext: MigrationServiceContext, migration: DatabaseMigration, serviceContextChangedEvent: vscode.EventEmitter): Promise { - if (serviceContext.azureAccount?.isStale) { + if (isAccountTokenStale(serviceContext.azureAccount)) { const accounts = await azdata.accounts.getAllAccounts(); - const account = accounts.find(a => !a.isStale && a.key.accountId === serviceContext.azureAccount?.key.accountId); + const account = accounts.find(a => !isAccountTokenStale(a) && a.key.accountId === serviceContext.azureAccount?.key.accountId); if (account) { const subscriptions = await getSubscriptions(account); const subscription = subscriptions.find(s => s.id === serviceContext.subscription?.id); @@ -55,7 +55,7 @@ export class MigrationLocalStorage { export function isServiceContextValid(serviceContext: MigrationServiceContext): boolean { return ( - serviceContext.azureAccount?.isStale === false && + !isAccountTokenStale(serviceContext.azureAccount) && serviceContext.location?.id !== undefined && serviceContext.migrationService?.id !== undefined && serviceContext.resourceGroup?.id !== undefined && diff --git a/extensions/sql-migration/src/telemetry.ts b/extensions/sql-migration/src/telemetry.ts index 7b8e60e686..6b8e8a96e1 100644 --- a/extensions/sql-migration/src/telemetry.ts +++ b/extensions/sql-migration/src/telemetry.ts @@ -24,6 +24,7 @@ export enum TelemetryViews { MigrationStatusDialog = 'MigrationStatusDialog', DashboardTab = 'DashboardTab', MigrationsTab = 'MigrationsTab', + MigrationDetailsTab = 'MigrationDetailsTab', MigrationWizardAccountSelectionPage = 'MigrationWizardAccountSelectionPage', MigrationWizardSkuRecommendationPage = 'MigrationWizardSkuRecommendationPage', MigrationWizardTargetSelectionPage = 'MigrationWizardTargetSelectionPage', @@ -56,6 +57,7 @@ export enum TelemetryAction { StartMigration = 'StartMigration', CutoverMigration = 'CutoverMigration', CancelMigration = 'CancelMigration', + DeleteMigration = 'DeleteMigration', MigrationStatus = 'MigrationStatus', PageButtonClick = 'PageButtonClick', Prev = 'prev', diff --git a/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts b/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts index 369ab26a5a..4cca537162 100644 --- a/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts @@ -877,7 +877,8 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage { private async populateTenantsDropdown(): Promise { try { this._accountTenantDropdown.loading = true; - if (this.migrationStateModel._azureAccount && this.migrationStateModel._azureAccount.isStale === false && this.migrationStateModel._azureAccount.properties.tenants.length > 0) { + if (!utils.isAccountTokenStale(this.migrationStateModel._azureAccount) && + this.migrationStateModel._azureAccount?.properties?.tenants?.length > 0) { this.migrationStateModel._accountTenants = utils.getAzureTenants(this.migrationStateModel._azureAccount); this._accountTenantDropdown.values = utils.getAzureTenantsDropdownValues(this.migrationStateModel._accountTenants); } diff --git a/extensions/sql-migration/src/wizard/targetSelectionPage.ts b/extensions/sql-migration/src/wizard/targetSelectionPage.ts index 5e9e080f48..6717fba15b 100644 --- a/extensions/sql-migration/src/wizard/targetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/targetSelectionPage.ts @@ -886,7 +886,7 @@ export class TargetSelectionPage extends MigrationWizardPage { private async populateTenantsDropdown(): Promise { try { this._accountTenantDropdown.loading = true; - if (this.migrationStateModel._azureAccount?.isStale === false && + if (!utils.isAccountTokenStale(this.migrationStateModel._azureAccount) && this.migrationStateModel._azureAccount?.properties?.tenants?.length > 0) { this.migrationStateModel._accountTenants = utils.getAzureTenants(this.migrationStateModel._azureAccount);