Add new tabbed dashboard, monitoring with breadcrumb navigation (#19995)

* SQL DB monitoring and  Dashboard refactor

* Merge remote-tracking branch 'origin/main' into dev/brih/feature/sql-migration-dashboard-tabs

* update filter text and optimize page load

* update migration column order, names and statusbox

* add column table sorting

* add new migration and pipeline status values, etc

* address review feedback
This commit is contained in:
brian-harris
2022-07-25 10:06:17 -07:00
committed by GitHub
parent db39571394
commit 78b7c3cfd4
27 changed files with 4192 additions and 2382 deletions
@@ -11,7 +11,8 @@ 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';
import { getMigrationTargetTypeEnum, isBlobMigration } from '../../constants/helper';
import { MigrationTargetType, ServiceTier } from '../../models/stateMachine';
export class ConfirmCutoverDialog {
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
@@ -32,7 +33,7 @@ export class ConfirmCutoverDialog {
}).component();
const sourceDatabaseText = view.modelBuilder.text().withProps({
value: this.migrationCutoverModel._migration.properties.sourceDatabaseName,
value: this.migrationCutoverModel.migration.properties.sourceDatabaseName,
CSSStyles: {
...styles.SMALL_NOTE_CSS,
'margin': '4px 0px 8px'
@@ -53,7 +54,7 @@ export class ConfirmCutoverDialog {
}
}).component();
const fileContainer = isBlobMigration(this.migrationCutoverModel.migrationStatus)
const fileContainer = isBlobMigration(this.migrationCutoverModel.migration)
? this.createBlobFileContainer()
: this.createNetworkShareFileContainer();
@@ -76,13 +77,13 @@ export class ConfirmCutoverDialog {
}).component();
let infoDisplay = 'none';
if (this.migrationCutoverModel._migration.id.toLocaleLowerCase().includes('managedinstances')) {
if (getMigrationTargetTypeEnum(this.migrationCutoverModel.migration) === MigrationTargetType.SQLMI) {
const targetInstance = await getMigrationTargetInstance(
this.migrationCutoverModel._serviceConstext.azureAccount!,
this.migrationCutoverModel._serviceConstext.subscription!,
this.migrationCutoverModel._migration);
this.migrationCutoverModel.serviceConstext.azureAccount!,
this.migrationCutoverModel.serviceConstext.subscription!,
this.migrationCutoverModel.migration);
if ((<SqlManagedInstance>targetInstance)?.sku?.tier === 'BusinessCritical') {
if ((<SqlManagedInstance>targetInstance)?.sku?.tier === ServiceTier.BusinessCritical) {
infoDisplay = 'inline';
}
}
@@ -116,7 +117,7 @@ export class ConfirmCutoverDialog {
await this.migrationCutoverModel.startCutover();
void vscode.window.showInformationMessage(
constants.CUTOVER_IN_PROGRESS(
this.migrationCutoverModel._migration.properties.sourceDatabaseName));
this.migrationCutoverModel.migration.properties.sourceDatabaseName));
}));
const formBuilder = view.modelBuilder.formContainer().withFormItems(
@@ -163,7 +164,7 @@ export class ConfirmCutoverDialog {
} catch (e) {
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
text: e.toString()
text: e.message
};
} finally {
refreshLoader.loading = false;
@@ -241,7 +242,7 @@ export class ConfirmCutoverDialog {
} catch (e) {
this._dialogObject.message = {
level: azdata.window.MessageLevel.Error,
text: e.toString()
text: e.message
};
} finally {
refreshLoader.loading = false;
@@ -1,852 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { IconPathHelper } from '../../constants/iconPathHelper';
import { BackupFileInfoStatus, MigrationServiceContext, MigrationStatus } from '../../models/migrationLocalStorage';
import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
import * as loc from '../../constants/strings';
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, getMigrationStatus, isBlobMigration, isOfflineMigation } from '../../constants/helper';
import { DatabaseMigration, getResourceName } from '../../api/azure';
const statusImageSize: number = 14;
export class MigrationCutoverDialog {
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
private _model: MigrationCutoverDialogModel;
private _databaseTitleName!: azdata.TextComponent;
private _cutoverButton!: azdata.ButtonComponent;
private _refreshButton!: azdata.ButtonComponent;
private _cancelButton!: azdata.ButtonComponent;
private _refreshLoader!: azdata.LoadingComponent;
private _copyDatabaseMigrationDetails!: azdata.ButtonComponent;
private _newSupportRequest!: azdata.ButtonComponent;
private _retryButton!: azdata.ButtonComponent;
private _sourceDatabaseInfoField!: InfoFieldSchema;
private _sourceDetailsInfoField!: InfoFieldSchema;
private _sourceVersionInfoField!: InfoFieldSchema;
private _targetDatabaseInfoField!: InfoFieldSchema;
private _targetServerInfoField!: InfoFieldSchema;
private _targetVersionInfoField!: InfoFieldSchema;
private _migrationStatusInfoField!: InfoFieldSchema;
private _fullBackupFileOnInfoField!: InfoFieldSchema;
private _backupLocationInfoField!: InfoFieldSchema;
private _lastLSNInfoField!: InfoFieldSchema;
private _lastAppliedBackupInfoField!: InfoFieldSchema;
private _lastAppliedBackupTakenOnInfoField!: InfoFieldSchema;
private _currentRestoringFileInfoField!: InfoFieldSchema;
private _fileCount!: azdata.TextComponent;
private _fileTable!: azdata.DeclarativeTableComponent;
private _disposables: vscode.Disposable[] = [];
private _emptyTableFill!: azdata.FlexContainer;
private isRefreshing = false;
readonly _infoFieldWidth: string = '250px';
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');
}
async initialize(): Promise<void> {
let tab = azdata.window.createTab('');
tab.registerContent(async (view: azdata.ModelView) => {
try {
this._view = view;
this._fileCount = view.modelBuilder.text().withProps({
width: '500px',
CSSStyles: {
...styles.BODY_CSS
}
}).component();
const rowCssStyle: azdata.CssStyles = {
'border': 'none',
'text-align': 'left',
'border-bottom': '1px solid',
'font-size': '12px'
};
const headerCssStyles: azdata.CssStyles = {
'border': 'none',
'text-align': 'left',
'border-bottom': '1px solid',
'font-weight': 'bold',
'padding-left': '0px',
'padding-right': '0px',
'font-size': '12px'
};
this._fileTable = view.modelBuilder.declarativeTable().withProps({
ariaLabel: loc.ACTIVE_BACKUP_FILES,
columns: [
{
displayName: loc.ACTIVE_BACKUP_FILES,
valueType: azdata.DeclarativeDataType.string,
width: '230px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.TYPE,
valueType: azdata.DeclarativeDataType.string,
width: '90px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.STATUS,
valueType: azdata.DeclarativeDataType.string,
width: '60px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.DATA_UPLOADED,
valueType: azdata.DeclarativeDataType.string,
width: '120px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.COPY_THROUGHPUT,
valueType: azdata.DeclarativeDataType.string,
width: '150px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.BACKUP_START_TIME,
valueType: azdata.DeclarativeDataType.string,
width: '130px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.FIRST_LSN,
valueType: azdata.DeclarativeDataType.string,
width: '120px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.LAST_LSN,
valueType: azdata.DeclarativeDataType.string,
width: '120px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
}
],
data: [],
width: '1100px',
height: '300px',
CSSStyles: {
...styles.BODY_CSS,
'display': 'none',
'padding-left': '0px'
}
}).component();
const _emptyTableImage = view.modelBuilder.image().withProps({
iconPath: IconPathHelper.emptyTable,
iconHeight: '100px',
iconWidth: '100px',
height: '100px',
width: '100px',
CSSStyles: {
'text-align': 'center'
}
}).component();
const _emptyTableText = view.modelBuilder.text().withProps({
value: loc.EMPTY_TABLE_TEXT,
CSSStyles: {
...styles.NOTE_CSS,
'margin-top': '8px',
'text-align': 'center',
'width': '300px'
}
}).component();
this._emptyTableFill = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column',
alignItems: 'center'
}).withItems([
_emptyTableImage,
_emptyTableText,
]).withProps({
width: 1000,
display: 'none'
}).component();
let formItems = [
{ component: this.migrationContainerHeader() },
{ component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component() },
{ component: await this.migrationInfoGrid() },
{ component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component() },
{ component: this._fileCount },
{ component: this._fileTable },
{ component: this._emptyTableFill }
];
const formBuilder = view.modelBuilder.formContainer().withFormItems(
formItems,
{ horizontal: false }
);
const form = formBuilder.withLayout({ width: '100%' }).component();
this._disposables.push(
this._view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
}));
await view.initializeModel(form);
await this.refreshStatus();
} catch (e) {
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);
}
});
this._dialogObject.content = [tab];
this._dialogObject.cancelButton.hidden = true;
this._dialogObject.okButton.label = loc.CLOSE;
azdata.window.openDialog(this._dialogObject);
}
private migrationContainerHeader(): azdata.FlexContainer {
const sqlDatbaseLogo = this._view.modelBuilder.image().withProps({
iconPath: IconPathHelper.sqlDatabaseLogo,
iconHeight: '32px',
iconWidth: '32px',
width: '32px',
height: '32px'
}).component();
this._databaseTitleName = this._view.modelBuilder.text().withProps({
CSSStyles: {
...styles.PAGE_TITLE_CSS
},
width: 950,
value: this._model._migration.properties.sourceDatabaseName
}).component();
const databaseSubTitle = this._view.modelBuilder.text().withProps({
CSSStyles: {
...styles.NOTE_CSS
},
width: 950,
value: loc.DATABASE
}).component();
const titleContainer = this._view.modelBuilder.flexContainer().withItems([
this._databaseTitleName,
databaseSubTitle
]).withLayout({
'flexFlow': 'column'
}).withProps({
width: 950
}).component();
const titleLogoContainer = this._view.modelBuilder.flexContainer().withProps({
width: 1000
}).component();
titleLogoContainer.addItem(sqlDatbaseLogo, {
flex: '0'
});
titleLogoContainer.addItem(titleContainer, {
flex: '0',
CSSStyles: {
'margin-left': '5px',
'width': '930px'
}
});
const headerActions = this._view.modelBuilder.flexContainer().withLayout({
}).withProps({
width: 1000
}).component();
this._cutoverButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.cutover,
iconHeight: '16px',
iconWidth: '16px',
label: loc.COMPLETE_CUTOVER,
height: '20px',
width: '140px',
enabled: false,
CSSStyles: {
...styles.BODY_CSS,
'display': isOfflineMigation(this._model._migration) ? 'none' : 'block'
}
}).component();
this._disposables.push(this._cutoverButton.onDidClick(async (e) => {
await this.refreshStatus();
const dialog = new ConfirmCutoverDialog(this._model);
await dialog.initialize();
if (this._model.CutoverError) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CUTOVER_ERROR, this._model.CutoverError);
}
}));
headerActions.addItem(this._cutoverButton, { flex: '0' });
this._cancelButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.cancel,
iconHeight: '16px',
iconWidth: '16px',
label: loc.CANCEL_MIGRATION,
height: '20px',
width: '140px',
enabled: false,
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
this._disposables.push(this._cancelButton.onDidClick((e) => {
void vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, { modal: true }, loc.YES, loc.NO).then(async (v) => {
if (v === loc.YES) {
await this._model.cancelMigration();
await this.refreshStatus();
if (this._model.CancelMigrationError) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CANCELLATION_ERROR, this._model.CancelMigrationError);
}
}
});
}));
headerActions.addItem(this._cancelButton, {
flex: '0'
});
this._retryButton = this._view.modelBuilder.button().withProps({
label: loc.RETRY_MIGRATION,
iconPath: IconPathHelper.retry,
enabled: false,
iconHeight: '16px',
iconWidth: '16px',
height: '20px',
width: '120px',
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
this._disposables.push(this._retryButton.onDidClick(
async (e) => {
await this.refreshStatus();
const retryMigrationDialog = new RetryMigrationDialog(
this._context,
this._serviceContext,
this._migration,
this._onClosedCallback);
await retryMigrationDialog.openDialog();
}
));
headerActions.addItem(this._retryButton, {
flex: '0',
});
this._refreshButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.refresh,
iconHeight: '16px',
iconWidth: '16px',
label: 'Refresh',
height: '20px',
width: '80px',
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
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' });
this._copyDatabaseMigrationDetails = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.copy,
iconHeight: '16px',
iconWidth: '16px',
label: loc.COPY_MIGRATION_DETAILS,
height: '20px',
width: '160px',
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
this._disposables.push(this._copyDatabaseMigrationDetails.onDidClick(async (e) => {
await this.refreshStatus();
await vscode.env.clipboard.writeText(this.getMigrationDetails());
void vscode.window.showInformationMessage(loc.DETAILS_COPIED);
}));
headerActions.addItem(this._copyDatabaseMigrationDetails, {
flex: '0',
CSSStyles: { 'margin-left': '5px' }
});
// create new support request button. Hiding button until sql migration support has been setup.
this._newSupportRequest = this._view.modelBuilder.button().withProps({
label: loc.NEW_SUPPORT_REQUEST,
iconPath: IconPathHelper.newSupportRequest,
iconHeight: '16px',
iconWidth: '16px',
height: '20px',
width: '160px',
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
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',
CSSStyles: {
'margin-left': '5px'
}
});
this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
loading: false,
CSSStyles: {
'height': '8px',
'margin-top': '4px'
}
}).component();
headerActions.addItem(this._refreshLoader, {
flex: '0',
CSSStyles: {
'margin-left': '16px'
}
});
const header = this._view.modelBuilder.flexContainer().withItems([
titleLogoContainer
]).withLayout({
flexFlow: 'column'
}).withProps({
CSSStyles: {
width: 1000
}
}).component();
header.addItem(headerActions, {
'CSSStyles': {
'margin-top': '16px'
}
});
return header;
}
private async migrationInfoGrid(): Promise<azdata.FlexContainer> {
const addInfoFieldToContainer = (infoField: InfoFieldSchema, container: azdata.FlexContainer): void => {
container.addItem(infoField.flexContainer, {
CSSStyles: {
width: this._infoFieldWidth,
}
});
};
const flexServer = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
this._sourceDatabaseInfoField = await this.createInfoField(loc.SOURCE_DATABASE, '');
this._sourceDetailsInfoField = await this.createInfoField(loc.SOURCE_SERVER, '');
this._sourceVersionInfoField = await this.createInfoField(loc.SOURCE_VERSION, '');
addInfoFieldToContainer(this._sourceDatabaseInfoField, flexServer);
addInfoFieldToContainer(this._sourceDetailsInfoField, flexServer);
addInfoFieldToContainer(this._sourceVersionInfoField, flexServer);
const flexTarget = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
this._targetDatabaseInfoField = await this.createInfoField(loc.TARGET_DATABASE_NAME, '');
this._targetServerInfoField = await this.createInfoField(loc.TARGET_SERVER, '');
this._targetVersionInfoField = await this.createInfoField(loc.TARGET_VERSION, '');
addInfoFieldToContainer(this._targetDatabaseInfoField, flexTarget);
addInfoFieldToContainer(this._targetServerInfoField, flexTarget);
addInfoFieldToContainer(this._targetVersionInfoField, flexTarget);
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._backupLocationInfoField = await this.createInfoField(loc.BACKUP_LOCATION, '');
addInfoFieldToContainer(this._migrationStatusInfoField, flexStatus);
addInfoFieldToContainer(this._fullBackupFileOnInfoField, flexStatus);
addInfoFieldToContainer(this._backupLocationInfoField, flexStatus);
const flexFile = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
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);
addInfoFieldToContainer(this._lastLSNInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupInfoField, flexFile);
addInfoFieldToContainer(this._lastAppliedBackupTakenOnInfoField, flexFile);
addInfoFieldToContainer(this._currentRestoringFileInfoField, flexFile);
const flexInfoProps = {
flex: '0',
CSSStyles: {
'flex': '0',
'width': this._infoFieldWidth
}
};
const flexInfo = this._view.modelBuilder.flexContainer().withProps({
width: 1000
}).component();
flexInfo.addItem(flexServer, flexInfoProps);
flexInfo.addItem(flexTarget, flexInfoProps);
flexInfo.addItem(flexStatus, flexInfoProps);
flexInfo.addItem(flexFile, flexInfoProps);
return flexInfo;
}
private getMigrationDetails(): string {
return JSON.stringify(this._model.migrationStatus, undefined, 2);
}
private async refreshStatus(): Promise<void> {
if (this.isRefreshing) {
return;
}
try {
clearDialogMessage(this._dialogObject);
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.migrationStatus.properties.provisioningError);
errors.push(this._model.migrationStatus.properties.migrationFailureError?.message);
errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []);
errors.push(this._model.migrationStatus.properties.migrationStatusDetails?.restoreBlockingReason);
this._dialogObject.message = {
// remove undefined and duplicate error entries
text: errors
.filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
.join(EOL),
level: this._model.migrationStatus.properties.migrationStatus === MigrationStatus.InProgress
|| this._model.migrationStatus.properties.migrationStatus === MigrationStatus.Completing
? azdata.window.MessageLevel.Warning
: azdata.window.MessageLevel.Error,
description: this.getMigrationDetails()
};
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
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.name;
const targetServerName = getResourceName(this._model._migration.properties.scope);
let targetServerVersion;
if (this._model.migrationStatus.id.includes('managedInstances')) {
targetServerVersion = loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
} else {
targetServerVersion = loc.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
}
let lastAppliedSSN: string;
let lastAppliedBackupFileTakenOn: string;
const tableData: ActiveBackupFileSchema[] = [];
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 (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;
this._sourceVersionInfoField.text.value = `${sqlServerVersion} ${sqlServerInfo.serverVersion}`;
this._targetDatabaseInfoField.text.value = targetDatabaseName;
this._targetServerInfoField.text.value = targetServerName;
this._targetVersionInfoField.text.value = targetServerVersion;
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 = isBlobMigration(this._model._migration);
// Displaying storage accounts and blob container for azure blob backups.
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.properties.backupConfiguration?.sourceLocation?.fileShare;
backupLocation = fileShare?.path! ?? '-';
}
this._backupLocationInfoField.text.value = backupLocation ?? '-';
this._lastLSNInfoField.text.value = lastAppliedSSN! ?? '-';
this._lastAppliedBackupInfoField.text.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename ?? '-';
this._lastAppliedBackupTakenOnInfoField.text.value = lastAppliedBackupFileTakenOn! ? convertIsoTimeToLocalTime(lastAppliedBackupFileTakenOn).toLocaleString() : '-';
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) {
this._currentRestoringFileInfoField.text.value = loc.ALL_BACKUPS_RESTORED;
} else {
this._currentRestoringFileInfoField.text.value = this._model.migrationStatus.properties.migrationStatusDetails?.currentRestoringFilename;
}
}
if (this._shouldDisplayBackupFileTable()) {
await this._fileCount.updateCssStyles({
...styles.SECTION_HEADER_CSS,
display: 'inline'
});
await this._fileTable.updateCssStyles({
display: 'inline'
});
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
if (tableData.length === 0) {
await this._emptyTableFill.updateCssStyles({
'display': 'flex'
});
this._fileTable.height = '50px';
} else {
await this._emptyTableFill.updateCssStyles({
'display': 'none'
});
this._fileTable.height = '300px';
// Sorting files in descending order of backupStartTime
tableData.sort((file1, file2) => new Date(file1.backupStartTime) > new Date(file2.backupStartTime) ? - 1 : 1);
this._fileTable.data = tableData.map((row) => {
return [
row.fileName,
row.type,
row.status,
row.dataUploaded,
row.copyThroughput,
convertIsoTimeToLocalTime(row.backupStartTime).toLocaleString(),
row.firstLSN,
row.lastLSN
];
});
}
}
this._cutoverButton.enabled = false;
if (migrationStatusTextValue === MigrationStatus.InProgress) {
if (_isBlobMigration) {
if (this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
this._cutoverButton.enabled = true;
}
} else {
const restoredCount = this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.filter(
(a) => a.listOfBackupFiles[0].status === BackupFileInfoStatus.Restored)?.length ?? 0;
if (restoredCount > 0) {
this._cutoverButton.enabled = true;
}
}
}
this._cancelButton.enabled =
migrationStatusTextValue === MigrationStatus.Creating ||
migrationStatusTextValue === MigrationStatus.InProgress;
this._retryButton.enabled = canRetryMigration(migrationStatusTextValue);
} catch (e) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e);
console.log(e);
} finally {
this.isRefreshing = false;
this._refreshLoader.loading = false;
}
}
private async createInfoField(label: string, value: string, defaultHidden: boolean = false, iconPath?: azdata.IconPath): Promise<{
flexContainer: azdata.FlexContainer,
text: azdata.TextComponent,
icon?: azdata.ImageComponent
}> {
const flexContainer = this._view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'flex-direction': 'column',
'padding-right': '12px'
}
}).component();
if (defaultHidden) {
await flexContainer.updateCssStyles({
'display': 'none'
});
}
const labelComponent = this._view.modelBuilder.text().withProps({
value: label,
CSSStyles: {
...styles.LIGHT_LABEL_CSS,
'margin-bottom': '0',
}
}).component();
flexContainer.addItem(labelComponent);
const textComponent = this._view.modelBuilder.text().withProps({
value: value,
CSSStyles: {
...styles.BODY_CSS,
'margin': '4px 0 12px',
'width': '100%',
'overflow': 'visible',
'overflow-wrap': 'break-word'
}
}).component();
let iconComponent;
if (iconPath) {
iconComponent = this._view.modelBuilder.image().withProps({
iconPath: (iconPath === ' ') ? undefined : iconPath,
iconHeight: statusImageSize,
iconWidth: statusImageSize,
height: statusImageSize,
width: statusImageSize,
CSSStyles: {
'margin': '7px 3px 0 0',
'padding': '0'
}
}).component();
const iconTextComponent = this._view.modelBuilder.flexContainer()
.withItems([
iconComponent,
textComponent
]).withProps({
CSSStyles: {
'margin': '0',
'padding': '0'
},
display: 'inline-flex'
}).component();
flexContainer.addItem(iconTextComponent);
} else {
flexContainer.addItem(textComponent);
}
return {
flexContainer: flexContainer,
text: textComponent,
icon: iconComponent
};
}
private _shouldDisplayBackupFileTable(): boolean {
return !isBlobMigration(this._model._migration);
}
private _getMigrationStatus(): string {
return this._model.migrationStatus
? getMigrationStatus(this._model.migrationStatus)
: getMigrationStatus(this._model._migration);
}
}
interface ActiveBackupFileSchema {
fileName: string,
type: string,
status: string,
dataUploaded: string,
copyThroughput: string,
backupStartTime: string,
firstLSN: string,
lastLSN: string
}
interface InfoFieldSchema {
flexContainer: azdata.FlexContainer,
text: azdata.TextComponent,
icon?: azdata.ImageComponent
}
@@ -7,53 +7,45 @@ import { DatabaseMigration, startMigrationCutover, stopMigration, BackupFileInfo
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, isBlobMigration } from '../../constants/helper';
export class MigrationCutoverDialogModel {
public CutoverError?: Error;
public CancelMigrationError?: Error;
public migrationStatus!: DatabaseMigration;
constructor(
public _serviceConstext: MigrationServiceContext,
public _migration: DatabaseMigration
) {
}
public serviceConstext: MigrationServiceContext,
public migration: DatabaseMigration) { }
public async fetchStatus(): Promise<void> {
this.migrationStatus = await getMigrationDetails(
this._serviceConstext.azureAccount!,
this._serviceConstext.subscription!,
this._migration.id,
this._migration.properties?.migrationOperationId);
const migrationStatus = await getMigrationDetails(
this.serviceConstext.azureAccount!,
this.serviceConstext.subscription!,
this.migration.id,
this.migration.properties?.migrationOperationId);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.MigrationStatus,
{
'migrationStatus': this.migrationStatus.properties?.migrationStatus
},
{}
);
// Logging status to help debugging.
console.log(this.migrationStatus);
{ 'migrationStatus': migrationStatus.properties?.migrationStatus },
{});
this.migration = migrationStatus;
}
public async startCutover(): Promise<DatabaseMigration | undefined> {
try {
this.CutoverError = undefined;
if (this._migration) {
if (this.migration) {
const cutover = await startMigrationCutover(
this._serviceConstext.azureAccount!,
this._serviceConstext.subscription!,
this._migration!);
this.serviceConstext.azureAccount!,
this.serviceConstext.subscription!,
this.migration!);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.CutoverMigration,
{
...this.getTelemetryProps(this._serviceConstext, this._migration),
...this.getTelemetryProps(this.serviceConstext, this.migration),
'migrationEndTime': new Date().toString(),
},
{}
@@ -67,30 +59,21 @@ export class MigrationCutoverDialogModel {
return undefined!;
}
public async fetchErrors(): Promise<string> {
const errors = [];
await this.fetchStatus();
errors.push(this.migrationStatus.properties.migrationFailureError?.message);
return errors
.filter((e, i, arr) => e !== undefined && i === arr.indexOf(e))
.join(EOL);
}
public async cancelMigration(): Promise<void> {
try {
this.CancelMigrationError = undefined;
if (this.migrationStatus) {
if (this.migration) {
const cutoverStartTime = new Date().toString();
await stopMigration(
this._serviceConstext.azureAccount!,
this._serviceConstext.subscription!,
this.migrationStatus);
this.serviceConstext.azureAccount!,
this.serviceConstext.subscription!,
this.migration);
sendSqlMigrationActionEvent(
TelemetryViews.MigrationCutoverDialog,
TelemetryAction.CancelMigration,
{
...this.getTelemetryProps(this._serviceConstext, this._migration),
'migrationMode': getMigrationMode(this._migration),
...this.getTelemetryProps(this.serviceConstext, this.migration),
'migrationMode': getMigrationMode(this.migration),
'cutoverStartTime': cutoverStartTime,
},
{}
@@ -104,7 +87,7 @@ export class MigrationCutoverDialogModel {
}
public confirmCutoverStepsString(): string {
if (isBlobMigration(this.migrationStatus)) {
if (isBlobMigration(this.migration)) {
return `${constants.CUTOVER_HELP_STEP1}
${constants.CUTOVER_HELP_STEP2_BLOB_CONTAINER}
${constants.CUTOVER_HELP_STEP3_BLOB_CONTAINER}`;
@@ -116,16 +99,16 @@ export class MigrationCutoverDialogModel {
}
public getLastBackupFileRestoredName(): string | undefined {
return this.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename;
return this.migration.properties.migrationStatusDetails?.lastRestoredFilename;
}
public getPendingLogBackupsCount(): number | undefined {
return this.migrationStatus.properties.migrationStatusDetails?.pendingLogBackupsCount;
return this.migration.properties.migrationStatusDetails?.pendingLogBackupsCount;
}
public getPendingFiles(): BackupFileInfo[] {
const files: BackupFileInfo[] = [];
this.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach(abs => {
this.migration.properties.migrationStatusDetails?.activeBackupSets?.forEach(abs => {
abs.listOfBackupFiles.forEach(f => {
if (f.status !== BackupFileInfoStatus.Restored) {
files.push(f);
@@ -1,593 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { IconPathHelper } from '../../constants/iconPathHelper';
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 } 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';
import { DatabaseMigration, getResourceName } from '../../api/azure';
import { logError, TelemetryViews } from '../../telemtery';
import { SelectMigrationServiceDialog } from '../selectMigrationService/selectMigrationServiceDialog';
const MenuCommands = {
Cutover: 'sqlmigration.cutover',
ViewDatabase: 'sqlmigration.view.database',
ViewTarget: 'sqlmigration.view.target',
ViewService: 'sqlmigration.view.service',
CopyMigration: 'sqlmigration.copy.migration',
CancelMigration: 'sqlmigration.cancel.migration',
RetryMigration: 'sqlmigration.retry.migration',
};
export class MigrationStatusDialog {
private _context: vscode.ExtensionContext;
private _model: MigrationStatusDialogModel;
private _dialogObject!: azdata.window.Dialog;
private _view!: azdata.ModelView;
private _searchBox!: azdata.InputBoxComponent;
private _refresh!: azdata.ButtonComponent;
private _serviceContextButton!: azdata.ButtonComponent;
private _statusDropdown!: azdata.DropDownComponent;
private _statusTable!: azdata.TableComponent;
private _refreshLoader!: azdata.LoadingComponent;
private _disposables: vscode.Disposable[] = [];
private _filteredMigrations: DatabaseMigration[] = [];
private isRefreshing = false;
constructor(
context: vscode.ExtensionContext,
private _filter: AdsMigrationStatus,
private _onClosedCallback: () => Promise<void>) {
this._context = context;
this._model = new MigrationStatusDialogModel([]);
this._dialogObject = azdata.window.createModelViewDialog(
loc.MIGRATION_STATUS,
'MigrationControllerDialog',
'wide');
}
async initialize() {
let tab = azdata.window.createTab('');
tab.registerContent(async (view: azdata.ModelView) => {
this._view = view;
this.registerCommands();
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 { } });
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;
azdata.window.openDialog(this._dialogObject);
}
private canCancelMigration = (status: string | undefined) => status &&
(
status === MigrationStatus.InProgress ||
status === MigrationStatus.Creating ||
status === MigrationStatus.Completing ||
status === MigrationStatus.Canceling
);
private canCutoverMigration = (status: string | undefined) => status === MigrationStatus.InProgress;
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()));
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 => value.name === this._filter);
}
this._refreshLoader = this._view.modelBuilder.loadingComponent()
.withProps({ loading: false })
.component();
const searchLabel = this._view.modelBuilder.text()
.withProps({
value: 'Status',
CSSStyles: {
'font-size': '13px',
'font-weight': '600',
'margin': '3px 0 0 0',
},
}).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();
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 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.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();
if (cutoverDialogModel.CutoverError) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CUTOVER_ERROR, cutoverDialogModel.CutoverError);
}
} else {
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CUTOVER);
}
} catch (e) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CUTOVER_ERROR, e);
console.log(e);
}
}));
this._disposables.push(vscode.commands.registerCommand(
MenuCommands.ViewDatabase,
async (migrationId: string) => {
try {
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);
}
}));
this._disposables.push(vscode.commands.registerCommand(
MenuCommands.ViewTarget,
async (migrationId: string) => {
try {
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);
}
}));
this._disposables.push(vscode.commands.registerCommand(
MenuCommands.ViewService,
async (migrationId: string) => {
try {
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);
}
}));
this._disposables.push(vscode.commands.registerCommand(
MenuCommands.CopyMigration,
async (migrationId: string) => {
try {
clearDialogMessage(this._dialogObject);
const migration = this._model._migrations.find(migration => migration.id === migrationId);
const cutoverDialogModel = new MigrationCutoverDialogModel(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await cutoverDialogModel.fetchStatus();
await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migrationStatus, undefined, 2));
await vscode.window.showInformationMessage(loc.DETAILS_COPIED);
} catch (e) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e);
console.log(e);
}
}));
this._disposables.push(vscode.commands.registerCommand(
MenuCommands.CancelMigration,
async (migrationId: string) => {
try {
clearDialogMessage(this._dialogObject);
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(
await MigrationLocalStorage.getMigrationServiceContext(),
migration!);
await cutoverDialogModel.fetchStatus();
await cutoverDialogModel.cancelMigration();
if (cutoverDialogModel.CancelMigrationError) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CANNOT_CANCEL, cutoverDialogModel.CancelMigrationError);
}
}
});
} else {
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CANCEL);
}
} catch (e) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_CANCELLATION_ERROR, e);
console.log(e);
}
}));
this._disposables.push(vscode.commands.registerCommand(
MenuCommands.RetryMigration,
async (migrationId: string) => {
try {
clearDialogMessage(this._dialogObject);
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 {
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY);
}
} catch (e) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_RETRY_ERROR, e);
console.log(e);
}
}));
}
private async populateMigrationTable(): Promise<void> {
try {
this._filteredMigrations = filterMigrations(
this._model._migrations,
(<azdata.CategoryValue>this._statusDropdown.value).name,
this._searchBox.value!);
this._filteredMigrations.sort((m1, m2) => {
if (!m1.properties?.startedOn) {
return 1;
} else if (!m2.properties?.startedOn) {
return -1;
}
return new Date(m1.properties?.startedOn) > new Date(m2.properties?.startedOn) ? -1 : 1;
});
const data: any[] = this._filteredMigrations.map((migration, index) => {
return [
<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.updateProperty('data', data);
} catch (e) {
logError(TelemetryViews.MigrationStatusDialog, 'Error populating migrations list page', e);
}
}
private _getMigrationTime(migrationTime: string): string {
return migrationTime
? new Date(migrationTime).toLocaleString()
: '---';
}
private _getMigrationDuration(startDate: string, endDate: string): string {
if (startDate) {
if (endDate) {
return convertTimeDifferenceToDuration(
new Date(startDate),
new Date(endDate));
} else {
return convertTimeDifferenceToDuration(
new Date(startDate),
new Date());
}
}
return '---';
}
private _getMigrationStatus(migration: DatabaseMigration): string {
const properties = migration.properties;
const migrationStatus = properties.migrationStatus ?? properties.provisioningState;
let warningCount = 0;
if (properties.migrationFailureError?.message) {
warningCount++;
}
if (properties.migrationStatusDetails?.fileUploadBlockingErrors) {
warningCount += properties.migrationStatusDetails?.fileUploadBlockingErrors.length;
}
if (properties.migrationStatusDetails?.restoreBlockingReason) {
warningCount++;
}
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 tab: azdata.window.DialogTab = azdata.window.createTab('');
tab.registerContent(async view => {
const warningContentContainer = view.modelBuilder.divContainer().component();
const messageTextComponent = view.modelBuilder.text().withProps({
value: calloutMessageText,
CSSStyles: {
'font-size': '12px',
'line-height': '16px',
'margin': '0 0 12px 0',
'display': '-webkit-box',
'-webkit-box-orient': 'vertical',
'-webkit-line-clamp': '5',
'overflow': 'hidden'
}
}).component();
warningContentContainer.addItem(messageTextComponent);
await view.initializeModel(warningContentContainer);
});
dialog.content = [tab];
azdata.window.openDialog(dialog);
}
private async refreshTable(): Promise<void> {
if (this.isRefreshing) {
return;
}
this.isRefreshing = true;
try {
clearDialogMessage(this._dialogObject);
this._refreshLoader.loading = true;
this._model._migrations = await getCurrentMigrations();
await this.populateMigrationTable();
} catch (e) {
displayDialogErrorMessage(this._dialogObject, loc.MIGRATION_STATUS_REFRESH_ERROR, e);
console.log(e);
} finally {
this.isRefreshing = false;
this._refreshLoader.loading = false;
}
}
private createStatusTable(): azdata.TableComponent {
const headerCssStyles = undefined;
const rowCssStyles = undefined;
this._statusTable = this._view.modelBuilder.table().withProps({
ariaLabel: loc.MIGRATION_STATUS,
data: [],
forceFitColumns: azdata.ColumnSizingMode.ForceFit,
height: '600px',
width: '1095px',
display: 'grid',
columns: [
<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,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.MIGRATION_MODE,
value: 'mode',
width: 85,
type: azdata.ColumnType.text,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.AZURE_SQL_TARGET,
value: 'targetType',
width: 120,
type: azdata.ColumnType.text,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
value: 'targetName',
width: 125,
type: azdata.ColumnType.text,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.DATABASE_MIGRATION_SERVICE,
value: 'migrationService',
width: 140,
type: azdata.ColumnType.text,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.DURATION,
value: 'duration',
width: 50,
type: azdata.ColumnType.text,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.START_TIME,
value: 'startTime',
width: 115,
type: azdata.ColumnType.text,
},
{
cssClass: rowCssStyles,
headerCssClass: headerCssStyles,
name: loc.FINISH_TIME,
value: 'finishTime',
width: 115,
type: azdata.ColumnType.text,
},
]
}).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;
}
}
@@ -1,44 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { DatabaseMigration } from '../../api/azure';
import * as loc from '../../constants/strings';
export class MigrationStatusDialogModel {
public statusDropdownValues: azdata.CategoryValue[] = [
{
displayName: loc.STATUS_ALL,
name: AdsMigrationStatus.ALL
}, {
displayName: loc.STATUS_ONGOING,
name: AdsMigrationStatus.ONGOING
}, {
displayName: loc.STATUS_COMPLETING,
name: AdsMigrationStatus.COMPLETING
}, {
displayName: loc.STATUS_SUCCEEDED,
name: AdsMigrationStatus.SUCCEEDED
}, {
displayName: loc.STATUS_FAILED,
name: AdsMigrationStatus.FAILED
}
];
constructor(public _migrations: DatabaseMigration[]) {
}
}
/**
* This enum is used to categorize migrations internally in ADS. A migration has 2 statuses: Provisioning Status and Migration Status. The values from both the statuses are mapped to different values in this enum
*/
export enum AdsMigrationStatus {
ALL = 'all',
ONGOING = 'ongoing',
SUCCEEDED = 'succeeded',
FAILED = 'failed',
COMPLETING = 'completing'
}