Dev/brih/feature/switch ads to portal context (#18963)

* Add CodeQL Analysis workflow (#10195)

* Add CodeQL Analysis workflow

* Fix path

* dashboard refactor

* update version, readme, minor ui changes

* fix merge issue

* Revert "Add CodeQL Analysis workflow (#10195)"

This reverts commit fe98d586cd75be4758ac544649bb4983accf4acd.

* fix context switching issue

* fix resource id parsing error and mi api version

* mv refresh btn, rm autorefresh, align cards

* remove missed autorefresh code

* improve error handling and messages

* fix typos

* remove duplicate/unnecessary  _populate* calls

* change clear configuration button text

* remove confusing watermark text

* add stale account handling

Co-authored-by: Justin Hutchings <jhutchings1@users.noreply.github.com>
This commit is contained in:
brian-harris
2022-04-12 16:26:40 -07:00
committed by GitHub
parent d98a421035
commit 3a0ac7279a
30 changed files with 2163 additions and 1701 deletions

View File

@@ -22,7 +22,10 @@ export class SavedAssessmentDialog {
private context: vscode.ExtensionContext;
private _disposables: vscode.Disposable[] = [];
constructor(context: vscode.ExtensionContext, stateModel: MigrationStateModel) {
constructor(
context: vscode.ExtensionContext,
stateModel: MigrationStateModel,
private readonly _onClosedCallback: () => Promise<void>) {
this.stateModel = stateModel;
this.context = context;
}
@@ -53,7 +56,7 @@ export class SavedAssessmentDialog {
dialog.registerCloseValidator(async () => {
if (this.stateModel.resumeAssessment) {
if (!this.stateModel.loadSavedInfo()) {
if (await !this.stateModel.loadSavedInfo()) {
void vscode.window.showInformationMessage(constants.OPEN_SAVED_INFO_ERROR);
return false;
}
@@ -77,7 +80,11 @@ export class SavedAssessmentDialog {
}
protected async execute() {
const wizardController = new WizardController(this.context, this.stateModel);
const wizardController = new WizardController(
this.context,
this.stateModel,
this._onClosedCallback);
await wizardController.openWizard(this.stateModel.sourceConnectionId);
this._isOpen = false;
}
@@ -103,11 +110,11 @@ export class SavedAssessmentDialog {
checked: true
}).component();
radioStart.onDidChangeCheckedState((e) => {
this._disposables.push(radioStart.onDidChangeCheckedState((e) => {
if (e) {
this.stateModel.resumeAssessment = false;
}
});
}));
const radioContinue = view.modelBuilder.radioButton().withProps({
label: constants.RESUME_SESSION,
name: buttonGroup,
@@ -117,11 +124,11 @@ export class SavedAssessmentDialog {
checked: false
}).component();
radioContinue.onDidChangeCheckedState((e) => {
this._disposables.push(radioContinue.onDidChangeCheckedState((e) => {
if (e) {
this.stateModel.resumeAssessment = true;
}
});
}));
const flex = view.modelBuilder.flexContainer()
.withLayout({

View File

@@ -95,7 +95,14 @@ export class CreateSqlMigrationServiceDialog {
try {
clearDialogMessage(this._dialogObject);
this._selectedResourceGroup = resourceGroup;
this._createdMigrationService = await createSqlMigrationService(this._model._azureAccount, subscription, resourceGroup, location, serviceName!, this._model._sessionId);
this._createdMigrationService = await createSqlMigrationService(
this._model._azureAccount,
subscription,
resourceGroup,
location,
serviceName!,
this._model._sessionId);
if (this._createdMigrationService.error) {
this.setDialogMessage(`${this._createdMigrationService.error.code} : ${this._createdMigrationService.error.message}`);
this._statusLoadingComponent.loading = false;
@@ -490,7 +497,12 @@ export class CreateSqlMigrationServiceDialog {
for (let i = 0; i < maxRetries; i++) {
try {
clearDialogMessage(this._dialogObject);
migrationServiceStatus = await getSqlMigrationService(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService.name, this._model._sessionId);
migrationServiceStatus = await getSqlMigrationService(
this._model._azureAccount,
subscription,
resourceGroup,
location,
this._createdMigrationService.name);
break;
} catch (e) {
this._dialogObject.message = {
@@ -502,7 +514,13 @@ export class CreateSqlMigrationServiceDialog {
}
await new Promise(r => setTimeout(r, 5000));
}
const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService!.name, this._model._sessionId);
const migrationServiceMonitoringStatus = await getSqlMigrationServiceMonitoringData(
this._model._azureAccount,
subscription,
resourceGroup,
location,
this._createdMigrationService!.name);
this.irNodes = migrationServiceMonitoringStatus.nodes.map((node) => {
return node.nodeName;
});
@@ -536,7 +554,12 @@ export class CreateSqlMigrationServiceDialog {
const subscription = this._model._targetSubscription;
const resourceGroup = (this.migrationServiceResourceGroupDropdown.value as azdata.CategoryValue).name;
const location = this._model._targetServerInstance.location;
const keys = await getSqlMigrationServiceAuthKeys(this._model._azureAccount, subscription, resourceGroup, location, this._createdMigrationService!.name, this._model._sessionId);
const keys = await getSqlMigrationServiceAuthKeys(
this._model._azureAccount,
subscription,
resourceGroup,
location,
this._createdMigrationService!.name);
this._copyKey1Button = this._view.modelBuilder.button().withProps({
title: constants.COPY_KEY1,

View File

@@ -7,10 +7,11 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
import * as constants from '../../constants/strings';
import { SqlManagedInstance } from '../../api/azure';
import { getMigrationTargetInstance, SqlManagedInstance } from '../../api/azure';
import { IconPathHelper } from '../../constants/iconPathHelper';
import { convertByteSizeToReadableUnit, get12HourTime } from '../../api/utils';
import * as styles from '../../constants/styles';
import { isBlobMigration } from '../../constants/helper';
export class ConfirmCutoverDialog {
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
@@ -21,20 +22,17 @@ export class ConfirmCutoverDialog {
}
async initialize(): Promise<void> {
let tab = azdata.window.createTab('');
const tab = azdata.window.createTab('');
tab.registerContent(async (view: azdata.ModelView) => {
this._view = view;
const completeCutoverText = view.modelBuilder.text().withProps({
value: constants.COMPLETE_CUTOVER,
CSSStyles: {
...styles.PAGE_TITLE_CSS
}
CSSStyles: { ...styles.PAGE_TITLE_CSS }
}).component();
const sourceDatabaseText = view.modelBuilder.text().withProps({
value: this.migrationCutoverModel._migration.migrationContext.properties.sourceDatabaseName,
value: this.migrationCutoverModel._migration.properties.sourceDatabaseName,
CSSStyles: {
...styles.SMALL_NOTE_CSS,
'margin': '4px 0px 8px'
@@ -42,12 +40,9 @@ export class ConfirmCutoverDialog {
}).component();
const separator = this._view.modelBuilder.separator().withProps({ width: '800px' }).component();
const helpMainText = this._view.modelBuilder.text().withProps({
value: constants.CUTOVER_HELP_MAIN,
CSSStyles: {
...styles.BODY_CSS
}
CSSStyles: { ...styles.BODY_CSS }
}).component();
const helpStepsText = this._view.modelBuilder.text().withProps({
@@ -58,8 +53,9 @@ export class ConfirmCutoverDialog {
}
}).component();
const fileContainer = this.migrationCutoverModel.isBlobMigration() ? this.createBlobFileContainer() : this.createNetworkShareFileContainer();
const fileContainer = isBlobMigration(this.migrationCutoverModel.migrationStatus)
? this.createBlobFileContainer()
: this.createNetworkShareFileContainer();
const confirmCheckbox = this._view.modelBuilder.checkBox().withProps({
CSSStyles: {
@@ -76,16 +72,19 @@ export class ConfirmCutoverDialog {
const cutoverWarning = this._view.modelBuilder.infoBox().withProps({
text: constants.COMPLETING_CUTOVER_WARNING,
style: 'warning',
CSSStyles: {
...styles.BODY_CSS
}
CSSStyles: { ...styles.BODY_CSS }
}).component();
let infoDisplay = 'none';
if (this.migrationCutoverModel._migration.targetManagedInstance.id.toLocaleLowerCase().includes('managedinstances')
&& (<SqlManagedInstance>this.migrationCutoverModel._migration.targetManagedInstance)?.sku?.tier === 'BusinessCritical') {
infoDisplay = 'inline';
if (this.migrationCutoverModel._migration.id.toLocaleLowerCase().includes('managedinstances')) {
const targetInstance = await getMigrationTargetInstance(
this.migrationCutoverModel._serviceConstext.azureAccount!,
this.migrationCutoverModel._serviceConstext.subscription!,
this.migrationCutoverModel._migration);
if ((<SqlManagedInstance>targetInstance)?.sku?.tier === 'BusinessCritical') {
infoDisplay = 'inline';
}
}
const businessCriticalInfoBox = this._view.modelBuilder.infoBox().withProps({
@@ -111,23 +110,18 @@ export class ConfirmCutoverDialog {
businessCriticalInfoBox
]).component();
this._dialogObject.okButton.enabled = false;
this._dialogObject.okButton.label = constants.COMPLETE_CUTOVER;
this._disposables.push(this._dialogObject.okButton.onClick(async (e) => {
await this.migrationCutoverModel.startCutover();
void vscode.window.showInformationMessage(constants.CUTOVER_IN_PROGRESS(this.migrationCutoverModel._migration.migrationContext.properties.sourceDatabaseName));
void vscode.window.showInformationMessage(
constants.CUTOVER_IN_PROGRESS(
this.migrationCutoverModel._migration.properties.sourceDatabaseName));
}));
const formBuilder = view.modelBuilder.formContainer().withFormItems(
[
{
component: container
}
],
{
horizontal: false
}
[{ component: container }],
{ horizontal: false }
);
const form = formBuilder.withLayout({ width: '100%' }).component();
@@ -144,18 +138,14 @@ export class ConfirmCutoverDialog {
private createBlobFileContainer(): azdata.FlexContainer {
const container = this._view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'margin': '8px 0'
}
CSSStyles: { 'margin': '8px 0' }
}).component();
const containerHeading = this._view.modelBuilder.text().withProps({
value: constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0),
width: 250,
CSSStyles: {
...styles.LABEL_CSS
}
CSSStyles: { ...styles.LABEL_CSS }
}).component();
container.addItem(containerHeading, { flex: '0' });
const refreshButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.refresh,
@@ -165,13 +155,7 @@ export class ConfirmCutoverDialog {
height: 20,
label: constants.REFRESH,
}).component();
container.addItem(containerHeading, {
flex: '0'
});
refreshButton.onDidClick(async e => {
this._disposables.push(refreshButton.onDidClick(async e => {
refreshLoader.loading = true;
try {
await this.migrationCutoverModel.fetchStatus();
@@ -184,11 +168,8 @@ export class ConfirmCutoverDialog {
} finally {
refreshLoader.loading = false;
}
});
container.addItem(refreshButton, {
flex: '0'
});
}));
container.addItem(refreshButton, { flex: '0' });
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
loading: false,
@@ -197,10 +178,8 @@ export class ConfirmCutoverDialog {
'margin-left': '8px'
}
}).component();
container.addItem(refreshLoader, { flex: '0' });
container.addItem(refreshLoader, {
flex: '0'
});
return container;
}
@@ -227,23 +206,18 @@ export class ConfirmCutoverDialog {
}
}).component();
containerHeading.onDidClick(async e => {
this._disposables.push(containerHeading.onDidClick(async e => {
if (expanded) {
containerHeading.iconPath = IconPathHelper.expandButtonClosed;
containerHeading.iconHeight = 12;
await fileTable.updateCssStyles({
'display': 'none'
});
await fileTable.updateCssStyles({ 'display': 'none' });
} else {
containerHeading.iconPath = IconPathHelper.expandButtonOpen;
containerHeading.iconHeight = 8;
await fileTable.updateCssStyles({
'display': 'inline'
});
await fileTable.updateCssStyles({ 'display': 'inline' });
}
expanded = !expanded;
});
}));
const refreshButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.refresh,
@@ -252,16 +226,12 @@ export class ConfirmCutoverDialog {
width: 70,
height: 20,
label: constants.REFRESH,
CSSStyles: {
'margin-top': '13px'
}
CSSStyles: { 'margin-top': '13px' }
}).component();
headingRow.addItem(containerHeading, {
flex: '0'
});
headingRow.addItem(containerHeading, { flex: '0' });
refreshButton.onDidClick(async e => {
this._disposables.push(refreshButton.onDidClick(async e => {
refreshLoader.loading = true;
try {
await this.migrationCutoverModel.fetchStatus();
@@ -276,11 +246,8 @@ export class ConfirmCutoverDialog {
} finally {
refreshLoader.loading = false;
}
});
headingRow.addItem(refreshButton, {
flex: '0'
});
}));
headingRow.addItem(refreshButton, { flex: '0' });
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
loading: false,
@@ -290,20 +257,13 @@ export class ConfirmCutoverDialog {
'height': '13px'
}
}).component();
headingRow.addItem(refreshLoader, {
flex: '0'
});
headingRow.addItem(refreshLoader, { flex: '0' });
container.addItem(headingRow);
const lastScanCompleted = this._view.modelBuilder.text().withProps({
value: constants.LAST_SCAN_COMPLETED(get12HourTime(new Date())),
CSSStyles: {
...styles.NOTE_CSS
}
CSSStyles: { ...styles.NOTE_CSS }
}).component();
container.addItem(lastScanCompleted);
const fileTable = this._view.modelBuilder.table().withProps({
@@ -327,9 +287,7 @@ export class ConfirmCutoverDialog {
data: [],
width: 400,
height: 150,
CSSStyles: {
'display': 'none'
}
CSSStyles: { 'display': 'none' }
}).component();
container.addItem(fileTable);
this.refreshFileTable(fileTable);
@@ -347,9 +305,7 @@ export class ConfirmCutoverDialog {
];
});
} else {
fileTable.data = [
[constants.NO_PENDING_BACKUPS]
];
fileTable.data = [[constants.NO_PENDING_BACKUPS]];
}
}

View File

@@ -6,26 +6,24 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { IconPathHelper } from '../../constants/iconPathHelper';
import { BackupFileInfoStatus, MigrationContext, MigrationStatus } from '../../models/migrationLocalStorage';
import { BackupFileInfoStatus, MigrationServiceContext, MigrationStatus } from '../../models/migrationLocalStorage';
import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
import * as loc from '../../constants/strings';
import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage, SupportedAutoRefreshIntervals, clearDialogMessage, displayDialogErrorMessage } from '../../api/utils';
import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getSqlServerName, getMigrationStatusImage, clearDialogMessage, displayDialogErrorMessage } from '../../api/utils';
import { EOL } from 'os';
import { ConfirmCutoverDialog } from './confirmCutoverDialog';
import { logError, TelemetryViews } from '../../telemtery';
import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog';
import * as styles from '../../constants/styles';
import { canRetryMigration } from '../../constants/helper';
import { canRetryMigration, getMigrationStatus, isBlobMigration, isOfflineMigation } from '../../constants/helper';
import { DatabaseMigration, getResourceName } from '../../api/azure';
const refreshFrequency: SupportedAutoRefreshIntervals = 30000;
const statusImageSize: number = 14;
export class MigrationCutoverDialog {
private _context: vscode.ExtensionContext;
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
private _model: MigrationCutoverDialogModel;
private _migration: MigrationContext;
private _databaseTitleName!: azdata.TextComponent;
private _cutoverButton!: azdata.ButtonComponent;
@@ -52,7 +50,6 @@ export class MigrationCutoverDialog {
private _fileCount!: azdata.TextComponent;
private _fileTable!: azdata.DeclarativeTableComponent;
private _autoRefreshHandle!: any;
private _disposables: vscode.Disposable[] = [];
private _emptyTableFill!: azdata.FlexContainer;
@@ -60,10 +57,13 @@ export class MigrationCutoverDialog {
readonly _infoFieldWidth: string = '250px';
constructor(context: vscode.ExtensionContext, migration: MigrationContext) {
this._context = context;
this._migration = migration;
this._model = new MigrationCutoverDialogModel(migration);
constructor(
private readonly _context: vscode.ExtensionContext,
private readonly _serviceContext: MigrationServiceContext,
private readonly _migration: DatabaseMigration,
private readonly _onClosedCallback: () => Promise<void>) {
this._model = new MigrationCutoverDialogModel(_serviceContext, _migration);
this._dialogObject = azdata.window.createModelViewDialog('', 'MigrationCutoverDialog', 'wide');
}
@@ -224,15 +224,14 @@ export class MigrationCutoverDialog {
);
const form = formBuilder.withLayout({ width: '100%' }).component();
this._disposables.push(this._view.onClosed(e => {
clearInterval(this._autoRefreshHandle);
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
this._disposables.push(
this._view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
return view.initializeModel(form).then(async (value) => {
await this.refreshStatus();
});
await view.initializeModel(form);
await this.refreshStatus();
} catch (e) {
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
}
@@ -242,9 +241,6 @@ export class MigrationCutoverDialog {
this._dialogObject.cancelButton.hidden = true;
this._dialogObject.okButton.label = loc.CLOSE;
this._disposables.push(this._dialogObject.okButton.onClick(e => {
clearInterval(this._autoRefreshHandle);
}));
azdata.window.openDialog(this._dialogObject);
}
@@ -262,7 +258,7 @@ export class MigrationCutoverDialog {
...styles.PAGE_TITLE_CSS
},
width: 950,
value: this._model._migration.migrationContext.properties.sourceDatabaseName
value: this._model._migration.properties.sourceDatabaseName
}).component();
const databaseSubTitle = this._view.modelBuilder.text().withProps({
@@ -282,8 +278,6 @@ export class MigrationCutoverDialog {
width: 950
}).component();
this.setAutoRefresh(refreshFrequency);
const titleLogoContainer = this._view.modelBuilder.flexContainer().withProps({
width: 1000
}).component();
@@ -314,7 +308,7 @@ export class MigrationCutoverDialog {
enabled: false,
CSSStyles: {
...styles.BODY_CSS,
'display': this._isOnlineMigration() ? 'block' : 'none'
'display': isOfflineMigation(this._model._migration) ? 'none' : 'block'
}
}).component();
@@ -322,16 +316,13 @@ export class MigrationCutoverDialog {
await this.refreshStatus();
const dialog = new ConfirmCutoverDialog(this._model);
await dialog.initialize();
await this.refreshStatus();
if (this._model.CutoverError) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CUTOVER_ERROR, this._model.CutoverError);
}
}));
headerActions.addItem(this._cutoverButton, {
flex: '0'
});
headerActions.addItem(this._cutoverButton, { flex: '0' });
this._cancelButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.cancel,
@@ -377,7 +368,11 @@ export class MigrationCutoverDialog {
this._disposables.push(this._retryButton.onDidClick(
async (e) => {
await this.refreshStatus();
let retryMigrationDialog = new RetryMigrationDialog(this._context, this._migration);
const retryMigrationDialog = new RetryMigrationDialog(
this._context,
this._serviceContext,
this._migration,
this._onClosedCallback);
await retryMigrationDialog.openDialog();
}
));
@@ -397,12 +392,14 @@ export class MigrationCutoverDialog {
}
}).component();
this._disposables.push(this._refreshButton.onDidClick(
async (e) => await this.refreshStatus()));
this._disposables.push(
this._refreshButton.onDidClick(async (e) => {
this._refreshButton.enabled = false;
await this.refreshStatus();
this._refreshButton.enabled = true;
}));
headerActions.addItem(this._refreshButton, {
flex: '0',
});
headerActions.addItem(this._refreshButton, { flex: '0' });
this._copyDatabaseMigrationDetails = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.copy,
@@ -425,9 +422,7 @@ export class MigrationCutoverDialog {
headerActions.addItem(this._copyDatabaseMigrationDetails, {
flex: '0',
CSSStyles: {
'margin-left': '5px'
}
CSSStyles: { 'margin-left': '5px' }
});
// create new support request button. Hiding button until sql migration support has been setup.
@@ -443,11 +438,11 @@ export class MigrationCutoverDialog {
}
}).component();
this._newSupportRequest.onDidClick(async (e) => {
const serviceId = this._model._migration.controller.id;
this._disposables.push(this._newSupportRequest.onDidClick(async (e) => {
const serviceId = this._model._migration.properties.migrationService;
const supportUrl = `https://portal.azure.com/#resource${serviceId}/supportrequest`;
await vscode.env.openExternal(vscode.Uri.parse(supportUrl));
});
}));
headerActions.addItem(this._newSupportRequest, {
flex: '0',
@@ -519,12 +514,12 @@ export class MigrationCutoverDialog {
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
const isBlobMigration = this._model.isBlobMigration();
const _isBlobMigration = isBlobMigration(this._model._migration);
const flexStatus = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
this._migrationStatusInfoField = await this.createInfoField(loc.MIGRATION_STATUS, '', false, ' ');
this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', isBlobMigration);
this._fullBackupFileOnInfoField = await this.createInfoField(loc.FULL_BACKUP_FILES, '', _isBlobMigration);
this._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
addInfoFieldToContainer(this._fullBackupFileOnInfoField, flexStatus);
@@ -533,10 +528,10 @@ export class MigrationCutoverDialog {
const flexFile = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', isBlobMigration);
this._lastLSNInfoField = await this.createInfoField(loc.LAST_APPLIED_LSN, '', _isBlobMigration);
this._lastAppliedBackupInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', isBlobMigration);
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', !isBlobMigration);
this._lastAppliedBackupTakenOnInfoField = await this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '', _isBlobMigration);
this._currentRestoringFileInfoField = await this.createInfoField(loc.CURRENTLY_RESTORING_FILE, '', !_isBlobMigration);
addInfoFieldToContainer(this._lastLSNInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile);
@@ -561,33 +556,8 @@ export class MigrationCutoverDialog {
return flexInfo;
}
private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void {
const shouldRefresh = (status: string | undefined) => !status
|| status === MigrationStatus.InProgress
|| status === MigrationStatus.Creating
|| status === MigrationStatus.Completing
|| status === MigrationStatus.Canceling;
if (shouldRefresh(this.getMigrationStatus())) {
const classVariable = this;
clearInterval(this._autoRefreshHandle);
if (interval !== -1) {
this._autoRefreshHandle = setInterval(async function () { await classVariable.refreshStatus(); }, interval);
}
}
}
private getMigrationDetails(): string {
if (this._model.migrationOpStatus) {
return (JSON.stringify(
{
'async-operation-details': this._model.migrationOpStatus,
'details': this._model.migrationStatus
}
, undefined, 2));
} else {
return (JSON.stringify(this._model.migrationStatus, undefined, 2));
}
return JSON.stringify(this._model.migrationStatus, undefined, 2);
}
private async refreshStatus(): Promise<void> {
@@ -598,18 +568,13 @@ export class MigrationCutoverDialog {
try {
clearDialogMessage(this._dialogObject);
if (this._isOnlineMigration()) {
await this._cutoverButton.updateCssStyles({
'display': 'block'
});
}
await this._cutoverButton.updateCssStyles(
{ 'display': isOfflineMigation(this._model._migration) ? 'none' : 'block' });
this.isRefreshing = true;
this._refreshLoader.loading = true;
await this._model.fetchStatus();
const errors = [];
errors.push(this._model.migrationOpStatus.error?.message);
errors.push(this._model._migration.asyncOperationResult?.error?.message);
errors.push(this._model.migrationStatus.properties.provisioningError);
errors.push(this._model.migrationStatus.properties.migrationFailureError?.message);
errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
@@ -626,12 +591,12 @@ export class MigrationCutoverDialog {
description: this.getMigrationDetails()
};
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
const sqlServerName = this._model._migration.sourceConnectionProfile.serverName;
const sourceDatabaseName = this._model._migration.migrationContext.properties.sourceDatabaseName;
const sqlServerName = this._model._migration.properties.sourceServerName;
const sourceDatabaseName = this._model._migration.properties.sourceDatabaseName;
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
const targetDatabaseName = this._model._migration.migrationContext.name;
const targetServerName = this._model._migration.targetManagedInstance.name;
const targetDatabaseName = this._model._migration.name;
const targetServerName = getResourceName(this._model._migration.id);
let targetServerVersion;
if (this._model.migrationStatus.id.includes('managedInstances')) {
targetServerVersion = loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
@@ -644,30 +609,30 @@ export class MigrationCutoverDialog {
const tableData: ActiveBackupFileSchema[] = [];
this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach((activeBackupSet) => {
this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach(
(activeBackupSet) => {
if (this._shouldDisplayBackupFileTable()) {
tableData.push(
...activeBackupSet.listOfBackupFiles.map(f => {
return {
fileName: f.fileName,
type: activeBackupSet.backupType,
status: f.status,
dataUploaded: `${convertByteSizeToReadableUnit(f.dataWritten)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
copyThroughput: (f.copyThroughput) ? (f.copyThroughput / 1024).toFixed(2) : '-',
backupStartTime: activeBackupSet.backupStartDate,
firstLSN: activeBackupSet.firstLSN,
lastLSN: activeBackupSet.lastLSN
};
})
);
}
if (this._shouldDisplayBackupFileTable()) {
tableData.push(
...activeBackupSet.listOfBackupFiles.map(f => {
return {
fileName: f.fileName,
type: activeBackupSet.backupType,
status: f.status,
dataUploaded: `${convertByteSizeToReadableUnit(f.dataWritten)} / ${convertByteSizeToReadableUnit(f.totalSize)}`,
copyThroughput: (f.copyThroughput) ? (f.copyThroughput / 1024).toFixed(2) : '-',
backupStartTime: activeBackupSet.backupStartDate,
firstLSN: activeBackupSet.firstLSN,
lastLSN: activeBackupSet.lastLSN
};
})
);
}
if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
lastAppliedSSN = activeBackupSet.lastLSN;
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
}
});
if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
lastAppliedSSN = activeBackupSet.lastLSN;
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
}
});
this._sourceDatabaseInfoField.text.value = sourceDatabaseName;
this._sourceDetailsInfoField.text.value = sqlServerName;
@@ -677,21 +642,23 @@ export class MigrationCutoverDialog {
this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion;
const migrationStatusTextValue = this.getMigrationStatus();
const migrationStatusTextValue = this._getMigrationStatus();
this._migrationStatusInfoField.text.value = migrationStatusTextValue ?? '-';
this._migrationStatusInfoField.icon!.iconPath = getMigrationStatusImage(migrationStatusTextValue);
this._fullBackupFileOnInfoField.text.value = this._model.migrationStatus?.properties?.migrationStatusDetails?.fullBackupSetInfo?.listOfBackupFiles[0]?.fileName! ?? '-';
let backupLocation;
const isBlobMigration = this._model.isBlobMigration();
const _isBlobMigration = isBlobMigration(this._model._migration);
// Displaying storage accounts and blob container for azure blob backups.
if (isBlobMigration) {
const storageAccountResourceId = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
const blobContainerName = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName;
backupLocation = `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`;
if (_isBlobMigration) {
const storageAccountResourceId = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.storageAccountResourceId;
const blobContainerName = this._model._migration.properties.backupConfiguration?.sourceLocation?.azureBlob?.blobContainerName;
backupLocation = storageAccountResourceId && blobContainerName
? `${storageAccountResourceId?.split('/').pop()} - ${blobContainerName}`
: undefined;
} else {
const fileShare = this._model._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.fileShare;
const fileShare = this._model._migration.properties.backupConfiguration?.sourceLocation?.fileShare;
backupLocation = fileShare?.path! ?? '-';
}
this._backupLocationInfoField.text.value = backupLocation ?? '-';
@@ -700,7 +667,7 @@ export class MigrationCutoverDialog {
this._lastAppliedBackupInfoField.text.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename ?? '-';
this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : '-';
if (isBlobMigration) {
if (_isBlobMigration) {
if (!this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) {
this._currentRestoringFileInfoField.text.value = '-';
} else if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename === this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename) {
@@ -752,7 +719,7 @@ export class MigrationCutoverDialog {
this._cutoverButton.enabled = false;
if (migrationStatusTextValue === MigrationStatus.InProgress) {
if (isBlobMigration) {
if (_isBlobMigration) {
if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
this._cutoverButton.enabled = true;
}
@@ -856,21 +823,14 @@ export class MigrationCutoverDialog {
};
}
private _isOnlineMigration(): boolean {
return this._model._migration.migrationContext.properties.offlineConfiguration?.offline?.valueOf() ? false : true;
}
private _shouldDisplayBackupFileTable(): boolean {
return !this._model.isBlobMigration();
return !isBlobMigration(this._model._migration);
}
private getMigrationStatus(): string {
if (this._model.migrationStatus) {
return this._model.migrationStatus.properties.migrationStatus
?? this._model.migrationStatus.properties.provisioningState;
}
return this._model._migration.migrationContext.properties.migrationStatus
?? this._model._migration.migrationContext.properties.provisioningState;
private _getMigrationStatus(): string {
return this._model.migrationStatus
? getMigrationStatus(this._model.migrationStatus)
: getMigrationStatus(this._model._migration);
}
}

View File

@@ -3,43 +3,36 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getMigrationStatus, DatabaseMigration, startMigrationCutover, stopMigration, getMigrationAsyncOperationDetails, AzureAsyncOperationResource, BackupFileInfo, getResourceGroupFromId } from '../../api/azure';
import { BackupFileInfoStatus, MigrationContext } from '../../models/migrationLocalStorage';
import { DatabaseMigration, startMigrationCutover, stopMigration, BackupFileInfo, getResourceGroupFromId, getMigrationDetails, getMigrationTargetName } from '../../api/azure';
import { BackupFileInfoStatus, MigrationServiceContext } from '../../models/migrationLocalStorage';
import { logError, sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery';
import * as constants from '../../constants/strings';
import { EOL } from 'os';
import { getMigrationTargetType, getMigrationMode } from '../../constants/helper';
import { getMigrationTargetType, getMigrationMode, isBlobMigration } from '../../constants/helper';
export class MigrationCutoverDialogModel {
public CutoverError?: Error;
public CancelMigrationError?: Error;
public migrationStatus!: DatabaseMigration;
public migrationOpStatus!: AzureAsyncOperationResource;
constructor(public _migration: MigrationContext) {
constructor(
public _serviceConstext: MigrationServiceContext,
public _migration: DatabaseMigration
) {
}
public async fetchStatus(): Promise<void> {
if (this._migration.asyncUrl) {
this.migrationOpStatus = await getMigrationAsyncOperationDetails(
this._migration.azureAccount,
this._migration.subscription,
this._migration.asyncUrl,
this._migration.sessionId!);
}
this.migrationStatus = await getMigrationStatus(
this._migration.azureAccount,
this._migration.subscription,
this._migration.migrationContext,
this._migration.sessionId!);
this.migrationStatus = await getMigrationDetails(
this._serviceConstext.azureAccount!,
this._serviceConstext.subscription!,
this._migration.id,
this._migration.properties?.migrationOperationId);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.MigrationStatus,
{
'sessionId': this._migration.sessionId!,
'migrationStatus': this.migrationStatus.properties?.migrationStatus
},
{}
@@ -51,18 +44,16 @@ export class MigrationCutoverDialogModel {
public async startCutover(): Promise<DatabaseMigration | undefined> {
try {
this.CutoverError = undefined;
if (this.migrationStatus) {
if (this._migration) {
const cutover = await startMigrationCutover(
this._migration.azureAccount,
this._migration.subscription,
this.migrationStatus,
this._migration.sessionId!
);
this._serviceConstext.azureAccount!,
this._serviceConstext.subscription!,
this._migration!);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.CutoverMigration,
{
...this.getTelemetryProps(this._migration),
...this.getTelemetryProps(this._serviceConstext, this._migration),
'migrationEndTime': new Date().toString(),
},
{}
@@ -79,8 +70,6 @@ export class MigrationCutoverDialogModel {
public async fetchErrors(): Promise<string> {
const errors = [];
await this.fetchStatus();
errors.push(this.migrationOpStatus.error?.message);
errors.push(this._migration.asyncOperationResult?.error?.message);
errors.push(this.migrationStatus.properties.migrationFailureError?.message);
return errors
.filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
@@ -93,18 +82,16 @@ export class MigrationCutoverDialogModel {
if (this.migrationStatus) {
const cutoverStartTime = new Date().toString();
await stopMigration(
this._migration.azureAccount,
this._migration.subscription,
this.migrationStatus,
this._migration.sessionId!
);
this._serviceConstext.azureAccount!,
this._serviceConstext.subscription!,
this.migrationStatus);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.CancelMigration,
{
...this.getTelemetryProps(this._migration),
...this.getTelemetryProps(this._serviceConstext, this._migration),
'migrationMode': getMigrationMode(this._migration),
'cutoverStartTime': cutoverStartTime
'cutoverStartTime': cutoverStartTime,
},
{}
);
@@ -116,12 +103,8 @@ export class MigrationCutoverDialogModel {
return undefined!;
}
public isBlobMigration(): boolean {
return this._migration.migrationContext.properties.backupConfiguration?.sourceLocation?.azureBlob !== undefined;
}
public confirmCutoverStepsString(): string {
if (this.isBlobMigration()) {
if (isBlobMigration(this.migrationStatus)) {
return `${constants.CUTOVER_HELP_STEP1}
${constants.CUTOVER_HELP_STEP2_BLOB_CONTAINER}
${constants.CUTOVER_HELP_STEP3_BLOB_CONTAINER}`;
@@ -152,16 +135,15 @@ export class MigrationCutoverDialogModel {
return files;
}
private getTelemetryProps(migration: MigrationContext) {
private getTelemetryProps(serviceContext: MigrationServiceContext, migration: DatabaseMigration) {
return {
'sessionId': migration.sessionId!,
'subscriptionId': migration.subscription.id,
'resourceGroup': getResourceGroupFromId(migration.targetManagedInstance.id),
'sqlServerName': migration.sourceConnectionProfile.serverName,
'sourceDatabaseName': migration.migrationContext.properties.sourceDatabaseName,
'subscriptionId': serviceContext.subscription!.id,
'resourceGroup': getResourceGroupFromId(migration.id),
'sqlServerName': migration.properties.sourceServerName,
'sourceDatabaseName': migration.properties.sourceDatabaseName,
'targetType': getMigrationTargetType(migration),
'targetDatabaseName': migration.migrationContext.name,
'targetServerName': migration.targetManagedInstance.name,
'targetDatabaseName': migration.name,
'targetServerName': getMigrationTargetName(migration),
};
}
}

View File

@@ -6,22 +6,19 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { IconPathHelper } from '../../constants/iconPathHelper';
import { MigrationContext, MigrationLocalStorage, MigrationStatus } from '../../models/migrationLocalStorage';
import { getCurrentMigrations, getSelectedServiceStatus, MigrationLocalStorage, MigrationStatus } from '../../models/migrationLocalStorage';
import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog';
import { AdsMigrationStatus, MigrationStatusDialogModel } from './migrationStatusDialogModel';
import * as loc from '../../constants/strings';
import { clearDialogMessage, convertTimeDifferenceToDuration, displayDialogErrorMessage, filterMigrations, getMigrationStatusImage, SupportedAutoRefreshIntervals } from '../../api/utils';
import { clearDialogMessage, convertTimeDifferenceToDuration, displayDialogErrorMessage, filterMigrations, getMigrationStatusImage } from '../../api/utils';
import { SqlMigrationServiceDetailsDialog } from '../sqlMigrationService/sqlMigrationServiceDetailsDialog';
import { ConfirmCutoverDialog } from '../migrationCutover/confirmCutoverDialog';
import { MigrationCutoverDialogModel } from '../migrationCutover/migrationCutoverDialogModel';
import { getMigrationTargetType, getMigrationMode, canRetryMigration } from '../../constants/helper';
import { RetryMigrationDialog } from '../retryMigration/retryMigrationDialog';
const refreshFrequency: SupportedAutoRefreshIntervals = 180000;
const statusImageSize: number = 14;
const imageCellStyles: azdata.CssStyles = { 'margin': '3px 3px 0 0', 'padding': '0' };
const statusCellStyles: azdata.CssStyles = { 'margin': '0', 'padding': '0' };
import { DatabaseMigration, getResourceName } from '../../api/azure';
import { logError, TelemetryViews } from '../../telemtery';
import { SelectMigrationServiceDialog } from '../selectMigrationService/selectMigrationServiceDialog';
const MenuCommands = {
Cutover: 'sqlmigration.cutover',
@@ -40,53 +37,56 @@ export class MigrationStatusDialog {
private _view!: azdata.ModelView;
private _searchBox!: azdata.InputBoxComponent;
private _refresh!: azdata.ButtonComponent;
private _serviceContextButton!: azdata.ButtonComponent;
private _statusDropdown!: azdata.DropDownComponent;
private _statusTable!: azdata.DeclarativeTableComponent;
private _statusTable!: azdata.TableComponent;
private _refreshLoader!: azdata.LoadingComponent;
private _autoRefreshHandle!: NodeJS.Timeout;
private _disposables: vscode.Disposable[] = [];
private _filteredMigrations: DatabaseMigration[] = [];
private isRefreshing = false;
constructor(context: vscode.ExtensionContext, migrations: MigrationContext[], private _filter: AdsMigrationStatus) {
constructor(
context: vscode.ExtensionContext,
private _filter: AdsMigrationStatus,
private _onClosedCallback: () => Promise<void>) {
this._context = context;
this._model = new MigrationStatusDialogModel(migrations);
this._dialogObject = azdata.window.createModelViewDialog(loc.MIGRATION_STATUS, 'MigrationControllerDialog', 'wide');
this._model = new MigrationStatusDialogModel([]);
this._dialogObject = azdata.window.createModelViewDialog(
loc.MIGRATION_STATUS,
'MigrationControllerDialog',
'wide');
}
initialize() {
async initialize() {
let tab = azdata.window.createTab('');
tab.registerContent(async (view: azdata.ModelView) => {
this._view = view;
this.registerCommands();
const formBuilder = view.modelBuilder.formContainer().withFormItems(
[
{
component: this.createSearchAndRefreshContainer()
},
{
component: this.createStatusTable()
}
],
{
horizontal: false
}
);
const form = formBuilder.withLayout({ width: '100%' }).component();
this._disposables.push(this._view.onClosed(e => {
clearInterval(this._autoRefreshHandle);
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
const form = view.modelBuilder.formContainer()
.withFormItems(
[
{ component: await this.createSearchAndRefreshContainer() },
{ component: this.createStatusTable() }
],
{ horizontal: false }
).withLayout({ width: '100%' })
.component();
this._disposables.push(
this._view.onClosed(async e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
return view.initializeModel(form);
await this._onClosedCallback();
}));
await view.initializeModel(form);
return await this.refreshTable();
});
this._dialogObject.content = [tab];
this._dialogObject.cancelButton.hidden = true;
this._dialogObject.okButton.label = loc.CLOSE;
this._disposables.push(this._dialogObject.okButton.onClick(e => {
clearInterval(this._autoRefreshHandle);
}));
azdata.window.openDialog(this._dialogObject);
}
@@ -100,111 +100,124 @@ export class MigrationStatusDialog {
private canCutoverMigration = (status: string | undefined) => status === MigrationStatus.InProgress;
private createSearchAndRefreshContainer(): azdata.FlexContainer {
this._searchBox = this._view.modelBuilder.inputBox().withProps({
stopEnterPropagation: true,
placeHolder: loc.SEARCH_FOR_MIGRATIONS,
width: '360px'
}).component();
this._disposables.push(this._searchBox.onTextChanged(async (value) => {
await this.populateMigrationTable();
}));
this._refresh = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.refresh,
iconHeight: '16px',
iconWidth: '20px',
height: '30px',
label: loc.REFRESH_BUTTON_LABEL,
}).component();
private async createSearchAndRefreshContainer(): Promise<azdata.FlexContainer> {
this._searchBox = this._view.modelBuilder.inputBox()
.withProps({
stopEnterPropagation: true,
placeHolder: loc.SEARCH_FOR_MIGRATIONS,
width: '360px'
}).component();
this._disposables.push(
this._searchBox.onTextChanged(
async (value) => await this.populateMigrationTable()));
this._refresh = this._view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.refresh,
iconHeight: '16px',
iconWidth: '20px',
label: loc.REFRESH_BUTTON_LABEL,
}).component();
this._disposables.push(
this._refresh.onDidClick(
async (e) => { await this.refreshTable(); }));
async (e) => await this.refreshTable()));
const flexContainer = this._view.modelBuilder.flexContainer().withProps({
width: 900,
CSSStyles: {
'justify-content': 'left'
},
}).component();
flexContainer.addItem(this._searchBox, {
flex: '0'
});
this._statusDropdown = this._view.modelBuilder.dropDown().withProps({
ariaLabel: loc.MIGRATION_STATUS_FILTER,
values: this._model.statusDropdownValues,
width: '220px'
}).component();
this._disposables.push(this._statusDropdown.onValueChanged(async (value) => {
await this.populateMigrationTable();
}));
this._statusDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: loc.MIGRATION_STATUS_FILTER,
values: this._model.statusDropdownValues,
width: '220px'
}).component();
this._disposables.push(
this._statusDropdown.onValueChanged(
async (value) => await this.populateMigrationTable()));
if (this._filter) {
this._statusDropdown.value = (<azdata.CategoryValue[]>this._statusDropdown.values).find((value) => {
return value.name === this._filter;
});
this._statusDropdown.value =
(<azdata.CategoryValue[]>this._statusDropdown.values)
.find(value => value.name === this._filter);
}
flexContainer.addItem(this._statusDropdown, {
flex: '0',
CSSStyles: {
'margin-left': '20px'
}
});
this._refreshLoader = this._view.modelBuilder.loadingComponent()
.withProps({ loading: false })
.component();
flexContainer.addItem(this._refresh, {
flex: '0',
CSSStyles: {
'margin-left': '20px'
}
});
const searchLabel = this._view.modelBuilder.text()
.withProps({
value: 'Status',
CSSStyles: {
'font-size': '13px',
'font-weight': '600',
'margin': '3px 0 0 0',
},
}).component();
this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
loading: false,
height: '55px'
}).component();
const serviceContextLabel = await getSelectedServiceStatus();
this._serviceContextButton = this._view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.sqlMigrationService,
iconHeight: 22,
iconWidth: 22,
label: serviceContextLabel,
title: serviceContextLabel,
description: loc.MIGRATION_SERVICE_DESCRIPTION,
buttonType: azdata.ButtonType.Informational,
width: 270,
}).component();
flexContainer.addItem(this._refreshLoader, {
flex: '0 0 auto',
CSSStyles: {
'margin-left': '20px'
}
});
this.setAutoRefresh(refreshFrequency);
const container = this._view.modelBuilder.flexContainer().withProps({
width: 1000
}).component();
container.addItem(flexContainer, {
flex: '0 0 auto',
CSSStyles: {
'width': '980px'
}
});
const onDialogClosed = async (): Promise<void> => {
const label = await getSelectedServiceStatus();
this._serviceContextButton.label = label;
this._serviceContextButton.title = label;
await this.refreshTable();
};
this._disposables.push(
this._serviceContextButton.onDidClick(
async () => {
const dialog = new SelectMigrationServiceDialog(onDialogClosed);
await dialog.initialize();
}));
const flexContainer = this._view.modelBuilder.flexContainer()
.withProps({
width: '100%',
CSSStyles: {
'justify-content': 'left',
'align-items': 'center',
'padding': '0px',
'display': 'flex',
'flex-direction': 'row',
},
}).component();
flexContainer.addItem(this._searchBox, { flex: '0' });
flexContainer.addItem(this._serviceContextButton, { flex: '0', CSSStyles: { 'margin-left': '20px' } });
flexContainer.addItem(searchLabel, { flex: '0', CSSStyles: { 'margin-left': '20px' } });
flexContainer.addItem(this._statusDropdown, { flex: '0', CSSStyles: { 'margin-left': '5px' } });
flexContainer.addItem(this._refresh, { flex: '0', CSSStyles: { 'margin-left': '20px' } });
flexContainer.addItem(this._refreshLoader, { flex: '0 0 auto', CSSStyles: { 'margin-left': '20px' } });
const container = this._view.modelBuilder.flexContainer()
.withProps({ width: 1245 })
.component();
container.addItem(flexContainer, { flex: '0 0 auto', });
return container;
}
private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void {
const classVariable = this;
clearInterval(this._autoRefreshHandle);
if (interval !== -1) {
this._autoRefreshHandle = setInterval(async function () { await classVariable.refreshTable(); }, interval);
}
}
private registerCommands(): void {
this._disposables.push(vscode.commands.registerCommand(
MenuCommands.Cutover,
async (migrationId: string) => {
try {
clearDialogMessage(this._dialogObject);
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
if (this.canCutoverMigration(migration?.migrationContext.properties.migrationStatus)) {
const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
const migration = this._model._migrations.find(
migration => migration.id === migrationId);
if (this.canCutoverMigration(migration?.properties.migrationStatus)) {
const cutoverDialogModel = new MigrationCutoverDialogModel(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await cutoverDialogModel.fetchStatus();
const dialog = new ConfirmCutoverDialog(cutoverDialogModel);
await dialog.initialize();
@@ -224,8 +237,12 @@ export class MigrationStatusDialog {
MenuCommands.ViewDatabase,
async (migrationId: string) => {
try {
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
const dialog = new MigrationCutoverDialog(this._context, migration!);
const migration = this._model._migrations.find(migration => migration.id === migrationId);
const dialog = new MigrationCutoverDialog(
this._context,
await MigrationLocalStorage.getMigrationServiceContext(),
migration!,
this._onClosedCallback);
await dialog.initialize();
} catch (e) {
console.log(e);
@@ -236,8 +253,8 @@ export class MigrationStatusDialog {
MenuCommands.ViewTarget,
async (migrationId: string) => {
try {
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
const url = 'https://portal.azure.com/#resource/' + migration!.targetManagedInstance.id;
const migration = this._model._migrations.find(migration => migration.id === migrationId);
const url = 'https://portal.azure.com/#resource/' + migration!.properties.scope;
await vscode.env.openExternal(vscode.Uri.parse(url));
} catch (e) {
console.log(e);
@@ -248,8 +265,10 @@ export class MigrationStatusDialog {
MenuCommands.ViewService,
async (migrationId: string) => {
try {
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
const dialog = new SqlMigrationServiceDetailsDialog(migration!);
const migration = this._model._migrations.find(migration => migration.id === migrationId);
const dialog = new SqlMigrationServiceDetailsDialog(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await dialog.initialize();
} catch (e) {
console.log(e);
@@ -261,17 +280,12 @@ export class MigrationStatusDialog {
async (migrationId: string) => {
try {
clearDialogMessage(this._dialogObject);
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
const migration = this._model._migrations.find(migration => migration.id === migrationId);
const cutoverDialogModel = new MigrationCutoverDialogModel(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await cutoverDialogModel.fetchStatus();
if (cutoverDialogModel.migrationOpStatus) {
await vscode.env.clipboard.writeText(JSON.stringify({
'async-operation-details': cutoverDialogModel.migrationOpStatus,
'details': cutoverDialogModel.migrationStatus
}, undefined, 2));
} else {
await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migrationStatus, undefined, 2));
}
await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migrationStatus, undefined, 2));
await vscode.window.showInformationMessage(loc.DETAILS_COPIED);
} catch (e) {
@@ -285,11 +299,13 @@ export class MigrationStatusDialog {
async (migrationId: string) => {
try {
clearDialogMessage(this._dialogObject);
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
if (this.canCancelMigration(migration?.migrationContext.properties.migrationStatus)) {
const migration = this._model._migrations.find(migration => migration.id === migrationId);
if (this.canCancelMigration(migration?.properties.migrationStatus)) {
void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => {
if (v === loc.YES) {
const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
const cutoverDialogModel = new MigrationCutoverDialogModel(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await cutoverDialogModel.fetchStatus();
await cutoverDialogModel.cancelMigration();
@@ -312,9 +328,13 @@ export class MigrationStatusDialog {
async (migrationId: string) => {
try {
clearDialogMessage(this._dialogObject);
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
if (canRetryMigration(migration?.migrationContext.properties.migrationStatus)) {
let retryMigrationDialog = new RetryMigrationDialog(this._context, migration!);
const migration = this._model._migrations.find(migration => migration.id === migrationId);
if (canRetryMigration(migration?.properties.migrationStatus)) {
let retryMigrationDialog = new RetryMigrationDialog(
this._context,
await MigrationLocalStorage.getMigrationServiceContext(),
migration!,
this._onClosedCallback);
await retryMigrationDialog.openDialog();
}
else {
@@ -329,75 +349,43 @@ export class MigrationStatusDialog {
private async populateMigrationTable(): Promise<void> {
try {
const migrations = filterMigrations(
this._filteredMigrations = filterMigrations(
this._model._migrations,
(<azdata.CategoryValue>this._statusDropdown.value).name,
this._searchBox.value!);
migrations.sort((m1, m2) => {
return new Date(m1.migrationContext.properties?.startedOn) > new Date(m2.migrationContext.properties?.startedOn) ? -1 : 1;
this._filteredMigrations.sort((m1, m2) => {
return new Date(m1.properties?.startedOn) > new Date(m2.properties?.startedOn) ? -1 : 1;
});
const data: azdata.DeclarativeTableCellValue[][] = migrations.map((migration, index) => {
const data: any[] = this._filteredMigrations.map((migration, index) => {
return [
{ value: this._getDatabaserHyperLink(migration) },
{ value: this._getMigrationStatus(migration) },
{ value: getMigrationMode(migration) },
{ value: getMigrationTargetType(migration) },
{ value: migration.targetManagedInstance.name },
{ value: migration.controller.name },
{
value: this._getMigrationDuration(
migration.migrationContext.properties.startedOn,
migration.migrationContext.properties.endedOn)
},
{ value: this._getMigrationTime(migration.migrationContext.properties.startedOn) },
{ value: this._getMigrationTime(migration.migrationContext.properties.endedOn) },
{
value: {
commands: this._getMenuCommands(migration),
context: migration.migrationContext.id
},
}
<azdata.HyperlinkColumnCellValue>{
icon: IconPathHelper.sqlDatabaseLogo,
title: migration.properties.sourceDatabaseName ?? '-',
}, // database
<azdata.HyperlinkColumnCellValue>{
icon: getMigrationStatusImage(migration.properties.migrationStatus),
title: this._getMigrationStatus(migration),
}, // statue
getMigrationMode(migration), // mode
getMigrationTargetType(migration), // targetType
getResourceName(migration.id), // targetName
getResourceName(migration.properties.migrationService), // migrationService
this._getMigrationDuration(
migration.properties.startedOn,
migration.properties.endedOn), // duration
this._getMigrationTime(migration.properties.startedOn), // startTime
this._getMigrationTime(migration.properties.endedOn), // endTime
];
});
await this._statusTable.setDataValues(data);
await this._statusTable.updateProperty('data', data);
} catch (e) {
console.log(e);
logError(TelemetryViews.MigrationStatusDialog, 'Error populating migrations list page', e);
}
}
private _getDatabaserHyperLink(migration: MigrationContext): azdata.FlexContainer {
const imageControl = this._view.modelBuilder.image()
.withProps({
iconPath: IconPathHelper.sqlDatabaseLogo,
iconHeight: statusImageSize,
iconWidth: statusImageSize,
height: statusImageSize,
width: statusImageSize,
CSSStyles: imageCellStyles
})
.component();
const databaseHyperLink = this._view.modelBuilder
.hyperlink()
.withProps({
label: migration.migrationContext.properties.sourceDatabaseName,
url: '',
CSSStyles: statusCellStyles
}).component();
this._disposables.push(databaseHyperLink.onDidClick(
async (e) => await (new MigrationCutoverDialog(this._context, migration)).initialize()));
return this._view.modelBuilder
.flexContainer()
.withItems([imageControl, databaseHyperLink])
.withProps({ CSSStyles: statusCellStyles, display: 'inline-flex' })
.component();
}
private _getMigrationTime(migrationTime: string): string {
return migrationTime
? new Date(migrationTime).toLocaleString()
@@ -420,39 +408,11 @@ export class MigrationStatusDialog {
return '---';
}
private _getMenuCommands(migration: MigrationContext): string[] {
const menuCommands: string[] = [];
const migrationStatus = migration?.migrationContext?.properties?.migrationStatus;
if (getMigrationMode(migration) === loc.ONLINE &&
this.canCutoverMigration(migrationStatus)) {
menuCommands.push(MenuCommands.Cutover);
}
menuCommands.push(...[
MenuCommands.ViewDatabase,
MenuCommands.ViewTarget,
MenuCommands.ViewService,
MenuCommands.CopyMigration]);
if (this.canCancelMigration(migrationStatus)) {
menuCommands.push(MenuCommands.CancelMigration);
}
if (canRetryMigration(migrationStatus)) {
menuCommands.push(MenuCommands.RetryMigration);
}
return menuCommands;
}
private _getMigrationStatus(migration: MigrationContext): azdata.FlexContainer {
const properties = migration.migrationContext.properties;
private _getMigrationStatus(migration: DatabaseMigration): string {
const properties = migration.properties;
const migrationStatus = properties.migrationStatus ?? properties.provisioningState;
let warningCount = 0;
if (migration.asyncOperationResult?.error?.message) {
warningCount++;
}
if (properties.migrationFailureError?.message) {
warningCount++;
}
@@ -463,17 +423,16 @@ export class MigrationStatusDialog {
warningCount++;
}
return this._getStatusControl(migrationStatus, warningCount, migration);
return loc.STATUS_VALUE(migrationStatus, warningCount) + (loc.STATUS_WARNING_COUNT(migrationStatus, warningCount) ?? '');
}
public openCalloutDialog(dialogHeading: string, dialogName?: string, calloutMessageText?: string): void {
const dialog = azdata.window.createModelViewDialog(dialogHeading, dialogName, 288, 'callout', 'left', true, false,
{
xPos: 0,
yPos: 0,
width: 20,
height: 20
});
const dialog = azdata.window.createModelViewDialog(dialogHeading, dialogName, 288, 'callout', 'left', true, false, {
xPos: 0,
yPos: 0,
width: 20,
height: 20
});
const tab: azdata.window.DialogTab = azdata.window.createTab('');
tab.registerContent(async view => {
const warningContentContainer = view.modelBuilder.divContainer().component();
@@ -499,73 +458,6 @@ export class MigrationStatusDialog {
azdata.window.openDialog(dialog);
}
private _getStatusControl(status: string, count: number, migration: MigrationContext): azdata.DivContainer {
const control = this._view.modelBuilder
.divContainer()
.withItems([
// migration status icon
this._view.modelBuilder.image()
.withProps({
iconPath: getMigrationStatusImage(status),
iconHeight: statusImageSize,
iconWidth: statusImageSize,
height: statusImageSize,
width: statusImageSize,
CSSStyles: imageCellStyles
})
.component(),
// migration status text
this._view.modelBuilder.text().withProps({
value: loc.STATUS_VALUE(status, count),
height: statusImageSize,
CSSStyles: statusCellStyles,
}).component()
])
.withProps({ CSSStyles: statusCellStyles, display: 'inline-flex' })
.component();
if (count > 0) {
const migrationWarningImage = this._view.modelBuilder.image()
.withProps({
iconPath: this._statusInfoMap(status),
iconHeight: statusImageSize,
iconWidth: statusImageSize,
height: statusImageSize,
width: statusImageSize,
CSSStyles: imageCellStyles
}).component();
const migrationWarningCount = this._view.modelBuilder.hyperlink()
.withProps({
label: loc.STATUS_WARNING_COUNT(status, count) ?? '',
ariaLabel: loc.ERROR,
url: '',
height: statusImageSize,
CSSStyles: statusCellStyles,
}).component();
control.addItems([
migrationWarningImage,
migrationWarningCount
]);
this._disposables.push(migrationWarningCount.onDidClick(async () => {
const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
const errors = await cutoverDialogModel.fetchErrors();
this.openCalloutDialog(
status === MigrationStatus.InProgress
|| status === MigrationStatus.Completing
? loc.WARNING
: loc.ERROR,
'input-table-row-dialog',
errors
);
}));
}
return control;
}
private async refreshTable(): Promise<void> {
if (this.isRefreshing) {
return;
@@ -575,8 +467,7 @@ export class MigrationStatusDialog {
try {
clearDialogMessage(this._dialogObject);
this._refreshLoader.loading = true;
const currentConnection = await azdata.connection.getCurrentConnection();
this._model._migrations = await MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection, true);
this._model._migrations = await getCurrentMigrations();
await this.populateMigrationTable();
} catch (e) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e);
@@ -587,115 +478,111 @@ export class MigrationStatusDialog {
}
}
private createStatusTable(): azdata.DeclarativeTableComponent {
const rowCssStyle: azdata.CssStyles = {
'border': 'none',
'text-align': 'left',
'border-bottom': '1px solid',
};
private createStatusTable(): azdata.TableComponent {
const headerCssStyles = undefined;
const rowCssStyles = undefined;
const headerCssStyles: azdata.CssStyles = {
'border': 'none',
'text-align': 'left',
'border-bottom': '1px solid',
'font-weight': 'bold',
'padding-left': '0px',
'padding-right': '0px'
};
this._statusTable = this._view.modelBuilder.declarativeTable().withProps({
this._statusTable = this._view.modelBuilder.table().withProps({
ariaLabel: loc.MIGRATION_STATUS,
data: [],
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
height: '600px',
width: '1095px',
display: 'grid',
columns: [
{
displayName: loc.DATABASE,
valueType: azdata.DeclarativeDataType.component,
width: '90px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
<azdata.HyperlinkColumn>{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.DATABASE,
value: 'database',
width: 190,
type: azdata.ColumnType.hyperlink,
icon: IconPathHelper.sqlDatabaseLogo,
showText: true,
},
<azdata.HyperlinkColumn>{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.STATUS_COLUMN,
value: 'status',
width: 120,
type: azdata.ColumnType.hyperlink,
},
{
displayName: loc.MIGRATION_STATUS,
valueType: azdata.DeclarativeDataType.component,
width: '170px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.MIGRATION_MODE,
value: 'mode',
width: 85,
type: azdata.ColumnType.text,
},
{
displayName: loc.MIGRATION_MODE,
valueType: azdata.DeclarativeDataType.string,
width: '90px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.AZURE_SQL_TARGET,
value: 'targetType',
width: 120,
type: azdata.ColumnType.text,
},
{
displayName: loc.AZURE_SQL_TARGET,
valueType: azdata.DeclarativeDataType.string,
width: '130px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
value: 'targetName',
width: 125,
type: azdata.ColumnType.text,
},
{
displayName: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
valueType: azdata.DeclarativeDataType.string,
width: '130px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.DATABASE_MIGRATION_SERVICE,
value: 'migrationService',
width: 140,
type: azdata.ColumnType.text,
},
{
displayName: loc.DATABASE_MIGRATION_SERVICE,
valueType: azdata.DeclarativeDataType.string,
width: '150px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.DURATION,
value: 'duration',
width: 50,
type: azdata.ColumnType.text,
},
{
displayName: loc.DURATION,
valueType: azdata.DeclarativeDataType.string,
width: '55px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.START_TIME,
value: 'startTime',
width: 115,
type: azdata.ColumnType.text,
},
{
displayName: loc.START_TIME,
valueType: azdata.DeclarativeDataType.string,
width: '140px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.FINISH_TIME,
value: 'finishTime',
width: 115,
type: azdata.ColumnType.text,
},
{
displayName: loc.FINISH_TIME,
valueType: azdata.DeclarativeDataType.string,
width: '140px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: '',
valueType: azdata.DeclarativeDataType.menu,
width: '20px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles,
}
]
}).component();
this._disposables.push(this._statusTable.onCellAction!(async (rowState: azdata.ICellActionEventArgs) => {
const buttonState = <azdata.ICellActionEventArgs>rowState;
switch (buttonState?.column) {
case 0:
case 1:
const migration = this._filteredMigrations[rowState.row];
const dialog = new MigrationCutoverDialog(
this._context,
await MigrationLocalStorage.getMigrationServiceContext(),
migration,
this._onClosedCallback);
await dialog.initialize();
break;
}
}));
return this._statusTable;
}
private _statusInfoMap(status: string): azdata.IconPath {
return status === MigrationStatus.InProgress
|| status === MigrationStatus.Creating
|| status === MigrationStatus.Completing
? IconPathHelper.warning
: IconPathHelper.error;
}
}

View File

@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { DatabaseMigration } from '../../api/azure';
import * as loc from '../../constants/strings';
import { MigrationContext } from '../../models/migrationLocalStorage';
export class MigrationStatusDialogModel {
public statusDropdownValues: azdata.CategoryValue[] = [
@@ -27,7 +27,7 @@ export class MigrationStatusDialogModel {
}
];
constructor(public _migrations: MigrationContext[]) {
constructor(public _migrations: DatabaseMigration[]) {
}
}

View File

@@ -7,26 +7,26 @@ import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as mssql from 'mssql';
import { azureResource } from 'azureResource';
import { getLocations, getResourceGroupFromId, getBlobContainerId, getFullResourceGroupFromId, getResourceName } from '../../api/azure';
import { getLocations, getResourceGroupFromId, getBlobContainerId, getFullResourceGroupFromId, getResourceName, DatabaseMigration, getMigrationTargetInstance } from '../../api/azure';
import { MigrationMode, MigrationStateModel, NetworkContainerType, SavedInfo } from '../../models/stateMachine';
import { MigrationContext } from '../../models/migrationLocalStorage';
import { MigrationServiceContext } from '../../models/migrationLocalStorage';
import { WizardController } from '../../wizard/wizardController';
import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper';
import * as constants from '../../constants/strings';
export class RetryMigrationDialog {
private _context: vscode.ExtensionContext;
private _migration: MigrationContext;
constructor(context: vscode.ExtensionContext, migration: MigrationContext) {
this._context = context;
this._migration = migration;
constructor(
private readonly _context: vscode.ExtensionContext,
private readonly _serviceContext: MigrationServiceContext,
private readonly _migration: DatabaseMigration,
private readonly _onClosedCallback: () => Promise<void>) {
}
private createMigrationStateModel(migration: MigrationContext, connectionId: string, serverName: string, api: mssql.IExtension, location: azureResource.AzureLocation): MigrationStateModel {
private async createMigrationStateModel(serviceContext: MigrationServiceContext, migration: DatabaseMigration, connectionId: string, serverName: string, api: mssql.IExtension, location: azureResource.AzureLocation): Promise<MigrationStateModel> {
let stateModel = new MigrationStateModel(this._context, connectionId, api.sqlMigration);
const sourceDatabaseName = migration.migrationContext.properties.sourceDatabaseName;
const sourceDatabaseName = migration.properties.sourceDatabaseName;
let savedInfo: SavedInfo;
savedInfo = {
closedPage: 0,
@@ -41,53 +41,56 @@ export class RetryMigrationDialog {
migrationTargetType: getMigrationTargetTypeEnum(migration)!,
// TargetSelection
azureAccount: migration.azureAccount,
azureTenant: migration.azureAccount.properties.tenants[0],
subscription: migration.subscription,
azureAccount: serviceContext.azureAccount!,
azureTenant: serviceContext.azureAccount!.properties.tenants[0]!,
subscription: serviceContext.subscription!,
location: location,
resourceGroup: {
id: getFullResourceGroupFromId(migration.targetManagedInstance.id),
name: getResourceGroupFromId(migration.targetManagedInstance.id),
subscription: migration.subscription
id: getFullResourceGroupFromId(migration.id),
name: getResourceGroupFromId(migration.id),
subscription: serviceContext.subscription!,
},
targetServerInstance: migration.targetManagedInstance,
targetServerInstance: await getMigrationTargetInstance(
serviceContext.azureAccount!,
serviceContext.subscription!,
migration),
// MigrationMode
migrationMode: getMigrationModeEnum(migration),
// DatabaseBackup
targetDatabaseNames: [migration.migrationContext.name],
targetDatabaseNames: [migration.name],
networkContainerType: null,
networkShares: [],
blobs: [],
// Integration Runtime
sqlMigrationService: migration.controller,
sqlMigrationService: serviceContext.migrationService,
};
const getStorageAccountResourceGroup = (storageAccountResourceId: string) => {
const getStorageAccountResourceGroup = (storageAccountResourceId: string): azureResource.AzureResourceResourceGroup => {
return {
id: getFullResourceGroupFromId(storageAccountResourceId!),
name: getResourceGroupFromId(storageAccountResourceId!),
subscription: migration.subscription
subscription: this._serviceContext.subscription!
};
};
const getStorageAccount = (storageAccountResourceId: string) => {
const getStorageAccount = (storageAccountResourceId: string): azureResource.AzureGraphResource => {
const storageAccountName = getResourceName(storageAccountResourceId);
return {
type: 'microsoft.storage/storageaccounts',
id: storageAccountResourceId!,
tenantId: savedInfo.azureTenant?.id!,
subscriptionId: migration.subscription.id,
subscriptionId: this._serviceContext.subscription?.id!,
name: storageAccountName,
location: savedInfo.location!.name,
};
};
const sourceLocation = migration.migrationContext.properties.backupConfiguration.sourceLocation;
const sourceLocation = migration.properties.backupConfiguration?.sourceLocation;
if (sourceLocation?.fileShare) {
savedInfo.networkContainerType = NetworkContainerType.NETWORK_SHARE;
const storageAccountResourceId = migration.migrationContext.properties.backupConfiguration.targetLocation?.storageAccountResourceId!;
const storageAccountResourceId = migration.properties.backupConfiguration?.targetLocation?.storageAccountResourceId!;
savedInfo.networkShares = [
{
password: '',
@@ -106,9 +109,9 @@ export class RetryMigrationDialog {
blobContainer: {
id: getBlobContainerId(getFullResourceGroupFromId(storageAccountResourceId!), getResourceName(storageAccountResourceId!), sourceLocation?.azureBlob.blobContainerName),
name: sourceLocation?.azureBlob.blobContainerName,
subscription: migration.subscription
subscription: this._serviceContext.subscription!
},
lastBackupFile: getMigrationModeEnum(migration) === MigrationMode.OFFLINE ? migration.migrationContext.properties.offlineConfiguration.lastBackupName! : undefined,
lastBackupFile: getMigrationModeEnum(migration) === MigrationMode.OFFLINE ? migration.properties.offlineConfiguration?.lastBackupName! : undefined,
storageAccount: getStorageAccount(storageAccountResourceId!),
resourceGroup: getStorageAccountResourceGroup(storageAccountResourceId!),
storageKey: ''
@@ -123,10 +126,18 @@ export class RetryMigrationDialog {
}
public async openDialog(dialogName?: string) {
const locations = await getLocations(this._migration.azureAccount, this._migration.subscription);
const locations = await getLocations(
this._serviceContext.azureAccount!,
this._serviceContext.subscription!);
const targetInstance = await getMigrationTargetInstance(
this._serviceContext.azureAccount!,
this._serviceContext.subscription!,
this._migration);
let location: azureResource.AzureLocation;
locations.forEach(azureLocation => {
if (azureLocation.name === this._migration.targetManagedInstance.location) {
if (azureLocation.name === targetInstance.location) {
location = azureLocation;
}
});
@@ -146,10 +157,13 @@ export class RetryMigrationDialog {
}
const api = (await vscode.extensions.getExtension(mssql.extension.name)?.activate()) as mssql.IExtension;
const stateModel = this.createMigrationStateModel(this._migration, connectionId, serverName, api, location!);
const stateModel = await this.createMigrationStateModel(this._serviceContext, this._migration, connectionId, serverName, api, location!);
if (stateModel.loadSavedInfo()) {
const wizardController = new WizardController(this._context, stateModel);
if (await stateModel.loadSavedInfo()) {
const wizardController = new WizardController(
this._context,
stateModel,
this._onClosedCallback);
await wizardController.openWizard(stateModel.sourceConnectionId);
} else {
void vscode.window.showInformationMessage(constants.MIGRATION_CANNOT_RETRY);

View File

@@ -0,0 +1,597 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as azurecore from 'azurecore';
import { MigrationLocalStorage, MigrationServiceContext } from '../../models/migrationLocalStorage';
import { azureResource } from 'azureResource';
import * as styles from '../../constants/styles';
import * as constants from '../../constants/strings';
import { findDropDownItemIndex, selectDefaultDropdownValue, deepClone } from '../../api/utils';
import { getFullResourceGroupFromId, getLocations, getSqlMigrationServices, getSubscriptions, SqlMigrationService } from '../../api/azure';
import { logError, TelemetryViews } from '../../telemtery';
const CONTROL_MARGIN = '20px';
const INPUT_COMPONENT_WIDTH = '100%';
const STYLE_HIDE = { 'display': 'none' };
const STYLE_ShOW = { 'display': 'inline' };
export const BODY_CSS = {
'font-size': '13px',
'line-height': '18px',
'margin': '4px 0',
};
const LABEL_CSS = {
...styles.LABEL_CSS,
'margin': '0 0 0 0',
'font-weight': '600',
};
const DROPDOWN_CSS = {
'margin': '-1em 0 0 0',
};
const TENANT_DROPDOWN_CSS = {
'margin': '1em 0 0 0',
};
export class SelectMigrationServiceDialog {
private _dialog: azdata.window.Dialog;
private _view!: azdata.ModelView;
private _disposables: vscode.Disposable[] = [];
private _serviceContext!: MigrationServiceContext;
private _azureAccounts!: azdata.Account[];
private _accountTenants!: azurecore.Tenant[];
private _subscriptions!: azureResource.AzureResourceSubscription[];
private _locations!: azureResource.AzureLocation[];
private _resourceGroups!: azureResource.AzureResourceResourceGroup[];
private _sqlMigrationServices!: SqlMigrationService[];
private _azureAccountsDropdown!: azdata.DropDownComponent;
private _accountTenantDropdown!: azdata.DropDownComponent;
private _accountTenantFlexContainer!: azdata.FlexContainer;
private _azureSubscriptionDropdown!: azdata.DropDownComponent;
private _azureLocationDropdown!: azdata.DropDownComponent;
private _azureResourceGroupDropdown!: azdata.DropDownComponent;
private _azureServiceDropdownLabel!: azdata.TextComponent;
private _azureServiceDropdown!: azdata.DropDownComponent;
private _deleteButton!: azdata.window.Button;
constructor(
private readonly _onClosedCallback: () => Promise<void>) {
this._dialog = azdata.window.createModelViewDialog(
constants.MIGRATION_SERVICE_SELECT_TITLE,
'SelectMigraitonServiceDialog',
460,
'normal');
}
async initialize(): Promise<void> {
this._serviceContext = await MigrationLocalStorage.getMigrationServiceContext();
await this._dialog.registerContent(async (view: azdata.ModelView) => {
this._disposables.push(
view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
await this.registerContent(view);
});
this._dialog.okButton.label = constants.MIGRATION_SERVICE_SELECT_APPLY_LABEL;
this._dialog.okButton.position = 'left';
this._dialog.cancelButton.position = 'right';
this._deleteButton = azdata.window.createButton(
constants.MIGRATION_SERVICE_CLEAR,
'right');
this._disposables.push(
this._deleteButton.onClick(async (value) => {
await MigrationLocalStorage.saveMigrationServiceContext({});
await this._onClosedCallback();
azdata.window.closeDialog(this._dialog);
}));
this._dialog.customButtons = [this._deleteButton];
azdata.window.openDialog(this._dialog);
}
protected async registerContent(view: azdata.ModelView): Promise<void> {
this._view = view;
const flexContainer = this._view.modelBuilder
.flexContainer()
.withItems([
this._createHeading(),
this._createAzureAccountsDropdown(),
this._createAzureTenantContainer(),
this._createServiceSelectionContainer(),
])
.withLayout({ flexFlow: 'column' })
.withProps({ CSSStyles: { 'padding': CONTROL_MARGIN } })
.component();
await this._view.initializeModel(flexContainer);
await this._populateAzureAccountsDropdown();
}
private _createHeading(): azdata.TextComponent {
return this._view.modelBuilder.text()
.withProps({
value: constants.MIGRATION_SERVICE_SELECT_HEADING,
CSSStyles: { ...styles.BODY_CSS }
}).component();
}
private _createAzureAccountsDropdown(): azdata.FlexContainer {
const azureAccountLabel = this._view.modelBuilder.text()
.withProps({
value: constants.ACCOUNTS_SELECTION_PAGE_TITLE,
requiredIndicator: true,
CSSStyles: { ...LABEL_CSS }
}).component();
this._azureAccountsDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.ACCOUNTS_SELECTION_PAGE_TITLE,
width: INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_AN_ACCOUNT,
CSSStyles: { ...DROPDOWN_CSS },
}).component();
this._disposables.push(
this._azureAccountsDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(this._azureAccountsDropdown, value);
this._serviceContext.azureAccount = (selectedIndex > -1)
? deepClone(this._azureAccounts[selectedIndex])
: undefined!;
await this._populateTentantsDropdown();
}));
const linkAccountButton = this._view.modelBuilder.hyperlink()
.withProps({
label: constants.ACCOUNT_LINK_BUTTON_LABEL,
url: '',
CSSStyles: { ...styles.BODY_CSS },
}).component();
this._disposables.push(
linkAccountButton.onDidClick(async (event) => {
await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount');
await this._populateAzureAccountsDropdown();
}));
return this._view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([
azureAccountLabel,
this._azureAccountsDropdown,
linkAccountButton,
]).component();
}
private _createAzureTenantContainer(): azdata.FlexContainer {
const azureTenantDropdownLabel = this._view.modelBuilder.text()
.withProps({
value: constants.AZURE_TENANT,
CSSStyles: { ...LABEL_CSS, ...TENANT_DROPDOWN_CSS },
}).component();
this._accountTenantDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.AZURE_TENANT,
width: INPUT_COMPONENT_WIDTH,
editable: true,
fireOnTextChange: true,
placeholder: constants.SELECT_A_TENANT,
}).component();
this._disposables.push(
this._accountTenantDropdown.onValueChanged(async value => {
const selectedIndex = findDropDownItemIndex(this._accountTenantDropdown, value);
this._serviceContext.tenant = (selectedIndex > -1)
? deepClone(this._accountTenants[selectedIndex])
: undefined!;
await this._populateSubscriptionDropdown();
}));
this._accountTenantFlexContainer = this._view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([
azureTenantDropdownLabel,
this._accountTenantDropdown,
])
.withProps({ CSSStyles: { ...STYLE_HIDE, } })
.component();
return this._accountTenantFlexContainer;
}
private _createServiceSelectionContainer(): azdata.FlexContainer {
const subscriptionDropdownLabel = this._view.modelBuilder.text()
.withProps({
value: constants.SUBSCRIPTION,
description: constants.TARGET_SUBSCRIPTION_INFO,
requiredIndicator: true,
CSSStyles: { ...LABEL_CSS }
}).component();
this._azureSubscriptionDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.SUBSCRIPTION,
width: INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_A_SUBSCRIPTION,
CSSStyles: { ...DROPDOWN_CSS },
}).component();
this._disposables.push(
this._azureSubscriptionDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(this._azureSubscriptionDropdown, value);
this._serviceContext.subscription = (selectedIndex > -1)
? deepClone(this._subscriptions[selectedIndex])
: undefined!;
await this._populateLocationDropdown();
}));
const azureLocationLabel = this._view.modelBuilder.text()
.withProps({
value: constants.LOCATION,
description: constants.TARGET_LOCATION_INFO,
requiredIndicator: true,
CSSStyles: { ...LABEL_CSS }
}).component();
this._azureLocationDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.LOCATION,
width: INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_A_LOCATION,
CSSStyles: { ...DROPDOWN_CSS },
}).component();
this._disposables.push(
this._azureLocationDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(this._azureLocationDropdown, value);
this._serviceContext.location = (selectedIndex > -1)
? deepClone(this._locations[selectedIndex])
: undefined!;
await this._populateResourceGroupDropdown();
}));
const azureResourceGroupLabel = this._view.modelBuilder.text()
.withProps({
value: constants.RESOURCE_GROUP,
description: constants.TARGET_RESOURCE_GROUP_INFO,
requiredIndicator: true,
CSSStyles: { ...LABEL_CSS }
}).component();
this._azureResourceGroupDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.RESOURCE_GROUP,
width: INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_A_RESOURCE_GROUP,
CSSStyles: { ...DROPDOWN_CSS },
}).component();
this._disposables.push(
this._azureResourceGroupDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(this._azureResourceGroupDropdown, value);
this._serviceContext.resourceGroup = (selectedIndex > -1)
? deepClone(this._resourceGroups[selectedIndex])
: undefined!;
await this._populateMigrationServiceDropdown();
}));
this._azureServiceDropdownLabel = this._view.modelBuilder.text()
.withProps({
value: constants.MIGRATION_SERVICE_SELECT_SERVICE_LABEL,
description: constants.TARGET_RESOURCE_INFO,
requiredIndicator: true,
CSSStyles: { ...LABEL_CSS }
}).component();
this._azureServiceDropdown = this._view.modelBuilder.dropDown()
.withProps({
ariaLabel: constants.MIGRATION_SERVICE_SELECT_SERVICE_LABEL,
width: INPUT_COMPONENT_WIDTH,
editable: true,
required: true,
fireOnTextChange: true,
placeholder: constants.SELECT_A_SERVICE,
CSSStyles: { ...DROPDOWN_CSS },
}).component();
this._disposables.push(
this._azureServiceDropdown.onValueChanged(async (value) => {
const selectedIndex = findDropDownItemIndex(this._azureServiceDropdown, value, true);
this._serviceContext.migrationService = (selectedIndex > -1)
? deepClone(this._sqlMigrationServices.find(service => service.name === value))
: undefined!;
await this._updateButtonState();
}));
this._disposables.push(
this._dialog.okButton.onClick(async (value) => {
await MigrationLocalStorage.saveMigrationServiceContext(this._serviceContext);
await this._onClosedCallback();
}));
return this._view.modelBuilder.flexContainer()
.withItems([
subscriptionDropdownLabel,
this._azureSubscriptionDropdown,
azureLocationLabel,
this._azureLocationDropdown,
azureResourceGroupLabel,
this._azureResourceGroupDropdown,
this._azureServiceDropdownLabel,
this._azureServiceDropdown,
]).withLayout({ flexFlow: 'column' })
.component();
}
private async _updateButtonState(): Promise<void> {
this._dialog.okButton.enabled = this._serviceContext.migrationService !== undefined;
}
private async _populateAzureAccountsDropdown(): Promise<void> {
try {
this._azureAccountsDropdown.loading = true;
this._azureAccountsDropdown.values = await this._getAccountDropdownValues();
if (this._azureAccountsDropdown.values.length > 0) {
selectDefaultDropdownValue(
this._azureAccountsDropdown,
this._serviceContext.azureAccount?.displayInfo?.userId,
false);
this._azureAccountsDropdown.loading = false;
}
} catch (error) {
logError(TelemetryViews.SelectMigrationServiceDialog, '_populateAzureAccountsDropdown', error);
void vscode.window.showErrorMessage(
constants.SELECT_ACCOUNT_ERROR,
error.message);
} finally {
this._azureAccountsDropdown.loading = false;
}
}
private async _populateTentantsDropdown(): Promise<void> {
try {
this._accountTenantDropdown.loading = true;
this._accountTenantDropdown.values = this._getTenantDropdownValues(
this._serviceContext.azureAccount);
await this._accountTenantFlexContainer.updateCssStyles(
this._accountTenants.length > 1
? STYLE_ShOW
: STYLE_HIDE);
if (this._accountTenantDropdown.values.length > 0) {
selectDefaultDropdownValue(
this._accountTenantDropdown,
this._serviceContext.tenant?.id,
false);
this._accountTenantDropdown.loading = false;
}
} catch (error) {
logError(TelemetryViews.SelectMigrationServiceDialog, '_populateTentantsDropdown', error);
void vscode.window.showErrorMessage(
constants.SELECT_TENANT_ERROR,
error.message);
} finally {
this._accountTenantDropdown.loading = false;
}
}
private async _populateSubscriptionDropdown(): Promise<void> {
try {
this._azureSubscriptionDropdown.loading = true;
this._azureSubscriptionDropdown.values = await this._getSubscriptionDropdownValues(
this._serviceContext.azureAccount);
if (this._azureSubscriptionDropdown.values.length > 0) {
selectDefaultDropdownValue(
this._azureSubscriptionDropdown,
this._serviceContext.subscription?.id,
false);
this._azureSubscriptionDropdown.loading = false;
}
} catch (error) {
logError(TelemetryViews.SelectMigrationServiceDialog, '_populateSubscriptionDropdown', error);
void vscode.window.showErrorMessage(
constants.SELECT_SUBSCRIPTION_ERROR,
error.message);
} finally {
this._azureSubscriptionDropdown.loading = false;
}
}
private async _populateLocationDropdown(): Promise<void> {
try {
this._azureLocationDropdown.loading = true;
this._azureLocationDropdown.values = await this._getAzureLocationDropdownValues(
this._serviceContext.azureAccount,
this._serviceContext.subscription);
if (this._azureLocationDropdown.values.length > 0) {
selectDefaultDropdownValue(
this._azureLocationDropdown,
this._serviceContext.location?.displayName,
true);
this._azureLocationDropdown.loading = false;
}
} catch (error) {
logError(TelemetryViews.SelectMigrationServiceDialog, '_populateLocationDropdown', error);
void vscode.window.showErrorMessage(
constants.SELECT_LOCATION_ERROR,
error.message);
} finally {
this._azureLocationDropdown.loading = false;
}
}
private async _populateResourceGroupDropdown(): Promise<void> {
try {
this._azureResourceGroupDropdown.loading = true;
this._azureResourceGroupDropdown.values = await this._getAzureResourceGroupDropdownValues(
this._serviceContext.location);
if (this._azureResourceGroupDropdown.values.length > 0) {
selectDefaultDropdownValue(
this._azureResourceGroupDropdown,
this._serviceContext.resourceGroup?.id,
false);
this._azureResourceGroupDropdown.loading = false;
}
} catch (error) {
logError(TelemetryViews.SelectMigrationServiceDialog, '_populateResourceGroupDropdown', error);
void vscode.window.showErrorMessage(
constants.SELECT_RESOURCE_GROUP_ERROR,
error.message);
} finally {
this._azureResourceGroupDropdown.loading = false;
}
}
private async _populateMigrationServiceDropdown(): Promise<void> {
try {
this._azureServiceDropdown.loading = true;
this._azureServiceDropdown.values = await this._getMigrationServiceDropdownValues(
this._serviceContext.azureAccount,
this._serviceContext.subscription,
this._serviceContext.location,
this._serviceContext.resourceGroup);
if (this._azureServiceDropdown.values.length > 0) {
selectDefaultDropdownValue(
this._azureServiceDropdown,
this._serviceContext?.migrationService?.id,
false);
}
} catch (error) {
logError(TelemetryViews.SelectMigrationServiceDialog, '_populateMigrationServiceDropdown', error);
void vscode.window.showErrorMessage(
constants.SELECT_SERVICE_ERROR,
error.message);
} finally {
this._azureServiceDropdown.loading = false;
}
}
private async _getAccountDropdownValues(): Promise<azdata.CategoryValue[]> {
this._azureAccounts = await azdata.accounts.getAllAccounts() || [];
return this._azureAccounts.map(account => {
return {
name: account.displayInfo.userId,
displayName: account.isStale
? constants.ACCOUNT_CREDENTIALS_REFRESH(account.displayInfo.displayName)
: account.displayInfo.displayName,
};
});
}
private async _getSubscriptionDropdownValues(account?: azdata.Account): Promise<azdata.CategoryValue[]> {
this._subscriptions = [];
if (account?.isStale === false) {
try {
this._subscriptions = await getSubscriptions(account);
this._subscriptions.sort((a, b) => a.name.localeCompare(b.name));
} catch (error) {
logError(TelemetryViews.SelectMigrationServiceDialog, '_getSubscriptionDropdownValues', error);
void vscode.window.showErrorMessage(
constants.SELECT_SUBSCRIPTION_ERROR,
error.message);
}
}
return this._subscriptions.map(subscription => {
return {
name: subscription.id,
displayName: `${subscription.name} - ${subscription.id}`,
};
});
}
private _getTenantDropdownValues(account?: azdata.Account): azdata.CategoryValue[] {
this._accountTenants = account?.isStale === false
? account?.properties?.tenants ?? []
: [];
return this._accountTenants.map(tenant => {
return {
name: tenant.id,
displayName: tenant.displayName,
};
});
}
private async _getAzureLocationDropdownValues(
account?: azdata.Account,
subscription?: azureResource.AzureResourceSubscription): Promise<azdata.CategoryValue[]> {
let locations: azureResource.AzureLocation[] = [];
if (account && subscription) {
// get all available locations
locations = await getLocations(account, subscription);
this._sqlMigrationServices = await getSqlMigrationServices(
account,
subscription) || [];
this._sqlMigrationServices.sort((a, b) => a.name.localeCompare(b.name));
} else {
this._sqlMigrationServices = [];
}
// keep locaitons with services only
this._locations = locations.filter(
(loc, i) => this._sqlMigrationServices.some(service => service.location === loc.name));
this._locations.sort((a, b) => a.name.localeCompare(b.name));
return this._locations.map(loc => {
return {
name: loc.name,
displayName: loc.displayName,
};
});
}
private async _getAzureResourceGroupDropdownValues(location?: azureResource.AzureLocation): Promise<azdata.CategoryValue[]> {
this._resourceGroups = location
? this._getMigrationServicesResourceGroups(location)
: [];
this._resourceGroups.sort((a, b) => a.name.localeCompare(b.name));
return this._resourceGroups.map(rg => {
return {
name: rg.id,
displayName: rg.name,
};
});
}
private _getMigrationServicesResourceGroups(location?: azureResource.AzureLocation): azureResource.AzureResourceResourceGroup[] {
const resourceGroups = this._sqlMigrationServices
.filter(service => service.location === location?.name)
.map(service => service.properties.resourceGroup);
return resourceGroups
.filter((rg, i, arr) => arr.indexOf(rg) === i)
.map(rg => {
return <azureResource.AzureResourceResourceGroup>{
id: getFullResourceGroupFromId(rg),
name: rg,
};
});
}
private async _getMigrationServiceDropdownValues(
account?: azdata.Account,
subscription?: azureResource.AzureResourceSubscription,
location?: azureResource.AzureLocation,
resourceGroup?: azureResource.AzureResourceResourceGroup): Promise<azdata.CategoryValue[]> {
const locationName = location?.name?.toLowerCase();
const resourceGroupName = resourceGroup?.name?.toLowerCase();
return this._sqlMigrationServices
.filter(service =>
service.location?.toLowerCase() === locationName &&
service.properties?.resourceGroup?.toLowerCase() === resourceGroupName)
.map(service => {
return ({
name: service.id,
displayName: `${service.name}`,
});
});
}
}

View File

@@ -5,10 +5,10 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, regenerateSqlMigrationServiceAuthKey } from '../../api/azure';
import { DatabaseMigration, getSqlMigrationServiceAuthKeys, getSqlMigrationServiceMonitoringData, regenerateSqlMigrationServiceAuthKey } from '../../api/azure';
import { IconPathHelper } from '../../constants/iconPathHelper';
import * as constants from '../../constants/strings';
import { MigrationContext } from '../../models/migrationLocalStorage';
import { MigrationServiceContext } from '../../models/migrationLocalStorage';
import * as styles from '../../constants/styles';
const CONTROL_MARGIN = '10px';
@@ -28,7 +28,10 @@ export class SqlMigrationServiceDetailsDialog {
private _migrationServiceAuthKeyTable!: azdata.DeclarativeTableComponent;
private _disposables: vscode.Disposable[] = [];
constructor(private migrationContext: MigrationContext) {
constructor(
private _serviceContext: MigrationServiceContext,
private _migration: DatabaseMigration) {
this._dialog = azdata.window.createModelViewDialog(
'',
'SqlMigrationServiceDetailsDialog',
@@ -46,7 +49,8 @@ export class SqlMigrationServiceDetailsDialog {
await this.createServiceContent(
view,
this.migrationContext);
this._serviceContext,
this._migration);
});
this._dialog.okButton.label = constants.SQL_MIGRATION_SERVICE_DETAILS_BUTTON_LABEL;
@@ -55,33 +59,31 @@ export class SqlMigrationServiceDetailsDialog {
azdata.window.openDialog(this._dialog);
}
private async createServiceContent(view: azdata.ModelView, migrationContext: MigrationContext): Promise<void> {
private async createServiceContent(view: azdata.ModelView, serviceContext: MigrationServiceContext, migration: DatabaseMigration): Promise<void> {
this._migrationServiceAuthKeyTable = this._createIrTable(view);
const serviceNode = (await getSqlMigrationServiceMonitoringData(
migrationContext.azureAccount,
migrationContext.subscription,
migrationContext.controller.properties.resourceGroup,
migrationContext.controller.location,
migrationContext.controller.name,
this.migrationContext.sessionId!
));
serviceContext.azureAccount!,
serviceContext.subscription!,
serviceContext.migrationService?.properties.resourceGroup!,
serviceContext.migrationService?.location!,
serviceContext.migrationService?.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),
this._createHeading(view, this._migration),
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, serviceContext.subscription?.name!, VALUE_MARGIN),
this._createTextItem(view, constants.LOCATION, LABEL_MARGIN),
this._createTextItem(view, migrationContext.controller.location.toUpperCase(), VALUE_MARGIN),
this._createTextItem(view, serviceContext.migrationService?.location?.toUpperCase()!, VALUE_MARGIN),
this._createTextItem(view, constants.RESOURCE_GROUP, LABEL_MARGIN),
this._createTextItem(view, migrationContext.controller.properties.resourceGroup, VALUE_MARGIN),
this._createTextItem(view, serviceContext.migrationService?.properties.resourceGroup!, VALUE_MARGIN),
this._createTextItem(view, constants.SQL_MIGRATION_SERVICE_DETAILS_IR_LABEL, LABEL_MARGIN),
this._createTextItem(view, serviceNodeName, VALUE_MARGIN),
this._createTextItem(
@@ -96,10 +98,10 @@ export class SqlMigrationServiceDetailsDialog {
.component();
await view.initializeModel(flexContainer);
return await this._refreshAuthTable(view, migrationContext);
return await this._refreshAuthTable(view, serviceContext, migration);
}
private _createHeading(view: azdata.ModelView, migrationContext: MigrationContext): azdata.FlexContainer {
private _createHeading(view: azdata.ModelView, migration: DatabaseMigration): azdata.FlexContainer {
return view.modelBuilder
.flexContainer()
.withItems([
@@ -120,19 +122,15 @@ export class SqlMigrationServiceDetailsDialog {
view.modelBuilder
.text()
.withProps({
value: migrationContext.controller.name,
CSSStyles: {
...styles.SECTION_HEADER_CSS
}
value: this._serviceContext.migrationService?.name,
CSSStyles: { ...styles.SECTION_HEADER_CSS }
})
.component(),
view.modelBuilder
.text()
.withProps({
value: constants.SQL_MIGRATION_SERVICE_DETAILS_SUB_TITLE,
CSSStyles: {
...styles.SMALL_NOTE_CSS
}
CSSStyles: { ...styles.SMALL_NOTE_CSS }
})
.component(),
])
@@ -197,15 +195,14 @@ export class SqlMigrationServiceDetailsDialog {
};
}
private async _regenerateAuthKey(view: azdata.ModelView, migrationContext: MigrationContext, keyName: string): Promise<void> {
private async _regenerateAuthKey(view: azdata.ModelView, serviceContext: MigrationServiceContext, migration: DatabaseMigration, keyName: string): Promise<void> {
const keys = await regenerateSqlMigrationServiceAuthKey(
migrationContext.azureAccount,
migrationContext.subscription,
migrationContext.controller.properties.resourceGroup,
migrationContext.controller.location.toUpperCase(),
migrationContext.controller.name,
keyName,
migrationContext.sessionId!);
serviceContext.azureAccount!,
serviceContext.subscription!,
serviceContext.migrationService?.properties.resourceGroup!,
serviceContext.migrationService?.properties.location?.toUpperCase()!,
serviceContext.migrationService?.name!,
keyName);
if (keys?.authKey1 && keyName === AUTH_KEY1) {
await this._updateTableCell(this._migrationServiceAuthKeyTable, 0, 1, keys.authKey1, constants.SERVICE_KEY1_LABEL);
@@ -223,14 +220,13 @@ export class SqlMigrationServiceDetailsDialog {
await vscode.window.showInformationMessage(constants.AUTH_KEY_REFRESHED(keyName));
}
private async _refreshAuthTable(view: azdata.ModelView, migrationContext: MigrationContext): Promise<void> {
private async _refreshAuthTable(view: azdata.ModelView, serviceContext: MigrationServiceContext, migration: DatabaseMigration): Promise<void> {
const keys = await getSqlMigrationServiceAuthKeys(
migrationContext.azureAccount,
migrationContext.subscription,
migrationContext.controller.properties.resourceGroup,
migrationContext.controller.location.toUpperCase(),
migrationContext.controller.name,
migrationContext.sessionId!);
serviceContext.azureAccount!,
serviceContext.subscription!,
serviceContext.migrationService?.properties.resourceGroup!,
serviceContext.migrationService?.location.toUpperCase()!,
serviceContext.migrationService?.name!);
const copyKey1Button = view.modelBuilder
.button()
@@ -275,7 +271,7 @@ export class SqlMigrationServiceDetailsDialog {
})
.component();
this._disposables.push(refreshKey1Button.onDidClick(
async (e) => await this._regenerateAuthKey(view, migrationContext, AUTH_KEY1)));
async (e) => await this._regenerateAuthKey(view, serviceContext, migration, AUTH_KEY1)));
const refreshKey2Button = view.modelBuilder
.button()
@@ -288,7 +284,7 @@ export class SqlMigrationServiceDetailsDialog {
})
.component();
this._disposables.push(refreshKey2Button.onDidClick(
async (e) => await this._regenerateAuthKey(view, migrationContext, AUTH_KEY2)));
async (e) => await this._regenerateAuthKey(view, serviceContext, migration, AUTH_KEY2)));
await this._migrationServiceAuthKeyTable.updateProperties({
dataValues: [