From 37894c9e96e90344a848346db3e05f6c7396e750 Mon Sep 17 00:00:00 2001 From: Aasim Khan Date: Thu, 22 Apr 2021 10:19:36 -0700 Subject: [PATCH] Migration extensions - UI fixes and vBump (#15199) * Fixing Migration Cutover Dialog Adding support for target file share Fixing request body Correcting localized strings * Redesigned IR page Adding additional details in migration status dialog * vbump * Fixed the perpetual loading * Fixed duration logic * Adding icon for migration extension * Adding helper commenst to util function localizing some strings logging console errors * enabling cutover buttons for ignored files --- extensions/sql-migration/images/extension.png | Bin 3338 -> 50670 bytes extensions/sql-migration/images/migration.svg | 57 +- extensions/sql-migration/package.json | 2 +- extensions/sql-migration/src/api/azure.ts | 17 +- extensions/sql-migration/src/api/utils.ts | 26 + .../sql-migration/src/constants/strings.ts | 21 +- .../src/dashboard/sqlServerDashboard.ts | 2 +- .../createSqlMigrationServiceDialog.ts | 130 ++-- .../migrationCutoverDialog.ts | 163 +++-- .../migrationStatus/migrationStatusDialog.ts | 90 ++- .../src/models/migrationLocalStorage.ts | 2 + .../sql-migration/src/models/stateMachine.ts | 5 +- .../src/wizard/accountsSelectionPage.ts | 31 +- .../src/wizard/integrationRuntimePage.ts | 574 ++++++++++-------- 14 files changed, 694 insertions(+), 426 deletions(-) diff --git a/extensions/sql-migration/images/extension.png b/extensions/sql-migration/images/extension.png index c86d6d1e009f12f1ccb246db45303707052175e1..d074d1b0b0bb00913a52806d6a71506140e1ca9f 100644 GIT binary patch literal 50670 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

@pTc?IU=gRjqj*Va7(H-F|Sx%I2hRKPE1 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index ccb60670ae..b728a72c20 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.9", + "version": "0.0.10", "publisher": "Microsoft", "preview": true, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index 77327bbc99..ba9d7966a0 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -350,19 +350,8 @@ export interface StartDatabaseMigrationRequest { targetLocation: { storageAccountResourceId: string, accountKey: string, - } - sourceLocation: { - fileShare?: { - path: string, - username: string, - password: string, - }, - azureBlob?: { - storageAccountResourceId: string, - accountKey: string, - blobContainerName: string - } }, + sourceLocation: SourceLocation }, sourceSqlConnection: { authentication: string, @@ -454,8 +443,8 @@ export interface BackupSetInfo { } export interface SourceLocation { - fileShare: DatabaseMigrationFileShare; - azureBlob: DatabaseMigrationAzureBlob; + fileShare?: DatabaseMigrationFileShare; + azureBlob?: DatabaseMigrationAzureBlob; } export interface TargetLocation { diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts index ee9ae1f294..5cfb1f1e92 100644 --- a/extensions/sql-migration/src/api/utils.ts +++ b/extensions/sql-migration/src/api/utils.ts @@ -3,6 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DAYS, HRS, MINUTE, SEC } from '../constants/strings'; + export function deepClone(obj: T): T { if (!obj || typeof obj !== 'object') { return obj; @@ -40,3 +42,27 @@ export function getSqlServerName(majorVersion: number): string | undefined { return undefined; } } + +/** + * Generates a wordy time difference between start and end time. + * @returns stringified duration like '10.0 days', '12.0 hrs', '1.0 min' + */ +export function convertTimeDifferenceToDuration(startTime: Date, endTime: Date): string { + const time = endTime.getTime() - startTime.getTime(); + let seconds = (time / 1000).toFixed(1); + let minutes = (time / (1000 * 60)).toFixed(1); + let hours = (time / (1000 * 60 * 60)).toFixed(1); + let days = (time / (1000 * 60 * 60 * 24)).toFixed(1); + if (time / 1000 < 60) { + return SEC(parseFloat(seconds)); + } + else if (time / (1000 * 60) < 60) { + return MINUTE(parseFloat(minutes)); + } + else if (time / (1000 * 60 * 60) < 24) { + return HRS(parseFloat(hours)); + } + else { + return DAYS(parseFloat(days)); + } +} diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 1af84da94b..c089b29a7d 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -270,7 +270,7 @@ export const EASTUS2EUAP = localize('sql.migration.eastus2euap', 'East US 2 EUAP //Migration cutover dialog export const MIGRATION_CUTOVER = localize('sql.migration.cutover', "Migration cutover"); -export const SOURCE_DATABASE = localize('sql.migration.source.database', "Source database"); +export const SOURCE_DATABASE = localize('sql.migration.source.database', "Source database name"); 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"); @@ -306,7 +306,12 @@ export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migratio export const ONLINE = localize('sql.migration.online', "Online"); export const OFFLINE = localize('sql.migration.offline', "Offline"); export const DATABASE = localize('sql.migration.database', "Database"); -export const TARGET_AZURE_SQL_INSTANCE_NAME = localize('sql.migration.target.azure.sql.instance.name', "Target Azure SQL Instance Name"); +export const DATABASE_MIGRATION_SERVICE = localize('sql.migration.database.migration.service', "Database Migration Service"); +export const DURATION = localize('sql.migration.duration', "Duration"); +export const AZURE_SQL_TARGET = localize('sql.migration.azure.sql.target', "Azure SQL Target"); +export const SQL_MANAGED_INSTANCE = localize('sql.migration.sql.managed.instance', "SQL Managed Instance"); +export const SQL_VIRTUAL_MACHINE = localize('sql.migration.sql.virtual.machine', "SQL Virtual Machine"); +export const TARGET_AZURE_SQL_INSTANCE_NAME = localize('sql.migration.target.azure.sql.instance.name', "Azure SQL Target Name"); 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"); @@ -330,7 +335,19 @@ export function STATUS_WARNING_COUNT(status: string, count: number): string { return localize('sql.migration.status.error.count.multiple', "{0} ({1} Errors)", status, count); } } +} +export function HRS(hrs: number): string { + return hrs > 1 ? localize('sql.migration.hrs', "{0} hrs", hrs) : localize('sql.migration.hr', "{0} hr", hrs); +} +export function DAYS(days: number): string { + return days > 1 ? localize('sql.migration.days', "{0} days", days) : localize('sql.migration.day', "{0} day", days); +} +export function MINUTE(mins: number): string { + return mins > 1 ? localize('sql.migration.mins', "{0} mins", mins) : localize('sql.migration.min', "{0} min", mins); +} +export function SEC(sec: number): string { + return localize('sql.migration.sec', "{0} sec", sec); } //Source Credentials page. diff --git a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts index 009dea4cd6..8be02d8868 100644 --- a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts +++ b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts @@ -212,7 +212,7 @@ export class DashboardWidget { height: maxHeight, iconHeight: 32, iconPath: taskMetaData.iconPath, - iconWidth: 36, + iconWidth: 32, label: taskMetaData.title, title: taskMetaData.title, width: maxWidth, diff --git a/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts b/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts index 1bb3369219..91ab61401f 100644 --- a/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts +++ b/extensions/sql-migration/src/dialog/createSqlMigrationService/createSqlMigrationServiceDialog.ts @@ -58,7 +58,9 @@ export class CreateSqlMigrationServiceDialog { text: '' }; this._statusLoadingComponent.loading = true; - this._formSubmitButton.enabled = false; + this.migrationServiceResourceGroupDropdown.loading = false; + this.setFormEnabledState(false); + const subscription = this.migrationStateModel._targetSubscription; const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name; @@ -70,7 +72,7 @@ export class CreateSqlMigrationServiceDialog { if (formValidationErrors.length > 0) { this.setDialogMessage(formValidationErrors); this._statusLoadingComponent.loading = false; - this._formSubmitButton.enabled = true; + this.setFormEnabledState(true); return; } @@ -79,7 +81,7 @@ export class CreateSqlMigrationServiceDialog { if (this.createdMigrationService.error) { this.setDialogMessage(`${this.createdMigrationService.error.code} : ${this.createdMigrationService.error.message}`); this._statusLoadingComponent.loading = false; - this._formSubmitButton.enabled = true; + this.setFormEnabledState(true); return; } this._dialogObject.message = { @@ -93,7 +95,7 @@ export class CreateSqlMigrationServiceDialog { console.log(e); this.setDialogMessage(e.message); this._statusLoadingComponent.loading = false; - this._formSubmitButton.enabled = true; + this.setFormEnabledState(true); return; } }); @@ -138,17 +140,24 @@ export class CreateSqlMigrationServiceDialog { this._dialogObject.cancelButton.onClick((e) => { }); this._dialogObject.okButton.onClick((e) => { - this.irPage.populateMigrationService(this.createdMigrationService, this.createdMigrationServiceNodeNames); + this.irPage.populateMigrationService(this.createdMigrationService, this.createdMigrationServiceNodeNames, (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name); }); } private async migrationServiceDropdownContainer(): Promise { const dialogDescription = this._view.modelBuilder.text().withProps({ - value: constants.MIGRATION_SERVICE_DIALOG_DESCRIPTION + value: constants.MIGRATION_SERVICE_DIALOG_DESCRIPTION, + CSSStyles: { + 'font-size': '13px' + } }).component(); const subscriptionDropdownLabel = this._view.modelBuilder.text().withProps({ - value: constants.SUBSCRIPTION + value: constants.SUBSCRIPTION, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } }).component(); this.migrationServiceSubscription = this._view.modelBuilder.inputBox().withProps({ @@ -157,7 +166,11 @@ export class CreateSqlMigrationServiceDialog { }).component(); const resourceGroupDropdownLabel = this._view.modelBuilder.text().withProps({ - value: constants.RESOURCE_GROUP + value: constants.RESOURCE_GROUP, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } }).component(); this.migrationServiceResourceGroupDropdown = this._view.modelBuilder.dropDown().withProps({ @@ -165,13 +178,21 @@ export class CreateSqlMigrationServiceDialog { }).component(); const migrationServiceNameLabel = this._view.modelBuilder.text().withProps({ - value: constants.NAME + value: constants.NAME, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } }).component(); this.migrationServiceNameText = this._view.modelBuilder.inputBox().component(); const locationDropdownLabel = this._view.modelBuilder.text().withProps({ - value: constants.LOCATION + value: constants.LOCATION, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } }).component(); this.migrationServiceLocation = this._view.modelBuilder.inputBox().withProps({ @@ -181,7 +202,11 @@ export class CreateSqlMigrationServiceDialog { }).component(); const targetlabel = this._view.modelBuilder.text().withProps({ - value: constants.TARGET + value: constants.TARGET, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold' + } }).component(); const targetText = this._view.modelBuilder.inputBox().withProps({ @@ -259,20 +284,30 @@ export class CreateSqlMigrationServiceDialog { const setupIRHeadingText = this._view.modelBuilder.text().withProps({ value: constants.SERVICE_CONTAINER_HEADING, CSSStyles: { - 'font-weight': 'bold' + 'font-weight': 'bold', + 'font-size': '13px' } }).component(); const setupIRdescription1 = this._view.modelBuilder.text().withProps({ value: constants.SERVICE_CONTAINER_DESCRIPTION1, + CSSStyles: { + 'font-size': '13px' + } }).component(); const setupIRdescription2 = this._view.modelBuilder.text().withProps({ value: constants.SERVICE_CONTAINER_DESCRIPTION2, + CSSStyles: { + 'font-size': '13px' + } }).component(); const irSetupStep1Text = this._view.modelBuilder.text().withProps({ value: constants.SERVICE_STEP1, + CSSStyles: { + 'font-size': '13px' + }, links: [ { text: constants.SERVICE_STEP1_LINK, @@ -282,7 +317,10 @@ export class CreateSqlMigrationServiceDialog { }).component(); const irSetupStep2Text = this._view.modelBuilder.text().withProps({ - value: constants.SERVICE_STEP2 + value: constants.SERVICE_STEP2, + CSSStyles: { + 'font-size': '13px' + } }).component(); const irSetupStep3Text = this._view.modelBuilder.hyperlink().withProps({ @@ -290,7 +328,8 @@ export class CreateSqlMigrationServiceDialog { url: '', CSSStyles: { 'margin-top': '10px', - 'margin-bottom': '10px' + 'margin-bottom': '10px', + 'font-size': '13px' } }).component(); @@ -311,14 +350,23 @@ export class CreateSqlMigrationServiceDialog { }); - this._connectionStatus = this._view.modelBuilder.infoBox().component(); + this._connectionStatus = this._view.modelBuilder.infoBox().withProps({ + text: '', + style: 'error', + CSSStyles: { + 'font-size': '13px' + } + }).component(); this._connectionStatus.CSSStyles = { 'width': '350px' }; const refreshLoadingIndicator = this._view.modelBuilder.loadingComponent().withProps({ - loading: false + loading: false, + CSSStyles: { + 'font-size': '13px' + } }).component(); @@ -330,7 +378,10 @@ export class CreateSqlMigrationServiceDialog { width: '50px', isReadOnly: true, rowCssStyles: { - 'text-align': 'center' + 'font-size': '13px' + }, + headerCssStyles: { + 'font-size': '13px' } }, { @@ -339,20 +390,23 @@ export class CreateSqlMigrationServiceDialog { width: '500px', isReadOnly: true, rowCssStyles: { - overflow: 'scroll' + 'font-size': '13px' + }, + headerCssStyles: { + 'font-size': '13px' } }, { displayName: '', valueType: azdata.DeclarativeDataType.component, - width: '15px', - isReadOnly: true, - }, - { - displayName: '', - valueType: azdata.DeclarativeDataType.component, - width: '15px', + width: '30px', isReadOnly: true, + rowCssStyles: { + 'font-size': '13px' + }, + headerCssStyles: { + 'font-size': '13px' + } } ], CSSStyles: { @@ -399,14 +453,20 @@ export class CreateSqlMigrationServiceDialog { if (state === 'Online') { this._connectionStatus.updateProperties({ text: constants.SERVICE_READY(this.createdMigrationService!.name, this.createdMigrationServiceNodeNames.join(', ')), - style: 'success' + style: 'success', + CSSStyles: { + 'font-size': '13px' + } }); this._dialogObject.okButton.enabled = true; } else { this._connectionStatus.text = constants.SERVICE_NOT_READY(this.createdMigrationService!.name); this._connectionStatus.updateProperties({ text: constants.SERVICE_NOT_READY(this.createdMigrationService!.name), - style: 'warning' + style: 'warning', + CSSStyles: { + 'font-size': '13px' + } }); this._dialogObject.okButton.enabled = false; } @@ -461,10 +521,7 @@ export class CreateSqlMigrationServiceDialog { value: keys.authKey1 }, { - value: this._copyKey1Button - }, - { - value: this._refreshKey1Button + value: this._view.modelBuilder.flexContainer().withItems([this._copyKey1Button, this._refreshKey1Button]).component() } ], [ @@ -475,10 +532,7 @@ export class CreateSqlMigrationServiceDialog { value: keys.authKey2 }, { - value: this._copyKey2Button - }, - { - value: this._refreshKey2Button + value: this._view.modelBuilder.flexContainer().withItems([this._copyKey2Button, this._refreshKey2Button]).component() } ] ] @@ -492,4 +546,10 @@ export class CreateSqlMigrationServiceDialog { level: level }; } + + private setFormEnabledState(enable: boolean): void { + this._formSubmitButton.enabled = enable; + this.migrationServiceResourceGroupDropdown.enabled = enable; + this.migrationServiceNameText.enabled = enable; + } } diff --git a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts index 3432021726..f27fb9ee4b 100644 --- a/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationCutover/migrationCutoverDialog.ts @@ -32,6 +32,7 @@ export class MigrationCutoverDialog { private _targetVersion!: azdata.TextComponent; private _migrationStatus!: azdata.TextComponent; private _fullBackupFile!: azdata.TextComponent; + private _backupLocation!: azdata.TextComponent; private _lastAppliedLSN!: azdata.TextComponent; private _lastAppliedBackupFile!: azdata.TextComponent; private _lastAppliedBackupTakenOn!: azdata.TextComponent; @@ -44,7 +45,7 @@ export class MigrationCutoverDialog { constructor(migration: MigrationContext) { this._model = new MigrationCutoverDialogModel(migration); - this._dialogObject = azdata.window.createModelViewDialog(loc.MIGRATION_CUTOVER, 'MigrationCutoverDialog', 1000); + this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 1000); } async initialize(): Promise { @@ -65,17 +66,17 @@ export class MigrationCutoverDialog { flexServer.addItem(sourceDatabase.flexContainer, { CSSStyles: { - 'width': '150px' + 'width': '200px' } }); flexServer.addItem(sourceDetails.flexContainer, { CSSStyles: { - 'width': '150px' + 'width': '200px' } }); flexServer.addItem(sourceVersion.flexContainer, { CSSStyles: { - 'width': '150px' + 'width': '200px' } }); @@ -93,26 +94,28 @@ export class MigrationCutoverDialog { flexTarget.addItem(targetDatabase.flexContainer, { CSSStyles: { - 'width': '230px' + 'width': '200px' } }); flexTarget.addItem(targetServer.flexContainer, { CSSStyles: { - 'width': '230px' + 'width': '200px' } }); flexTarget.addItem(targetVersion.flexContainer, { CSSStyles: { - 'width': '230px' + 'width': '200px' } }); const migrationStatus = this.createInfoField(loc.MIGRATION_STATUS, ''); const fullBackupFileOn = this.createInfoField(loc.FULL_BACKUP_FILES, ''); + const backupLocation = this.createInfoField(loc.BACKUP_LOCATION, ''); this._migrationStatus = migrationStatus.text; this._fullBackupFile = fullBackupFileOn.text; + this._backupLocation = backupLocation.text; const flexStatus = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' @@ -120,12 +123,17 @@ export class MigrationCutoverDialog { flexStatus.addItem(migrationStatus.flexContainer, { CSSStyles: { - 'width': '180px' + 'width': '200px' } }); flexStatus.addItem(fullBackupFileOn.flexContainer, { CSSStyles: { - 'width': '180px' + 'width': '200px' + } + }); + flexStatus.addItem(backupLocation.flexContainer, { + CSSStyles: { + 'width': '200px' } }); @@ -133,6 +141,7 @@ export class MigrationCutoverDialog { const lastAppliedBackup = this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, ''); const lastAppliedBackupOn = this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, ''); + this._lastAppliedLSN = lastSSN.text; this._lastAppliedBackupFile = lastAppliedBackup.text; this._lastAppliedBackupTakenOn = lastAppliedBackupOn.text; @@ -142,22 +151,22 @@ export class MigrationCutoverDialog { }).component(); flexFile.addItem(lastSSN.flexContainer, { CSSStyles: { - 'width': '230px' + 'width': '200px' } }); flexFile.addItem(lastAppliedBackup.flexContainer, { CSSStyles: { - 'width': '230px' + 'width': '200px' } }); flexFile.addItem(lastAppliedBackupOn.flexContainer, { CSSStyles: { - 'width': '230px' + 'width': '200px' } }); const flexInfo = view.modelBuilder.flexContainer().withProps({ CSSStyles: { - 'width': '700px' + 'width': '800px', } }).component(); @@ -165,7 +174,7 @@ export class MigrationCutoverDialog { flex: '0', CSSStyles: { 'flex': '0', - 'width': '150px' + 'width': '200px' } }); @@ -173,7 +182,7 @@ export class MigrationCutoverDialog { flex: '0', CSSStyles: { 'flex': '0', - 'width': '230px' + 'width': '200px' } }); @@ -181,7 +190,7 @@ export class MigrationCutoverDialog { flex: '0', CSSStyles: { 'flex': '0', - 'width': '180px' + 'width': '200px' } }); @@ -240,11 +249,17 @@ export class MigrationCutoverDialog { const formBuilder = view.modelBuilder.formContainer().withFormItems( [ { - component: await this.migrationContainerHeader() + component: this.migrationContainerHeader() + }, + { + component: this._view.modelBuilder.separator().withProps({ width: '800px' }).component() }, { component: flexInfo }, + { + component: this._view.modelBuilder.separator().withProps({ width: '800px' }).component() + }, { component: this._fileCount }, @@ -267,30 +282,59 @@ export class MigrationCutoverDialog { private migrationContainerHeader(): azdata.FlexContainer { - const header = this._view.modelBuilder.flexContainer().withLayout({ + const sqlDatbaseLogo = this._view.modelBuilder.image().withProps({ + iconPath: IconPathHelper.sqlDatabaseLogo, + iconHeight: '32px', + iconWidth: '32px', + width: '32px', + height: '32px' }).component(); this._databaseTitleName = this._view.modelBuilder.text().withProps({ CSSStyles: { - 'font-size': 'large', - 'width': '400px' + 'font-size': '16px', + 'font-weight': 'bold', + 'margin': '0px' }, - value: this._model._migration.migrationContext.name + value: this._model._migration.migrationContext.properties.sourceDatabaseName }).component(); - header.addItem(this._databaseTitleName, { - flex: '0', + const databaseSubTitle = this._view.modelBuilder.text().withProps({ CSSStyles: { - 'width': '500px' + 'font-size': '10px', + 'margin': '5px 0px' + }, + value: loc.DATABASE + }).component(); + + const titleContainer = this._view.modelBuilder.flexContainer().withItems([ + this._databaseTitleName, + databaseSubTitle + ]).withLayout({ + 'flexFlow': 'column' + }).component(); + + + const titleLogoContainer = this._view.modelBuilder.flexContainer().component(); + + titleLogoContainer.addItem(sqlDatbaseLogo, { + flex: '0' + }); + titleLogoContainer.addItem(titleContainer, { + CSSStyles: { + 'margin-left': '5px' } }); + const headerActions = this._view.modelBuilder.flexContainer().withLayout({ + }).component(); + this._cutoverButton = this._view.modelBuilder.button().withProps({ iconPath: IconPathHelper.cutover, iconHeight: '14px', iconWidth: '12px', label: 'Start Cutover', - height: '55px', + height: '20px', width: '100px', enabled: false }).component(); @@ -307,11 +351,8 @@ export class MigrationCutoverDialog { } }); - header.addItem(this._cutoverButton, { - flex: '0', - CSSStyles: { - 'width': '100px' - } + headerActions.addItem(this._cutoverButton, { + flex: '0' }); this._cancelButton = this._view.modelBuilder.button().withProps({ @@ -319,19 +360,16 @@ export class MigrationCutoverDialog { iconHeight: '16px', iconWidth: '16px', label: loc.CANCEL_MIGRATION, - height: '55px', - width: '130px' + height: '20px', + width: '120px' }).component(); this._cancelButton.onDidClick((e) => { this.cancelMigration(); }); - header.addItem(this._cancelButton, { - flex: '0', - CSSStyles: { - 'width': '130px' - } + headerActions.addItem(this._cancelButton, { + flex: '0' }); @@ -340,19 +378,16 @@ export class MigrationCutoverDialog { iconHeight: '16px', iconWidth: '16px', label: 'Refresh', - height: '55px', - width: '100px' + height: '20px', + width: '65px' }).component(); this._refreshButton.onDidClick((e) => { this.refreshStatus(); }); - header.addItem(this._refreshButton, { + headerActions.addItem(this._refreshButton, { flex: '0', - CSSStyles: { - 'width': '100px' - } }); this._copyDatabaseMigrationDetails = this._view.modelBuilder.button().withProps({ @@ -360,8 +395,8 @@ export class MigrationCutoverDialog { iconHeight: '16px', iconWidth: '16px', label: loc.COPY_MIGRATION_DETAILS, - height: '55px', - width: '100px' + height: '20px', + width: '150px' }).component(); this._copyDatabaseMigrationDetails.onDidClick(async (e) => { @@ -378,22 +413,34 @@ export class MigrationCutoverDialog { vscode.window.showInformationMessage(loc.DETAILS_COPIED); }); - header.addItem(this._copyDatabaseMigrationDetails, { + headerActions.addItem(this._copyDatabaseMigrationDetails, { flex: '0', CSSStyles: { - 'width': '100px' + 'margin-left': '5px' } }); this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({ loading: false, - height: '55px' + height: '15px' }).component(); - header.addItem(this._refreshLoader, { + headerActions.addItem(this._refreshLoader, { flex: '0', CSSStyles: { - 'margin-top': '15px' + 'margin-left': '16px' + } + }); + + const header = this._view.modelBuilder.flexContainer().withItems([ + titleLogoContainer + ]).withLayout({ + flexFlow: 'column' + }).component(); + + header.addItem(headerActions, { + 'CSSStyles': { + 'margin-top': '16px' } }); @@ -461,19 +508,19 @@ export class MigrationCutoverDialog { this._sourceDatabase.value = sourceDatabaseName; this._serverName.value = sqlServerName; - this._serverVersion.value = `${sqlServerVersion} - ${sqlServerInfo.serverVersion}`; + this._serverVersion.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`; this._targetDatabase.value = targetDatabaseName; this._targetServer.value = targetServerName; this._targetVersion.value = targetServerVersion; - this._migrationStatus.value = migrationStatusTextValue; - this._fullBackupFile.value = fullBackupFileName!; + this._migrationStatus.value = migrationStatusTextValue ?? '---'; + this._fullBackupFile.value = fullBackupFileName! ?? '-'; + this._backupLocation.value = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.fileShare?.path! ?? '-'; - this._lastAppliedLSN.value = lastAppliedSSN!; - this._lastAppliedBackupFile.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename; - this._lastAppliedBackupTakenOn.value = lastAppliedBackupFileTakenOn! ? new Date(lastAppliedBackupFileTakenOn).toLocaleString() : ''; + this._lastAppliedLSN.value = lastAppliedSSN! ?? '-'; + this._lastAppliedBackupFile.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename ?? '-'; + this._lastAppliedBackupTakenOn.value = lastAppliedBackupFileTakenOn! ? new Date(lastAppliedBackupFileTakenOn).toLocaleString() : '-'; this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length); @@ -495,7 +542,7 @@ export class MigrationCutoverDialog { } if (migrationStatusTextValue === MigrationStatus.InProgress) { - const fileNotRestored = await tableData.some(file => file.status !== 'Restored'); + const fileNotRestored = await tableData.some(file => file.status !== 'Restored' && file.status !== 'Ignored'); this._cutoverButton.enabled = !fileNotRestored; this._cancelButton.enabled = true; } else { diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts index 9b5906a547..55a92916a5 100644 --- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts +++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts @@ -10,6 +10,7 @@ import { MigrationContext, MigrationLocalStorage } from '../../models/migrationL import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog'; import { MigrationCategory, MigrationStatusDialogModel } from './migrationStatusDialogModel'; import * as loc from '../../constants/strings'; +import { convertTimeDifferenceToDuration } from '../../api/utils'; export class MigrationStatusDialog { private _model: MigrationStatusDialogModel; private _dialogObject!: azdata.window.Dialog; @@ -138,7 +139,7 @@ export class MigrationStatusDialog { const migrationRow: azdata.DeclarativeTableCellValue[] = []; const databaseHyperLink = this._view.modelBuilder.hyperlink().withProps({ - label: migration.migrationContext.name, + label: migration.migrationContext.properties.sourceDatabaseName, url: '' }).component(); databaseHyperLink.onDidClick(async (e) => { @@ -148,13 +149,10 @@ export class MigrationStatusDialog { value: databaseHyperLink, }); - const targetMigrationIcon = this._view.modelBuilder.image().withProps({ - iconPath: (migration.targetManagedInstance.type === 'microsoft.sql/managedinstances') ? IconPathHelper.sqlMiLogo : IconPathHelper.sqlVmLogo, - iconWidth: '16px', - iconHeight: '16px', - width: '32px', - height: '20px' - }).component(); + migrationRow.push({ + value: (migration.targetManagedInstance.type === 'microsoft.sql/managedinstances') ? loc.SQL_MANAGED_INSTANCE : loc.SQL_VIRTUAL_MACHINE + }); + const sqlMigrationName = this._view.modelBuilder.hyperlink().withProps({ label: migration.targetManagedInstance.name, url: '' @@ -163,25 +161,20 @@ export class MigrationStatusDialog { vscode.window.showInformationMessage(loc.COMING_SOON); }); - const sqlMigrationContainer = this._view.modelBuilder.flexContainer().withProps({ - CSSStyles: { - 'justify-content': 'left' - } - }).component(); - sqlMigrationContainer.addItem(targetMigrationIcon, { - flex: '0', - CSSStyles: { - 'width': '32px' - } - }); - sqlMigrationContainer.addItem(sqlMigrationName, - { - CSSStyles: { - 'width': 'auto' - } - }); migrationRow.push({ - value: sqlMigrationContainer + value: sqlMigrationName + }); + + const dms = this._view.modelBuilder.hyperlink().withProps({ + label: migration.controller.name, + url: '' + }).component(); + dms.onDidClick((e) => { + vscode.window.showInformationMessage(loc.COMING_SOON); + }); + + migrationRow.push({ + value: dms }); migrationRow.push({ @@ -209,6 +202,17 @@ export class MigrationStatusDialog { value: loc.STATUS_WARNING_COUNT(migrationStatus, warningCount) }); + let duration; + if (migration.migrationContext.properties.endedOn) { + duration = convertTimeDifferenceToDuration(new Date(migration.migrationContext.properties.startedOn), new Date(migration.migrationContext.properties.endedOn)); + } else { + duration = convertTimeDifferenceToDuration(new Date(migration.migrationContext.properties.startedOn), new Date()); + } + + migrationRow.push({ + value: (migration.migrationContext.properties.startedOn) ? duration : '---' + }); + migrationRow.push({ value: (migration.migrationContext.properties.startedOn) ? new Date(migration.migrationContext.properties.startedOn).toLocaleString() : '---' }); @@ -237,14 +241,16 @@ export class MigrationStatusDialog { const rowCssStyle: azdata.CssStyles = { 'border': 'none', 'text-align': 'left', - 'border-bottom': '1px solid' + 'border-bottom': '1px solid', }; const headerCssStyles: azdata.CssStyles = { 'border': 'none', 'text-align': 'left', 'border-bottom': '1px solid', - 'font-weight': 'bold' + 'font-weight': 'bold', + 'padding-left': '0px', + 'padding-right': '0px' }; this._statusTable = this._view.modelBuilder.declarativeTable().withProps({ @@ -252,7 +258,15 @@ export class MigrationStatusDialog { { displayName: loc.DATABASE, valueType: azdata.DeclarativeDataType.component, - width: '100px', + width: '90px', + isReadOnly: true, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles + }, + { + displayName: loc.AZURE_SQL_TARGET, + valueType: azdata.DeclarativeDataType.string, + width: '140px', isReadOnly: true, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles @@ -260,7 +274,15 @@ export class MigrationStatusDialog { { displayName: loc.TARGET_AZURE_SQL_INSTANCE_NAME, valueType: azdata.DeclarativeDataType.component, - width: '170px', + width: '160px', + isReadOnly: true, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles + }, + { + displayName: loc.DATABASE_MIGRATION_SERVICE, + valueType: azdata.DeclarativeDataType.component, + width: '150px', isReadOnly: true, rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles @@ -281,6 +303,14 @@ export class MigrationStatusDialog { rowCssStyles: rowCssStyle, headerCssStyles: headerCssStyles }, + { + displayName: loc.DURATION, + valueType: azdata.DeclarativeDataType.string, + width: '55px', + isReadOnly: true, + rowCssStyles: rowCssStyle, + headerCssStyles: headerCssStyles + }, { displayName: loc.START_TIME, valueType: azdata.DeclarativeDataType.string, diff --git a/extensions/sql-migration/src/models/migrationLocalStorage.ts b/extensions/sql-migration/src/models/migrationLocalStorage.ts index 4c5dfe8b96..447e2f5989 100644 --- a/extensions/sql-migration/src/models/migrationLocalStorage.ts +++ b/extensions/sql-migration/src/models/migrationLocalStorage.ts @@ -27,11 +27,13 @@ export class MigrationLocalStorage { if (migration.sourceConnectionProfile.serverName === connectionProfile.serverName) { if (refreshStatus) { try { + const backupConfiguration = migration.migrationContext.properties.backupConfiguration; migration.migrationContext = await getMigrationStatus( migration.azureAccount, migration.subscription, migration.migrationContext ); + migration.migrationContext.properties.backupConfiguration = backupConfiguration; if (migration.asyncUrl) { migration.asyncOperationResult = await getMigrationAsyncOperationDetails( migration.azureAccount, diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index b130a3d94f..86bb7d5a64 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -602,10 +602,10 @@ export class MigrationStateModel implements Model, vscode.Disposable { } - public async getSqlMigrationServiceValues(subscription: azureResource.AzureResourceSubscription, managedInstance: SqlManagedInstance): Promise { + public async getSqlMigrationServiceValues(subscription: azureResource.AzureResourceSubscription, managedInstance: SqlManagedInstance, resourceGroupName: string): Promise { let sqlMigrationServiceValues: azdata.CategoryValue[] = []; try { - this._sqlMigrationServices = (await getSqlMigrationServices(this._azureAccount, subscription, managedInstance.location)).filter(sms => sms.location.toLowerCase() === this._targetServerInstance.location.toLowerCase()); + this._sqlMigrationServices = (await getSqlMigrationServices(this._azureAccount, subscription, managedInstance.location)).filter(sms => sms.location.toLowerCase() === this._targetServerInstance.location.toLowerCase() && sms.properties.resourceGroup.toLowerCase() === resourceGroupName?.toLowerCase()); this._sqlMigrationServices.forEach((sqlMigrationService) => { sqlMigrationServiceValues.push({ name: sqlMigrationService.id, @@ -687,6 +687,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { this._targetDatabaseNames[i], requestBody ); + response.databaseMigration.properties.backupConfiguration = requestBody.properties.backupConfiguration!; if (response.status === 201 || response.status === 200) { MigrationLocalStorage.saveMigration( currentConnection!, diff --git a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts index df693b4a5d..6146abb700 100644 --- a/extensions/sql-migration/src/wizard/accountsSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/accountsSelectionPage.ts @@ -48,23 +48,26 @@ export class AccountsSelectionPage extends MigrationWizardPage { width: WIZARD_INPUT_COMPONENT_WIDTH }) .withValidation((c) => { - if ((c.value).displayName === constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR) { + if (c.value) { + if ((c.value).displayName === constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR) { + this.wizard.message = { + text: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR, + level: azdata.window.MessageLevel.Error + }; + return false; + } + if (this.migrationStateModel._azureAccount?.isStale) { + this.wizard.message = { + text: constants.ACCOUNT_STALE_ERROR(this.migrationStateModel._azureAccount) + }; + return false; + } this.wizard.message = { - text: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR, - level: azdata.window.MessageLevel.Error + text: '' }; - return false; + return true; } - if (this.migrationStateModel._azureAccount?.isStale) { - this.wizard.message = { - text: constants.ACCOUNT_STALE_ERROR(this.migrationStateModel._azureAccount) - }; - return false; - } - this.wizard.message = { - text: '' - }; - return true; + return false; }).component(); this._azureAccountsDropdown.onValueChanged(async (value) => { diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts index 0c5b739b41..5bfb5b2d1a 100644 --- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts +++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts @@ -9,17 +9,30 @@ import { MigrationWizardPage } from '../models/migrationWizardPage'; import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine'; import { CreateSqlMigrationServiceDialog } from '../dialog/createSqlMigrationService/createSqlMigrationServiceDialog'; import * as constants from '../constants/strings'; -import { createInformationRow, WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; -import { getSqlMigrationService, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, SqlMigrationService } from '../api/azure'; +import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; +import { getLocationDisplayName, getSqlMigrationService, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, SqlMigrationService } from '../api/azure'; import { IconPathHelper } from '../constants/iconPathHelper'; export class IntergrationRuntimePage extends MigrationWizardPage { - private migrationServiceDropdown!: azdata.DropDownComponent; private _view!: azdata.ModelView; private _form!: azdata.FormBuilder; private _statusLoadingComponent!: azdata.LoadingComponent; - private _migrationDetailsContainer!: azdata.FlexContainer; + private _subscription!: azdata.InputBoxComponent; + private _location!: azdata.InputBoxComponent; + private _resourceGroupDropdown!: azdata.DropDownComponent; + private _dmsDropdown!: azdata.DropDownComponent; + + private _dmsStatusInfoBox!: azdata.InfoBoxComponent; + private _authKeyTable!: azdata.DeclarativeTableComponent; + private _refreshButton!: azdata.ButtonComponent; + private _connectionStatusLoader!: azdata.LoadingComponent; + + private _copy1!: azdata.ButtonComponent; + private _copy2!: azdata.ButtonComponent; + private _refresh1!: azdata.ButtonComponent; + private _refresh2!: azdata.ButtonComponent; + constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.IR_PAGE_TITLE), migrationStateModel); @@ -30,7 +43,10 @@ export class IntergrationRuntimePage extends MigrationWizardPage { const createNewMigrationService = view.modelBuilder.hyperlink().withProps({ label: constants.CREATE_NEW, - url: '' + url: '', + CSSStyles: { + 'font-size': '13px' + } }).component(); createNewMigrationService.onDidClick((e) => { @@ -38,10 +54,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage { dialog.initialize(); }); - this._migrationDetailsContainer = view.modelBuilder.flexContainer().withLayout({ - flexFlow: 'column' - }).component(); - this._statusLoadingComponent = view.modelBuilder.loadingComponent().withItem(this._migrationDetailsContainer).component(); + this._statusLoadingComponent = view.modelBuilder.loadingComponent().withItem(this.createDMSDetailsContainer()).component(); this._form = view.modelBuilder.formContainer() .withFormItems( @@ -104,60 +117,305 @@ export class IntergrationRuntimePage extends MigrationWizardPage { private migrationServiceDropdownContainer(): azdata.FlexContainer { const descriptionText = this._view.modelBuilder.text().withProps({ - value: constants.IR_PAGE_DESCRIPTION + value: constants.IR_PAGE_DESCRIPTION, + width: WIZARD_INPUT_COMPONENT_WIDTH, + CSSStyles: { + 'font-size': '13px', + } }).component(); + const subscriptionLabel = this._view.modelBuilder.text().withProps({ + value: constants.SUBSCRIPTION, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold', + } + }).component(); + this._subscription = this._view.modelBuilder.inputBox().withProps({ + enabled: false, + width: WIZARD_INPUT_COMPONENT_WIDTH, + }).component(); + + const locationLabel = this._view.modelBuilder.text().withProps({ + value: constants.LOCATION, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold', + } + }).component(); + this._location = this._view.modelBuilder.inputBox().withProps({ + enabled: false, + width: WIZARD_INPUT_COMPONENT_WIDTH, + }).component(); + + + const resourceGroupLabel = this._view.modelBuilder.text().withProps({ + value: constants.RESOURCE_GROUP, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold', + } + }).component(); + this._resourceGroupDropdown = this._view.modelBuilder.dropDown().withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH, + editable: true + }).component(); + + this._resourceGroupDropdown.onValueChanged(async (value) => { + if (value) { + this.populateDms(value); + } + }); + const migrationServcieDropdownLabel = this._view.modelBuilder.text().withProps({ - value: constants.SELECT_A_SQL_MIGRATION_SERVICE + value: constants.IR_PAGE_TITLE, + CSSStyles: { + 'font-size': '13px', + 'font-weight': 'bold', + } }).component(); - this.migrationServiceDropdown = this._view.modelBuilder.dropDown().withProps({ + this._dmsDropdown = this._view.modelBuilder.dropDown().withProps({ required: true, - width: WIZARD_INPUT_COMPONENT_WIDTH + width: WIZARD_INPUT_COMPONENT_WIDTH, + editable: true }).component(); - this.migrationServiceDropdown.onValueChanged(async (value) => { - if (value.selected) { + this._dmsDropdown.onValueChanged(async (value) => { + if (value && value !== constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR) { this.wizard.message = { text: '' }; - this.migrationStateModel._sqlMigrationService = this.migrationStateModel.getMigrationService(value.index); - if (value !== constants.SQL_MIGRATION_SERVICE_NOT_FOUND_ERROR) { - await this.loadMigrationServiceStatus(); - } + const selectedIndex = (this._dmsDropdown.values)?.findIndex((v) => v.displayName === value); + this.migrationStateModel._sqlMigrationService = this.migrationStateModel.getMigrationService(selectedIndex); + this.loadMigrationServiceStatus(); } }); const flexContainer = this._view.modelBuilder.flexContainer().withItems([ descriptionText, + subscriptionLabel, + this._subscription, + locationLabel, + this._location, + resourceGroupLabel, + this._resourceGroupDropdown, migrationServcieDropdownLabel, - this.migrationServiceDropdown + this._dmsDropdown ]).withLayout({ flexFlow: 'column' }).component(); return flexContainer; } - public async populateMigrationService(sqlMigrationService?: SqlMigrationService, serviceNodes?: string[]): Promise { - this.migrationServiceDropdown.loading = true; + private createDMSDetailsContainer(): azdata.FlexContainer { + const container = this._view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' + }).component(); + + const connectionStatusLabel = this._view.modelBuilder.text().withProps({ + value: constants.SERVICE_CONNECTION_STATUS, + CSSStyles: { + 'font-weight': 'bold', + 'font-size': '13px', + 'width': '130px', + 'margin': '0' + } + }).component(); + + this._refreshButton = this._view.modelBuilder.button().withProps({ + iconWidth: '18px', + iconHeight: '18px', + iconPath: IconPathHelper.refresh, + height: '18px', + width: '18px' + }).component(); + + this._refreshButton.onDidClick((e) => { + this.loadStatus(); + }); + + const connectionLabelContainer = this._view.modelBuilder.flexContainer().withProps({ + CSSStyles: { + 'margin-bottom': '13px' + } + }).component(); + + connectionLabelContainer.addItem(connectionStatusLabel, { + flex: '0' + }); + + connectionLabelContainer.addItem(this._refreshButton, { + flex: '0', + CSSStyles: { 'margin-right': '10px' } + }); + + const statusContainer = this._view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column' + }).component(); + + this._dmsStatusInfoBox = this._view.modelBuilder.infoBox().withProps({ + width: WIZARD_INPUT_COMPONENT_WIDTH, + style: 'error', + text: '', + CSSStyles: { + 'font-size': '13px' + } + }).withValidation(component => { + if (component.style === 'error') { + return false; + } + return true; + }).component(); + + const authenticationKeysLabel = this._view.modelBuilder.text().withProps({ + value: constants.AUTHENTICATION_KEYS, + CSSStyles: { + 'font-weight': 'bold', + 'font-size': '13px' + } + }).component(); + + + this._copy1 = this._view.modelBuilder.button().withProps({ + iconPath: IconPathHelper.copy, + }).component(); + + this._copy1.onDidClick(async (e) => { + await vscode.env.clipboard.writeText(this._authKeyTable.dataValues![0][1].value); + vscode.window.showInformationMessage(constants.SERVICE_KEY_COPIED_HELP); + }); + + this._copy2 = this._view.modelBuilder.button().withProps({ + iconPath: IconPathHelper.copy + }).component(); + + this._copy2.onDidClick(async (e) => { + await vscode.env.clipboard.writeText(this._authKeyTable.dataValues![1][1].value); + vscode.window.showInformationMessage(constants.SERVICE_KEY_COPIED_HELP); + }); + + this._refresh1 = this._view.modelBuilder.button().withProps({ + iconPath: IconPathHelper.refresh + }).component(); + + this._refresh2 = this._view.modelBuilder.button().withProps({ + iconPath: IconPathHelper.refresh, + }).component(); + + this._authKeyTable = this._view.modelBuilder.declarativeTable().withProps({ + columns: [ + { + displayName: constants.NAME, + valueType: azdata.DeclarativeDataType.string, + width: '50px', + isReadOnly: true, + rowCssStyles: { + 'font-size': '13px' + }, + headerCssStyles: { + 'font-size': '13px' + } + }, + { + displayName: constants.AUTH_KEY_COLUMN_HEADER, + valueType: azdata.DeclarativeDataType.string, + width: '500px', + isReadOnly: true, + rowCssStyles: { + 'font-size': '13px', + + }, + headerCssStyles: { + 'font-size': '13px' + } + }, + { + displayName: '', + valueType: azdata.DeclarativeDataType.component, + width: '30px', + isReadOnly: true, + rowCssStyles: { + 'font-size': '13px' + }, + headerCssStyles: { + 'font-size': '13px' + } + } + ], + CSSStyles: { + 'margin-top': '5px', + 'width': WIZARD_INPUT_COMPONENT_WIDTH + } + }).component(); + + statusContainer.addItems([ + this._dmsStatusInfoBox, + authenticationKeysLabel, + this._authKeyTable + ]); + + this._connectionStatusLoader = this._view.modelBuilder.loadingComponent().withItem( + statusContainer + ).component(); + + container.addItems( + [ + connectionLabelContainer, + this._connectionStatusLoader + ] + ); + + return container; + } + + public async populateMigrationService(sqlMigrationService?: SqlMigrationService, serviceNodes?: string[], resourceGroupName?: string): Promise { + this._resourceGroupDropdown.loading = true; + this._dmsDropdown.loading = true; if (sqlMigrationService && serviceNodes) { this.migrationStateModel._sqlMigrationService = sqlMigrationService; this.migrationStateModel._nodeNames = serviceNodes; } + try { - this.migrationServiceDropdown.values = await this.migrationStateModel.getSqlMigrationServiceValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._targetServerInstance); - if (this.migrationStateModel._sqlMigrationService) { - this.migrationServiceDropdown.value = { - name: this.migrationStateModel._sqlMigrationService.id, - displayName: this.migrationStateModel._sqlMigrationService.name - }; + this._subscription.value = this.migrationStateModel._targetSubscription.name; + this._location.value = await getLocationDisplayName(this.migrationStateModel._targetServerInstance.location); + this._resourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._targetSubscription); + if (resourceGroupName) { + const index = (this._resourceGroupDropdown.values).findIndex(v => v.displayName.toLowerCase() === resourceGroupName.toLowerCase()); + if (resourceGroupName.toLowerCase() === (this._resourceGroupDropdown.value).displayName.toLowerCase()) { + this.populateDms(resourceGroupName); + } else { + this._resourceGroupDropdown.value = this._resourceGroupDropdown.values[index]; + } } else { - this.migrationStateModel._sqlMigrationService = this.migrationStateModel.getMigrationService(0); + this._resourceGroupDropdown.value = this._resourceGroupDropdown.values[0]; } } catch (error) { console.log(error); } finally { - this.migrationServiceDropdown.loading = false; + this._resourceGroupDropdown.loading = false; + } + + } + + public async populateDms(resourceGroupName: string): Promise { + this._dmsDropdown.loading = true; + try { + this._dmsDropdown.values = await this.migrationStateModel.getSqlMigrationServiceValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._targetServerInstance, resourceGroupName); + let index = -1; + if (this.migrationStateModel._sqlMigrationService) { + index = (this._dmsDropdown.values).findIndex(v => v.displayName.toLowerCase() === this.migrationStateModel._sqlMigrationService.name.toLowerCase()); + } + if (index !== -1) { + this._dmsDropdown.value = this._dmsDropdown.values[index]; + } else { + this._dmsDropdown.value = this._dmsDropdown.values[0]; + } + } catch (e) { + console.log(e); + } finally { + this._dmsDropdown.loading = false; } } @@ -165,8 +423,17 @@ export class IntergrationRuntimePage extends MigrationWizardPage { private async loadMigrationServiceStatus(): Promise { this._statusLoadingComponent.loading = true; try { - this._migrationDetailsContainer.clearItems(); + await this.loadStatus(); + } catch (error) { + console.log(error); + } finally { + this._statusLoadingComponent.loading = false; + } + } + private async loadStatus(): Promise { + this._connectionStatusLoader.loading = true; + try { if (this.migrationStateModel._sqlMigrationService) { const migrationService = await getSqlMigrationService( this.migrationStateModel._azureAccount, @@ -181,9 +448,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage { this.migrationStateModel._sqlMigrationService.properties.resourceGroup, this.migrationStateModel._sqlMigrationService.location, this.migrationStateModel._sqlMigrationService!.name); - this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map((node) => { - return node.nodeName; - }); + this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map(node => node.nodeName); const migrationServiceAuthKeys = await getSqlMigrationServiceAuthKeys( this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription, @@ -192,228 +457,61 @@ export class IntergrationRuntimePage extends MigrationWizardPage { this.migrationStateModel._sqlMigrationService!.name ); - const migrationServiceTitle = this._view.modelBuilder.text().withProps({ - value: constants.SQL_MIGRATION_SERVICE_DETAILS_HEADER(migrationService.name), - CSSStyles: { - 'font-weight': 'bold' - } - }).component(); - - const connectionStatusLabel = this._view.modelBuilder.text().withProps({ - value: constants.SERVICE_CONNECTION_STATUS, - CSSStyles: { - 'font-weight': 'bold', - 'width': '150px' - } - }).component(); - - const refreshStatus = this._view.modelBuilder.button().withProps({ - label: constants.REFRESH, - secondary: true, - width: '50px' - }).component(); - - - - const connectionLabelContainer = this._view.modelBuilder.flexContainer().withLayout({ - flexFlow: 'row', - alignItems: 'center' - }).withItems( - [ - connectionStatusLabel, - refreshStatus - ], - { - CSSStyles: { 'margin-right': '5px' } - } - ).component(); - - const connectionStatus = this._view.modelBuilder.infoBox().component(); - const connectionStatusLoader = this._view.modelBuilder.loadingComponent().withItem(connectionStatus).withProps({ - loading: false - }).component(); - refreshStatus.onDidClick(async (e) => { - connectionStatusLoader.loading = true; - - const migrationService = await getSqlMigrationService( - this.migrationStateModel._azureAccount, - this.migrationStateModel._targetSubscription, - this.migrationStateModel._sqlMigrationService.properties.resourceGroup, - this.migrationStateModel._sqlMigrationService.location, - this.migrationStateModel._sqlMigrationService.name); - this.migrationStateModel._sqlMigrationService = migrationService; - const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData( - this.migrationStateModel._azureAccount, - this.migrationStateModel._targetSubscription, - this.migrationStateModel._sqlMigrationService.properties.resourceGroup, - this.migrationStateModel._sqlMigrationService.location, - this.migrationStateModel._sqlMigrationService!.name); - this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map((node) => { - return node.nodeName; - }); - - const state = migrationService.properties.integrationRuntimeState; - if (state === 'Online') { - connectionStatus.updateProperties({ - text: constants.SERVICE_READY(this.migrationStateModel._sqlMigrationService!.name, this.migrationStateModel._nodeNames.join(', ')), - style: 'success' - }); - } else { - connectionStatus.updateProperties({ - text: constants.SERVICE_NOT_READY(this.migrationStateModel._sqlMigrationService!.name), - style: 'error' - }); - } - - connectionStatusLoader.loading = false; + this.migrationStateModel._nodeNames = migrationServiceMonitoringStatus.nodes.map((node) => { + return node.nodeName; }); const state = migrationService.properties.integrationRuntimeState; - if (migrationService) { - if (state === 'Online') { - connectionStatus.updateProperties({ - text: constants.SERVICE_READY(this.migrationStateModel._sqlMigrationService!.name, this.migrationStateModel._nodeNames.join(', ')), - style: 'success' - }); - } else { - connectionStatus.updateProperties({ - text: constants.SERVICE_NOT_READY(this.migrationStateModel._sqlMigrationService!.name), - style: 'error' - }); - } + if (state === 'Online') { + this._dmsStatusInfoBox.updateProperties({ + text: constants.SERVICE_READY(this.migrationStateModel._sqlMigrationService!.name, this.migrationStateModel._nodeNames.join(', ')), + style: 'success' + }); + } else { + this._dmsStatusInfoBox.updateProperties({ + text: constants.SERVICE_NOT_READY(this.migrationStateModel._sqlMigrationService!.name), + style: 'error' + }); } - const authenticationKeysLabel = this._view.modelBuilder.text().withProps({ - value: constants.AUTHENTICATION_KEYS, - CSSStyles: { - 'font-weight': 'bold' - } - }).component(); + this._dmsStatusInfoBox.validate(); - const migrationServiceAuthKeyTable = this._view.modelBuilder.declarativeTable().withProps({ - columns: [ + + + const data = [ + [ { - displayName: constants.NAME, - valueType: azdata.DeclarativeDataType.string, - width: '50px', - isReadOnly: true, - rowCssStyles: { - 'text-align': 'center' - } + value: constants.SERVICE_KEY1_LABEL }, { - displayName: constants.AUTH_KEY_COLUMN_HEADER, - valueType: azdata.DeclarativeDataType.string, - width: '500px', - isReadOnly: true, - rowCssStyles: { - overflow: 'scroll' - } + value: migrationServiceAuthKeys.authKey1 }, { - displayName: '', - valueType: azdata.DeclarativeDataType.component, - width: '15px', - isReadOnly: true, - }, - { - displayName: '', - valueType: azdata.DeclarativeDataType.component, - width: '15px', - isReadOnly: true, + value: this._view.modelBuilder.flexContainer().withItems([this._copy1, this._refresh1]).component() } ], - CSSStyles: { - 'margin-top': '5px' - } - }).component(); - - - const copyKey1Button = this._view.modelBuilder.button().withProps({ - iconPath: IconPathHelper.copy - }).component(); - - copyKey1Button.onDidClick((e) => { - vscode.env.clipboard.writeText(migrationServiceAuthKeyTable.dataValues![0][1].value); - vscode.window.showInformationMessage(constants.SERVICE_KEY_COPIED_HELP); - }); - - const copyKey2Button = this._view.modelBuilder.button().withProps({ - iconPath: IconPathHelper.copy - }).component(); - - copyKey2Button.onDidClick((e) => { - vscode.env.clipboard.writeText(migrationServiceAuthKeyTable.dataValues![1][1].value); - vscode.window.showInformationMessage(constants.SERVICE_KEY_COPIED_HELP); - }); - - const refreshKey1Button = this._view.modelBuilder.button().withProps({ - iconPath: IconPathHelper.refresh - }).component(); - - refreshKey1Button.onDidClick((e) => {//TODO: add refresh logic - }); - - const refreshKey2Button = this._view.modelBuilder.button().withProps({ - iconPath: IconPathHelper.refresh - }).component(); - - refreshKey2Button.onDidClick((e) => {//TODO: add refresh logic - }); - - migrationServiceAuthKeyTable.updateProperties({ - dataValues: [ - [ - { - value: constants.SERVICE_KEY1_LABEL - }, - { - value: migrationServiceAuthKeys.authKey1 - }, - { - value: copyKey1Button - }, - { - value: refreshKey1Button - } - ], - [ - { - value: constants.SERVICE_KEY2_LABEL - }, - { - value: migrationServiceAuthKeys.authKey2 - }, - { - value: copyKey2Button - }, - { - value: refreshKey2Button - } - ] - ] - }); - - this._migrationDetailsContainer.addItems( [ - migrationServiceTitle, - createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name), - createInformationRow(this._view, constants.RESOURCE_GROUP, migrationService.properties.resourceGroup), - createInformationRow(this._view, constants.LOCATION, await this.migrationStateModel.getLocationDisplayName(migrationService.location)), - connectionLabelContainer, - connectionStatusLoader, - authenticationKeysLabel, - migrationServiceAuthKeyTable + { + value: constants.SERVICE_KEY2_LABEL + }, + { + value: migrationServiceAuthKeys.authKey2 + }, + { + value: this._view.modelBuilder.flexContainer().withItems([this._copy2, this._refresh2]).component() + } ] - ); + ]; + + this._authKeyTable.updateProperties({ + dataValues: data + }); } - } catch (error) { - console.log(error); - this._migrationDetailsContainer.clearItems(); + } catch (e) { + console.log(e); } finally { - this._statusLoadingComponent.loading = false; + this._connectionStatusLoader.loading = false; } } } -