diff --git a/extensions/sql-migration/images/extension.png b/extensions/sql-migration/images/extension.png index c86d6d1e00..d074d1b0b0 100644 Binary files a/extensions/sql-migration/images/extension.png and b/extensions/sql-migration/images/extension.png differ diff --git a/extensions/sql-migration/images/migration.svg b/extensions/sql-migration/images/migration.svg index 35d958684b..765f93985a 100644 --- a/extensions/sql-migration/images/migration.svg +++ b/extensions/sql-migration/images/migration.svg @@ -1,32 +1,27 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + 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; } } } -