diff --git a/extensions/sql-migration/images/migrationService.svg b/extensions/sql-migration/images/migrationService.svg
new file mode 100644
index 0000000000..fd3b819e43
--- /dev/null
+++ b/extensions/sql-migration/images/migrationService.svg
@@ -0,0 +1,52 @@
+
diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts
index 831d49ffd7..b613df7c44 100644
--- a/extensions/sql-migration/src/api/azure.ts
+++ b/extensions/sql-migration/src/api/azure.ts
@@ -175,6 +175,24 @@ export async function getSqlMigrationServiceAuthKeys(account: azdata.Account, su
};
}
+export async function regenerateSqlMigrationServiceAuthKey(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, keyName: string): Promise {
+ const api = await getAzureCoreAPI();
+ const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/regenerateAuthKeys?api-version=2020-09-01-preview`;
+ const requestBody = {
+ 'location': regionName,
+ 'keyName': keyName,
+ };
+
+ const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, requestBody, true);
+ if (response.errors.length > 0) {
+ throw new Error(response.errors.toString());
+ }
+ return {
+ authKey1: response?.response?.data?.authKey1 ?? '',
+ authKey2: response?.response?.data?.authKey2 ?? ''
+ };
+}
+
export async function getStorageAccountAccessKeys(account: azdata.Account, subscription: Subscription, storageAccount: StorageAccount): Promise {
const api = await getAzureCoreAPI();
const path = `/subscriptions/${subscription.id}/resourceGroups/${storageAccount.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}/listKeys?api-version=2019-06-01`;
diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts
index c0c62a58e3..53cd081cdd 100644
--- a/extensions/sql-migration/src/constants/iconPathHelper.ts
+++ b/extensions/sql-migration/src/constants/iconPathHelper.ts
@@ -32,6 +32,7 @@ export class IconPathHelper {
public static info: IconPath;
public static error: IconPath;
public static completingCutover: IconPath;
+ public static migrationService: IconPath;
public static setExtensionContext(context: vscode.ExtensionContext) {
IconPathHelper.copy = {
@@ -118,5 +119,9 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/completingCutover.svg'),
dark: context.asAbsolutePath('images/completingCutover.svg')
};
+ IconPathHelper.migrationService = {
+ light: context.asAbsolutePath('images/migrationService.svg'),
+ dark: context.asAbsolutePath('images/migrationService.svg')
+ };
}
}
diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts
index 1de7080822..0bb430a9ef 100644
--- a/extensions/sql-migration/src/constants/strings.ts
+++ b/extensions/sql-migration/src/constants/strings.ts
@@ -190,6 +190,9 @@ export const SERVICE_KEY_COPIED_HELP = localize('sql.migration.key.copied', "Key
export const REFRESH_KEYS = localize('sql.migration.refresh.keys', "Refresh keys");
export const COPY_KEY = localize('sql.migration.copy.key', "Copy key");
export const AUTH_KEY_COLUMN_HEADER = localize('sql.migration.authkeys.header', "Authentication key");
+export function AUTH_KEY_REFRESHED(keyName: string): string {
+ return localize('sql.migration.authkeys.refresh.message', "Authentication key '{0}' has been refreshed.", keyName);
+}
export function SERVICE_NOT_READY(serviceName: string): string {
return localize('sql.migration.service.not.ready', "Azure Database Migration Service is not registered. Azure Database Migration Service '{0}' needs to be registered with self-hosted Integration Runtime on any node.", serviceName);
}
@@ -389,6 +392,14 @@ export function SEC(sec: number): string {
return localize('sql.migration.sec', "{0} sec", sec);
}
+// SQL Migration Service Details page.
+export const SQL_MIGRATION_SERVICE_DETAILS_SUB_TITLE = localize('sql.migration.service.details.dialog.title', "Azure Database Migration Service");
+export const SQL_MIGRATION_SERVICE_DETAILS_BUTTON_LABEL = localize('sql.migration.service.details.button.label', "Close");
+export const SQL_MIGRATION_SERVICE_DETAILS_IR_LABEL = localize('sql.migration.service.details.ir.label', "Self-hosted Integration Runtime node");
+export const SQL_MIGRATION_SERVICE_DETAILS_AUTH_KEYS_LABEL = localize('sql.migration.service.details.authkeys.label', "Authentication keys");
+export const SQL_MIGRATION_SERVICE_DETAILS_AUTH_KEYS_TITLE = localize('sql.migration.service.details.authkeys.title', "Authentication keys used to connect to the Self-hosted Integration Runtime node");
+export const SQL_MIGRATION_SERVICE_DETAILS_STATUS_UNAVAILABLE = localize('sql.migration.service.details.status.unavailable', "-- unavailable --");
+
//Source Credentials page.
export const SOURCE_CONFIGURATION = localize('sql.migration.source.configuration', "Source Configuration");
export const SOURCE_CREDENTIALS = localize('sql.migration.source.credentials', "Source Credentials");
diff --git a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts
index b90960ae12..28cc48c7d8 100644
--- a/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts
+++ b/extensions/sql-migration/src/dialog/migrationStatus/migrationStatusDialog.ts
@@ -11,6 +11,8 @@ import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDial
import { AdsMigrationStatus, MigrationStatusDialogModel } from './migrationStatusDialogModel';
import * as loc from '../../constants/strings';
import { convertTimeDifferenceToDuration, filterMigrations } from '../../api/utils';
+import { SqlMigrationServiceDetailsDialog } from '../sqlMigrationService/sqlMigrationServiceDetailsDialog';
+
export class MigrationStatusDialog {
private _model: MigrationStatusDialogModel;
private _dialogObject!: azdata.window.Dialog;
@@ -171,7 +173,7 @@ export class MigrationStatusDialog {
url: ''
}).component();
dms.onDidClick((e) => {
- vscode.window.showInformationMessage(loc.COMING_SOON);
+ (new SqlMigrationServiceDetailsDialog(migration)).initialize();
});
migrationRow.push({
diff --git a/extensions/sql-migration/src/dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog.ts b/extensions/sql-migration/src/dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog.ts
new file mode 100644
index 0000000000..255b01bcaa
--- /dev/null
+++ b/extensions/sql-migration/src/dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog.ts
@@ -0,0 +1,305 @@
+// /*---------------------------------------------------------------------------------------------
+// * Copyright (c) Microsoft Corporation. All rights reserved.
+// * Licensed under the Source EULA. See License.txt in the project root for license information.
+// *--------------------------------------------------------------------------------------------*/
+
+import * as azdata from 'azdata';
+import * as vscode from 'vscode';
+import { getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, regenerateSqlMigrationServiceAuthKey } from '../../api/azure';
+import { IconPathHelper } from '../../constants/iconPathHelper';
+import * as constants from '../../constants/strings';
+import { MigrationContext } from '../../models/migrationLocalStorage';
+
+const CONTROL_MARGIN = '10px';
+const COLUMN_WIDTH = '50px';
+const STRETCH_WIDTH = '100%';
+const LABEL_MARGIN = '0 10px 0 10px';
+const VALUE_MARGIN = '0 10px 10px 10px';
+const INFO_VALUE_MARGIN = '0 10px 0 0';
+const NO_MARGIN = '0';
+const ICON_SIZE = '28px';
+const IMAGE_SIZE = '21px';
+const TITLE_FONT_SIZE = '14px';
+const DESCRIPTION_FONT_SIZE = '10px';
+const FONT_SIZE = '13px';
+const FONT_WEIGHT_BOLD = 'bold';
+const AUTH_KEY1 = 'authKey1';
+const AUTH_KEY2 = 'authKey2';
+
+export class SqlMigrationServiceDetailsDialog {
+
+ private _dialog: azdata.window.Dialog;
+ private _migrationServiceAuthKeyTable!: azdata.DeclarativeTableComponent;
+
+ constructor(private migrationContext: MigrationContext) {
+ this._dialog = azdata.window.createModelViewDialog(
+ '',
+ 'SqlMigrationServiceDetailsDialog',
+ 580,
+ 'flyout');
+ }
+
+ async initialize(): Promise {
+ this._dialog.registerContent(
+ async (view: azdata.ModelView) => await this.createServiceContent(
+ view,
+ this.migrationContext));
+
+ this._dialog.okButton.label = constants.SQL_MIGRATION_SERVICE_DETAILS_BUTTON_LABEL;
+ this._dialog.okButton.focused = true;
+ this._dialog.cancelButton.hidden = true;
+ azdata.window.openDialog(this._dialog);
+ }
+
+ private async createServiceContent(view: azdata.ModelView, migrationContext: MigrationContext): Promise {
+ this._migrationServiceAuthKeyTable = this._createIrTable(view);
+ const serviceNode = (await getSqlMigrationServiceMonitoringData(
+ migrationContext.azureAccount,
+ migrationContext.subscription,
+ migrationContext.controller.properties.resourceGroup,
+ migrationContext.controller.location,
+ migrationContext.controller.name
+ ));
+ const serviceNodeName = serviceNode.nodes?.map(node => node.nodeName).join(', ')
+ || constants.SQL_MIGRATION_SERVICE_DETAILS_STATUS_UNAVAILABLE;
+
+ const flexContainer = view.modelBuilder
+ .flexContainer()
+ .withItems([
+ this._createHeading(view, migrationContext),
+ view.modelBuilder
+ .separator()
+ .withProps({ width: STRETCH_WIDTH })
+ .component(),
+ this._createTextItem(view, constants.SUBSCRIPTION, LABEL_MARGIN),
+ this._createTextItem(view, migrationContext.subscription.name, VALUE_MARGIN),
+ this._createTextItem(view, constants.LOCATION, LABEL_MARGIN),
+ this._createTextItem(view, migrationContext.controller.location.toUpperCase(), VALUE_MARGIN),
+ this._createTextItem(view, constants.RESOURCE_GROUP, LABEL_MARGIN),
+ this._createTextItem(view, migrationContext.controller.properties.resourceGroup, VALUE_MARGIN),
+ this._createTextItem(view, constants.SQL_MIGRATION_SERVICE_DETAILS_IR_LABEL, LABEL_MARGIN),
+ this._createTextItem(view, serviceNodeName, VALUE_MARGIN),
+ this._createTextItem(
+ view,
+ constants.SQL_MIGRATION_SERVICE_DETAILS_AUTH_KEYS_LABEL,
+ INFO_VALUE_MARGIN,
+ constants.SQL_MIGRATION_SERVICE_DETAILS_AUTH_KEYS_TITLE),
+ this._migrationServiceAuthKeyTable,
+ ])
+ .withLayout({ flexFlow: 'column' })
+ .withProps({ CSSStyles: { 'padding': CONTROL_MARGIN } })
+ .component();
+
+ await view.initializeModel(flexContainer);
+ return await this._refreshAuthTable(view, migrationContext);
+ }
+
+ private _createHeading(view: azdata.ModelView, migrationContext: MigrationContext): azdata.FlexContainer {
+ return view.modelBuilder
+ .flexContainer()
+ .withItems([
+ view.modelBuilder
+ .image()
+ .withProps({
+ iconPath: IconPathHelper.migrationService,
+ iconHeight: ICON_SIZE,
+ iconWidth: ICON_SIZE,
+ height: ICON_SIZE,
+ width: ICON_SIZE,
+ })
+ .withProps({ CSSStyles: { 'margin-right': CONTROL_MARGIN } })
+ .component(),
+ view.modelBuilder
+ .flexContainer()
+ .withItems([
+ view.modelBuilder
+ .text()
+ .withProps({
+ value: migrationContext.controller.name,
+ CSSStyles: {
+ 'font-size': TITLE_FONT_SIZE,
+ 'font-weight': FONT_WEIGHT_BOLD,
+ 'margin': NO_MARGIN,
+ }
+ })
+ .component(),
+ view.modelBuilder
+ .text()
+ .withProps({
+ value: constants.SQL_MIGRATION_SERVICE_DETAILS_SUB_TITLE,
+ CSSStyles: {
+ 'font-size': DESCRIPTION_FONT_SIZE,
+ 'margin': NO_MARGIN,
+ }
+ })
+ .component(),
+ ])
+ .withLayout({
+ flexFlow: 'column',
+ textAlign: 'left',
+ })
+ .component(),
+ ])
+ .withLayout({ flexFlow: 'row' })
+ .withProps({
+ display: 'inline-flex',
+ CSSStyles: { 'margin': LABEL_MARGIN },
+ })
+ .component();
+ }
+
+ private _createTextItem(view: azdata.ModelView, value: string, margin: string, description?: string): azdata.TextComponent {
+ return view.modelBuilder
+ .text()
+ .withProps({
+ value: value,
+ description: description,
+ title: value,
+ CSSStyles: {
+ 'font-size': FONT_SIZE,
+ 'margin': margin,
+ }
+ })
+ .component();
+ }
+
+ private _createIrTable(view: azdata.ModelView): azdata.DeclarativeTableComponent {
+ return view.modelBuilder
+ .declarativeTable()
+ .withProps({
+ columns: [
+ this._createColumn(constants.NAME, COLUMN_WIDTH, azdata.DeclarativeDataType.string),
+ this._createColumn(constants.AUTH_KEY_COLUMN_HEADER, STRETCH_WIDTH, azdata.DeclarativeDataType.string),
+ this._createColumn('', COLUMN_WIDTH, azdata.DeclarativeDataType.component),
+ ],
+ CSSStyles: {
+ 'margin': VALUE_MARGIN,
+ 'text-align': 'left',
+ },
+ })
+ .component();
+ }
+
+ private _createColumn(name: string, width: string, valueType: azdata.DeclarativeDataType): azdata.DeclarativeTableColumn {
+ return {
+ displayName: name,
+ valueType: valueType,
+ width: width,
+ isReadOnly: true,
+ rowCssStyles: { 'font-size': FONT_SIZE },
+ headerCssStyles: {
+ 'font-size': FONT_SIZE,
+ 'font-weight': 'normal',
+ },
+ };
+ }
+
+ private async _regenerateAuthKey(view: azdata.ModelView, migrationContext: MigrationContext, keyName: string): Promise {
+ const keys = await regenerateSqlMigrationServiceAuthKey(
+ migrationContext.azureAccount,
+ migrationContext.subscription,
+ migrationContext.controller.properties.resourceGroup,
+ migrationContext.controller.location.toUpperCase(),
+ migrationContext.controller.name,
+ keyName);
+
+ if (keys?.authKey1 && keyName === AUTH_KEY1) {
+ await this._updateTableCell(this._migrationServiceAuthKeyTable, 0, 1, keys.authKey1, constants.SERVICE_KEY1_LABEL);
+ }
+ else if (keys?.authKey2 && keyName === AUTH_KEY2) {
+ await this._updateTableCell(this._migrationServiceAuthKeyTable, 1, 1, keys.authKey2, constants.SERVICE_KEY2_LABEL);
+ }
+ }
+
+ private async _updateTableCell(table: azdata.DeclarativeTableComponent, row: number, col: number, value: string, keyName: string): Promise {
+ const dataValues = table.dataValues;
+ dataValues![row][col].value = value;
+ table.dataValues = [];
+ table.dataValues = dataValues;
+ await vscode.window.showInformationMessage(constants.AUTH_KEY_REFRESHED(keyName));
+ }
+
+ private async _refreshAuthTable(view: azdata.ModelView, migrationContext: MigrationContext): Promise {
+ const keys = await getSqlMigrationServiceAuthKeys(
+ migrationContext.azureAccount,
+ migrationContext.subscription,
+ migrationContext.controller.properties.resourceGroup,
+ migrationContext.controller.location.toUpperCase(),
+ migrationContext.controller.name);
+
+ const copyKey1Button = view.modelBuilder
+ .button()
+ .withProps({
+ iconPath: IconPathHelper.copy,
+ height: IMAGE_SIZE,
+ width: IMAGE_SIZE,
+ })
+ .component();
+
+ copyKey1Button.onDidClick((e) => {
+ vscode.env.clipboard.writeText(keys.authKey1);
+ vscode.window.showInformationMessage(constants.SERVICE_KEY_COPIED_HELP);
+ });
+
+ const copyKey2Button = view.modelBuilder
+ .button()
+ .withProps({
+ iconPath: IconPathHelper.copy,
+ height: IMAGE_SIZE,
+ width: IMAGE_SIZE,
+ })
+ .component();
+
+ copyKey2Button.onDidClick((e) => {
+ vscode.env.clipboard.writeText(keys.authKey2);
+ vscode.window.showInformationMessage(constants.SERVICE_KEY_COPIED_HELP);
+ });
+
+ const refreshKey1Button = view.modelBuilder
+ .button()
+ .withProps({
+ iconPath: IconPathHelper.refresh,
+ height: IMAGE_SIZE,
+ width: IMAGE_SIZE,
+ })
+ .component();
+ refreshKey1Button.onDidClick(
+ async (e) => await this._regenerateAuthKey(view, migrationContext, AUTH_KEY1));
+
+ const refreshKey2Button = view.modelBuilder
+ .button()
+ .withProps({
+ iconPath: IconPathHelper.refresh,
+ height: IMAGE_SIZE,
+ width: IMAGE_SIZE,
+ })
+ .component();
+ refreshKey2Button.onDidClick(
+ async (e) => await this._regenerateAuthKey(view, migrationContext, AUTH_KEY2));
+
+ this._migrationServiceAuthKeyTable.updateProperties({
+ dataValues: [
+ [
+ { value: constants.SERVICE_KEY1_LABEL },
+ { value: keys.authKey1 },
+ {
+ value: view.modelBuilder
+ .flexContainer()
+ .withItems([copyKey1Button, refreshKey1Button])
+ .component()
+ }
+ ],
+ [
+ { value: constants.SERVICE_KEY2_LABEL },
+ { value: keys.authKey2 },
+ {
+ value: view.modelBuilder
+ .flexContainer()
+ .withItems([copyKey2Button, refreshKey2Button])
+ .component()
+ }
+ ]
+ ]
+ });
+ }
+}