diff --git a/extensions/sql-migration/images/retry.svg b/extensions/sql-migration/images/retry.svg new file mode 100644 index 0000000000..b217549642 --- /dev/null +++ b/extensions/sql-migration/images/retry.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index f1367b6d2c..6b180e4ad6 100644 --- a/extensions/sql-migration/package.json +++ b/extensions/sql-migration/package.json @@ -2,7 +2,7 @@ "name": "sql-migration", "displayName": "%displayName%", "description": "%description%", - "version": "0.1.8", + "version": "0.1.9", "publisher": "Microsoft", "preview": true, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", @@ -81,6 +81,11 @@ "command": "sqlmigration.cancel.migration", "title": "%cancel-migration-menu%", "category": "%migration-context-menu-category%" + }, + { + "command": "sqlmigration.retry.migration", + "title": "%retry-migration-menu%", + "category": "%migration-context-menu-category%" } ], "menu": { @@ -116,6 +121,10 @@ { "command": "sqlmigration.cancel.migration", "when": "false" + }, + { + "command": "sqlmigration.retry.migration", + "when": "false" } ] }, @@ -174,4 +183,4 @@ "devDependencies": { "@types/uuid": "^8.3.1" } -} \ No newline at end of file +} diff --git a/extensions/sql-migration/package.nls.json b/extensions/sql-migration/package.nls.json index ab535bbd8e..e38506c24a 100644 --- a/extensions/sql-migration/package.nls.json +++ b/extensions/sql-migration/package.nls.json @@ -14,5 +14,6 @@ "view-target-menu": "Azure SQL Target details", "view-service-menu": "Database Migration Service details", "copy-migration-menu": "Copy migration details", - "cancel-migration-menu": "Cancel migration" + "cancel-migration-menu": "Cancel 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 a6c8cb08d5..65f5cbfba8 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -362,6 +362,15 @@ export function getFullResourceGroupFromId(id: string): string { return id.replace(RegExp('/providers/.*'), '').toLowerCase(); } +export function getResourceName(id: string): string { + const splitResourceId = id.split('/'); + return splitResourceId[splitResourceId.length - 1]; +} + +export function getBlobContainerId(resourceGroupId: string, storageAccountName: string, blobContainerName: string): string { + return `${resourceGroupId}/providers/Microsoft.Storage/storageAccounts/${storageAccountName}/blobServices/default/containers/${blobContainerName}`; +} + export interface SqlMigrationServiceProperties { name: string; subscriptionId: string; diff --git a/extensions/sql-migration/src/constants/helper.ts b/extensions/sql-migration/src/constants/helper.ts index 43b8ce0906..096cb5572b 100644 --- a/extensions/sql-migration/src/constants/helper.ts +++ b/extensions/sql-migration/src/constants/helper.ts @@ -3,7 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MigrationContext } from '../models/migrationLocalStorage'; +import { MigrationContext, MigrationStatus } from '../models/migrationLocalStorage'; +import { MigrationMode, MigrationTargetType } from '../models/stateMachine'; import * as loc from './strings'; export enum SQLTargetAssetType { @@ -22,6 +23,28 @@ export function getMigrationTargetType(migration: MigrationContext): string { } } -export function getMigrationMode(migration: MigrationContext): string { - return migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? loc.OFFLINE : loc.OFFLINE; +export function getMigrationTargetTypeEnum(migration: MigrationContext): MigrationTargetType | undefined { + switch (migration.targetManagedInstance.type) { + case SQLTargetAssetType.SQLMI: + return MigrationTargetType.SQLMI; + case SQLTargetAssetType.SQLVM: + return MigrationTargetType.SQLVM; + default: + return undefined; + } +} + +export function getMigrationMode(migration: MigrationContext): string { + return migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? loc.OFFLINE : loc.ONLINE; +} + +export function getMigrationModeEnum(migration: MigrationContext): MigrationMode { + return migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? MigrationMode.OFFLINE : MigrationMode.ONLINE; +} + +export function canRetryMigration(status: string | undefined): boolean { + return status === undefined || + status === MigrationStatus.Failed || + status === MigrationStatus.Succeeded || + status === MigrationStatus.Canceled; } diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts index b9723f6154..7916658fbb 100644 --- a/extensions/sql-migration/src/constants/iconPathHelper.ts +++ b/extensions/sql-migration/src/constants/iconPathHelper.ts @@ -39,6 +39,7 @@ export class IconPathHelper { public static newSupportRequest: IconPath; public static emptyTable: IconPath; public static addAzureAccount: IconPath; + public static retry: IconPath; public static setExtensionContext(context: vscode.ExtensionContext) { IconPathHelper.copy = { @@ -153,5 +154,9 @@ export class IconPathHelper { light: context.asAbsolutePath('images/noAzureAccount.svg'), dark: context.asAbsolutePath('images/noAzureAccount.svg') }; + IconPathHelper.retry = { + light: context.asAbsolutePath('images/retry.svg'), + dark: context.asAbsolutePath('images/retry.svg') + }; } } diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index ea0c79bad6..d2d8343509 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -537,7 +537,14 @@ export const AUTHENTICATION_TYPE = localize('sql.migration.authentication.type', export const REFRESH_BUTTON_LABEL = localize('sql.migration.status.refresh.label', 'Refresh'); // Saved Assessment Dialog - export const NEXT_LABEL = localize('sql.migration.saved.assessment.next', "Next"); export const CANCEL_LABEL = localize('sql.migration.saved.assessment.cancel', "Cancel"); export const SAVED_ASSESSMENT_RESULT = localize('sql.migration.saved.assessment.result', "Saved assessment result"); + +// Retry Migration +export const MIGRATION_CANNOT_RETRY = localize('sql.migration.cannot.retry', 'Migration cannot be retried.'); +export const RETRY_MIGRATION = localize('sql.migration.retry.migration', "Retry migration"); +export const MIGRATION_RETRY_ERROR = localize('sql.migration.retry.migration.error', 'An error occurred while retrying the migration.'); + +export const INVALID_OWNER_URI = localize('sql.migration.invalid.owner.uri.error', 'Cannot connect to the database due to invalid OwnerUri (Parameter \'OwnerUri\')'); +export const DATABASE_BACKUP_PAGE_LOAD_ERROR = localize('sql.migration.database.backup.load.error', 'An error occurred while accessing database details.'); diff --git a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts index a5e780ce70..aea2db2bae 100644 --- a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts +++ b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts @@ -33,6 +33,7 @@ interface StatusCard { } export class DashboardWidget { + private _context: vscode.ExtensionContext; private _migrationStatusCardsContainer!: azdata.FlexContainer; private _migrationStatusCardLoadingContainer!: azdata.LoadingComponent; @@ -52,7 +53,8 @@ export class DashboardWidget { private isRefreshing: boolean = false; - constructor() { + constructor(context: vscode.ExtensionContext) { + this._context = context; } private async getCurrentMigrations(): Promise { @@ -470,7 +472,7 @@ export class DashboardWidget { this._disposables.push(this._viewAllMigrationsButton.onDidClick(async (e) => { const migrationStatus = await this.getCurrentMigrations(); - new MigrationStatusDialog(migrationStatus ? migrationStatus : await this.getMigrations(), AdsMigrationStatus.ALL).initialize(); + new MigrationStatusDialog(this._context, migrationStatus ? migrationStatus : await this.getMigrations(), AdsMigrationStatus.ALL).initialize(); })); const refreshButton = view.modelBuilder.hyperlink().withProps({ @@ -596,7 +598,7 @@ export class DashboardWidget { loc.MIGRATION_IN_PROGRESS ); this._disposables.push(this._inProgressMigrationButton.container.onDidClick(async (e) => { - const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING); + const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING); dialog.initialize(); })); @@ -610,7 +612,7 @@ export class DashboardWidget { true ); this._disposables.push(this._inProgressWarningMigrationButton.container.onDidClick(async (e) => { - const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING); + const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.ONGOING); dialog.initialize(); })); @@ -623,7 +625,7 @@ export class DashboardWidget { loc.MIGRATION_COMPLETED ); this._disposables.push(this._successfulMigrationButton.container.onDidClick(async (e) => { - const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.SUCCEEDED); + const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.SUCCEEDED); dialog.initialize(); })); this._migrationStatusCardsContainer.addItem( @@ -636,7 +638,7 @@ export class DashboardWidget { loc.MIGRATION_CUTOVER_CARD ); this._disposables.push(this._completingMigrationButton.container.onDidClick(async (e) => { - const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.COMPLETING); + const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.COMPLETING); dialog.initialize(); })); this._migrationStatusCardsContainer.addItem( @@ -648,7 +650,7 @@ export class DashboardWidget { loc.MIGRATION_FAILED ); this._disposables.push(this._failedMigrationButton.container.onDidClick(async (e) => { - const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), AdsMigrationStatus.FAILED); + const dialog = new MigrationStatusDialog(this._context, await this.getCurrentMigrations(), AdsMigrationStatus.FAILED); dialog.initialize(); })); this._migrationStatusCardsContainer.addItem( diff --git a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts index ad9ea60fca..f2d45dd518 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts @@ -5,7 +5,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; -import { MigrationStateModel, MigrationTargetType } from '../../models/stateMachine'; +import { MigrationStateModel, MigrationTargetType, Page } from '../../models/stateMachine'; import { SqlDatabaseTree } from './sqlDatabasesTree'; import { SqlMigrationImpactedObjectInfo } from '../../../../mssql/src/mssql'; import { SKURecommendationPage } from '../../wizard/skuRecommendationPage'; @@ -32,7 +32,7 @@ export class AssessmentResultsDialog { constructor(public ownerUri: string, public model: MigrationStateModel, public title: string, private _skuRecommendationPage: SKURecommendationPage, private _targetType: MigrationTargetType) { this._model = model; - if (this._model.resumeAssessment && this._model.savedInfo.closedPage >= 2) { + if (this._model.resumeAssessment && this._model.savedInfo.closedPage >= Page.DatabaseBackup) { this._model._databaseAssessment = this._model.savedInfo.databaseAssessment; } this._tree = new SqlDatabaseTree(this._model, this._targetType); diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts index 5fa1ea1cea..cedbf6ff98 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts @@ -5,7 +5,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { SqlMigrationAssessmentResultItem, SqlMigrationImpactedObjectInfo } from '../../../../mssql/src/mssql'; -import { MigrationStateModel, MigrationTargetType } from '../../models/stateMachine'; +import { MigrationStateModel, MigrationTargetType, Page } from '../../models/stateMachine'; import * as constants from '../../constants/strings'; import { debounce } from '../../api/utils'; import { IconPath, IconPathHelper } from '../../constants/iconPathHelper'; @@ -142,7 +142,7 @@ export class SqlDatabaseTree { ...styles.BOLD_NOTE_CSS, 'margin': '0px 15px 0px 15px' }, - value: constants.DATABASES(0, this._model._databaseAssessment.length) + value: constants.DATABASES(0, this._model._databaseAssessment?.length) }).component(); return this._databaseCount; } @@ -187,10 +187,7 @@ export class SqlDatabaseTree { ).component(); this._disposables.push(this._databaseTable.onDataChanged(async () => { - await this._databaseCount.updateProperties({ - 'value': constants.DATABASES(this.selectedDbs().length, this._model._databaseAssessment.length) - }); - this._model._databaseSelection = this._databaseTable.dataValues; + await this.updateValuesOnSelection(); })); this._disposables.push(this._databaseTable.onRowSelected(async (e) => { @@ -200,7 +197,7 @@ export class SqlDatabaseTree { this._activeIssues = []; } this._dbName.value = this._dbNames[e.row]; - this._recommendationTitle.value = constants.ISSUES_COUNT(this._activeIssues.length); + this._recommendationTitle.value = constants.ISSUES_COUNT(this._activeIssues?.length); this._recommendation.value = constants.ISSUES_DETAILS; await this._resultComponent.updateCssStyles({ 'display': 'block' @@ -307,7 +304,7 @@ export class SqlDatabaseTree { 'display': 'none' }); this._recommendation.value = constants.WARNINGS_DETAILS; - this._recommendationTitle.value = constants.WARNINGS_COUNT(this._activeIssues.length); + this._recommendationTitle.value = constants.WARNINGS_COUNT(this._activeIssues?.length); if (this._targetType === MigrationTargetType.SQLMI) { await this.refreshResults(); } @@ -424,7 +421,7 @@ export class SqlDatabaseTree { } private handleFailedAssessment(): boolean { - const failedAssessment: boolean = this._model._assessmentResults.assessmentError !== undefined + const failedAssessment: boolean = this._model._assessmentResults?.assessmentError !== undefined || (this._model._assessmentResults?.errors?.length || 0) > 0; if (failedAssessment) { this._dialog.message = { @@ -439,12 +436,12 @@ export class SqlDatabaseTree { private getAssessmentError(): string { const errors: string[] = []; - const assessmentError = this._model._assessmentResults.assessmentError; + const assessmentError = this._model._assessmentResults?.assessmentError; if (assessmentError) { errors.push(`message: ${assessmentError.message}${EOL}stack: ${assessmentError.stack}`); } if (this._model?._assessmentResults?.errors?.length! > 0) { - errors.push(...this._model._assessmentResults.errors?.map( + errors.push(...this._model._assessmentResults?.errors?.map( e => `message: ${e.message}${EOL}errorSummary: ${e.errorSummary}${EOL}possibleCauses: ${e.possibleCauses}${EOL}guidance: ${e.guidance}${EOL}errorId: ${e.errorId}`)!); } @@ -791,7 +788,7 @@ export class SqlDatabaseTree { public async refreshResults(): Promise { if (this._targetType === MigrationTargetType.SQLMI) { - if (this._activeIssues.length === 0) { + if (this._activeIssues?.length === 0) { /// show no issues here await this._assessmentsTable.updateCssStyles({ 'display': 'none', @@ -858,7 +855,7 @@ export class SqlDatabaseTree { || []; await this._assessmentResultsTable.setDataValues(assessmentResults); - this._assessmentResultsTable.selectedRow = assessmentResults.length > 0 ? 0 : -1; + this._assessmentResultsTable.selectedRow = assessmentResults?.length > 0 ? 0 : -1; } public async refreshAssessmentDetails(selectedIssue?: SqlMigrationAssessmentResultItem): Promise { @@ -872,7 +869,7 @@ export class SqlDatabaseTree { await this._impactedObjectsTable.setDataValues(this._impactedObjects.map( (object) => [{ value: object.objectType }, { value: object.name }])); - this._impactedObjectsTable.selectedRow = this._impactedObjects.length > 0 ? 0 : -1; + this._impactedObjectsTable.selectedRow = this._impactedObjects?.length > 0 ? 0 : -1; } public refreshImpactedObject(impactedObject?: SqlMigrationImpactedObjectInfo): void { @@ -927,17 +924,17 @@ export class SqlDatabaseTree { style: styleLeft }, { - value: this._model._assessmentResults.issues.length, + value: this._model._assessmentResults?.issues?.length, style: styleRight } ] ]; - this._model._assessmentResults.databaseAssessments.sort((db1, db2) => { - return db2.issues.length - db1.issues.length; + this._model._assessmentResults?.databaseAssessments.sort((db1, db2) => { + return db2.issues?.length - db1.issues?.length; }); // Reset the dbName list so that it is in sync with the table - this._dbNames = this._model._assessmentResults.databaseAssessments.map(da => da.name); - this._model._assessmentResults.databaseAssessments.forEach((db) => { + this._dbNames = this._model._assessmentResults?.databaseAssessments.map(da => da.name); + this._model._assessmentResults?.databaseAssessments.forEach((db) => { let selectable = true; if (db.issues.find(item => item.databaseRestoreFails)) { selectable = false; @@ -954,7 +951,7 @@ export class SqlDatabaseTree { style: styleLeft }, { - value: db.issues.length, + value: db.issues?.length, style: styleRight } ] @@ -962,13 +959,27 @@ export class SqlDatabaseTree { }); } await this._instanceTable.setDataValues(instanceTableValues); - if (this._model.resumeAssessment && this._model.savedInfo.closedPage >= 2) { + if (this._model.resumeAssessment && this._model.savedInfo.closedPage >= Page.SKURecommendation && this._targetType === this._model.savedInfo.migrationTargetType) { await this._databaseTable.setDataValues(this._model.savedInfo.migrationDatabases); } else { + if (this._model.retryMigration && this._targetType === this._model.savedInfo.migrationTargetType) { + const sourceDatabaseName = this._model.savedInfo.databaseList[0]; + const sourceDatabaseIndex = this._dbNames.indexOf(sourceDatabaseName); + this._databaseTableValues[sourceDatabaseIndex][0].value = true; + } + await this._databaseTable.setDataValues(this._databaseTableValues); + await this.updateValuesOnSelection(); } } + private async updateValuesOnSelection() { + await this._databaseCount.updateProperties({ + 'value': constants.DATABASES(this.selectedDbs()?.length, this._model._databaseAssessment?.length) + }); + this._model._databaseSelection = this._databaseTable.dataValues; + } + // undo when bug #16445 is fixed private createIconTextCell(icon: IconPath, text: string): string { return text; diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts index 5f666edce9..e063040668 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts @@ -12,15 +12,19 @@ import * as loc from '../../constants/strings'; import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage, SupportedAutoRefreshIntervals, clearDialogMessage, displayDialogErrorMessage } from '../../api/utils'; import { EOL } from 'os'; import { ConfirmCutoverDialog } from './confirmCutoverDialog'; +import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog'; import * as styles from '../../constants/styles'; +import { canRetryMigration } from '../../constants/helper'; const refreshFrequency: SupportedAutoRefreshIntervals = 30000; const statusImageSize: number = 14; export class MigrationCutoverDialog { + private _context: vscode.ExtensionContext; private _dialogObject!: azdata.window.Dialog; private _view!: azdata.ModelView; private _model: MigrationCutoverDialogModel; + private _migration: MigrationContext; private _databaseTitleName!: azdata.TextComponent; private _cutoverButton!: azdata.ButtonComponent; @@ -29,6 +33,7 @@ export class MigrationCutoverDialog { private _refreshLoader!: azdata.LoadingComponent; private _copyDatabaseMigrationDetails!: azdata.ButtonComponent; private _newSupportRequest!: azdata.ButtonComponent; + private _retryButton!: azdata.ButtonComponent; private _sourceDatabaseInfoField!: InfoFieldSchema; private _sourceDetailsInfoField!: InfoFieldSchema; @@ -53,7 +58,9 @@ export class MigrationCutoverDialog { readonly _infoFieldWidth: string = '250px'; - constructor(migration: MigrationContext) { + constructor(context: vscode.ExtensionContext, migration: MigrationContext) { + this._context = context; + this._migration = migration; this._model = new MigrationCutoverDialogModel(migration); this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 'wide'); } @@ -301,11 +308,11 @@ export class MigrationCutoverDialog { iconWidth: '16px', label: loc.COMPLETE_CUTOVER, height: '20px', - width: '150px', + width: '140px', enabled: false, CSSStyles: { ...styles.BODY_CSS, - 'display': this._isOnlineMigration() ? 'inline' : 'none' + 'display': this._isOnlineMigration() ? 'block' : 'none' } }).component(); @@ -330,7 +337,7 @@ export class MigrationCutoverDialog { iconWidth: '16px', label: loc.CANCEL_MIGRATION, height: '20px', - width: '150px', + width: '140px', enabled: false, CSSStyles: { ...styles.BODY_CSS, @@ -353,6 +360,28 @@ export class MigrationCutoverDialog { flex: '0' }); + this._retryButton = this._view.modelBuilder.button().withProps({ + label: loc.RETRY_MIGRATION, + iconPath: IconPathHelper.retry, + enabled: false, + iconHeight: '16px', + iconWidth: '16px', + height: '20px', + width: '120px', + CSSStyles: { + ...styles.BODY_CSS, + } + }).component(); + this._disposables.push(this._retryButton.onDidClick( + async (e) => { + await this.refreshStatus(); + let retryMigrationDialog = new RetryMigrationDialog(this._context, this._migration); + await retryMigrationDialog.openDialog(); + } + )); + headerActions.addItem(this._retryButton, { + flex: '0', + }); this._refreshButton = this._view.modelBuilder.button().withProps({ iconPath: IconPathHelper.refresh, @@ -360,7 +389,7 @@ export class MigrationCutoverDialog { iconWidth: '16px', label: 'Refresh', height: '20px', - width: '100px', + width: '80px', CSSStyles: { ...styles.BODY_CSS, } @@ -379,7 +408,7 @@ export class MigrationCutoverDialog { iconWidth: '16px', label: loc.COPY_MIGRATION_DETAILS, height: '20px', - width: '200px', + width: '160px', CSSStyles: { ...styles.BODY_CSS, } @@ -406,7 +435,7 @@ export class MigrationCutoverDialog { iconHeight: '16px', iconWidth: '16px', height: '20px', - width: '180px', + width: '160px', CSSStyles: { ...styles.BODY_CSS, } @@ -567,7 +596,7 @@ export class MigrationCutoverDialog { if (this._isOnlineMigration()) { await this._cutoverButton.updateCssStyles({ - 'display': 'inline' + 'display': 'block' }); } @@ -720,6 +749,9 @@ export class MigrationCutoverDialog { this._cancelButton.enabled = migrationStatusTextValue === MigrationStatus.Creating || migrationStatusTextValue === MigrationStatus.InProgress; + + this._retryButton.enabled = canRetryMigration(migrationStatusTextValue); + } catch (e) { displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e); console.log(e); diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts index e62a930ebe..e4eb74540b 100644 --- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts @@ -14,7 +14,8 @@ import { clearDialogMessage, convertTimeDifferenceToDuration, displayDialogError import { SqlMigrationServiceDetailsDialog } from '../sqlMigrationService/sqlMigrationServiceDetailsDialog'; import { ConfirmCutoverDialog } from '../migrationCutover/confirmCutoverDialog'; import { MigrationCutoverDialogModel } from '../migrationCutover/migrationCutoverDialogModel'; -import { getMigrationTargetType, getMigrationMode } from '../../constants/helper'; +import { getMigrationTargetType, getMigrationMode, canRetryMigration } from '../../constants/helper'; +import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog'; const refreshFrequency: SupportedAutoRefreshIntervals = 180000; @@ -29,9 +30,11 @@ const MenuCommands = { ViewService: 'sqlmigration.view.service', CopyMigration: 'sqlmigration.copy.migration', CancelMigration: 'sqlmigration.cancel.migration', + RetryMigration: 'sqlmigration.retry.migration', }; export class MigrationStatusDialog { + private _context: vscode.ExtensionContext; private _model: MigrationStatusDialogModel; private _dialogObject!: azdata.window.Dialog; private _view!: azdata.ModelView; @@ -45,7 +48,8 @@ export class MigrationStatusDialog { private isRefreshing = false; - constructor(migrations: MigrationContext[], private _filter: AdsMigrationStatus) { + constructor(context: vscode.ExtensionContext, migrations: MigrationContext[], private _filter: AdsMigrationStatus) { + this._context = context; this._model = new MigrationStatusDialogModel(migrations); this._dialogObject = azdata.window.createModelViewDialog(loc.MIGRATION_STATUS, 'MigrationControllerDialog', 'wide'); } @@ -221,7 +225,7 @@ export class MigrationStatusDialog { async (migrationId: string) => { try { const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); - const dialog = new MigrationCutoverDialog(migration!); + const dialog = new MigrationCutoverDialog(this._context, migration!); await dialog.initialize(); } catch (e) { console.log(e); @@ -302,6 +306,25 @@ export class MigrationStatusDialog { console.log(e); } })); + + this._disposables.push(vscode.commands.registerCommand( + MenuCommands.RetryMigration, + async (migrationId: string) => { + try { + clearDialogMessage(this._dialogObject); + const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId); + if (canRetryMigration(migration?.migrationContext.properties.migrationStatus)) { + let retryMigrationDialog = new RetryMigrationDialog(this._context, migration!); + await retryMigrationDialog.openDialog(); + } + else { + await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY); + } + } catch (e) { + displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_RETRY_ERROR, e); + console.log(e); + } + })); } private async populateMigrationTable(): Promise { @@ -366,7 +389,7 @@ export class MigrationStatusDialog { }).component(); this._disposables.push(databaseHyperLink.onDidClick( - async (e) => await (new MigrationCutoverDialog(migration)).initialize())); + async (e) => await (new MigrationCutoverDialog(this._context, migration)).initialize())); return this._view.modelBuilder .flexContainer() @@ -416,6 +439,10 @@ export class MigrationStatusDialog { menuCommands.push(MenuCommands.CancelMigration); } + if (canRetryMigration(migrationStatus)) { + menuCommands.push(MenuCommands.RetryMigration); + } + return menuCommands; } diff --git a/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts b/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts new file mode 100644 index 0000000000..d3e0c05727 --- /dev/null +++ b/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import * as mssql from '../../../../mssql'; +import { azureResource } from 'azureResource'; +import { getLocations, getResourceGroupFromId, getBlobContainerId, getFullResourceGroupFromId, getResourceName } from '../../api/azure'; +import { MigrationMode, MigrationStateModel, NetworkContainerType, SavedInfo, Page } from '../../models/stateMachine'; +import { MigrationContext } from '../../models/migrationLocalStorage'; +import { WizardController } from '../../wizard/wizardController'; +import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper'; + +export class RetryMigrationDialog { + private _context: vscode.ExtensionContext; + private _migration: MigrationContext; + + constructor(context: vscode.ExtensionContext, migration: MigrationContext) { + this._context = context; + this._migration = migration; + } + + private createMigrationStateModel(migration: MigrationContext, connectionId: string, serverName: string, api: mssql.IExtension, location: azureResource.AzureLocation): MigrationStateModel { + let stateModel = new MigrationStateModel(this._context, connectionId, api.sqlMigration); + + const sourceDatabaseName = migration.migrationContext.properties.sourceDatabaseName; + let savedInfo: SavedInfo; + savedInfo = { + closedPage: Page.AzureAccount, + + // AzureAccount + azureAccount: migration.azureAccount, + azureTenant: migration.azureAccount.properties.tenants[0], + + // DatabaseSelector + selectedDatabases: [], + + // SKURecommendation + databaseAssessment: [], + databaseList: [sourceDatabaseName], + migrationDatabases: [], + serverAssessment: null, + + migrationTargetType: getMigrationTargetTypeEnum(migration)!, + subscription: migration.subscription, + location: location, + resourceGroup: { + id: getFullResourceGroupFromId(migration.targetManagedInstance.id), + name: getResourceGroupFromId(migration.targetManagedInstance.id), + subscription: migration.subscription + }, + targetServerInstance: migration.targetManagedInstance, + + // MigrationMode + migrationMode: getMigrationModeEnum(migration), + + // DatabaseBackup + targetSubscription: migration.subscription, + targetDatabaseNames: [migration.migrationContext.name], + networkContainerType: null, + networkShare: null, + blobs: [], + + // Integration Runtime + migrationServiceId: migration.migrationContext.properties.migrationService, + }; + + const getStorageAccountResourceGroup = (storageAccountResourceId: string) => { + return { + id: getFullResourceGroupFromId(storageAccountResourceId!), + name: getResourceGroupFromId(storageAccountResourceId!), + subscription: migration.subscription + }; + }; + const getStorageAccount = (storageAccountResourceId: string) => { + const storageAccountName = getResourceName(storageAccountResourceId); + return { + type: 'microsoft.storage/storageaccounts', + id: storageAccountResourceId!, + tenantId: savedInfo.azureTenant?.id!, + subscriptionId: migration.subscription.id, + name: storageAccountName, + location: savedInfo.location!.name, + }; + }; + + const sourceLocation = migration.migrationContext.properties.backupConfiguration.sourceLocation; + if (sourceLocation?.fileShare) { + savedInfo.networkContainerType = NetworkContainerType.NETWORK_SHARE; + const storageAccountResourceId = migration.migrationContext.properties.backupConfiguration.targetLocation?.storageAccountResourceId!; + savedInfo.networkShare = { + password: '', + networkShareLocation: sourceLocation?.fileShare?.path!, + windowsUser: sourceLocation?.fileShare?.username!, + storageAccount: getStorageAccount(storageAccountResourceId!), + resourceGroup: getStorageAccountResourceGroup(storageAccountResourceId!), + storageKey: '' + }; + } else if (sourceLocation?.azureBlob) { + savedInfo.networkContainerType = NetworkContainerType.BLOB_CONTAINER; + const storageAccountResourceId = sourceLocation?.azureBlob?.storageAccountResourceId!; + savedInfo.blobs = [ + { + blobContainer: { + id: getBlobContainerId(getFullResourceGroupFromId(storageAccountResourceId!), getResourceName(storageAccountResourceId!), sourceLocation?.azureBlob.blobContainerName), + name: sourceLocation?.azureBlob.blobContainerName, + subscription: migration.subscription + }, + lastBackupFile: getMigrationModeEnum(migration) === MigrationMode.OFFLINE ? migration.migrationContext.properties.offlineConfiguration.lastBackupName! : undefined, + storageAccount: getStorageAccount(storageAccountResourceId!), + resourceGroup: getStorageAccountResourceGroup(storageAccountResourceId!), + storageKey: '' + } + ]; + } + + stateModel.retryMigration = true; + stateModel.savedInfo = savedInfo; + stateModel.serverName = serverName; + return stateModel; + } + + public async openDialog(dialogName?: string) { + const locations = await getLocations(this._migration.azureAccount, this._migration.subscription); + let location: azureResource.AzureLocation; + locations.forEach(azureLocation => { + if (azureLocation.name === this._migration.targetManagedInstance.location) { + location = azureLocation; + } + }); + + let activeConnection = await azdata.connection.getCurrentConnection(); + let connectionId: string = ''; + let serverName: string = ''; + if (!activeConnection) { + const connection = await azdata.connection.openConnectionDialog(); + if (connection) { + connectionId = connection.connectionId; + serverName = connection.options.server; + } + } else { + connectionId = activeConnection.connectionId; + serverName = activeConnection.serverName; + } + + const api = (await vscode.extensions.getExtension(mssql.extension.name)?.activate()) as mssql.IExtension; + const stateModel = this.createMigrationStateModel(this._migration, connectionId, serverName, api, location!); + + const wizardController = new WizardController(this._context, stateModel); + await wizardController.openWizard(stateModel.sourceConnectionId); + } +} diff --git a/extensions/sql-migration/src/main.ts b/extensions/sql-migration/src/main.ts index 2ddfffbe48..263ed816f1 100644 --- a/extensions/sql-migration/src/main.ts +++ b/extensions/sql-migration/src/main.ts @@ -108,11 +108,7 @@ class SQLMigration { await wizardController.openWizard(connectionId); } } - } - - - } private checkSavedInfo(serverName: string): SavedInfo | undefined { @@ -138,7 +134,7 @@ let sqlMigration: SQLMigration; export async function activate(context: vscode.ExtensionContext) { sqlMigration = new SQLMigration(context); await sqlMigration.registerCommands(); - let widget = new DashboardWidget(); + let widget = new DashboardWidget(context); widget.register(); } diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index e1ff2fa30e..ec56c9a81f 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -71,6 +71,7 @@ export enum Page { export enum WizardEntryPoint { Default = 'Default', SaveAndClose = 'SaveAndClose', + RetryMigration = 'RetryMigration', } export interface DatabaseBackupModel { @@ -188,6 +189,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { public refreshDatabaseBackupPage!: boolean; public _databaseSelection!: azdata.DeclarativeTableCellValue[][]; + public retryMigration!: boolean; public resumeAssessment!: boolean; public savedInfo!: SavedInfo; public closedPage!: number; @@ -293,7 +295,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { private async generateAssessmentTelemetry(): Promise { try { - let serverIssues = this._assessmentResults.issues.map(i => { + let serverIssues = this._assessmentResults?.issues.map(i => { return { ruleId: i.ruleId, count: i.impactedObjects.length @@ -337,10 +339,10 @@ export class MigrationStateModel implements Model, vscode.Disposable { 'serverErrors': JSON.stringify(serverErrors), }, { - 'issuesCount': this._assessmentResults.issues.length, - 'warningsCount': this._assessmentResults.databaseAssessments.reduce((count, d) => count + d.issues.length, 0), + 'issuesCount': this._assessmentResults?.issues.length, + 'warningsCount': this._assessmentResults?.databaseAssessments.reduce((count, d) => count + d.issues.length, 0), 'durationInMilliseconds': endTime.getTime() - startTime.getTime(), - 'databaseCount': this._assessmentResults.databaseAssessments.length, + 'databaseCount': this._assessmentResults?.databaseAssessments.length, 'serverHostCpuCount': this._assessmentApiResponse?.assessmentResult?.cpuCoreCount, 'serverHostPhysicalMemoryInBytes': this._assessmentApiResponse?.assessmentResult?.physicalServerMemory, 'serverDatabases': this._assessmentApiResponse?.assessmentResult?.numberOfUserDatabases, @@ -626,12 +628,12 @@ export class MigrationStateModel implements Model, vscode.Disposable { public async getManagedInstanceValues(subscription: azureResource.AzureResourceSubscription, location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise { let managedInstanceValues: azdata.CategoryValue[] = []; - if (!this._azureAccount) { + if (!this._azureAccount || !subscription) { return managedInstanceValues; } try { this._targetManagedInstances = (await getAvailableManagedInstanceProducts(this._azureAccount, subscription)).filter((mi) => { - if (mi.location.toLowerCase() === location.name.toLowerCase() && mi.resourceGroup?.toLowerCase() === resourceGroup?.name.toLowerCase()) { + if (mi.location.toLowerCase() === location?.name.toLowerCase() && mi.resourceGroup?.toLowerCase() === resourceGroup?.name.toLowerCase()) { return true; } return false; @@ -678,7 +680,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { try { if (this._azureAccount && subscription && resourceGroup) { this._targetSqlVirtualMachines = (await getAvailableSqlVMs(this._azureAccount, subscription, resourceGroup)).filter((virtualMachine) => { - if (virtualMachine.location === location.name) { + if (virtualMachine?.location?.toLowerCase() === location?.name?.toLowerCase()) { if (virtualMachine.properties.sqlImageOffer) { return virtualMachine.properties.sqlImageOffer.toLowerCase().includes('-ws'); //filtering out all non windows sql vms. } @@ -996,6 +998,8 @@ export class MigrationStateModel implements Model, vscode.Disposable { let wizardEntryPoint = WizardEntryPoint.Default; if (this.resumeAssessment) { wizardEntryPoint = WizardEntryPoint.SaveAndClose; + } else if (this.retryMigration) { + wizardEntryPoint = WizardEntryPoint.RetryMigration; } if (response.status === 201 || response.status === 200) { sendSqlMigrationActionEvent( diff --git a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts index 6bd4a92e7f..e1d98d7c24 100644 --- a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts @@ -6,7 +6,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { MigrationWizardPage } from '../models/migrationWizardPage'; -import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; +import { MigrationStateModel, Page, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../constants/strings'; import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; import { deepClone, findDropDownItemIndex, selectDropDownIndex } from '../api/utils'; @@ -111,9 +111,9 @@ export class AccountsSelectionPage extends MigrationWizardPage { await this._accountTenantFlexContainer.updateCssStyles({ 'display': 'none' }); - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= 0) { + if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.AzureAccount)) { (this._azureAccountsDropdown.values)?.forEach((account, index) => { - if (account.name === this.migrationStateModel.savedInfo.azureAccount?.displayInfo.userId) { + if (account.name.toLowerCase() === this.migrationStateModel.savedInfo.azureAccount?.displayInfo.userId.toLowerCase()) { selectDropDownIndex(this._azureAccountsDropdown, index); } }); diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index 291d54094b..258b9fcc08 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -283,7 +283,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { } return true; }).component(); - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.MigrationMode) { + if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.MigrationMode)) { this._networkSharePath.value = this.migrationStateModel.savedInfo.networkShare?.networkShareLocation; } this._disposables.push(this._networkSharePath.onTextChanged(async (value) => { @@ -331,7 +331,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { } return true; }).component(); - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup) { + if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup)) { this._windowsUserAccountText.value = this.migrationStateModel.savedInfo.networkShare?.windowsUser; } this._disposables.push(this._windowsUserAccountText.onTextChanged((value) => { @@ -458,7 +458,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { return flexContainer; } - private createTargetDatabaseContainer(): azdata.FlexContainer { const headerCssStyles: azdata.CssStyles = { ...styles.LABEL_CSS, @@ -755,253 +754,265 @@ export class DatabaseBackupPage extends MigrationWizardPage { return container; } - public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { if (this.migrationStateModel.refreshDatabaseBackupPage) { - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup) { - this.migrationStateModel._migrationDbs = this.migrationStateModel.savedInfo.databaseList; - } - const isOfflineMigration = this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE; - const lastBackupFileColumnIndex = this._blobContainerTargetDatabaseNamesTable.columns.length - 1; - this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden = !isOfflineMigration; - this._blobContainerTargetDatabaseNamesTable.columns.forEach(column => { - column.width = isOfflineMigration ? WIZARD_TABLE_COLUMN_WIDTH_SMALL : WIZARD_TABLE_COLUMN_WIDTH; - }); + try { + if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup)) { + this.migrationStateModel._migrationDbs = this.migrationStateModel.savedInfo.databaseList; + } + const isOfflineMigration = this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE; + const lastBackupFileColumnIndex = this._blobContainerTargetDatabaseNamesTable.columns.length - 1; + this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden = !isOfflineMigration; + this._blobContainerTargetDatabaseNamesTable.columns.forEach(column => { + column.width = isOfflineMigration ? WIZARD_TABLE_COLUMN_WIDTH_SMALL : WIZARD_TABLE_COLUMN_WIDTH; + }); - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.MigrationMode) { - if (this.migrationStateModel.savedInfo.networkContainerType === NetworkContainerType.NETWORK_SHARE) { - this._networkShareButton.checked = true; + if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.MigrationMode)) { + if (this.migrationStateModel.savedInfo.networkContainerType === NetworkContainerType.NETWORK_SHARE) { + this._networkShareButton.checked = true; + } else { + this._networkShareButton.checked = false; + this._networkTableContainer.display = 'none'; + await this._networkShareContainer.updateCssStyles({ 'display': 'none' }); + } } else { this._networkShareButton.checked = false; this._networkTableContainer.display = 'none'; await this._networkShareContainer.updateCssStyles({ 'display': 'none' }); } - } else { - this._networkShareButton.checked = false; - this._networkTableContainer.display = 'none'; - await this._networkShareContainer.updateCssStyles({ 'display': 'none' }); - } - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.MigrationMode) { - if (this.migrationStateModel.savedInfo.networkContainerType === NetworkContainerType.BLOB_CONTAINER) { - this._blobContainerButton.checked = true; + if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.MigrationMode)) { + if (this.migrationStateModel.savedInfo.networkContainerType === NetworkContainerType.BLOB_CONTAINER) { + this._blobContainerButton.checked = true; + } else { + this._blobContainerButton.checked = false; + this._blobTableContainer.display = 'none'; + await this._blobContainer.updateCssStyles({ 'display': 'none' }); + } } else { this._blobContainerButton.checked = false; this._blobTableContainer.display = 'none'; await this._blobContainer.updateCssStyles({ 'display': 'none' }); } - } else { - this._blobContainerButton.checked = false; - this._blobTableContainer.display = 'none'; - await this._blobContainer.updateCssStyles({ 'display': 'none' }); - } - await this._targetDatabaseContainer.updateCssStyles({ 'display': 'none' }); - await this._networkShareStorageAccountDetails.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()'; - const results = await queryProvider.runQueryAndReturn(await (azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId)), query); - const username = results.rows[0][0].displayValue; - this.migrationStateModel._authenticationType = connectionProfile.authenticationType === 'SqlLogin' ? MigrationSourceAuthenticationType.Sql : connectionProfile.authenticationType === 'Integrated' ? MigrationSourceAuthenticationType.Integrated : undefined!; - this._sourceHelpText.value = constants.SQL_SOURCE_DETAILS(this.migrationStateModel._authenticationType, connectionProfile.serverName); - this._sqlSourceUsernameInput.value = username; - this._sqlSourcePassword.value = (await azdata.connection.getCredentials(this.migrationStateModel.sourceConnectionId)).password; + await this._targetDatabaseContainer.updateCssStyles({ 'display': 'none' }); + await this._networkShareStorageAccountDetails.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()'; + const results = await queryProvider.runQueryAndReturn(await (azdata.connection.getUriForConnection(this.migrationStateModel.sourceConnectionId)), query); + const username = results.rows[0][0].displayValue; + this.migrationStateModel._authenticationType = connectionProfile.authenticationType === 'SqlLogin' ? MigrationSourceAuthenticationType.Sql : connectionProfile.authenticationType === 'Integrated' ? MigrationSourceAuthenticationType.Integrated : undefined!; + this._sourceHelpText.value = constants.SQL_SOURCE_DETAILS(this.migrationStateModel._authenticationType, connectionProfile.serverName); + this._sqlSourceUsernameInput.value = username; + this._sqlSourcePassword.value = (await azdata.connection.getCredentials(this.migrationStateModel.sourceConnectionId)).password; - this._networkShareTargetDatabaseNames = []; - this._blobContainerTargetDatabaseNames = []; - this._blobContainerResourceGroupDropdowns = []; - this._blobContainerStorageAccountDropdowns = []; - this._blobContainerDropdowns = []; - this._blobContainerLastBackupFileDropdowns = []; + this._networkShareTargetDatabaseNames = []; + this._blobContainerTargetDatabaseNames = []; + this._blobContainerResourceGroupDropdowns = []; + this._blobContainerStorageAccountDropdowns = []; + this._blobContainerDropdowns = []; + this._blobContainerLastBackupFileDropdowns = []; - if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI) { - this._existingDatabases = await this.migrationStateModel.getManagedDatabases(); - } - this.migrationStateModel._targetDatabaseNames = []; - this.migrationStateModel._databaseBackup.blobs = []; - this.migrationStateModel._migrationDbs.forEach((db, index) => { - this.migrationStateModel._targetDatabaseNames.push(''); - this.migrationStateModel._databaseBackup.blobs.push({}); - const targetDatabaseInput = this._view.modelBuilder.inputBox().withProps({ - required: true, - value: db, - width: WIZARD_TABLE_COLUMN_WIDTH - }).withValidation(c => { - if (this._networkShareTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values. - c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR; - return false; - } - if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { // Making sure if database with same name is not present on the target Azure SQL - c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name); - return false; - } - if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) { - c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR; - return false; - } - return true; - }).component(); - this._disposables.push(targetDatabaseInput.onTextChanged(async (value) => { - this.migrationStateModel._targetDatabaseNames[index] = value.trim(); - await this.validateFields(); - })); - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup) { - targetDatabaseInput.value = this.migrationStateModel.savedInfo.targetDatabaseNames[index]; - } else { - targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index]; + if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI) { + this._existingDatabases = await this.migrationStateModel.getManagedDatabases(); } - this._networkShareTargetDatabaseNames.push(targetDatabaseInput); - - const blobTargetDatabaseInput = this._view.modelBuilder.inputBox().withProps({ - required: true, - value: db, - }).withValidation(c => { - if (this._blobContainerTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values. - c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR; - return false; - } - if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { // Making sure if database with same name is not present on the target Azure SQL - c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name); - return false; - } - if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) { - c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR; - return false; - } - return true; - }).component(); - this._disposables.push(blobTargetDatabaseInput.onTextChanged((value) => { - this.migrationStateModel._targetDatabaseNames[index] = value.trim(); - })); - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup) { - blobTargetDatabaseInput.value = this.migrationStateModel.savedInfo.targetDatabaseNames[index]; - } else { - targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index]; - } - this._blobContainerTargetDatabaseNames.push(blobTargetDatabaseInput); - - const blobContainerResourceDropdown = this._view.modelBuilder.dropDown().withProps({ - ariaLabel: constants.BLOB_CONTAINER_RESOURCE_GROUP, - editable: true, - fireOnTextChange: true, - required: true, - }).component(); - - const blobContainerStorageAccountDropdown = this._view.modelBuilder.dropDown().withProps({ - ariaLabel: constants.BLOB_CONTAINER_STORAGE_ACCOUNT, - editable: true, - fireOnTextChange: true, - required: true, - enabled: false, - }).component(); - - const blobContainerDropdown = this._view.modelBuilder.dropDown().withProps({ - ariaLabel: constants.BLOB_CONTAINER, - editable: true, - fireOnTextChange: true, - required: true, - enabled: false, - }).component(); - - const blobContainerLastBackupFileDropdown = this._view.modelBuilder.dropDown().withProps({ - ariaLabel: constants.BLOB_CONTAINER_LAST_BACKUP_FILE, - editable: true, - fireOnTextChange: true, - required: true, - enabled: false, - }).component(); - - this._disposables.push(blobContainerResourceDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(blobContainerResourceDropdown, value); - if (selectedIndex > -1 && !blobResourceGroupErrorStrings.includes(value)) { - this.migrationStateModel._databaseBackup.blobs[index].resourceGroup = this.migrationStateModel.getAzureResourceGroup(selectedIndex); - await this.loadBlobStorageDropdown(index); - await blobContainerStorageAccountDropdown.updateProperties({ enabled: true }); - } else { - await this.disableBlobTableDropdowns(index, constants.RESOURCE_GROUP); - } - })); - this._blobContainerResourceGroupDropdowns.push(blobContainerResourceDropdown); - - this._disposables.push(blobContainerStorageAccountDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(blobContainerStorageAccountDropdown, value); - if (selectedIndex > -1 && !blobStorageAccountErrorStrings.includes(value)) { - this.migrationStateModel._databaseBackup.blobs[index].storageAccount = this.migrationStateModel.getStorageAccount(selectedIndex); - await this.loadBlobContainerDropdown(index); - await blobContainerDropdown.updateProperties({ enabled: true }); - } else { - await this.disableBlobTableDropdowns(index, constants.STORAGE_ACCOUNT); - } - })); - this._blobContainerStorageAccountDropdowns.push(blobContainerStorageAccountDropdown); - - this._disposables.push(blobContainerDropdown.onValueChanged(async (value) => { - const selectedIndex = findDropDownItemIndex(blobContainerDropdown, value); - 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); - await blobContainerLastBackupFileDropdown.updateProperties({ enabled: true }); + this.migrationStateModel._targetDatabaseNames = []; + this.migrationStateModel._databaseBackup.blobs = []; + this.migrationStateModel._migrationDbs.forEach((db, index) => { + this.migrationStateModel._targetDatabaseNames.push(''); + this.migrationStateModel._databaseBackup.blobs.push({}); + const targetDatabaseInput = this._view.modelBuilder.inputBox().withProps({ + required: true, + value: db, + width: WIZARD_TABLE_COLUMN_WIDTH + }).withValidation(c => { + if (this._networkShareTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values. + c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR; + return false; } + if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { // Making sure if database with same name is not present on the target Azure SQL + c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name); + return false; + } + if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) { + c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR; + return false; + } + return true; + }).component(); + this._disposables.push(targetDatabaseInput.onTextChanged(async (value) => { + this.migrationStateModel._targetDatabaseNames[index] = value.trim(); + await this.validateFields(); + })); + if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup)) { + targetDatabaseInput.value = this.migrationStateModel.savedInfo.targetDatabaseNames[index]; } else { - await this.disableBlobTableDropdowns(index, constants.BLOB_CONTAINER); + targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index]; } - })); - this._blobContainerDropdowns.push(blobContainerDropdown); + this._networkShareTargetDatabaseNames.push(targetDatabaseInput); - 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); + const blobTargetDatabaseInput = this._view.modelBuilder.inputBox().withProps({ + required: true, + value: db, + }).withValidation(c => { + if (this._blobContainerTargetDatabaseNames.filter(t => t.value === c.value).length > 1) { //Making sure no databases have duplicate values. + c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR; + return false; + } + if (this.migrationStateModel._targetType === MigrationTargetType.SQLMI && this._existingDatabases.includes(c.value!)) { // Making sure if database with same name is not present on the target Azure SQL + c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name); + return false; + } + if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) { + c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR; + return false; + } + return true; + }).component(); + this._disposables.push(blobTargetDatabaseInput.onTextChanged((value) => { + this.migrationStateModel._targetDatabaseNames[index] = value.trim(); + })); + if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup)) { + blobTargetDatabaseInput.value = this.migrationStateModel.savedInfo.targetDatabaseNames[index]; + } else { + targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index]; + } + this._blobContainerTargetDatabaseNames.push(blobTargetDatabaseInput); + + const blobContainerResourceDropdown = this._view.modelBuilder.dropDown().withProps({ + ariaLabel: constants.BLOB_CONTAINER_RESOURCE_GROUP, + editable: true, + fireOnTextChange: true, + required: true, + }).component(); + + const blobContainerStorageAccountDropdown = this._view.modelBuilder.dropDown().withProps({ + ariaLabel: constants.BLOB_CONTAINER_STORAGE_ACCOUNT, + editable: true, + fireOnTextChange: true, + required: true, + enabled: false, + }).component(); + + const blobContainerDropdown = this._view.modelBuilder.dropDown().withProps({ + ariaLabel: constants.BLOB_CONTAINER, + editable: true, + fireOnTextChange: true, + required: true, + enabled: false, + }).component(); + + const blobContainerLastBackupFileDropdown = this._view.modelBuilder.dropDown().withProps({ + ariaLabel: constants.BLOB_CONTAINER_LAST_BACKUP_FILE, + editable: true, + fireOnTextChange: true, + required: true, + enabled: false, + }).component(); + + this._disposables.push(blobContainerResourceDropdown.onValueChanged(async (value) => { + const selectedIndex = findDropDownItemIndex(blobContainerResourceDropdown, value); + if (selectedIndex > -1 && !blobResourceGroupErrorStrings.includes(value)) { + this.migrationStateModel._databaseBackup.blobs[index].resourceGroup = this.migrationStateModel.getAzureResourceGroup(selectedIndex); + await this.loadBlobStorageDropdown(index); + await blobContainerStorageAccountDropdown.updateProperties({ enabled: true }); + } else { + await this.disableBlobTableDropdowns(index, constants.RESOURCE_GROUP); } })); - this._blobContainerLastBackupFileDropdowns.push(blobContainerLastBackupFileDropdown); + this._blobContainerResourceGroupDropdowns.push(blobContainerResourceDropdown); + + this._disposables.push(blobContainerStorageAccountDropdown.onValueChanged(async (value) => { + const selectedIndex = findDropDownItemIndex(blobContainerStorageAccountDropdown, value); + if (selectedIndex > -1 && !blobStorageAccountErrorStrings.includes(value)) { + this.migrationStateModel._databaseBackup.blobs[index].storageAccount = this.migrationStateModel.getStorageAccount(selectedIndex); + await this.loadBlobContainerDropdown(index); + await blobContainerDropdown.updateProperties({ enabled: true }); + } else { + await this.disableBlobTableDropdowns(index, constants.STORAGE_ACCOUNT); + } + })); + this._blobContainerStorageAccountDropdowns.push(blobContainerStorageAccountDropdown); + + this._disposables.push(blobContainerDropdown.onValueChanged(async (value) => { + const selectedIndex = findDropDownItemIndex(blobContainerDropdown, value); + 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); + await blobContainerLastBackupFileDropdown.updateProperties({ enabled: true }); + } + } else { + await this.disableBlobTableDropdowns(index, constants.BLOB_CONTAINER); + } + })); + this._blobContainerDropdowns.push(blobContainerDropdown); + + if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) { + this._disposables.push(blobContainerLastBackupFileDropdown.onValueChanged(value => { + 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); + } + }); + + + let data: azdata.DeclarativeTableCellValue[][] = []; + this.migrationStateModel._migrationDbs.forEach((db, index) => { + const targetRow: azdata.DeclarativeTableCellValue[] = []; + targetRow.push({ + value: db + }); + targetRow.push({ + value: this._networkShareTargetDatabaseNames[index] + }); + data.push(targetRow); + }); + this._networkShareTargetDatabaseNamesTable.dataValues = data; + + data = []; + this.migrationStateModel._migrationDbs.forEach((db, index) => { + const targetRow: azdata.DeclarativeTableCellValue[] = []; + targetRow.push({ + value: db + }); + targetRow.push({ + value: this._blobContainerTargetDatabaseNames[index] + }); + targetRow.push({ + value: this._blobContainerResourceGroupDropdowns[index] + }); + targetRow.push({ + value: this._blobContainerStorageAccountDropdowns[index] + }); + targetRow.push({ + value: this._blobContainerDropdowns[index] + }); + targetRow.push({ + value: this._blobContainerLastBackupFileDropdowns[index] + }); + data.push(targetRow); + }); + await this._blobContainerTargetDatabaseNamesTable.setDataValues(data); + + await this.getSubscriptionValues(); + this.migrationStateModel.refreshDatabaseBackupPage = false; + } catch (error) { + console.log(error); + let errorText = error?.message; + if (errorText === constants.INVALID_OWNER_URI) { + errorText = constants.DATABASE_BACKUP_PAGE_LOAD_ERROR; } - }); - - - let data: azdata.DeclarativeTableCellValue[][] = []; - this.migrationStateModel._migrationDbs.forEach((db, index) => { - const targetRow: azdata.DeclarativeTableCellValue[] = []; - targetRow.push({ - value: db - }); - targetRow.push({ - value: this._networkShareTargetDatabaseNames[index] - }); - data.push(targetRow); - }); - this._networkShareTargetDatabaseNamesTable.dataValues = data; - - data = []; - this.migrationStateModel._migrationDbs.forEach((db, index) => { - const targetRow: azdata.DeclarativeTableCellValue[] = []; - targetRow.push({ - value: db - }); - targetRow.push({ - value: this._blobContainerTargetDatabaseNames[index] - }); - targetRow.push({ - value: this._blobContainerResourceGroupDropdowns[index] - }); - targetRow.push({ - value: this._blobContainerStorageAccountDropdowns[index] - }); - targetRow.push({ - value: this._blobContainerDropdowns[index] - }); - targetRow.push({ - value: this._blobContainerLastBackupFileDropdowns[index] - }); - data.push(targetRow); - }); - await this._blobContainerTargetDatabaseNamesTable.setDataValues(data); - - await this.getSubscriptionValues(); - this.migrationStateModel.refreshDatabaseBackupPage = false; + this.wizard.message = { + text: errorText, + description: error?.stack, + level: azdata.window.MessageLevel.Error + }; + } } this.wizard.registerNavigationValidator((pageChangeInfo) => { @@ -1131,7 +1142,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._blobTableContainer.display = (containerType === NetworkContainerType.BLOB_CONTAINER) ? 'inline' : 'none'; //Preserving the database Names between the 2 tables. - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup) { + if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup)) { this.migrationStateModel._targetDatabaseNames = this.migrationStateModel.savedInfo.targetDatabaseNames; } @@ -1150,7 +1161,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { await this.validateFields(); } - private async validateFields(): Promise { await this._sqlSourceUsernameInput.validate(); await this._sqlSourcePassword.validate(); @@ -1175,7 +1185,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { } private async getSubscriptionValues(): Promise { - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup) { + if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup)) { this.migrationStateModel._targetSubscription = this.migrationStateModel.savedInfo.targetSubscription; this.migrationStateModel._targetServerInstance = this.migrationStateModel.savedInfo.targetServerInstance; } @@ -1196,7 +1206,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { this._networkShareStorageAccountResourceGroupDropdown.loading = true; try { this._networkShareStorageAccountResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._databaseBackup.subscription); - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup && this._networkShareStorageAccountResourceGroupDropdown.values) { + if (this.hasSavedInfo(NetworkContainerType.NETWORK_SHARE, this._networkShareStorageAccountResourceGroupDropdown.values)) { this._networkShareStorageAccountResourceGroupDropdown.values.forEach((resource, index) => { if ((resource).name.toLowerCase() === this.migrationStateModel.savedInfo?.networkShare?.resourceGroup?.id?.toLowerCase()) { selectDropDownIndex(this._networkShareStorageAccountResourceGroupDropdown, index); @@ -1231,7 +1241,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { const resourceGroupValues = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._databaseBackup.subscription); this._blobContainerResourceGroupDropdowns.forEach((dropDown, index) => { dropDown.values = resourceGroupValues; - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup && dropDown.values) { + if (this.hasSavedInfo(NetworkContainerType.BLOB_CONTAINER, dropDown.values)) { dropDown.values.forEach((resource, resourceIndex) => { if ((resource).name.toLowerCase() === this.migrationStateModel.savedInfo?.blobs[index]?.resourceGroup?.id?.toLowerCase()) { selectDropDownIndex(dropDown, resourceIndex); @@ -1251,8 +1261,8 @@ export class DatabaseBackupPage extends MigrationWizardPage { 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); - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup && this._blobContainerStorageAccountDropdowns[index].values && this.migrationStateModel.savedInfo.blobs[index].storageAccount) { + this._blobContainerStorageAccountDropdowns[index].values = await this.migrationStateModel.getStorageAccountValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index]?.resourceGroup); + if (this.hasSavedInfo(NetworkContainerType.BLOB_CONTAINER, this._blobContainerStorageAccountDropdowns[index].values && this.migrationStateModel.savedInfo.blobs[index]?.storageAccount)) { this._blobContainerStorageAccountDropdowns[index].values!.forEach((resource, resourceIndex) => { if ((resource).name.toLowerCase() === this.migrationStateModel.savedInfo?.blobs[index]?.storageAccount?.id?.toLowerCase()) { selectDropDownIndex(this._blobContainerStorageAccountDropdowns[index], resourceIndex); @@ -1271,9 +1281,9 @@ export class DatabaseBackupPage extends MigrationWizardPage { private async loadBlobContainerDropdown(index: number): Promise { this._blobContainerDropdowns[index].loading = true; try { - const blobContainerValues = await this.migrationStateModel.getBlobContainerValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index].storageAccount); + const blobContainerValues = await this.migrationStateModel.getBlobContainerValues(this.migrationStateModel._databaseBackup.subscription, this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount); this._blobContainerDropdowns[index].values = blobContainerValues; - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup && this._blobContainerDropdowns[index].values && this.migrationStateModel.savedInfo.blobs[index].blobContainer) { + if (this.hasSavedInfo(NetworkContainerType.BLOB_CONTAINER, this._blobContainerDropdowns[index].values && this.migrationStateModel.savedInfo.blobs[index]?.blobContainer)) { this._blobContainerDropdowns[index].values!.forEach((resource, resourceIndex) => { if ((resource).name.toLowerCase() === this.migrationStateModel.savedInfo?.blobs[index]?.blobContainer?.id?.toLowerCase()) { selectDropDownIndex(this._blobContainerDropdowns[index], resourceIndex); @@ -1292,9 +1302,9 @@ 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); + 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; - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup && this._blobContainerLastBackupFileDropdowns[index].values && this.migrationStateModel.savedInfo.blobs[index].lastBackupFile) { + if (this.hasSavedInfo(NetworkContainerType.BLOB_CONTAINER, this._blobContainerLastBackupFileDropdowns[index].values && this.migrationStateModel.savedInfo.blobs[index]?.lastBackupFile)) { this._blobContainerLastBackupFileDropdowns[index].values!.forEach((resource, resourceIndex) => { if ((resource).name.toLowerCase() === this.migrationStateModel.savedInfo?.blobs[index]?.lastBackupFile!.toLowerCase()) { selectDropDownIndex(this._blobContainerLastBackupFileDropdowns[index], resourceIndex); @@ -1334,4 +1344,12 @@ export class DatabaseBackupPage extends MigrationWizardPage { selectDropDownIndex(this._blobContainerStorageAccountDropdowns[rowIndex], 0); await this._blobContainerStorageAccountDropdowns[rowIndex].updateProperties(dropdownProps); } + + private hasSavedInfo(networkContainerType: NetworkContainerType, values: any): boolean { + if (this.migrationStateModel._databaseBackup.networkContainerType === networkContainerType && + (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseBackup) && values)) { + return true; + } + return false; + } } diff --git a/extensions/sql-migration/src/wizard/databaseSelectorPage.ts b/extensions/sql-migration/src/wizard/databaseSelectorPage.ts index b3f4b2f355..424225fd18 100644 --- a/extensions/sql-migration/src/wizard/databaseSelectorPage.ts +++ b/extensions/sql-migration/src/wizard/databaseSelectorPage.ts @@ -6,7 +6,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { MigrationWizardPage } from '../models/migrationWizardPage'; -import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; +import { MigrationStateModel, Page, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../constants/strings'; import { IconPath, IconPathHelper } from '../constants/iconPathHelper'; import { debounce } from '../api/utils'; @@ -275,17 +275,26 @@ export class DatabaseSelectorPage extends MigrationWizardPage { } ).component(); - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= 1) { + if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.DatabaseSelector) { await this._databaseSelectorTable.setDataValues(this.migrationStateModel.savedInfo.selectedDatabases); } else { + if (this.migrationStateModel.retryMigration) { + const sourceDatabaseName = this.migrationStateModel.savedInfo.databaseList[0]; + this._databaseTableValues.forEach((row, index) => { + const dbName = row[1].value as string; + if (dbName?.toLowerCase() === sourceDatabaseName?.toLowerCase()) { + row[0].value = true; + } else { + row[0].enabled = false; + } + }); + } await this._databaseSelectorTable.setDataValues(this._databaseTableValues); + await this.updateValuesOnSelection(); } + this._disposables.push(this._databaseSelectorTable.onDataChanged(async () => { - await this._dbCount.updateProperties({ - 'value': constants.DATABASES_SELECTED(this.selectedDbs().length, this._databaseTableValues.length) - }); - this.migrationStateModel._databaseAssessment = this.selectedDbs(); - this.migrationStateModel.databaseSelectorTableValues = this._databaseSelectorTable.dataValues; + await this.updateValuesOnSelection(); })); const flex = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', @@ -314,6 +323,14 @@ export class DatabaseSelectorPage extends MigrationWizardPage { return result; } + private async updateValuesOnSelection() { + await this._dbCount.updateProperties({ + 'value': constants.DATABASES_SELECTED(this.selectedDbs().length, this._databaseTableValues.length) + }); + this.migrationStateModel._databaseAssessment = this.selectedDbs(); + this.migrationStateModel.databaseSelectorTableValues = this._databaseSelectorTable.dataValues; + } + // undo when bug #16445 is fixed private createIconTextCell(icon: IconPath, text: string): string { return text; diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts index 6c3b652928..5318e86f1c 100644 --- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts +++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts @@ -86,7 +86,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage { } public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.IntegrationRuntime) { + if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.IntegrationRuntime)) { this.migrationStateModel._targetSubscription = this.migrationStateModel.savedInfo.targetSubscription; this.migrationStateModel._targetServerInstance = this.migrationStateModel.savedInfo.targetServerInstance; } @@ -391,7 +391,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage { this._resourceGroupDropdown.loading = true; try { this._resourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._targetSubscription); - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.IntegrationRuntime && this._resourceGroupDropdown.values) { + if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.IntegrationRuntime && this._resourceGroupDropdown.values)) { this._resourceGroupDropdown.values.forEach((resource, resourceIndex) => { const resourceId = this.migrationStateModel.savedInfo?.migrationServiceId?.toLowerCase(); if (resourceId && (resource).name.toLowerCase() === getFullResourceGroupFromId(resourceId)) { @@ -409,8 +409,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage { try { this._dmsDropdown.values = await this.migrationStateModel.getSqlMigrationServiceValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._targetServerInstance, resourceGroupName); const selectedSqlMigrationService = this._dmsDropdown.values.find(v => v.displayName.toLowerCase() === this.migrationStateModel._sqlMigrationService?.name?.toLowerCase()); - - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.IntegrationRuntime && this._dmsDropdown.values) { + if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.IntegrationRuntime && this._dmsDropdown.values)) { this._dmsDropdown.values.forEach((resource, resourceIndex) => { if ((resource).name.toLowerCase() === this.migrationStateModel.savedInfo?.migrationServiceId?.toLowerCase()) { selectDropDownIndex(this._dmsDropdown, resourceIndex); diff --git a/extensions/sql-migration/src/wizard/migrationModePage.ts b/extensions/sql-migration/src/wizard/migrationModePage.ts index 0d46126476..fca2167ea8 100644 --- a/extensions/sql-migration/src/wizard/migrationModePage.ts +++ b/extensions/sql-migration/src/wizard/migrationModePage.ts @@ -6,7 +6,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { MigrationWizardPage } from '../models/migrationWizardPage'; -import { MigrationMode, MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; +import { MigrationMode, MigrationStateModel, Page, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../constants/strings'; import * as styles from '../constants/styles'; @@ -113,7 +113,7 @@ export class MigrationModePage extends MigrationWizardPage { } }).component(); - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= 3) { + if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.MigrationMode)) { if (this.migrationStateModel.savedInfo.migrationMode === MigrationMode.ONLINE) { onlineButton.checked = true; offlineButton.checked = false; diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index d615ef456c..56669964bd 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -197,6 +197,14 @@ export class SKURecommendationPage extends MigrationWizardPage { })); await this._view.initializeModel(this._rootContainer); + + if (this.hasSavedInfo()) { + if (this.migrationStateModel.savedInfo.migrationTargetType === MigrationTargetType.SQLMI) { + this.migrationStateModel._miDbs = this.migrationStateModel.savedInfo.databaseList; + } else { + this.migrationStateModel._vmDbs = this.migrationStateModel.savedInfo.databaseList; + } + } } private createStatusComponent(view: azdata.ModelView): azdata.TextComponent { @@ -300,7 +308,7 @@ export class SKURecommendationPage extends MigrationWizardPage { }).component(); let serverName = ''; - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.serverName) { + if (this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.serverName)) { serverName = this.migrationStateModel.serverName; } else { serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName; @@ -505,7 +513,7 @@ export class SKURecommendationPage extends MigrationWizardPage { this.migrationStateModel._migrationDbs = miDbs; } else { this._viewAssessmentsHelperText.value = constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_VM; - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.SKURecommendation) { + if ((this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.SKURecommendation)) { this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED(this.migrationStateModel.savedInfo.databaseList.length, this.migrationStateModel._databaseAssessment.length); } else { this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED(vmDbs.length, this.migrationStateModel._databaseAssessment.length); @@ -540,12 +548,12 @@ export class SKURecommendationPage extends MigrationWizardPage { await this.migrationStateModel.getDatabaseAssessments(MigrationTargetType.SQLMI); } - const assessmentError = this.migrationStateModel._assessmentResults.assessmentError; + const assessmentError = this.migrationStateModel._assessmentResults?.assessmentError; if (assessmentError) { errors.push(`message: ${assessmentError.message}${EOL}stack: ${assessmentError.stack}`); } if (this.migrationStateModel?._assessmentResults?.errors?.length! > 0) { - errors.push(...this.migrationStateModel._assessmentResults.errors?.map( + errors.push(...this.migrationStateModel._assessmentResults?.errors?.map( e => `message: ${e.message}${EOL}errorSummary: ${e.errorSummary}${EOL}possibleCauses: ${e.possibleCauses}${EOL}guidance: ${e.guidance}${EOL}errorId: ${e.errorId}`)!); } @@ -566,11 +574,11 @@ export class SKURecommendationPage extends MigrationWizardPage { } else { this._assessmentStatusIcon.iconPath = IconPathHelper.completedMigration; this._igComponent.value = constants.ASSESSMENT_COMPLETED(serverName); - this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL(this.migrationStateModel._assessmentResults.databaseAssessments.length); + this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL(this.migrationStateModel._assessmentResults?.databaseAssessments?.length); } } - if ((this.migrationStateModel.resumeAssessment) && this.migrationStateModel.savedInfo.closedPage >= Page.SKURecommendation) { + if (this.hasSavedInfo()) { if (this.migrationStateModel.savedInfo.migrationTargetType) { this._rbg.selectedCardId = this.migrationStateModel.savedInfo.migrationTargetType; await this.refreshCardText(); @@ -602,9 +610,9 @@ export class SKURecommendationPage extends MigrationWizardPage { await this.assessmentGroupContainer.updateCssStyles({ 'display': display }); this.assessmentGroupContainer.display = display; - display = this._rbg.selectedCardId + display = (this._rbg.selectedCardId && (!failedAssessment || this._skipAssessmentCheckbox.checked) - && this.migrationStateModel._migrationDbs.length > 0 + && this.migrationStateModel._migrationDbs.length > 0) ? 'inline' : 'none'; await this._targetContainer.updateCssStyles({ 'display': display }); @@ -614,7 +622,7 @@ export class SKURecommendationPage extends MigrationWizardPage { } private async populateSubscriptionDropdown(): Promise { - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.SKURecommendation) { + if (this.hasSavedInfo()) { this.migrationStateModel._azureAccount = this.migrationStateModel.savedInfo.azureAccount; } if (!this.migrationStateModel._targetSubscription) { @@ -628,9 +636,9 @@ export class SKURecommendationPage extends MigrationWizardPage { this._managedInstanceSubscriptionDropdown.loading = false; this._resourceDropdown.loading = false; } - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= 2 && this._managedInstanceSubscriptionDropdown.values) { - this._managedInstanceSubscriptionDropdown.values.forEach((subscription, index) => { - if ((subscription).name === this.migrationStateModel.savedInfo?.subscription?.id) { + if (this.hasSavedInfo() && this._managedInstanceSubscriptionDropdown.values) { + this._managedInstanceSubscriptionDropdown.values!.forEach((subscription, index) => { + if ((subscription).name.toLowerCase() === this.migrationStateModel.savedInfo?.subscription?.id.toLowerCase()) { selectDropDownIndex(this._managedInstanceSubscriptionDropdown, index); } }); @@ -645,9 +653,9 @@ export class SKURecommendationPage extends MigrationWizardPage { this._azureLocationDropdown.loading = true; try { this._azureResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._targetSubscription); - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= 2 && this._azureResourceGroupDropdown.values) { + if (this.hasSavedInfo() && this._azureResourceGroupDropdown.values) { this._azureResourceGroupDropdown.values.forEach((resourceGroup, index) => { - if (resourceGroup.name === this.migrationStateModel.savedInfo?.resourceGroup?.id) { + if (resourceGroup.name.toLowerCase() === this.migrationStateModel.savedInfo?.resourceGroup?.id.toLowerCase()) { selectDropDownIndex(this._azureResourceGroupDropdown, index); } }); @@ -655,7 +663,7 @@ export class SKURecommendationPage extends MigrationWizardPage { selectDropDownIndex(this._azureResourceGroupDropdown, 0); } this._azureLocationDropdown.values = await this.migrationStateModel.getAzureLocationDropdownValues(this.migrationStateModel._targetSubscription); - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= 2 && this._azureLocationDropdown.values) { + if (this.hasSavedInfo() && this._azureLocationDropdown.values) { this._azureLocationDropdown.values.forEach((location, index) => { if (location.displayName === this.migrationStateModel.savedInfo?.location?.displayName) { selectDropDownIndex(this._azureLocationDropdown, index); @@ -690,9 +698,9 @@ export class SKURecommendationPage extends MigrationWizardPage { this.migrationStateModel._location, this.migrationStateModel._resourceGroup); } - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= 2 && this._resourceDropdown.values) { + if (this.hasSavedInfo() && this._resourceDropdown.values) { this._resourceDropdown.values.forEach((resource, index) => { - if (resource.displayName === this.migrationStateModel.savedInfo?.targetServerInstance?.name) { + if (resource.displayName.toLowerCase() === this.migrationStateModel.savedInfo?.targetServerInstance?.name.toLowerCase()) { selectDropDownIndex(this._resourceDropdown, index); } }); @@ -708,9 +716,6 @@ export class SKURecommendationPage extends MigrationWizardPage { public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { this.wizard.registerNavigationValidator((pageChangeInfo) => { - if (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.SKURecommendation) { - this.migrationStateModel._migrationDbs = this.migrationStateModel.savedInfo.databaseList; - } const errors: string[] = []; this.wizard.message = { text: '', @@ -785,8 +790,8 @@ export class SKURecommendationPage extends MigrationWizardPage { this._targetContainer.display = (this.migrationStateModel._migrationDbs.length === 0) ? 'none' : 'inline'; if (this.migrationStateModel._assessmentResults) { - const dbCount = this.migrationStateModel._assessmentResults.databaseAssessments?.length; - const dbWithoutIssuesCount = this.migrationStateModel._assessmentResults.databaseAssessments?.filter(db => db.issues?.length === 0).length; + const dbCount = this.migrationStateModel._assessmentResults?.databaseAssessments?.length; + const dbWithoutIssuesCount = this.migrationStateModel._assessmentResults?.databaseAssessments?.filter(db => db.issues?.length === 0).length; this._rbg.cards[0].descriptions[1].textValue = constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount); this._rbg.cards[1].descriptions[1].textValue = constants.CAN_BE_MIGRATED(dbCount, dbCount); @@ -837,6 +842,10 @@ export class SKURecommendationPage extends MigrationWizardPage { }).component(); return this._assessmentInfo; } + + private hasSavedInfo(): boolean { + return this.migrationStateModel.retryMigration || (this.migrationStateModel.resumeAssessment && this.migrationStateModel.savedInfo.closedPage >= Page.SKURecommendation); + } } diff --git a/extensions/sql-migration/src/wizard/summaryPage.ts b/extensions/sql-migration/src/wizard/summaryPage.ts index d5f2aebd96..9adbe93f91 100644 --- a/extensions/sql-migration/src/wizard/summaryPage.ts +++ b/extensions/sql-migration/src/wizard/summaryPage.ts @@ -112,7 +112,7 @@ export class SummaryPage extends MigrationWizardPage { ] ); - if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE && this.migrationStateModel._nodeNames.length > 0) { + if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE && this.migrationStateModel._nodeNames?.length > 0) { this._flexContainer.addItem(createInformationRow(this._view, constants.SHIR, this.migrationStateModel._nodeNames.join(', '))); } } diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index 482af4d8f0..bb9646c4da 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -64,7 +64,7 @@ export class WizardController { const wizardSetupPromises: Thenable[] = []; wizardSetupPromises.push(...pages.map(p => p.registerWizardContent())); wizardSetupPromises.push(this._wizardObject.open()); - if (this._model.resumeAssessment) { + if (this._model.retryMigration || this._model.resumeAssessment) { if (this._model.savedInfo.closedPage >= Page.MigrationMode) { this._model.refreshDatabaseBackupPage = true; }