Archived
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:
@@ -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
|
||||
}
|
||||
+26
-43
@@ -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'
|
||||
}
|
||||
Reference in New Issue
Block a user