diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index d7f83a4827..3566c45f21 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -132,6 +132,14 @@ export async function getBlobContainers(account: azdata.Account, subscription: S return blobContainers!; } +export async function getBlobs(account: azdata.Account, subscription: Subscription, storageAccount: StorageAccount, containerName: string): Promise { + const api = await getAzureCoreAPI(); + let result = await api.getBlobs(account, subscription, storageAccount, containerName, true); + let blobNames = result.blobs; + sortResourceArrayByName(blobNames); + return blobNames!; +} + export async function getSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise { const api = await getAzureCoreAPI(); const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=2020-09-01-preview`; @@ -320,7 +328,7 @@ export async function getLocationDisplayName(location: string): Promise return await api.getRegionDisplayName(location); } -type SortableAzureResources = AzureProduct | azureResource.FileShare | azureResource.BlobContainer | azureResource.AzureResourceSubscription | SqlMigrationService; +type SortableAzureResources = AzureProduct | azureResource.FileShare | azureResource.BlobContainer | azureResource.Blob | azureResource.AzureResourceSubscription | SqlMigrationService; function sortResourceArrayByName(resourceArray: SortableAzureResources[]): void { if (!resourceArray) { return; @@ -405,7 +413,10 @@ export interface StartDatabaseMigrationRequest { password: string }, scope: string, - autoCutoverConfiguration?: AutoCutoverConfiguration + autoCutoverConfiguration?: { + autoCutover?: boolean, + lastBackupName?: string + }, } } @@ -469,6 +480,7 @@ export interface BackupConfiguration { } export interface AutoCutoverConfiguration { + autoCutover: boolean; lastBackupName: string; } diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts index 239d126722..31605e5aa8 100644 --- a/extensions/sql-migration/src/api/utils.ts +++ b/extensions/sql-migration/src/api/utils.ts @@ -3,7 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CategoryValue, DropDownComponent } from 'azdata'; +import { 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 { MigrationContext } from '../models/migrationLocalStorage'; @@ -199,3 +200,21 @@ export function getSessionIdHeader(sessionId: string): { [key: string]: string } 'SqlMigrationSessionId': sessionId }; } + +export function getMigrationStatusImage(status: string): IconPath { + switch (status) { + case 'InProgress': + return IconPathHelper.inProgressMigration; + case 'Succeeded': + return IconPathHelper.completedMigration; + case 'Creating': + return IconPathHelper.notStartedMigration; + case 'Completing': + return IconPathHelper.completingCutover; + case 'Canceling': + return IconPathHelper.cancel; + case 'Failed': + default: + return IconPathHelper.error; + } +} diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 39d3e3565a..15bce1a99a 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -133,11 +133,23 @@ export const NO_LOCATION_FOUND = localize('sql.migration.no.location.found', "No export const NO_STORAGE_ACCOUNT_FOUND = localize('sql.migration.no.storageAccount.found', "No storage account found"); export const NO_FILESHARES_FOUND = localize('sql.migration.no.fileShares.found', "No file shares found"); export const NO_BLOBCONTAINERS_FOUND = localize('sql.migration.no.blobContainers.found', "No blob containers found"); +export const NO_BLOBFILES_FOUND = localize('sql.migration.no.blobFiles.found', "No blob files found"); export const INVALID_SUBSCRIPTION_ERROR = localize('sql.migration.invalid.subscription.error', "Please select a valid subscription to proceed."); export const INVALID_LOCATION_ERROR = localize('sql.migration.invalid.location.error', "Please select a valid location to proceed."); export const INVALID_STORAGE_ACCOUNT_ERROR = localize('sql.migration.invalid.storageAccount.error', "Please select a valid storage account to proceed."); export const INVALID_FILESHARE_ERROR = localize('sql.migration.invalid.fileShare.error', "Please select a valid file share to proceed."); -export const INVALID_BLOBCONTAINER_ERROR = localize('sql.migration.invalid.blobContainer.error', "Please select a valid blob container to proceed."); +export function INVALID_BLOB_RESOURCE_GROUP_ERROR(sourceDb: string): string { + return localize('sql.migration.invalid.blob.resourceGroup.error', "Please select a valid resource group for source database '{0}' to proceed.", sourceDb); +} +export function INVALID_BLOB_STORAGE_ACCOUNT_ERROR(sourceDb: string): string { + return localize('sql.migration.invalid.blob.storageAccount.error', "Please select a valid storage account for source database '{0}' to proceed.", sourceDb); +} +export function INVALID_BLOB_CONTAINER_ERROR(sourceDb: string): string { + return localize('sql.migration.invalid.blob.container.error', "Please select a valid blob container for source database '{0}' to proceed.", sourceDb); +} +export function INVALID_BLOB_LAST_BACKUP_FILE_ERROR(sourceDb: string): string { + return localize('sql.migration.invalid.blob.lastBackupFile.error', "Please select a valid last backup file for source database '{0}' to proceed.", sourceDb); +} export const INVALID_NETWORK_SHARE_LOCATION = localize('sql.migration.invalid.network.share.location', "Invalid network share location format. Example: {0}", '\\\\Servername.domainname.com\\Backupfolder'); export const INVALID_USER_ACCOUNT = localize('sql.migration.invalid.user.account', "Invalid user account format. Example: {0}", 'Domain\\username'); export function TARGET_NETWORK_SHARE_LOCATION(dbName: string): string { @@ -162,6 +174,9 @@ export function SQL_SOURCE_DETAILS(authMethod: MigrationSourceAuthenticationType return localize('sql.migration.source.details.sqlAuth', "Enter the SQL Authentication credential used for connecting to SQL Server Instance {0}. ​ This credential will be used to for connecting to SQL Server instance and identifying valid backup file(s)", serverName); } } +export const SELECT_RESOURCE_GROUP = localize('sql.migration.blob.resourceGroup.select', "Select a resource group value first."); +export const SELECT_STORAGE_ACCOUNT = localize('sql.migration.blob.storageAccount.select', "Select a storage account value first."); +export const SELECT_BLOB_CONTAINER = localize('sql.migration.blob.container.select', "Select a blob container value first."); // integration runtime page export const IR_PAGE_TITLE = localize('sql.migration.ir.page.title', "Azure Database Migration Service"); @@ -270,6 +285,7 @@ export const SUMMARY_AZURE_STORAGE = localize('sql.migration.summary.azure.stora export const SUMMARY_IR_NODE = localize('sql.migration.ir.node', "Integration Runtime node"); export const NETWORK_SHARE = localize('sql.migration.network.share', "Network Share"); export const BLOB_CONTAINER = localize('sql.migration.blob.container.title', "Blob Container"); +export const BLOB_CONTAINER_LAST_BACKUP_FILE = localize('sql.migration.blob.container.last.backup.file.label', "Last Backup File"); export const BLOB_CONTAINER_RESOURCE_GROUP = localize('sql.migration.blob.container.label', "Blob container resource group"); export const BLOB_CONTAINER_STORAGE_ACCOUNT = localize('sql.migration.blob.container.storage.account.label', "Blob container storage account"); export const FILE_SHARE = localize('sql.migration.file.share.title', "File Share"); diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts index e25eddc2d0..c4e9543870 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts @@ -9,11 +9,13 @@ import { IconPathHelper } from '../../constants/iconPathHelper'; import { MigrationContext } from '../../models/migrationLocalStorage'; import { MigrationCutoverDialogModel, MigrationStatus } from './migrationCutoverDialogModel'; import * as loc from '../../constants/strings'; -import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, SupportedAutoRefreshIntervals } from '../../api/utils'; +import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage, SupportedAutoRefreshIntervals } from '../../api/utils'; import { EOL } from 'os'; import { ConfirmCutoverDialog } from './confirmCutoverDialog'; +import { MigrationMode } from '../../models/stateMachine'; const refreshFrequency: SupportedAutoRefreshIntervals = 30000; +const statusImageSize: number = 14; export class MigrationCutoverDialog { private _dialogObject!: azdata.window.Dialog; @@ -27,20 +29,21 @@ export class MigrationCutoverDialog { private _refreshLoader!: azdata.LoadingComponent; private _copyDatabaseMigrationDetails!: azdata.ButtonComponent; - private _serverName!: azdata.TextComponent; - private _serverVersion!: azdata.TextComponent; - private _sourceDatabase!: azdata.TextComponent; - private _targetDatabase!: azdata.TextComponent; - private _targetServer!: azdata.TextComponent; - private _targetVersion!: azdata.TextComponent; - private _migrationStatus!: azdata.TextComponent; - private _fullBackupFile!: azdata.TextComponent; - private _backupLocation!: azdata.TextComponent; - private _lastAppliedLSN!: azdata.TextComponent; - private _lastAppliedBackupFile!: azdata.TextComponent; - private _lastAppliedBackupTakenOn!: azdata.TextComponent; + private _sourceDatabaseInfoField!: InfoFieldSchema; + private _sourceDetailsInfoField!: InfoFieldSchema; + private _sourceVersionInfoField!: InfoFieldSchema; + private _targetDatabaseInfoField!: InfoFieldSchema; + private _targetServerInfoField!: InfoFieldSchema; + private _targetVersionInfoField!: InfoFieldSchema; + private _migrationStatusInfoField!: InfoFieldSchema; + private _fullBackupFileOnInfoField!: InfoFieldSchema; + private _backupLocationInfoField!: InfoFieldSchema; + private _lastLSNInfoField!: InfoFieldSchema; + private _lastAppliedBackupInfoField!: InfoFieldSchema; + private _lastAppliedBackupTakenOnInfoField!: InfoFieldSchema; + private _fileCount!: azdata.TextComponent; - private fileTable!: azdata.TableComponent; + private _fileTable!: azdata.TableComponent; private _autoRefreshHandle!: any; private _disposables: vscode.Disposable[] = []; @@ -58,151 +61,6 @@ export class MigrationCutoverDialog { tab.registerContent(async (view: azdata.ModelView) => { try { this._view = view; - const sourceDatabase = this.createInfoField(loc.SOURCE_DATABASE, ''); - const sourceDetails = this.createInfoField(loc.SOURCE_SERVER, ''); - const sourceVersion = this.createInfoField(loc.SOURCE_VERSION, ''); - - this._sourceDatabase = sourceDatabase.text; - this._serverName = sourceDetails.text; - this._serverVersion = sourceVersion.text; - - const flexServer = view.modelBuilder.flexContainer().withLayout({ - flexFlow: 'column' - }).component(); - - flexServer.addItem(sourceDatabase.flexContainer, { - CSSStyles: { - 'width': this._infoFieldWidth - } - }); - flexServer.addItem(sourceDetails.flexContainer, { - CSSStyles: { - 'width': this._infoFieldWidth - } - }); - flexServer.addItem(sourceVersion.flexContainer, { - CSSStyles: { - 'width': this._infoFieldWidth - } - }); - - const targetDatabase = this.createInfoField(loc.TARGET_DATABASE_NAME, ''); - const targetServer = this.createInfoField(loc.TARGET_SERVER, ''); - const targetVersion = this.createInfoField(loc.TARGET_VERSION, ''); - - this._targetDatabase = targetDatabase.text; - this._targetServer = targetServer.text; - this._targetVersion = targetVersion.text; - - const flexTarget = view.modelBuilder.flexContainer().withLayout({ - flexFlow: 'column' - }).component(); - - flexTarget.addItem(targetDatabase.flexContainer, { - CSSStyles: { - 'width': this._infoFieldWidth - } - }); - flexTarget.addItem(targetServer.flexContainer, { - CSSStyles: { - 'width': this._infoFieldWidth - } - }); - flexTarget.addItem(targetVersion.flexContainer, { - CSSStyles: { - 'width': this._infoFieldWidth - } - }); - - const migrationStatus = this.createInfoField(loc.MIGRATION_STATUS, ''); - const fullBackupFileOn = this.createInfoField(loc.FULL_BACKUP_FILES, ''); - const backupLocation = this.createInfoField(loc.BACKUP_LOCATION, ''); - - this._migrationStatus = migrationStatus.text; - this._fullBackupFile = fullBackupFileOn.text; - this._backupLocation = backupLocation.text; - - const flexStatus = view.modelBuilder.flexContainer().withLayout({ - flexFlow: 'column' - }).component(); - - flexStatus.addItem(migrationStatus.flexContainer, { - CSSStyles: { - 'width': this._infoFieldWidth - } - }); - flexStatus.addItem(fullBackupFileOn.flexContainer, { - CSSStyles: { - 'width': this._infoFieldWidth - } - }); - flexStatus.addItem(backupLocation.flexContainer, { - CSSStyles: { - 'width': this._infoFieldWidth - } - }); - - const lastSSN = this.createInfoField(loc.LAST_APPLIED_LSN, ''); - const lastAppliedBackup = this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, ''); - const lastAppliedBackupOn = this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, ''); - - this._lastAppliedLSN = lastSSN.text; - this._lastAppliedBackupFile = lastAppliedBackup.text; - this._lastAppliedBackupTakenOn = lastAppliedBackupOn.text; - - const flexFile = view.modelBuilder.flexContainer().withLayout({ - flexFlow: 'column' - }).component(); - flexFile.addItem(lastSSN.flexContainer, { - CSSStyles: { - 'width': this._infoFieldWidth - } - }); - flexFile.addItem(lastAppliedBackup.flexContainer, { - CSSStyles: { - 'width': this._infoFieldWidth - } - }); - flexFile.addItem(lastAppliedBackupOn.flexContainer, { - CSSStyles: { - 'width': this._infoFieldWidth - } - }); - const flexInfo = view.modelBuilder.flexContainer().withProps({ - width: 1000 - }).component(); - - flexInfo.addItem(flexServer, { - flex: '0', - CSSStyles: { - 'flex': '0', - 'width': this._infoFieldWidth - } - }); - - flexInfo.addItem(flexTarget, { - flex: '0', - CSSStyles: { - 'flex': '0', - 'width': this._infoFieldWidth - } - }); - - flexInfo.addItem(flexStatus, { - flex: '0', - CSSStyles: { - 'flex': '0', - 'width': this._infoFieldWidth - } - }); - - flexInfo.addItem(flexFile, { - flex: '0', - CSSStyles: { - 'flex': '0', - 'width': this._infoFieldWidth - } - }); this._fileCount = view.modelBuilder.text().withProps({ width: '500px', @@ -212,7 +70,7 @@ export class MigrationCutoverDialog { } }).component(); - this.fileTable = view.modelBuilder.table().withProps({ + this._fileTable = view.modelBuilder.table().withProps({ ariaLabel: loc.ACTIVE_BACKUP_FILES, columns: [ { @@ -259,18 +117,23 @@ export class MigrationCutoverDialog { data: [], width: '1100px', height: '300px', - fontSize: '12px' + fontSize: '12px', + CSSStyles: { + 'display': 'none', + } }).component(); + let formItems = [ + { component: this.migrationContainerHeader() }, + { component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component() }, + { component: this.migrationInfoGrid() }, + { component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component() }, + { component: this._fileCount }, + { component: this._fileTable } + ]; + const formBuilder = view.modelBuilder.formContainer().withFormItems( - [ - { component: this.migrationContainerHeader() }, - { component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component() }, - { component: flexInfo }, - { component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component() }, - { component: this._fileCount }, - { component: this.fileTable } - ], + formItems, { horizontal: false } ); const form = formBuilder.withLayout({ width: '100%' }).component(); @@ -367,7 +230,8 @@ export class MigrationCutoverDialog { width: '150px', enabled: false, CSSStyles: { - 'font-size': '13px' + 'font-size': '13px', + 'display': 'none' } }).component(); @@ -389,6 +253,7 @@ export class MigrationCutoverDialog { label: loc.CANCEL_MIGRATION, height: '20px', width: '150px', + enabled: false, CSSStyles: { 'font-size': '13px' } @@ -491,14 +356,84 @@ export class MigrationCutoverDialog { return header; } - private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void { - const classVariable = this; - clearInterval(this._autoRefreshHandle); - if (interval !== -1) { - this._autoRefreshHandle = setInterval(async function () { await classVariable.refreshStatus(); }, interval); - } + private migrationInfoGrid(): azdata.FlexContainer { + const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => { + container.addItem(infoField.flexContainer, { + CSSStyles: { + width: this._infoFieldWidth, + } + }); + }; + + const flexServer = this._view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' + }).component(); + this._sourceDatabaseInfoField = this.createInfoField(loc.SOURCE_DATABASE, ''); + this._sourceDetailsInfoField = this.createInfoField(loc.SOURCE_SERVER, ''); + this._sourceVersionInfoField = this.createInfoField(loc.SOURCE_VERSION, ''); + addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer); + addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer); + addInfoFieldToContainer(this._sourceVersionInfoField, flexServer); + + const flexTarget = this._view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' + }).component(); + this._targetDatabaseInfoField = this.createInfoField(loc.TARGET_DATABASE_NAME, ''); + this._targetServerInfoField = this.createInfoField(loc.TARGET_SERVER, ''); + this._targetVersionInfoField = this.createInfoField(loc.TARGET_VERSION, ''); + addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget); + addInfoFieldToContainer(this._targetServerInfoField, flexTarget); + addInfoFieldToContainer(this._targetVersionInfoField, flexTarget); + + const flexStatus = this._view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' + }).component(); + this._migrationStatusInfoField = this.createInfoField(loc.MIGRATION_STATUS, '', false, ' '); + this._fullBackupFileOnInfoField = this.createInfoField(loc.FULL_BACKUP_FILES, '', true); + this._backupLocationInfoField = this.createInfoField(loc.BACKUP_LOCATION, ''); + addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus); + addInfoFieldToContainer(this._fullBackupFileOnInfoField, flexStatus); + addInfoFieldToContainer(this._backupLocationInfoField, flexStatus); + + const flexFile = this._view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' + }).component(); + this._lastLSNInfoField = this.createInfoField(loc.LAST_APPLIED_LSN, '', true); + this._lastAppliedBackupInfoField = this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, ''); + this._lastAppliedBackupTakenOnInfoField = this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', true); + addInfoFieldToContainer(this._lastLSNInfoField, flexFile); + addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile); + addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile); + + const flexInfoProps = { + flex: '0', + CSSStyles: { + 'flex': '0', + 'width': this._infoFieldWidth + } + }; + + const flexInfo = this._view.modelBuilder.flexContainer().withProps({ + width: 1000 + }).component(); + flexInfo.addItem(flexServer, flexInfoProps); + flexInfo.addItem(flexTarget, flexInfoProps); + flexInfo.addItem(flexStatus, flexInfoProps); + flexInfo.addItem(flexFile, flexInfoProps); + + return flexInfo; } + private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void { + const shouldRefresh = (status: string | undefined) => !status || ['InProgress', 'Creating', 'Completing', 'Creating'].includes(status); + if (shouldRefresh(this.getMigrationStatus())) { + const classVariable = this; + clearInterval(this._autoRefreshHandle); + if (interval !== -1) { + this._autoRefreshHandle = setInterval(async function () { await classVariable.refreshStatus(); }, interval); + } + } + } private async refreshStatus(): Promise { if (this.isRefreshing) { @@ -506,10 +441,14 @@ export class MigrationCutoverDialog { } try { + if (this._isProvisioned() && this._isOnlineMigration()) { + this._cutoverButton.updateCssStyles({ + 'display': 'inline' + }); + } + this.isRefreshing = true; this._refreshLoader.loading = true; - this._cutoverButton.enabled = false; - this._cancelButton.enabled = false; await this._model.fetchStatus(); const errors = []; errors.push(this._model.migrationOpStatus.error?.message); @@ -534,81 +473,92 @@ export class MigrationCutoverDialog { targetServerVersion = loc.AZURE_SQL_DATABASE_VIRTUAL_MACHINE; } - const migrationStatusTextValue = this._model.migrationStatus.properties.migrationStatus ? this._model.migrationStatus.properties.migrationStatus : this._model.migrationStatus.properties.provisioningState; - let lastAppliedSSN: string; let lastAppliedBackupFileTakenOn: string; - const tableData: ActiveBackupFileSchema[] = []; this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach((activeBackupSet) => { - tableData.push( - { - fileName: activeBackupSet.listOfBackupFiles[0].fileName, - type: activeBackupSet.backupType, - status: activeBackupSet.listOfBackupFiles[0].status, - dataUploaded: `${convertByteSizeToReadableUnit(activeBackupSet.listOfBackupFiles[0].dataWritten)}/ ${convertByteSizeToReadableUnit(activeBackupSet.listOfBackupFiles[0].totalSize)}`, - copyThroughput: (activeBackupSet.listOfBackupFiles[0].copyThroughput / 1024).toFixed(2), - backupStartTime: activeBackupSet.backupStartDate, - firstLSN: activeBackupSet.firstLSN, - lastLSN: activeBackupSet.lastLSN + if (this._shouldDisplayBackupFileTable()) { + tableData.push( + { + fileName: activeBackupSet.listOfBackupFiles[0].fileName, + type: activeBackupSet.backupType, + status: activeBackupSet.listOfBackupFiles[0].status, + dataUploaded: `${convertByteSizeToReadableUnit(activeBackupSet.listOfBackupFiles[0].dataWritten)}/ ${convertByteSizeToReadableUnit(activeBackupSet.listOfBackupFiles[0].totalSize)}`, + copyThroughput: (activeBackupSet.listOfBackupFiles[0].copyThroughput) ? (activeBackupSet.listOfBackupFiles[0].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; } }); - this._sourceDatabase.value = sourceDatabaseName; - this._serverName.value = sqlServerName; - this._serverVersion.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`; + this._sourceDatabaseInfoField.text.value = sourceDatabaseName; + this._sourceDetailsInfoField.text.value = sqlServerName; + this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`; - this._targetDatabase.value = targetDatabaseName; - this._targetServer.value = targetServerName; - this._targetVersion.value = targetServerVersion; + this._targetDatabaseInfoField.text.value = targetDatabaseName; + this._targetServerInfoField.text.value = targetServerName; + this._targetVersionInfoField.text.value = targetServerVersion; + + 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! ?? '-'; + this.showInfoField(this._fullBackupFileOnInfoField); - this._migrationStatus.value = migrationStatusTextValue ?? '---'; - this._fullBackupFile.value = this._model.migrationStatus?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? '-'; let backupLocation; - - const isBlobMigration = this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob !== undefined; + const isBlobMigration = this._isBlobMigration(); // Displaying storage accounts and blob container for azure blob backups. if (isBlobMigration) { backupLocation = `${this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob?.storageAccountResourceId.split('/').pop()} - ${this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob?.blobContainerName}`; - this._fileCount.display = 'none'; - this.fileTable.updateCssStyles({ - 'display': 'none' - }); } else { backupLocation = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.fileShare?.path! ?? '-'; } - this._backupLocation.value = backupLocation ?? '-'; + this._backupLocationInfoField.text.value = backupLocation ?? '-'; - this._lastAppliedLSN.value = lastAppliedSSN! ?? '-'; - this._lastAppliedBackupFile.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename ?? '-'; - this._lastAppliedBackupTakenOn.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : '-'; + this._lastLSNInfoField.text.value = lastAppliedSSN! ?? '-'; + this._lastAppliedBackupInfoField.text.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename ?? '-'; + this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : '-'; + this.showInfoField(this._lastLSNInfoField); + this.showInfoField(this._lastAppliedBackupTakenOnInfoField); - this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length); + if (this._shouldDisplayBackupFileTable()) { + this._fileCount.updateCssStyles({ + display: 'inline' + }); + this._fileTable.updateCssStyles({ + display: 'inline' + }); - // Sorting files in descending order of backupStartTime - tableData.sort((file1, file2) => new Date(file1.backupStartTime) > new Date(file2.backupStartTime) ? - 1 : 1); + this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length); - this.fileTable.data = tableData.map((row) => { - return [ - row.fileName, - row.type, - row.status, - row.dataUploaded, - row.copyThroughput, - convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(), - row.firstLSN, - row.lastLSN - ]; - }); + // Sorting files in descending order of backupStartTime + tableData.sort((file1, file2) => new Date(file1.backupStartTime) > new Date(file2.backupStartTime) ? - 1 : 1); + + this._fileTable.data = tableData.map((row) => { + return [ + row.fileName, + row.type, + row.status, + row.dataUploaded, + row.copyThroughput, + convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(), + row.firstLSN, + row.lastLSN + ]; + }); + } if (migrationStatusTextValue === MigrationStatus.InProgress) { const restoredCount = (this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.filter(a => a.listOfBackupFiles[0].status === 'Restored'))?.length ?? 0; @@ -628,14 +578,21 @@ export class MigrationCutoverDialog { } } - private createInfoField(label: string, value: string): { + private createInfoField(label: string, value: string, defaultHidden: boolean = false, iconPath?: azdata.IconPath): { flexContainer: azdata.FlexContainer, - text: azdata.TextComponent + text: azdata.TextComponent, + icon?: azdata.ImageComponent } { const flexContainer = this._view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + if (defaultHidden) { + flexContainer.updateCssStyles({ + 'display': 'none' + }); + } + const labelComponent = this._view.modelBuilder.text().withProps({ value: label, CSSStyles: { @@ -657,12 +614,79 @@ export class MigrationCutoverDialog { 'font-size': '12px' } }).component(); - flexContainer.addItem(textComponent); + + let iconComponent; + if (iconPath) { + iconComponent = this._view.modelBuilder.image().withProps({ + iconPath: (iconPath === ' ') ? undefined : iconPath, + iconHeight: statusImageSize, + iconWidth: statusImageSize, + height: statusImageSize, + width: statusImageSize, + CSSStyles: { + 'margin': '7px 3px 0 0', + 'padding': '0' + } + }).component(); + + const iconTextComponent = this._view.modelBuilder.flexContainer() + .withItems([ + iconComponent, + textComponent + ]).withProps({ + CSSStyles: { + 'margin': '0', + 'padding': '0' + }, + display: 'inline-flex' + }).component(); + flexContainer.addItem(iconTextComponent); + } else { + flexContainer.addItem(textComponent); + } + return { flexContainer: flexContainer, - text: textComponent + text: textComponent, + icon: iconComponent }; } + + private showInfoField(infoField: InfoFieldSchema): void { + if (infoField.text.value !== '-') { + infoField.flexContainer.updateCssStyles({ + 'display': 'inline' + }); + } + } + + private _isProvisioned(): boolean { + const { migrationStatus, provisioningState } = this._model._migration.migrationContext.properties; + return provisioningState === 'Succeeded' || migrationStatus === 'Completing' || migrationStatus === 'Canceling'; + } + + private _isOnlineMigration(): boolean { + let migrationMode = null; + if (this._isProvisioned()) { + migrationMode = this._model._migration.migrationContext.properties.autoCutoverConfiguration?.autoCutover?.valueOf() ? MigrationMode.OFFLINE : MigrationMode.ONLINE; + } + return migrationMode === MigrationMode.ONLINE; + } + + private _isBlobMigration(): boolean { + return this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob !== undefined; + } + + private _shouldDisplayBackupFileTable(): boolean { + return this._isProvisioned() && this._isOnlineMigration() && !this._isBlobMigration(); + } + + 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; + } } interface ActiveBackupFileSchema { @@ -675,3 +699,9 @@ interface ActiveBackupFileSchema { firstLSN: string, lastLSN: string } + +interface InfoFieldSchema { + flexContainer: azdata.FlexContainer, + text: azdata.TextComponent, + icon?: azdata.ImageComponent +} diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts index d1bd0a5b36..9fa534b7e0 100644 --- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts @@ -10,7 +10,7 @@ import { MigrationContext, MigrationLocalStorage } from '../../models/migrationL import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog'; import { AdsMigrationStatus, MigrationStatusDialogModel } from './migrationStatusDialogModel'; import * as loc from '../../constants/strings'; -import { convertTimeDifferenceToDuration, filterMigrations, SupportedAutoRefreshIntervals } from '../../api/utils'; +import { convertTimeDifferenceToDuration, filterMigrations, getMigrationStatusImage, SupportedAutoRefreshIntervals } from '../../api/utils'; import { SqlMigrationServiceDetailsDialog } from '../sqlMigrationService/sqlMigrationServiceDetailsDialog'; import { ConfirmCutoverDialog } from '../migrationCutover/confirmCutoverDialog'; import { MigrationCutoverDialogModel } from '../migrationCutover/migrationCutoverDialogModel'; @@ -286,7 +286,7 @@ export class MigrationStatusDialog { return [ { value: this._getDatabaserHyperLink(migration) }, { value: this._getMigrationStatus(migration) }, - { value: loc.ONLINE }, + { value: this._getMigrationMode(migration) }, { value: this._getMigrationTargetType(migration) }, { value: migration.targetManagedInstance.name }, { value: migration.controller.name }, @@ -299,14 +299,7 @@ export class MigrationStatusDialog { { value: this._getMigrationTime(migration.migrationContext.properties.endedOn) }, { value: { - commands: [ - 'sqlmigration.cutover', - 'sqlmigration.view.database', - 'sqlmigration.view.target', - 'sqlmigration.view.service', - 'sqlmigration.copy.migration', - 'sqlmigration.cancel.migration', - ], + commands: this._getMenuCommands(migration), context: migration.migrationContext.id }, } @@ -377,6 +370,27 @@ export class MigrationStatusDialog { : loc.SQL_VIRTUAL_MACHINE; } + private _getMigrationMode(migration: MigrationContext): string { + if (migration.migrationContext.properties.provisioningState === 'Creating') { + return '---'; + } + return migration.migrationContext.properties.autoCutoverConfiguration?.autoCutover?.valueOf() ? loc.OFFLINE : loc.ONLINE; + } + + private _getMenuCommands(migration: MigrationContext): string[] { + let menuCommands = [ + 'sqlmigration.view.database', + 'sqlmigration.view.target', + 'sqlmigration.view.service', + 'sqlmigration.copy.migration', + 'sqlmigration.cancel.migration', + ]; + if (this._getMigrationMode(migration) === loc.ONLINE) { + menuCommands.unshift('sqlmigration.cutover'); + } + return menuCommands; + } + private _getMigrationStatus(migration: MigrationContext): azdata.FlexContainer { const properties = migration.migrationContext.properties; const migrationStatus = properties.migrationStatus ?? properties.provisioningState; @@ -404,7 +418,7 @@ export class MigrationStatusDialog { // migration status icon this._view.modelBuilder.image() .withProps({ - iconPath: this._statusImageMap(status), + iconPath: getMigrationStatusImage(status), iconHeight: statusImageSize, iconWidth: statusImageSize, height: statusImageSize, @@ -568,24 +582,6 @@ export class MigrationStatusDialog { return this._statusTable; } - private _statusImageMap(status: string): azdata.IconPath { - switch (status) { - case 'InProgress': - return IconPathHelper.inProgressMigration; - case 'Succeeded': - return IconPathHelper.completedMigration; - case 'Creating': - return IconPathHelper.notStartedMigration; - case 'Completing': - return IconPathHelper.completingCutover; - case 'Canceling': - return IconPathHelper.cancel; - case 'Failed': - default: - return IconPathHelper.error; - } - } - private _statusInfoMap(status: string): azdata.IconPath { switch (status) { case 'InProgress': diff --git a/extensions/sql-migration/src/dialog/targetDatabaseSummary/targetDatabaseSummaryDialog.ts b/extensions/sql-migration/src/dialog/targetDatabaseSummary/targetDatabaseSummaryDialog.ts index 8ef3d9880f..1a245e5784 100644 --- a/extensions/sql-migration/src/dialog/targetDatabaseSummary/targetDatabaseSummaryDialog.ts +++ b/extensions/sql-migration/src/dialog/targetDatabaseSummary/targetDatabaseSummaryDialog.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; -import { MigrationStateModel, NetworkContainerType } from '../../models/stateMachine'; +import { MigrationMode, MigrationStateModel, NetworkContainerType } from '../../models/stateMachine'; import * as constants from '../../constants/strings'; export class TargetDatabaseSummaryDialog { @@ -15,8 +15,8 @@ export class TargetDatabaseSummaryDialog { constructor(private _model: MigrationStateModel) { let dialogWidth: azdata.window.DialogWidth; if (this._model._databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) { - this._tableLength = 600; - dialogWidth = 'medium'; + this._tableLength = 800; + dialogWidth = 900; } else { this._tableLength = 200; dialogWidth = 'narrow'; @@ -109,6 +109,14 @@ export class TargetDatabaseSummaryDialog { width: columnWidth, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyle + }, { + valueType: azdata.DeclarativeDataType.string, + displayName: constants.BLOB_CONTAINER_LAST_BACKUP_FILE, + isReadOnly: true, + width: columnWidth, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyle, + hidden: this._model._databaseBackup.migrationMode === MigrationMode.ONLINE }); } @@ -131,6 +139,12 @@ export class TargetDatabaseSummaryDialog { }, { value: this._model._databaseBackup.blobs[index].blobContainer.name }); + + if (this._model._databaseBackup.migrationMode === MigrationMode.OFFLINE) { + tableRow.push({ + value: this._model._databaseBackup.blobs[index].lastBackupFile! + }); + } } tableRows.push(tableRow); }); diff --git a/extensions/sql-migration/src/models/migrationWizardPage.ts b/extensions/sql-migration/src/models/migrationWizardPage.ts index 041cdc11f0..a6fcdfca6d 100644 --- a/extensions/sql-migration/src/models/migrationWizardPage.ts +++ b/extensions/sql-migration/src/models/migrationWizardPage.ts @@ -33,8 +33,8 @@ export abstract class MigrationWizardPage { return this.wizardPage; } - public abstract onPageEnter(): Promise; - public abstract onPageLeave(): Promise; + public abstract onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise; + public abstract onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise; private readonly stateChanges: (() => Promise)[] = []; protected async onStateChangeEvent(e: StateChangeEvent) { diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 934c18e45e..5668902d96 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -8,7 +8,7 @@ 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, getResourceGroups, getLocationDisplayName, getSqlManagedInstanceDatabases } from '../api/azure'; +import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer, getLocations, getResourceGroups, getLocationDisplayName, getSqlManagedInstanceDatabases, getBlobs } from '../api/azure'; import { SKURecommendations } from './externalContract'; import * as constants from '../constants/strings'; import { MigrationLocalStorage } from './migrationLocalStorage'; @@ -80,6 +80,7 @@ export interface Blob { storageAccount: StorageAccount; blobContainer: azureResource.BlobContainer; storageKey: string; + lastBackupFile?: string; // _todo: does it make sense to store the last backup file here? } export interface Model { @@ -122,6 +123,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { public _storageAccounts!: StorageAccount[]; public _fileShares!: azureResource.FileShare[]; public _blobContainers!: azureResource.BlobContainer[]; + public _lastFileNames!: azureResource.Blob[]; public _refreshNetworkShareLocation!: azureResource.BlobContainer[]; public _targetDatabaseNames!: string[]; @@ -739,6 +741,40 @@ export class MigrationStateModel implements Model, vscode.Disposable { return this._blobContainers[index]; } + public async getBlobLastBackupFileNameValues(subscription: azureResource.AzureResourceSubscription, storageAccount: StorageAccount, blobContainer: azureResource.BlobContainer): Promise { + let blobLastBackupFileValues: azdata.CategoryValue[] = []; + try { + this._lastFileNames = await getBlobs(this._azureAccount, subscription, storageAccount, blobContainer.name); + if (this._lastFileNames.length === 0) { + blobLastBackupFileValues = [ + { + displayName: constants.NO_BLOBFILES_FOUND, + name: '' + } + ]; + } else { + this._lastFileNames.forEach((blob) => { + blobLastBackupFileValues.push({ + name: blob.name, + displayName: `${blob.name}`, + }); + }); + } + } catch (e) { + console.log(e); + blobLastBackupFileValues = [ + { + displayName: constants.NO_BLOBFILES_FOUND, + name: '' + } + ]; + } + return blobLastBackupFileValues; + } + + public getBlobLastBackupFileName(index: number): string { + return this._lastFileNames[index].name; + } public async getSqlMigrationServiceValues(subscription: azureResource.AzureResourceSubscription, managedInstance: SqlManagedInstance, resourceGroupName: string): Promise { let sqlMigrationServiceValues: azdata.CategoryValue[] = []; @@ -797,7 +833,8 @@ export class MigrationStateModel implements Model, vscode.Disposable { username: this._sqlServerUsername, password: this._sqlServerPassword }, - scope: this._targetServerInstance.id + scope: this._targetServerInstance.id, + autoCutoverConfiguration: {} } }; @@ -815,6 +852,13 @@ export class MigrationStateModel implements Model, vscode.Disposable { } } }; + + if (this._databaseBackup.migrationMode === MigrationMode.OFFLINE) { + requestBody.properties.autoCutoverConfiguration = { + autoCutover: (this._databaseBackup.migrationMode === MigrationMode.OFFLINE ? true : false), + lastBackupName: this._databaseBackup.blobs[i]?.lastBackupFile + }; + } break; case NetworkContainerType.NETWORK_SHARE: requestBody.properties.backupConfiguration = { @@ -830,6 +874,12 @@ export class MigrationStateModel implements Model, vscode.Disposable { } } }; + + if (this._databaseBackup.migrationMode === MigrationMode.OFFLINE) { + requestBody.properties.autoCutoverConfiguration = { + autoCutover: (this._databaseBackup.migrationMode === MigrationMode.OFFLINE ? true : false) + }; + } break; } requestBody.properties.sourceDatabaseName = this._migrationDbs[i]; diff --git a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts index ee1408e658..2b20bb3f53 100644 --- a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts @@ -202,7 +202,7 @@ export class AccountsSelectionPage extends MigrationWizardPage { selectDropDownIndex(this._azureAccountsDropdown, 0); } - public async onPageEnter(): Promise { + public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { this.wizard.registerNavigationValidator(async pageChangeInfo => { try { if (!this.migrationStateModel._azureAccount?.isStale) { @@ -221,7 +221,7 @@ export class AccountsSelectionPage extends MigrationWizardPage { }); } - public async onPageLeave(): Promise { + public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { } protected async handleStateChange(e: StateChangeEvent): Promise { diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index dcc5cbb972..71a878e8de 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -8,17 +8,25 @@ import * as vscode from 'vscode'; import { EOL } from 'os'; import { getStorageAccountAccessKeys } from '../api/azure'; import { MigrationWizardPage } from '../models/migrationWizardPage'; -import { Blob, MigrationSourceAuthenticationType, MigrationStateModel, MigrationTargetType, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; +import { Blob, MigrationMode, MigrationSourceAuthenticationType, MigrationStateModel, MigrationTargetType, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../constants/strings'; import { IconPathHelper } from '../constants/iconPathHelper'; import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; import { findDropDownItemIndex, selectDropDownIndex } from '../api/utils'; -const WIZARD_TABLE_COLUMN_WIDTH = '200px'; +const WIZARD_TABLE_COLUMN_WIDTH = '150px'; + +const blobResourceGroupErrorStrings = [constants.RESOURCE_GROUP_NOT_FOUND]; +const blobStorageAccountErrorStrings = [constants.NO_STORAGE_ACCOUNT_FOUND, constants.SELECT_RESOURCE_GROUP]; +const blobContainerErrorStrings = [constants.NO_BLOBCONTAINERS_FOUND, constants.SELECT_STORAGE_ACCOUNT]; +const blobFileErrorStrings = [constants.NO_BLOBFILES_FOUND, constants.SELECT_BLOB_CONTAINER]; export class DatabaseBackupPage extends MigrationWizardPage { private _view!: azdata.ModelView; + private _networkShareButton!: azdata.RadioButtonComponent; + private _blobContainerButton!: azdata.RadioButtonComponent; + private _networkShareContainer!: azdata.FlexContainer; private _windowsUserAccountText!: azdata.InputBoxComponent; private _passwordText!: azdata.InputBoxComponent; @@ -33,6 +41,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { private _blobContainerResourceGroupDropdowns!: azdata.DropDownComponent[]; private _blobContainerStorageAccountDropdowns!: azdata.DropDownComponent[]; private _blobContainerDropdowns!: azdata.DropDownComponent[]; + private _blobContainerLastBackupFileDropdowns!: azdata.DropDownComponent[]; private _networkShareStorageAccountDetails!: azdata.FlexContainer; private _networkShareContainerSubscription!: azdata.InputBoxComponent; @@ -107,7 +116,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { } }).component(); - const networkShareButton = this._view.modelBuilder.radioButton() + this._networkShareButton = this._view.modelBuilder.radioButton() .withProps({ name: buttonGroup, label: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL, @@ -116,13 +125,13 @@ export class DatabaseBackupPage extends MigrationWizardPage { } }).component(); - this._disposables.push(networkShareButton.onDidChangeCheckedState((e) => { + this._disposables.push(this._networkShareButton.onDidChangeCheckedState((e) => { if (e) { this.switchNetworkContainerFields(NetworkContainerType.NETWORK_SHARE); } })); - const blobContainerButton = this._view.modelBuilder.radioButton() + this._blobContainerButton = this._view.modelBuilder.radioButton() .withProps({ name: buttonGroup, label: constants.DATABASE_BACKUP_NC_BLOB_STORAGE_RADIO_LABEL, @@ -131,7 +140,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { } }).component(); - this._disposables.push(blobContainerButton.onDidChangeCheckedState((e) => { + this._disposables.push(this._blobContainerButton.onDidChangeCheckedState((e) => { if (e) { this.switchNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER); } @@ -140,8 +149,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { const flexContainer = this._view.modelBuilder.flexContainer().withItems( [ selectLocationText, - networkShareButton, - blobContainerButton + this._networkShareButton, + this._blobContainerButton ] ).withLayout({ flexFlow: 'column' @@ -472,6 +481,14 @@ export class DatabaseBackupPage extends MigrationWizardPage { headerCssStyles: headerCssStyles, isReadOnly: true, width: WIZARD_TABLE_COLUMN_WIDTH + }, + { + displayName: constants.BLOB_CONTAINER_LAST_BACKUP_FILE, + valueType: azdata.DeclarativeDataType.component, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles, + isReadOnly: true, + width: WIZARD_TABLE_COLUMN_WIDTH, } ] }).component(); @@ -640,9 +657,20 @@ export class DatabaseBackupPage extends MigrationWizardPage { } - public async onPageEnter(): Promise { - + public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { if (this.migrationStateModel.refreshDatabaseBackupPage) { + const isOfflineMigration = this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE; + const lastBackupFileColumnIndex = this._blobContainerTargetDatabaseNamesTable.columns.length - 1; + this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden = !isOfflineMigration; + + this._networkShareButton.checked = false; + this._networkTableContainer.display = 'none'; + this._networkShareContainer.updateCssStyles({ 'display': 'none' }); + + this._blobContainerButton.checked = false; + this._blobTableContainer.display = 'none'; + this._blobContainer.updateCssStyles({ 'display': 'none' }); + const connectionProfile = await this.migrationStateModel.getSourceConnectionProfile(); const queryProvider = azdata.dataprotocol.getProvider((await this.migrationStateModel.getSourceConnectionProfile()).providerId, azdata.DataProviderType.QueryProvider); const query = 'select SUSER_NAME()'; @@ -658,6 +686,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._blobContainerResourceGroupDropdowns = []; this._blobContainerStorageAccountDropdowns = []; this._blobContainerDropdowns = []; + this._blobContainerLastBackupFileDropdowns = []; if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI) { this._existingDatabases = await this.migrationStateModel.getManagedDatabases(); @@ -722,48 +751,83 @@ export class DatabaseBackupPage extends MigrationWizardPage { width: WIZARD_TABLE_COLUMN_WIDTH, editable: true, fireOnTextChange: true, + required: true, }).component(); + + const blobContainerStorageAccountDropdown = this._view.modelBuilder.dropDown().withProps({ + ariaLabel: constants.BLOB_CONTAINER_STORAGE_ACCOUNT, + width: WIZARD_TABLE_COLUMN_WIDTH, + editable: true, + fireOnTextChange: true, + required: true, + enabled: false, + }).component(); + + const blobContainerDropdown = this._view.modelBuilder.dropDown().withProps({ + ariaLabel: constants.BLOB_CONTAINER, + width: WIZARD_TABLE_COLUMN_WIDTH, + editable: true, + fireOnTextChange: true, + required: true, + enabled: false, + }).component(); + + const blobContainerLastBackupFileDropdown = this._view.modelBuilder.dropDown().withProps({ + ariaLabel: constants.BLOB_CONTAINER_LAST_BACKUP_FILE, + width: WIZARD_TABLE_COLUMN_WIDTH, + editable: true, + fireOnTextChange: true, + required: true, + enabled: false, + }).component(); + this._disposables.push(blobContainerResourceDropdown.onValueChanged(async (value) => { const selectedIndex = findDropDownItemIndex(blobContainerResourceDropdown, value); - if (selectedIndex > -1 && value !== constants.RESOURCE_GROUP_NOT_FOUND) { + if (selectedIndex > -1 && !blobResourceGroupErrorStrings.includes(value)) { this.migrationStateModel._databaseBackup.blobs[index].resourceGroup = this.migrationStateModel.getAzureResourceGroup(selectedIndex); + await this.loadBlobStorageDropdown(index); + blobContainerStorageAccountDropdown.updateProperties({ enabled: true }); + } else { + this.disableBlobTableDropdowns(index, constants.RESOURCE_GROUP); } - - await this.loadblobStorageDropdown(index); })); this._blobContainerResourceGroupDropdowns.push(blobContainerResourceDropdown); - const blobContainerStorageAccountDropdown = this._view.modelBuilder.dropDown() - .withProps({ - ariaLabel: constants.BLOB_CONTAINER_STORAGE_ACCOUNT, - width: WIZARD_TABLE_COLUMN_WIDTH, - editable: true, - fireOnTextChange: true, - }).component(); - this._disposables.push(blobContainerStorageAccountDropdown.onValueChanged(async (value) => { const selectedIndex = findDropDownItemIndex(blobContainerStorageAccountDropdown, value); - if (selectedIndex > -1 && value !== constants.NO_STORAGE_ACCOUNT_FOUND) { + if (selectedIndex > -1 && !blobStorageAccountErrorStrings.includes(value)) { this.migrationStateModel._databaseBackup.blobs[index].storageAccount = this.migrationStateModel.getStorageAccount(selectedIndex); + await this.loadBlobContainerDropdown(index); + blobContainerDropdown.updateProperties({ enabled: true }); + } else { + this.disableBlobTableDropdowns(index, constants.STORAGE_ACCOUNT); } - await this.loadBlobContainerDropdown(index); })); this._blobContainerStorageAccountDropdowns.push(blobContainerStorageAccountDropdown); - const blobContainerDropdown = this._view.modelBuilder.dropDown() - .withProps({ - ariaLabel: constants.BLOB_CONTAINER, - width: WIZARD_TABLE_COLUMN_WIDTH, - editable: true, - fireOnTextChange: true, - }).component(); - this._disposables.push(blobContainerDropdown.onValueChanged(value => { + this._disposables.push(blobContainerDropdown.onValueChanged(async (value) => { const selectedIndex = findDropDownItemIndex(blobContainerDropdown, value); - if (selectedIndex > -1 && value !== constants.NO_BLOBCONTAINERS_FOUND) { + if (selectedIndex > -1 && !blobContainerErrorStrings.includes(value)) { this.migrationStateModel._databaseBackup.blobs[index].blobContainer = this.migrationStateModel.getBlobContainer(selectedIndex); + if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { + await this.loadBlobLastBackupFileDropdown(index); + blobContainerLastBackupFileDropdown.updateProperties({ enabled: true }); + } + } else { + this.disableBlobTableDropdowns(index, constants.BLOB_CONTAINER); } })); this._blobContainerDropdowns.push(blobContainerDropdown); + + if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { + this._disposables.push(blobContainerLastBackupFileDropdown.onValueChanged(value => { + const selectedIndex = findDropDownItemIndex(blobContainerLastBackupFileDropdown, value); + if (selectedIndex > -1 && !blobFileErrorStrings.includes(value)) { + this.migrationStateModel._databaseBackup.blobs[index].lastBackupFile = this.migrationStateModel.getBlobLastBackupFileName(selectedIndex); + } + })); + this._blobContainerLastBackupFileDropdowns.push(blobContainerLastBackupFileDropdown); + } }); @@ -799,13 +863,17 @@ export class DatabaseBackupPage extends MigrationWizardPage { targetRow.push({ value: this._blobContainerDropdowns[index] }); + targetRow.push({ + value: this._blobContainerLastBackupFileDropdowns[index] + }); data.push(targetRow); }); - this._blobContainerTargetDatabaseNamesTable.dataValues = data; + await this._blobContainerTargetDatabaseNamesTable.setDataValues(data); + await this.getSubscriptionValues(); this.migrationStateModel.refreshDatabaseBackupPage = false; } - await this.getSubscriptionValues(); + this.wizard.registerNavigationValidator((pageChangeInfo) => { if (pageChangeInfo.newPage < pageChangeInfo.lastPage) { return true; @@ -823,22 +891,30 @@ export class DatabaseBackupPage extends MigrationWizardPage { } break; case NetworkContainerType.BLOB_CONTAINER: - this._blobContainerResourceGroupDropdowns.forEach(v => { - if ((v.value).displayName === constants.RESOURCE_GROUP_NOT_FOUND) { - errors.push(constants.INVALID_RESOURCE_GROUP_ERROR); + this._blobContainerResourceGroupDropdowns.forEach((v, index) => { + if (this.shouldDisplayBlobDropdownError(v, [constants.RESOURCE_GROUP_NOT_FOUND])) { + errors.push(constants.INVALID_BLOB_RESOURCE_GROUP_ERROR(this.migrationStateModel._migrationDbs[index])); } }); - this._blobContainerStorageAccountDropdowns.forEach(v => { - if ((v.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) { - errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR); + this._blobContainerStorageAccountDropdowns.forEach((v, index) => { + if (this.shouldDisplayBlobDropdownError(v, [constants.NO_STORAGE_ACCOUNT_FOUND, constants.SELECT_RESOURCE_GROUP])) { + errors.push(constants.INVALID_BLOB_STORAGE_ACCOUNT_ERROR(this.migrationStateModel._migrationDbs[index])); } }); - this._blobContainerDropdowns.forEach(v => { - if ((v.value).displayName === constants.NO_BLOBCONTAINERS_FOUND) { - errors.push(constants.INVALID_BLOBCONTAINER_ERROR); + this._blobContainerDropdowns.forEach((v, index) => { + if (this.shouldDisplayBlobDropdownError(v, [constants.NO_BLOBCONTAINERS_FOUND, constants.SELECT_STORAGE_ACCOUNT])) { + errors.push(constants.INVALID_BLOB_CONTAINER_ERROR(this.migrationStateModel._migrationDbs[index])); } }); + if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { + this._blobContainerLastBackupFileDropdowns.forEach((v, index) => { + if (this.shouldDisplayBlobDropdownError(v, [constants.NO_BLOBFILES_FOUND, constants.SELECT_BLOB_CONTAINER])) { + errors.push(constants.INVALID_BLOB_LAST_BACKUP_FILE_ERROR(this.migrationStateModel._migrationDbs[index])); + } + }); + } + if (errors.length > 0) { const duplicates: Map = new Map(); for (let i = 0; i < this.migrationStateModel._targetDatabaseNames.length; i++) { @@ -877,26 +953,27 @@ export class DatabaseBackupPage extends MigrationWizardPage { }); } - public async onPageLeave(): Promise { + public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { try { - switch (this.migrationStateModel._databaseBackup.networkContainerType) { - case NetworkContainerType.BLOB_CONTAINER: - for (let i = 0; i < this.migrationStateModel._databaseBackup.blobs.length; i++) { - const storageAccount = this.migrationStateModel._databaseBackup.blobs[i].storageAccount; - this.migrationStateModel._databaseBackup.blobs[i].storageKey = (await getStorageAccountAccessKeys( + if (pageChangeInfo.newPage > pageChangeInfo.lastPage) { + switch (this.migrationStateModel._databaseBackup.networkContainerType) { + case NetworkContainerType.BLOB_CONTAINER: + for (let i = 0; i < this.migrationStateModel._databaseBackup.blobs.length; i++) { + const storageAccount = this.migrationStateModel._databaseBackup.blobs[i].storageAccount; + this.migrationStateModel._databaseBackup.blobs[i].storageKey = (await getStorageAccountAccessKeys( + this.migrationStateModel._azureAccount, + this.migrationStateModel._databaseBackup.subscription, + storageAccount)).keyName1; + } + break; + case NetworkContainerType.NETWORK_SHARE: + const storageAccount = this.migrationStateModel._databaseBackup.networkShare.storageAccount; + this.migrationStateModel._databaseBackup.networkShare.storageKey = (await getStorageAccountAccessKeys( this.migrationStateModel._azureAccount, this.migrationStateModel._databaseBackup.subscription, storageAccount)).keyName1; - } - break; - case NetworkContainerType.NETWORK_SHARE: - const storageAccount = this.migrationStateModel._databaseBackup.networkShare.storageAccount; - - this.migrationStateModel._databaseBackup.networkShare.storageKey = (await getStorageAccountAccessKeys( - this.migrationStateModel._azureAccount, - this.migrationStateModel._databaseBackup.subscription, - storageAccount)).keyName1; - break; + break; + } } } finally { this.wizard.registerNavigationValidator((pageChangeInfo) => { @@ -924,7 +1001,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._blobTableContainer.display = (containerType === NetworkContainerType.BLOB_CONTAINER) ? 'inline' : 'none'; //Preserving the database Names between the 2 tables. - this.migrationStateModel._targetDatabaseNames.forEach((v, index) => { + this.migrationStateModel._targetDatabaseNames?.forEach((v, index) => { this._networkShareTargetDatabaseNames[index].value = v; this._blobContainerTargetDatabaseNames[index].value = v; }); @@ -961,6 +1038,10 @@ export class DatabaseBackupPage extends MigrationWizardPage { await this._blobContainerResourceGroupDropdowns[i].validate(); await this._blobContainerStorageAccountDropdowns[i].validate(); await this._blobContainerDropdowns[i].validate(); + + if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { + await this._blobContainerLastBackupFileDropdowns[i]?.validate(); + } } } @@ -976,7 +1057,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { this.loadNetworkStorageResourceGroup(); - this.loadblobResourceGroup(); + this.loadBlobResourceGroup(); } private async loadNetworkStorageResourceGroup(): Promise { @@ -1004,7 +1085,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { } } - private async loadblobResourceGroup(): Promise { + private async loadBlobResourceGroup(): Promise { this._blobContainerResourceGroupDropdowns.forEach(v => v.loading = true); try { const resourceGroupValues = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._databaseBackup.subscription); @@ -1019,7 +1100,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { } } - private async loadblobStorageDropdown(index: number): Promise { + private async loadBlobStorageDropdown(index: number): Promise { this._blobContainerStorageAccountDropdowns[index].loading = true; try { this._blobContainerStorageAccountDropdowns[index].values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index].resourceGroup); @@ -1044,4 +1125,41 @@ export class DatabaseBackupPage extends MigrationWizardPage { } } + private async loadBlobLastBackupFileDropdown(index: number): Promise { + this._blobContainerLastBackupFileDropdowns[index].loading = true; + try { + const blobLastBackupFileValues = await this.migrationStateModel.getBlobLastBackupFileNameValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index].storageAccount, this.migrationStateModel._databaseBackup.blobs[index].blobContainer); + this._blobContainerLastBackupFileDropdowns[index].values = blobLastBackupFileValues; + selectDropDownIndex(this._blobContainerLastBackupFileDropdowns[index], 0); + } catch (error) { + console.log(error); + } finally { + this._blobContainerLastBackupFileDropdowns[index].loading = false; + } + } + + private shouldDisplayBlobDropdownError(v: azdata.DropDownComponent, errorStrings: string[]) { + return v.value === undefined || errorStrings.includes((v.value)?.displayName); + } + + private disableBlobTableDropdowns(rowIndex: number, columnName: string): void { + const dropdownProps = { enabled: false, loading: false }; + const createDropdownValuesWithPrereq = (displayName: string, name: string = '') => [{ displayName, name }]; + + if (this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE) { + this._blobContainerLastBackupFileDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_BLOB_CONTAINER); + selectDropDownIndex(this._blobContainerLastBackupFileDropdowns[rowIndex], 0); + this._blobContainerLastBackupFileDropdowns[rowIndex]?.updateProperties(dropdownProps); + } + if (columnName === constants.BLOB_CONTAINER) { return; } + + this._blobContainerDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_STORAGE_ACCOUNT); + selectDropDownIndex(this._blobContainerDropdowns[rowIndex], 0); + this._blobContainerDropdowns[rowIndex].updateProperties(dropdownProps); + if (columnName === constants.STORAGE_ACCOUNT) { return; } + + this._blobContainerStorageAccountDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_RESOURCE_GROUP); + selectDropDownIndex(this._blobContainerStorageAccountDropdowns[rowIndex], 0); + this._blobContainerStorageAccountDropdowns[rowIndex].updateProperties(dropdownProps); + } } diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts index 028d216717..57a5482dfa 100644 --- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts +++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts @@ -101,7 +101,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage { await view.initializeModel(this._form.component()); } - public async onPageEnter(): Promise { + public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { this._subscription.value = this.migrationStateModel._targetSubscription.name; this._location.value = await getLocationDisplayName(this.migrationStateModel._targetServerInstance.location); @@ -137,7 +137,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage { }); } - public async onPageLeave(): Promise { + public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { this.wizard.registerNavigationValidator((pageChangeInfo) => { return true; }); diff --git a/extensions/sql-migration/src/wizard/migrationModePage.ts b/extensions/sql-migration/src/wizard/migrationModePage.ts index 928d38912f..f23a00467e 100644 --- a/extensions/sql-migration/src/wizard/migrationModePage.ts +++ b/extensions/sql-migration/src/wizard/migrationModePage.ts @@ -11,6 +11,7 @@ import * as constants from '../constants/strings'; export class MigrationModePage extends MigrationWizardPage { private _view!: azdata.ModelView; + private originalMigrationMode!: MigrationMode; private _disposables: vscode.Disposable[] = []; constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { @@ -34,12 +35,17 @@ export class MigrationModePage extends MigrationWizardPage { await view.initializeModel(form.component()); } - public async onPageEnter(): Promise { + public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { + this.originalMigrationMode = this.migrationStateModel._databaseBackup.migrationMode; this.wizard.registerNavigationValidator((e) => { return true; }); } - public async onPageLeave(): Promise { + public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { + if (this.originalMigrationMode !== this.migrationStateModel._databaseBackup.migrationMode) { + this.migrationStateModel.refreshDatabaseBackupPage = true; + } + this.wizard.registerNavigationValidator((e) => { return true; }); @@ -68,8 +74,6 @@ export class MigrationModePage extends MigrationWizardPage { } }).component(); - this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.ONLINE; - this._disposables.push(onlineButton.onDidChangeCheckedState((e) => { if (e) { this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.ONLINE; @@ -96,9 +100,7 @@ export class MigrationModePage extends MigrationWizardPage { this._disposables.push(offlineButton.onDidChangeCheckedState((e) => { if (e) { - vscode.window.showInformationMessage('Feature coming soon'); - onlineButton.checked = true; - //this.migrationStateModel._databaseBackup.migrationCutover = MigrationCutover.OFFLINE; TODO: Enable when offline mode is supported. + this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.OFFLINE; } })); diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index 5f3da71130..6fba41f99e 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -493,7 +493,7 @@ export class SKURecommendationPage extends MigrationWizardPage { } - public async onPageEnter(): Promise { + public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { this.wizard.registerNavigationValidator((pageChangeInfo) => { const errors: string[] = []; this.wizard.message = { @@ -552,7 +552,7 @@ export class SKURecommendationPage extends MigrationWizardPage { this.wizard.nextButton.enabled = true; } - public async onPageLeave(): Promise { + public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { this.eventListener?.dispose(); this.wizard.message = { text: '', diff --git a/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts b/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts index 299d93ccc2..7a57cc15db 100644 --- a/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts +++ b/extensions/sql-migration/src/wizard/sqlSourceConfigurationPage.ts @@ -37,12 +37,12 @@ export class SqlSourceConfigurationPage extends MigrationWizardPage { await view.initializeModel(form.component()); } - public async onPageEnter(): Promise { + public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { this.wizard.registerNavigationValidator((pageChangeInfo) => { return true; }); } - public async onPageLeave(): Promise { + public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { this.wizard.registerNavigationValidator((pageChangeInfo) => { return true; }); diff --git a/extensions/sql-migration/src/wizard/subscriptionSelectionPage.ts b/extensions/sql-migration/src/wizard/subscriptionSelectionPage.ts index 8a4c07f061..8ef86ae0ec 100644 --- a/extensions/sql-migration/src/wizard/subscriptionSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/subscriptionSelectionPage.ts @@ -173,12 +173,12 @@ export class SubscriptionSelectionPage extends MigrationWizardPage { selectDropDownIndex(this.productDropDown!.component, 0); } - public async onPageEnter(): Promise { + public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { this.disposables.push(this.migrationStateModel.stateChangeEvent(async (e) => this.onStateChangeEvent(e))); await this.populateAccountValues(); } - public async onPageLeave(): Promise { + public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { this.disposables.forEach(d => { try { d.dispose(); } catch { } }); } diff --git a/extensions/sql-migration/src/wizard/summaryPage.ts b/extensions/sql-migration/src/wizard/summaryPage.ts index ea8abd0795..56572bc50a 100644 --- a/extensions/sql-migration/src/wizard/summaryPage.ts +++ b/extensions/sql-migration/src/wizard/summaryPage.ts @@ -43,7 +43,7 @@ export class SummaryPage extends MigrationWizardPage { await view.initializeModel(form.component()); } - public async onPageEnter(): Promise { + public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { const targetDatabaseSummary = new TargetDatabaseSummaryDialog(this.migrationStateModel); const targetDatabaseHyperlink = this._view.modelBuilder.hyperlink().withProps({ url: '', @@ -120,7 +120,7 @@ export class SummaryPage extends MigrationWizardPage { } } - public async onPageLeave(): Promise { + public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { this._flexContainer.clearItems(); this.wizard.registerNavigationValidator(async (pageChangeInfo) => { return true; diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index 12c7fdb624..ddbcd5b0e7 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -67,8 +67,8 @@ export class WizardController { const newPage = pageChangeInfo.newPage; const lastPage = pageChangeInfo.lastPage; this.sendPageButtonClickEvent(pageChangeInfo).catch(e => console.log(e)); - await pages[lastPage]?.onPageLeave(); - await pages[newPage]?.onPageEnter(); + await pages[lastPage]?.onPageLeave(pageChangeInfo); + await pages[newPage]?.onPageEnter(pageChangeInfo); })); this._wizardObject.registerNavigationValidator(async validator => { @@ -82,7 +82,9 @@ export class WizardController { }); await Promise.all(wizardSetupPromises); - await pages[0].onPageEnter(); + this.extensionContext.subscriptions.push(this._wizardObject.onPageChanged(async (pageChangeInfo: azdata.window.WizardPageChangeInfo) => { + await pages[0].onPageEnter(pageChangeInfo); + })); this.extensionContext.subscriptions.push(this._wizardObject.doneButton.onClick(async (e) => { await stateModel.startMigration();