diff --git a/extensions/sql-migration/images/cancel.svg b/extensions/sql-migration/images/cancel.svg new file mode 100644 index 0000000000..3978d78eaa --- /dev/null +++ b/extensions/sql-migration/images/cancel.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/sql-migration/images/warning.svg b/extensions/sql-migration/images/warning.svg new file mode 100644 index 0000000000..5baa4db4e3 --- /dev/null +++ b/extensions/sql-migration/images/warning.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index 821b8dc64d..3b21dfdf42 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.0.7", + "version": "0.0.8", "publisher": "Microsoft", "preview": true, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts index 2dfb61f865..338ee60f11 100644 --- a/extensions/sql-migration/src/constants/iconPathHelper.ts +++ b/extensions/sql-migration/src/constants/iconPathHelper.ts @@ -27,6 +27,8 @@ export class IconPathHelper { public static sqlServerLogo: IconPath; public static sqlDatabaseLogo: IconPath; public static sqlDatabaseWarningLogo: IconPath; + public static cancel: IconPath; + public static warning: IconPath; public static setExtensionContext(context: vscode.ExtensionContext) { IconPathHelper.copy = { @@ -93,5 +95,13 @@ export class IconPathHelper { light: context.asAbsolutePath('images/sqlDatabaseWarning.svg'), dark: context.asAbsolutePath('images/sqlDatabaseWarning.svg') }; + IconPathHelper.cancel = { + light: context.asAbsolutePath('images/cancel.svg'), + dark: context.asAbsolutePath('images/cancel.svg') + }; + IconPathHelper.warning = { + light: context.asAbsolutePath('images/warning.svg'), + dark: context.asAbsolutePath('images/warning.svg') + }; } } diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 85d6a4ba2d..cdd16aba2e 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -134,6 +134,7 @@ export function TARGET_BLOB_CONTAINER(dbName: string): string { export const ENTER_NETWORK_SHARE_INFORMATION = localize('sql.migration.enter.network.share.information', "Enter target names for selected source database(s)"); export const ENTER_BLOB_CONTAINER_INFORMATION = localize('sql.migration.blob.container.information', "Enter the target name and select the blob container location for selected databases"); export const ENTER_FILE_SHARE_INFORMATION = localize('sql.migration.enter.file.share.information', "Enter the target name and select the file share location of selected databases"); +export const INVALID_TARGET_NAME_ERROR = localize('sql.migration.invalid.target.name.error', "Please enter a valid name for the target database."); // integration runtime page export const IR_PAGE_TITLE = localize('sql.migration.ir.page.title', "Azure Database Migration Service"); @@ -212,8 +213,8 @@ export const SUMMARY_PAGE_TITLE = localize('sql.migration.summary.page.title', " export const AZURE_ACCOUNT_LINKED = localize('sql.migration.summary.azure.account.linked', "Azure account linked"); export const MIGRATION_TARGET = localize('sql.migration.summary.migration.target', "Migration target"); export const SUMMARY_MI_TYPE = localize('sql.migration.summary.mi.type', "Azure SQL Managed Instance"); -export const SUMMARY_VM_TYPE = localize('sql.migration.summary.vm.type', "Azure SQL Virtual Machine"); -export const SUMMARY_DATABASE_COUNT_LABEL = localize('sql.migration.summary.database.count', "Number of database to be migrated"); +export const SUMMARY_VM_TYPE = localize('sql.migration.summary.vm.type', "SQL Server on Azure Virtual Machine"); +export const SUMMARY_DATABASE_COUNT_LABEL = localize('sql.migration.summary.database.count', "Database(s) to be migrated"); export const SUMMARY_AZURE_STORAGE_SUBSCRIPTION = localize('sql.migration.summary.azure.storage.subscription', "Azure storage subscription"); export const SUMMARY_AZURE_STORAGE = localize('sql.migration.summary.azure.storage', "Azure storage"); export const SUMMARY_IR_NODE = localize('sql.migration.ir.node', "Integration Runtime node"); @@ -221,6 +222,11 @@ export const NETWORK_SHARE = localize('sql.migration.network.share', "Network Sh export const BLOB_CONTAINER = localize('sql.migration.blob.container.title', "Blob Container"); export const FILE_SHARE = localize('sql.migration.file.share.title', "File Share"); export const MIGRATION_STARTED = localize('sql.migration.started.notification', "Migration in progress"); +export const SOURCE_DATABASES = localize('sql.migration.source.databases', "Source Database(s)"); +export const MODE = localize('sql.migration.mode', "Mode"); +export const BACKUP_LOCATION = localize('sql.migration.backup.location', "Backup Location"); +export const AZURE_STORAGE_ACCOUNT_TO_UPLOAD_BACKUPS = localize('sql.migration.azure.storage.account.to.upload.backups', "Azure Storage Account to Upload Backups"); +export const SHIR = localize('sql.migration.shir', "Self-hosted Integration Runtime node"); // Open notebook quick pick string export const NOTEBOOK_QUICK_PICK_PLACEHOLDER = localize('sql.migration.quick.pick.placeholder', "Select the operation you'd like to perform"); @@ -243,13 +249,21 @@ export const PRE_REQ_TITLE = localize('sql.migration.pre.req.title', "Things you export const PRE_REQ_1 = localize('sql.migration.pre.req.1', "Azure account details"); export const PRE_REQ_2 = localize('sql.migration.pre.req.2', "Azure SQL Managed Instance or SQL Server on Azure Virtual Machine"); export const PRE_REQ_3 = localize('sql.migration.pre.req.3', "Backup location details"); -export const MIGRATION_IN_PROGRESS = localize('sql.migration.migration.in.progress', "Migration in progress"); +export const MIGRATION_IN_PROGRESS = localize('sql.migration.migration.in.progress', "Database migration in progress"); export const LOG_SHIPPING_IN_PROGRESS = localize('sql.migration.log.shipping.in.progress', "Log shipping in progress"); -export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Migration completed"); +export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Database migration completed"); export const SUCCESSFULLY_MIGRATED_TO_AZURE_SQL = localize('sql.migration.successfully.migrated.to.azure.sql', "Successfully migrated to Azure SQL"); export const MIGRATION_NOT_STARTED = localize('sql.migration.migration.not.started', "Migration not started"); export const CHOOSE_TO_MIGRATE_TO_AZURE_SQL = localize('sql.migration.choose.to.migrate.to.azure.sql', "Choose to migrate to Azure SQL"); export const COMING_SOON = localize('sql.migration.coming.soon', "Coming soon"); +export function MIGRATION_INPROGRESS_WARNING(count: number) { + switch (count) { + case 1: + return localize('sql.migration.inprogress.warning.single', "{0} database has warnings", count); + default: + return localize('sql.migration.inprogress.warning.multiple', "{0} databases have warnings", count); + } +} // Azure APIs export const EASTUS2EUAP = localize('sql.migration.eastus2euap', 'East US 2 EUAP'); @@ -259,6 +273,7 @@ export const MIGRATION_CUTOVER = localize('sql.migration.cutover', "Migration cu export const SOURCE_DATABASE = localize('sql.migration.source.database', "Source database"); export const SOURCE_SERVER = localize('sql.migration.source.server', "Source server"); export const SOURCE_VERSION = localize('sql.migration.source.version', "Source version"); +export const TARGET_DATABASE_NAME = localize('sql.migration.target.database.name', "Target database name"); export const TARGET_SERVER = localize('sql.migration.target.server', "Target server"); export const TARGET_VERSION = localize('sql.migration.target.version', "Target version"); export const MIGRATION_STATUS = localize('sql.migration.migration.status', "Migration status"); @@ -295,6 +310,28 @@ export const TARGET_AZURE_SQL_INSTANCE_NAME = localize('sql.migration.target.azu export const MIGRATION_MODE = localize('sql.migration.cutover.type', "Migration Mode"); export const START_TIME = localize('sql.migration.start.time', "Start Time"); export const FINISH_TIME = localize('sql.migration.finish.time', "Finish Time"); +export function STATUS_WARNING_COUNT(status: string, count: number): string { + if (status === 'InProgress' || status === 'Creating' || status === 'Completing' || status === 'Creating') { + switch (count) { + case 0: + return localize('sql.migration.status.warning.count.none', "{0}", status); + case 1: + return localize('sql.migration.status.warning.count.single', "{0} ({1} Warning)", status, count); + default: + return localize('sql.migration.status.warning.count.multiple', "{0} ({1} Warnings)", status, count); + } + } else { + switch (count) { + case 0: + return localize('sql.migration.status.error.count.none', "{0}", status); + case 1: + return localize('sql.migration.status.error.count.single', "{0} ({1} Error)", status, count); + default: + return localize('sql.migration.status.error.count.multiple', "{0} ({1} Errors)", status, count); + } + } + +} //Source Credentials page. export const SOURCE_CONFIGURATION = localize('sql.migration.source.configuration', "Source Configuration"); diff --git a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts index 4ee1d1e4fb..009dea4cd6 100644 --- a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts +++ b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts @@ -23,7 +23,10 @@ const maxWidth = 800; interface StatusCard { container: azdata.DivContainer; - count: azdata.TextComponent + count: azdata.TextComponent, + textContainer?: azdata.FlexContainer, + warningContainer?: azdata.FlexContainer, + warningText?: azdata.TextComponent, } export class DashboardWidget { @@ -34,10 +37,10 @@ export class DashboardWidget { private _inProgressMigrationButton!: StatusCard; + private _inProgressWarningMigrationButton!: StatusCard; private _successfulMigrationButton!: StatusCard; private _notStartedMigrationCard!: StatusCard; - private _migrationStatus!: MigrationContext[]; - + private _migrationStatusMap: Map = new Map(); private _viewAllMigrationsButton!: azdata.ButtonComponent; constructor() { @@ -46,6 +49,16 @@ export class DashboardWidget { }); } + private async getCurrentMigrations(): Promise { + const connectionId = (await azdata.connection.getCurrentConnection()).connectionId; + return this._migrationStatusMap.get(connectionId)!; + } + + private async setCurrentMigrations(migrations: MigrationContext[]): Promise { + const connectionId = (await azdata.connection.getCurrentConnection()).connectionId; + this._migrationStatusMap.set(connectionId, migrations); + } + public register(): void { azdata.ui.registerModelViewProvider('migration.dashboard', async (view) => { this._view = view; @@ -199,7 +212,7 @@ export class DashboardWidget { height: maxHeight, iconHeight: 32, iconPath: taskMetaData.iconPath, - iconWidth: 32, + iconWidth: 36, label: taskMetaData.title, title: taskMetaData.title, width: maxWidth, @@ -219,26 +232,47 @@ export class DashboardWidget { this._viewAllMigrationsButton.enabled = false; this._migrationStatusCardLoadingContainer.loading = true; try { - this._migrationStatus = await this.getMigrations(); - - const inProgressMigrations = this._migrationStatus.filter((value) => { + this.setCurrentMigrations(await this.getMigrations()); + const migrationStatus = await this.getCurrentMigrations(); + const inProgressMigrations = migrationStatus.filter((value) => { const status = value.migrationContext.properties.migrationStatus; const provisioning = value.migrationContext.properties.provisioningState; return status === 'InProgress' || status === 'Creating' || status === 'Completing' || provisioning === 'Creating'; }); - this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString(); + let warningCount = 0; - const successfulMigration = this._migrationStatus.filter((value) => { + for (let i = 0; i < inProgressMigrations.length; i++) { + if ( + inProgressMigrations[i].asyncOperationResult?.error?.message || + inProgressMigrations[i].migrationContext.properties.migrationFailureError?.message || + inProgressMigrations[i].migrationContext.properties.migrationStatusDetails?.fileUploadBlockingErrors || + inProgressMigrations[i].migrationContext.properties.migrationStatusDetails?.restoreBlockingReason + ) { + warningCount += 1; + } + } + + if (warningCount > 0) { + this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount); + this._inProgressMigrationButton.container.display = 'none'; + this._inProgressWarningMigrationButton.container.display = 'inline'; + } else { + this._inProgressMigrationButton.container.display = 'inline'; + this._inProgressWarningMigrationButton.container.display = 'none'; + } + this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString(); + this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString(); + + const successfulMigration = migrationStatus.filter((value) => { const status = value.migrationContext.properties.migrationStatus; return status === 'Succeeded'; }); this._successfulMigrationButton.count.value = successfulMigration.length.toString(); - const currentConnection = (await azdata.connection.getCurrentConnection()); const migrationDatabases = new Set( - this._migrationStatus.map((value) => { + migrationStatus.map((value) => { return value.migrationContext.properties.sourceDatabaseName; })); const serverDatabases = await azdata.connection.listDatabases(currentConnection.connectionId); @@ -260,26 +294,18 @@ export class DashboardWidget { private createStatusCard( cardIconPath: IconPath, cardTitle: string, - cardDescription: string ): StatusCard { const cardTitleText = this._view.modelBuilder.text().withProps({ value: cardTitle }).withProps({ CSSStyles: { - 'height': '40px', + 'height': '23px', 'margin-top': '15px', 'margin-bottom': '0px', 'width': '300px', 'font-size': '14px', } }).component(); - const cardDescriptionText = this._view.modelBuilder.text().withProps({ value: cardDescription }).withProps({ - CSSStyles: { - 'height': '0px', - 'margin-top': '0px', - 'margin-bottom': '0px', - 'width': '300px' - } - }).component(); + const cardCount = this._view.modelBuilder.text().withProps({ value: '0', CSSStyles: { @@ -289,22 +315,123 @@ export class DashboardWidget { } }).component(); + const flex = this._view.modelBuilder.flexContainer().withProps({ + CSSStyles: { + 'width': '400px', + 'height': '50px', + 'margin-top': '10px', + 'border': '1px solid', + } + }).component(); + + const img = this._view.modelBuilder.image().withProps({ + iconPath: cardIconPath!.light, + iconHeight: 24, + iconWidth: 24, + width: 64, + height: 30, + CSSStyles: { + 'margin-top': '10px' + } + }).component(); + + flex.addItem(img, { + flex: '0' + }); + flex.addItem(cardTitleText, { + flex: '0', + CSSStyles: { + 'width': '300px' + } + }); + flex.addItem(cardCount, { + flex: '0' + }); + + const compositeButton = this._view.modelBuilder.divContainer().withItems([flex]).withProps({ + ariaRole: 'button', + ariaLabel: 'show status', + clickable: true + }).component(); + return { + container: compositeButton, + count: cardCount + }; + } + + private createStatusWithSubtextCard( + cardIconPath: IconPath, + cardTitle: string, + cardDescription: string + ): StatusCard { + + const cardTitleText = this._view.modelBuilder.text().withProps({ value: cardTitle }).withProps({ + CSSStyles: { + 'height': '23px', + 'margin-top': '15px', + 'margin-bottom': '0px', + 'width': '300px', + 'font-size': '14px', + } + }).component(); + + const cardDescriptionWarning = this._view.modelBuilder.image().withProps({ + iconPath: IconPathHelper.warning, + iconWidth: 12, + iconHeight: 12, + width: 12, + height: 17 + }).component(); + + const cardDescriptionText = this._view.modelBuilder.text().withProps({ value: cardDescription }).withProps({ + CSSStyles: { + 'height': '13px', + 'margin-top': '0px', + 'margin-bottom': '0px', + 'width': '250px', + 'font-height': '13px', + 'margin': '0 0 0 4px' + } + }).component(); + + const subTextContainer = this._view.modelBuilder.flexContainer().withProps({ + CSSStyles: { + 'justify-content': 'left', + } + }).component(); + + subTextContainer.addItem(cardDescriptionWarning, { + flex: '0 0 auto' + }); + + subTextContainer.addItem(cardDescriptionText, { + flex: '0 0 auto' + }); + + const cardCount = this._view.modelBuilder.text().withProps({ + value: '0', + CSSStyles: { + 'font-size': '28px', + 'line-height': '28px', + 'margin-top': '15px' + } + }).component(); + const flexContainer = this._view.modelBuilder.flexContainer().withItems([ cardTitleText, - cardDescriptionText + subTextContainer ]).withLayout({ flexFlow: 'column' }).withProps({ CSSStyles: { 'width': '300px', - 'height': '50px' } }).component(); const flex = this._view.modelBuilder.flexContainer().withProps({ CSSStyles: { 'width': '400px', - 'height': '50px', + 'height': '70px', 'margin-top': '10px', 'border': '1px solid' } @@ -312,10 +439,13 @@ export class DashboardWidget { const img = this._view.modelBuilder.image().withProps({ iconPath: cardIconPath!.light, - iconHeight: 16, - iconWidth: 16, + iconHeight: 24, + iconWidth: 24, width: 64, - height: 50 + height: 30, + CSSStyles: { + 'margin-top': '20px' + } }).component(); flex.addItem(img, { @@ -338,7 +468,10 @@ export class DashboardWidget { }).component(); return { container: compositeButton, - count: cardCount + count: cardCount, + textContainer: flexContainer, + warningContainer: subTextContainer, + warningText: cardDescriptionText }; } @@ -377,7 +510,7 @@ export class DashboardWidget { const statusContainerTitle = view.modelBuilder.text().withProps({ value: loc.DATABASE_MIGRATION_STATUS, CSSStyles: { - 'font-size': '18px', + 'font-size': '14px', 'font-weight': 'bold', 'margin': '0px', 'width': '290px' @@ -393,7 +526,8 @@ export class DashboardWidget { }).component(); this._viewAllMigrationsButton.onDidClick(async (e) => { - new MigrationStatusDialog(this._migrationStatus ? this._migrationStatus : await this.getMigrations(), MigrationCategory.ALL).initialize(); + const migrationStatus = await this.getCurrentMigrations(); + new MigrationStatusDialog(migrationStatus ? migrationStatus : await this.getMigrations(), MigrationCategory.ALL).initialize(); }); const refreshButton = view.modelBuilder.hyperlink().withProps({ @@ -444,24 +578,37 @@ export class DashboardWidget { this._inProgressMigrationButton = this.createStatusCard( IconPathHelper.inProgressMigration, - loc.MIGRATION_IN_PROGRESS, - '' + loc.MIGRATION_IN_PROGRESS ); - this._inProgressMigrationButton.container.onDidClick((e) => { - const dialog = new MigrationStatusDialog(this._migrationStatus, MigrationCategory.ONGOING); + this._inProgressMigrationButton.container.onDidClick(async (e) => { + const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), MigrationCategory.ONGOING); dialog.initialize(); }); + this._migrationStatusCardsContainer.addItem( this._inProgressMigrationButton.container ); - this._successfulMigrationButton = this.createStatusCard( - IconPathHelper.completedMigration, - loc.MIGRATION_COMPLETED, + this._inProgressWarningMigrationButton = this.createStatusWithSubtextCard( + IconPathHelper.inProgressMigration, + loc.MIGRATION_IN_PROGRESS, '' ); - this._successfulMigrationButton.container.onDidClick((e) => { - const dialog = new MigrationStatusDialog(this._migrationStatus, MigrationCategory.SUCCEEDED); + this._inProgressWarningMigrationButton.container.onDidClick(async (e) => { + const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), MigrationCategory.ONGOING); + dialog.initialize(); + }); + + this._migrationStatusCardsContainer.addItem( + this._inProgressWarningMigrationButton.container + ); + + this._successfulMigrationButton = this.createStatusCard( + IconPathHelper.completedMigration, + loc.MIGRATION_COMPLETED + ); + this._successfulMigrationButton.container.onDidClick(async (e) => { + const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), MigrationCategory.SUCCEEDED); dialog.initialize(); }); this._migrationStatusCardsContainer.addItem( @@ -470,8 +617,7 @@ export class DashboardWidget { this._notStartedMigrationCard = this.createStatusCard( IconPathHelper.notStartedMigration, - loc.MIGRATION_NOT_STARTED, - loc.CHOOSE_TO_MIGRATE_TO_AZURE_SQL + loc.MIGRATION_NOT_STARTED ); this._notStartedMigrationCard.container.onDidClick((e) => { vscode.window.showInformationMessage('Feature coming soon'); @@ -546,16 +692,6 @@ export class DashboardWidget { }); const videosContainer = this.createVideoLinkContainers(view, [ - { - iconPath: IconPathHelper.sqlMiVideoThumbnail, - description: loc.HELP_VIDEO1_TITLE, - link: 'https://www.youtube.com/watch?v=sE99cSoFOHs' //TODO: Fix Video link - }, - { - iconPath: IconPathHelper.sqlVmVideoThumbnail, - description: loc.HELP_VIDEO2_TITLE, - link: 'https://www.youtube.com/watch?v=R4GCBoxADyQ' //TODO: Fix video link - } ]); const viewPanelStyle = { 'padding': '10px 5px 10px 10px', diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts index 93283b1a07..a7b61e7c04 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts @@ -681,7 +681,7 @@ export class SqlDatabaseTree { this._assessmentTitle.value = this._selectedIssue.checkId; this._descriptionText.value = this._selectedIssue.description; this._moreInfo.url = this._selectedIssue.helpLink; - this._moreInfo.label = this._selectedIssue.helpLink; + this._moreInfo.label = this._selectedIssue.message; this._impactedObjects = this._selectedIssue.impactedObjects; this._recommendationText.value = this._selectedIssue.message; //TODO: Expose correct property for recommendation. this._impactedObjectsTable.dataValues = this._selectedIssue.impactedObjects.map((object) => { diff --git a/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts b/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts index f3f4d66c48..1bb3369219 100644 --- a/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts +++ b/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts @@ -218,7 +218,7 @@ export class CreateSqlMigrationServiceDialog { if (!location) { errors.push(constants.INVALID_REGION_ERROR); } - if (!migrationServiceName || migrationServiceName.length === 0) { + if (!migrationServiceName || migrationServiceName.length < 3 || migrationServiceName.length > 63 || !/^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/.test(migrationServiceName)) { errors.push(constants.INVALID_SERVICE_NAME_ERROR); } return errors.join(os.EOL); diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts index 279ac99987..3432021726 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts @@ -27,6 +27,7 @@ export class MigrationCutoverDialog { private _serverName!: azdata.TextComponent; private _serverVersion!: azdata.TextComponent; private _sourceDatabase!: azdata.TextComponent; + private _targetDatabase!: azdata.TextComponent; private _targetServer!: azdata.TextComponent; private _targetVersion!: azdata.TextComponent; private _migrationStatus!: azdata.TextComponent; @@ -78,9 +79,11 @@ export class MigrationCutoverDialog { } }); + const targetDatabase = this.createInfoField(loc.TARGET_DATABASE_NAME, ''); const targetServer = this.createInfoField(loc.TARGET_SERVER, ''); const targetVersion = this.createInfoField(loc.TARGET_VERSION, ''); + this._targetDatabase = targetDatabase.text; this._targetServer = targetServer.text; this._targetVersion = targetVersion.text; @@ -88,6 +91,11 @@ export class MigrationCutoverDialog { flexFlow: 'column' }).component(); + flexTarget.addItem(targetDatabase.flexContainer, { + CSSStyles: { + 'width': '230px' + } + }); flexTarget.addItem(targetServer.flexContainer, { CSSStyles: { 'width': '230px' @@ -198,7 +206,7 @@ export class MigrationCutoverDialog { { value: loc.ACTIVE_BACKUP_FILES, width: 280, - type: azdata.ColumnType.text + type: azdata.ColumnType.text, }, { value: loc.TYPE, @@ -226,7 +234,7 @@ export class MigrationCutoverDialog { ], data: [], width: '800px', - height: '600px', + height: '300px', }).component(); const formBuilder = view.modelBuilder.formContainer().withFormItems( @@ -307,7 +315,7 @@ export class MigrationCutoverDialog { }); this._cancelButton = this._view.modelBuilder.button().withProps({ - iconPath: IconPathHelper.discard, + iconPath: IconPathHelper.cancel, iconHeight: '16px', iconWidth: '16px', label: loc.CANCEL_MIGRATION, @@ -383,7 +391,10 @@ export class MigrationCutoverDialog { }).component(); header.addItem(this._refreshLoader, { - flex: '0' + flex: '0', + CSSStyles: { + 'margin-top': '15px' + } }); return header; @@ -397,19 +408,20 @@ export class MigrationCutoverDialog { this._cancelButton.enabled = false; await this._model.fetchStatus(); const errors = []; - errors.push(this._model.migrationOpStatus.error.message); + errors.push(this._model.migrationOpStatus.error?.message); errors.push(this._model.migrationStatus.properties.migrationFailureError?.message); errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []); errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.restoreBlockingReason); this._dialogObject.message = { text: errors.filter(e => e !== undefined).join(EOL), - level: this._model.migrationStatus.properties.migrationStatus === MigrationStatus.InProgress ? azdata.window.MessageLevel.Warning : azdata.window.MessageLevel.Error + level: (this._model.migrationStatus.properties.migrationStatus === MigrationStatus.InProgress || this._model.migrationStatus.properties.migrationStatus === 'Completing') ? azdata.window.MessageLevel.Warning : azdata.window.MessageLevel.Error }; const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId); const sqlServerName = this._model._migration.sourceConnectionProfile.serverName; const sourceDatabaseName = this._model._migration.migrationContext.properties.sourceDatabaseName; const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!); const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion; + const targetDatabaseName = this._model._migration.migrationContext.name; const targetServerName = this._model._migration.targetManagedInstance.name; let targetServerVersion; if (this._model.migrationStatus.id.includes('managedInstances')) { @@ -452,6 +464,7 @@ export class MigrationCutoverDialog { this._serverVersion.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`; + this._targetDatabase.value = targetDatabaseName; this._targetServer.value = targetServerName; this._targetVersion.value = targetServerVersion; diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts index 641ad15e25..9b5906a547 100644 --- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts @@ -6,11 +6,10 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { IconPathHelper } from '../../constants/iconPathHelper'; -import { MigrationContext } from '../../models/migrationLocalStorage'; +import { MigrationContext, MigrationLocalStorage } from '../../models/migrationLocalStorage'; import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog'; import { MigrationCategory, MigrationStatusDialogModel } from './migrationStatusDialogModel'; import * as loc from '../../constants/strings'; -import { getDatabaseMigration } from '../../api/azure'; export class MigrationStatusDialog { private _model: MigrationStatusDialogModel; private _dialogObject!: azdata.window.Dialog; @@ -81,7 +80,7 @@ export class MigrationStatusDialog { dark: IconPathHelper.refresh.dark }, iconHeight: '16px', - iconWidth: '16px', + iconWidth: '20px', height: '30px', label: 'Refresh', }).component(); @@ -90,7 +89,11 @@ export class MigrationStatusDialog { this.refreshTable(); }); - const flexContainer = this._view.modelBuilder.flexContainer().component(); + const flexContainer = this._view.modelBuilder.flexContainer().withProps({ + CSSStyles: { + 'justify-content': 'left' + } + }).component(); flexContainer.addItem(this._searchBox, { flex: '0' @@ -109,7 +112,10 @@ export class MigrationStatusDialog { }).component(); flexContainer.addItem(this._refreshLoader, { - flex: '0' + flex: '0 0 auto', + CSSStyles: { + 'margin-left': '20px' + } }); return flexContainer; @@ -128,7 +134,7 @@ export class MigrationStatusDialog { return new Date(m1.migrationContext.properties.startedOn) > new Date(m2.migrationContext.properties.startedOn) ? -1 : 1; }); - migrations.forEach((migration) => { + migrations.forEach((migration, index) => { const migrationRow: azdata.DeclarativeTableCellValue[] = []; const databaseHyperLink = this._view.modelBuilder.hyperlink().withProps({ @@ -142,10 +148,6 @@ export class MigrationStatusDialog { value: databaseHyperLink, }); - migrationRow.push({ - value: migration.migrationContext.properties.migrationStatus ? migration.migrationContext.properties.migrationStatus : migration.migrationContext.properties.provisioningState - }); - const targetMigrationIcon = this._view.modelBuilder.image().withProps({ iconPath: (migration.targetManagedInstance.type === 'microsoft.sql/managedinstances') ? IconPathHelper.sqlMiLogo : IconPathHelper.sqlVmLogo, iconWidth: '16px', @@ -163,7 +165,7 @@ export class MigrationStatusDialog { const sqlMigrationContainer = this._view.modelBuilder.flexContainer().withProps({ CSSStyles: { - 'justify-content': 'center' + 'justify-content': 'left' } }).component(); sqlMigrationContainer.addItem(targetMigrationIcon, { @@ -186,6 +188,27 @@ export class MigrationStatusDialog { value: loc.ONLINE }); + let migrationStatus = migration.migrationContext.properties.migrationStatus ? migration.migrationContext.properties.migrationStatus : migration.migrationContext.properties.provisioningState; + + let warningCount = 0; + + if (migration.asyncOperationResult?.error?.message) { + warningCount++; + } + if (migration.migrationContext.properties.migrationFailureError?.message) { + warningCount++; + } + if (migration.migrationContext.properties.migrationStatusDetails?.fileUploadBlockingErrors) { + warningCount += migration.migrationContext.properties.migrationStatusDetails?.fileUploadBlockingErrors.length; + } + if (migration.migrationContext.properties.migrationStatusDetails?.restoreBlockingReason) { + warningCount++; + } + + migrationRow.push({ + value: loc.STATUS_WARNING_COUNT(migrationStatus, warningCount) + }); + migrationRow.push({ value: (migration.migrationContext.properties.startedOn) ? new Date(migration.migrationContext.properties.startedOn).toLocaleString() : '---' }); @@ -202,21 +225,28 @@ export class MigrationStatusDialog { } } - private refreshTable(): void { + private async refreshTable(): Promise { this._refreshLoader.loading = true; - this._model._migrations.forEach(async (migration) => { - migration.migrationContext = await getDatabaseMigration( - migration.azureAccount, - migration.subscription, - migration.targetManagedInstance.location, - migration.migrationContext.id - ); - }); + const currentConnection = await azdata.connection.getCurrentConnection(); + this._model._migrations = await MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection, true); this.populateMigrationTable(); this._refreshLoader.loading = false; } private createStatusTable(): azdata.DeclarativeTableComponent { + const rowCssStyle: azdata.CssStyles = { + 'border': 'none', + 'text-align': 'left', + 'border-bottom': '1px solid' + }; + + const headerCssStyles: azdata.CssStyles = { + 'border': 'none', + 'text-align': 'left', + 'border-bottom': '1px solid', + 'font-weight': 'bold' + }; + this._statusTable = this._view.modelBuilder.declarativeTable().withProps({ columns: [ { @@ -224,54 +254,48 @@ export class MigrationStatusDialog { valueType: azdata.DeclarativeDataType.component, width: '100px', isReadOnly: true, - rowCssStyles: { - 'text-align': 'center' - } - }, - { - displayName: loc.MIGRATION_STATUS, - valueType: azdata.DeclarativeDataType.string, - width: '150px', - isReadOnly: true, - rowCssStyles: { - 'text-align': 'center' - } + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles }, { displayName: loc.TARGET_AZURE_SQL_INSTANCE_NAME, valueType: azdata.DeclarativeDataType.component, - width: '300px', + width: '170px', isReadOnly: true, - rowCssStyles: { - 'text-align': 'center' - } + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles }, { displayName: loc.MIGRATION_MODE, valueType: azdata.DeclarativeDataType.string, width: '100px', isReadOnly: true, - rowCssStyles: { - 'text-align': 'center' - } + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles + }, + { + displayName: loc.MIGRATION_STATUS, + valueType: azdata.DeclarativeDataType.string, + width: '150px', + isReadOnly: true, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles }, { displayName: loc.START_TIME, valueType: azdata.DeclarativeDataType.string, - width: '150px', + width: '120px', isReadOnly: true, - rowCssStyles: { - 'text-align': 'center' - } + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles }, { displayName: loc.FINISH_TIME, valueType: azdata.DeclarativeDataType.string, - width: '150px', + width: '120px', isReadOnly: true, - rowCssStyles: { - 'text-align': 'center' - } + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles } ] }).component(); diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts index 30ae26f14f..6b6c9d59af 100644 --- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts +++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialogModel.ts @@ -7,7 +7,6 @@ import * as azdata from 'azdata'; import { MigrationContext } from '../../models/migrationLocalStorage'; export class MigrationStatusDialogModel { - public statusDropdownValues: azdata.CategoryValue[] = [ { displayName: 'Status: All', diff --git a/extensions/sql-migration/src/models/migrationLocalStorage.ts b/extensions/sql-migration/src/models/migrationLocalStorage.ts index 3d8f201a47..4c5dfe8b96 100644 --- a/extensions/sql-migration/src/models/migrationLocalStorage.ts +++ b/extensions/sql-migration/src/models/migrationLocalStorage.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import { azureResource } from 'azureResource'; -import { DatabaseMigration, SqlMigrationService, SqlManagedInstance, getDatabaseMigration } from '../api/azure'; +import { DatabaseMigration, SqlMigrationService, SqlManagedInstance, getMigrationStatus, AzureAsyncOperationResource, getMigrationAsyncOperationDetails } from '../api/azure'; import * as azdata from 'azdata'; @@ -27,12 +27,18 @@ export class MigrationLocalStorage { if (migration.sourceConnectionProfile.serverName === connectionProfile.serverName) { if (refreshStatus) { try { - migration.migrationContext = await getDatabaseMigration( + migration.migrationContext = await getMigrationStatus( migration.azureAccount, migration.subscription, - migration.targetManagedInstance.location, - migration.migrationContext.id + migration.migrationContext ); + if (migration.asyncUrl) { + migration.asyncOperationResult = await getMigrationAsyncOperationDetails( + migration.azureAccount, + migration.subscription, + migration.asyncUrl + ); + } } catch (e) { // Keeping only valid migrations in cache. Clearing all the migrations which return ResourceDoesNotExit error. @@ -89,5 +95,6 @@ export interface MigrationContext { azureAccount: azdata.Account, subscription: azureResource.AzureResourceSubscription, controller: SqlMigrationService, - asyncUrl: string + asyncUrl: string, + asyncOperationResult?: AzureAsyncOperationResource } diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index d0f73b527c..0074ba1c33 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -44,7 +44,7 @@ export enum MigrationSourceAuthenticationType { Sql = 'SqlAuthentication' } -export enum MigrationCutover { +export enum MigrationMode { ONLINE, OFFLINE } @@ -62,7 +62,7 @@ export interface NetworkShare { } export interface DatabaseBackupModel { - migrationCutover: MigrationCutover; + migrationMode: MigrationMode; networkContainerType: NetworkContainerType; networkShareLocation: string; windowsUser: string; diff --git a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts index a0402f75e5..df693b4a5d 100644 --- a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts @@ -103,6 +103,7 @@ export class AccountsSelectionPage extends MigrationWizardPage { this.wizard.message = { text: '' }; + this._azureAccountsDropdown.validate(); }); const flexContainer = view.modelBuilder.flexContainer() diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index 82dfc704e8..197282ef85 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -271,7 +271,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { }).withValidation((component) => { if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { if (component.value) { - if (!/(?<=\\\\)[^\\]*/.test(component.value)) { + if (!/^[\\\/]{2,}[^\\\/]+[\\\/]+[^\\\/]+/.test(component.value)) { return false; } } @@ -304,7 +304,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { .withValidation((component) => { if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) { if (component.value) { - if (!/(?<=\\).*$/.test(component.value)) { + if (!/^[A-Za-z0-9\\\._-]{7,}$/.test(component.value)) { return false; } } @@ -512,10 +512,14 @@ export class DatabaseBackupPage extends MigrationWizardPage { c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(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(); targetNameNetworkInputBox.onTextChanged((value) => { - this.migrationStateModel._targetDatabaseNames[index] = value; + this.migrationStateModel._targetDatabaseNames[index] = value.trim(); }); this._targetDatabaseNames.push(targetNameNetworkInputBox); diff --git a/extensions/sql-migration/src/wizard/migrationModePage.ts b/extensions/sql-migration/src/wizard/migrationModePage.ts index 119f8d8e57..d9157708ec 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 { MigrationCutover, MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; +import { MigrationMode, MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../constants/strings'; export class MigrationModePage extends MigrationWizardPage { @@ -57,11 +57,11 @@ export class MigrationModePage extends MigrationWizardPage { } }).component(); - this.migrationStateModel._databaseBackup.migrationCutover = MigrationCutover.ONLINE; + this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.ONLINE; onlineButton.onDidChangeCheckedState((e) => { if (e) { - this.migrationStateModel._databaseBackup.migrationCutover = MigrationCutover.ONLINE; + this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.ONLINE; } }); diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index 64e50e7e71..8f425a1a86 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -23,6 +23,7 @@ export class SKURecommendationPage extends MigrationWizardPage { private _view!: azdata.ModelView; private _igComponent!: azdata.TextComponent; + private _assessmentStatusIcon!: azdata.ImageComponent; private _detailsComponent!: azdata.TextComponent; private _chooseTargetComponent!: azdata.DivContainer; private _azureSubscriptionText!: azdata.TextComponent; @@ -63,7 +64,31 @@ export class SKURecommendationPage extends MigrationWizardPage { protected async registerContent(view: azdata.ModelView) { this._view = view; this._igComponent = this.createStatusComponent(view); // The first component giving basic information + this._assessmentStatusIcon = this._view.modelBuilder.image().withProps({ + iconPath: IconPathHelper.completedMigration, + iconHeight: 17, + iconWidth: 17, + width: 17, + height: 20 + }).component(); + const igContainer = this._view.modelBuilder.flexContainer().component(); + igContainer.addItem(this._assessmentStatusIcon, { + flex: '0 0 auto' + }); + igContainer.addItem(this._igComponent, { + flex: '0 0 auto' + }); + this._detailsComponent = this.createDetailsComponent(view); // The details of what can be moved + + const statusContainer = this._view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' + }).withItems( + [ + igContainer, + this._detailsComponent + ] + ).component(); this._chooseTargetComponent = await this.createChooseTargetComponent(view); this._azureSubscriptionText = this.createAzureSubscriptionText(view); @@ -164,11 +189,7 @@ export class SKURecommendationPage extends MigrationWizardPage { [ { title: '', - component: this._igComponent - }, - { - title: '', - component: this._detailsComponent + component: statusContainer }, { title: constants.SKU_RECOMMENDATION_CHOOSE_A_TARGET, @@ -212,14 +233,20 @@ export class SKURecommendationPage extends MigrationWizardPage { private createStatusComponent(view: azdata.ModelView): azdata.TextComponent { const component = view.modelBuilder.text().withProps({ CSSStyles: { - 'font-size': '14px' + 'font-size': '14px', + 'margin': '0 0 0 8px', + 'line-height': '20px' } }).component(); return component; } private createDetailsComponent(view: azdata.ModelView): azdata.TextComponent { - const component = view.modelBuilder.text().component(); + const component = view.modelBuilder.text().withProps({ + CSSStyles: { + 'font-size': '13px' + } + }).component(); return component; } diff --git a/extensions/sql-migration/src/wizard/summaryPage.ts b/extensions/sql-migration/src/wizard/summaryPage.ts index 07072800fc..408d61a882 100644 --- a/extensions/sql-migration/src/wizard/summaryPage.ts +++ b/extensions/sql-migration/src/wizard/summaryPage.ts @@ -5,7 +5,7 @@ import * as azdata from 'azdata'; import { MigrationWizardPage } from '../models/migrationWizardPage'; -import { MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; +import { MigrationMode, MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine'; import * as constants from '../constants/strings'; import { createHeadingTextComponent, createInformationRow } from './wizardController'; @@ -36,19 +36,32 @@ export class SummaryPage extends MigrationWizardPage { public async onPageEnter(): Promise { this._flexContainer.addItems( [ - createHeadingTextComponent(this._view, constants.AZURE_ACCOUNT_LINKED), - createHeadingTextComponent(this._view, this.migrationStateModel._azureAccount.displayInfo.displayName), - createHeadingTextComponent(this._view, constants.MIGRATION_TARGET), - createInformationRow(this._view, constants.TYPE, (this.migrationStateModel._targetServerInstance.type === 'microsoft.compute/virtualmachines') ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE), - createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name), - createInformationRow(this._view, constants.SUMMARY_MI_TYPE, this.migrationStateModel._targetServerInstance.name), - createInformationRow(this._view, constants.SUMMARY_DATABASE_COUNT_LABEL, this.migrationStateModel._migrationDbs.length.toString()), - createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_PAGE_TITLE), - this.createNetworkContainerRows(), - createHeadingTextComponent(this._view, constants.IR_PAGE_TITLE), - createInformationRow(this._view, constants.IR_PAGE_TITLE, this.migrationStateModel._sqlMigrationService?.name!), - createInformationRow(this._view, constants.SUMMARY_IR_NODE, this.migrationStateModel._nodeNames.join(', ')), + createHeadingTextComponent(this._view, constants.ACCOUNTS_SELECTION_PAGE_TITLE), + createInformationRow(this._view, constants.ACCOUNTS_SELECTION_PAGE_TITLE, this.migrationStateModel._azureAccount.displayInfo.displayName), + createHeadingTextComponent(this._view, constants.SOURCE_DATABASES), + createInformationRow(this._view, constants.SUMMARY_DATABASE_COUNT_LABEL, this.migrationStateModel._migrationDbs.length.toString()), + + createHeadingTextComponent(this._view, constants.SKU_RECOMMENDATION_PAGE_TITLE), + createInformationRow(this._view, constants.SKU_RECOMMENDATION_PAGE_TITLE, (this.migrationStateModel._targetServerInstance.type === 'microsoft.compute/virtualmachines') ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE), + createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name), + createInformationRow(this._view, constants.LOCATION, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.location)), + createInformationRow(this._view, constants.RESOURCE_GROUP, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.resourceGroup!)), + createInformationRow(this._view, (this.migrationStateModel._targetServerInstance.type === 'microsoft.compute/virtualmachines') ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.name!)), + + createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_MIGRATION_MODE_LABEL), + createInformationRow(this._view, constants.MODE, this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.ONLINE ? constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL : constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL), + + createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_PAGE_TITLE), + await this.createNetworkContainerRows(), + + createHeadingTextComponent(this._view, constants.IR_PAGE_TITLE), + createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name), + createInformationRow(this._view, constants.LOCATION, this.migrationStateModel._sqlMigrationService.location), + createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._sqlMigrationService.properties.resourceGroup), + createInformationRow(this._view, constants.IR_PAGE_TITLE, this.migrationStateModel._targetSubscription.name), + createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._sqlMigrationService.name), + createInformationRow(this._view, constants.SHIR, this.migrationStateModel._nodeNames[0]), ] ); } @@ -63,7 +76,7 @@ export class SummaryPage extends MigrationWizardPage { protected async handleStateChange(e: StateChangeEvent): Promise { } - private createNetworkContainerRows(): azdata.FlexContainer { + private async createNetworkContainerRows(): Promise { const flexContainer = this._view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); @@ -71,11 +84,14 @@ export class SummaryPage extends MigrationWizardPage { case NetworkContainerType.NETWORK_SHARE: flexContainer.addItems( [ - createInformationRow(this._view, constants.TYPE, constants.NETWORK_SHARE), - createInformationRow(this._view, constants.DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL, this.migrationStateModel._databaseBackup.networkShareLocation), + createInformationRow(this._view, constants.BACKUP_LOCATION, constants.NETWORK_SHARE), + createInformationRow(this._view, constants.NETWORK_SHARE, this.migrationStateModel._databaseBackup.networkShareLocation), createInformationRow(this._view, constants.USER_ACCOUNT, this.migrationStateModel._databaseBackup.windowsUser), - createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name), - createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE, this.migrationStateModel._databaseBackup.storageAccount.name), + createHeadingTextComponent(this._view, constants.AZURE_STORAGE_ACCOUNT_TO_UPLOAD_BACKUPS), + createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name), + createInformationRow(this._view, constants.LOCATION, this.migrationStateModel._databaseBackup.storageAccount.location), + createInformationRow(this._view, constants.RESOURCE_GROUP, this.migrationStateModel._databaseBackup.storageAccount.resourceGroup!), + createInformationRow(this._view, constants.STORAGE_ACCOUNT, this.migrationStateModel._databaseBackup.storageAccount.name!), createHeadingTextComponent(this._view, 'Target Databases:') ] ); diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index 6868fffe8b..514e50f439 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -110,7 +110,7 @@ export function createHeadingTextComponent(view: azdata.ModelView, value: string const component = createTextCompononent(view, value); component.updateCssStyles({ 'font-size': '13px', - 'font-weight': 'bold' + 'font-weight': 'bold', }); return component; }