sql-migration: update migration status page latest design (#16099)

* add migrations status context menu and commands

* add migration status images

* remove test code, add account validation

* fix command registration to occure once

* fix typo
This commit is contained in:
brian-harris
2021-07-16 12:20:19 -07:00
committed by GitHub
parent 3f047ae15a
commit 78a144b5ca
6 changed files with 684 additions and 380 deletions

View File

@@ -12,9 +12,15 @@ import { AdsMigrationStatus, MigrationStatusDialogModel } from './migrationStatu
import * as loc from '../../constants/strings';
import { convertTimeDifferenceToDuration, filterMigrations, SupportedAutoRefreshIntervals } from '../../api/utils';
import { SqlMigrationServiceDetailsDialog } from '../sqlMigrationService/sqlMigrationServiceDetailsDialog';
import { ConfirmCutoverDialog } from '../migrationCutover/confirmCutoverDialog';
import { MigrationCutoverDialogModel } from '../migrationCutover/migrationCutoverDialogModel';
const refreshFrequency: SupportedAutoRefreshIntervals = 180000;
const statusImageSize: number = 14;
const imageCellStyles: azdata.CssStyles = { 'margin': '3px 3px 0 0', 'padding': '0' };
const statusCellStyles: azdata.CssStyles = { 'margin': '0', 'padding': '0' };
export class MigrationStatusDialog {
private _model: MigrationStatusDialogModel;
private _dialogObject!: azdata.window.Dialog;
@@ -52,6 +58,7 @@ export class MigrationStatusDialog {
});
}
this.registerCommands();
const formBuilder = view.modelBuilder.formContainer().withFormItems(
[
{
@@ -83,6 +90,9 @@ export class MigrationStatusDialog {
azdata.window.openDialog(this._dialogObject);
}
private canCancelMigration = (status: string | undefined) => status && status in ['InProgress', 'Creating', 'Completing', 'Creating'];
private canCutoverMigration = (status: string | undefined) => status === 'InProgress';
private createSearchAndRefreshContainer(): azdata.FlexContainer {
this._searchBox = this._view.modelBuilder.inputBox().withProps({
stopEnterPropagation: true,
@@ -102,7 +112,7 @@ export class MigrationStatusDialog {
iconHeight: '16px',
iconWidth: '20px',
height: '30px',
label: 'Refresh',
label: loc.REFRESH_BUTTON_LABEL,
}).component();
this._refresh.onDidClick((e) => {
@@ -152,122 +162,291 @@ export class MigrationStatusDialog {
}
private setAutoRefresh(interval: SupportedAutoRefreshIntervals): void {
let classVariable = this;
const classVariable = this;
clearInterval(this._autoRefreshHandle);
if (interval !== -1) {
this._autoRefreshHandle = setInterval(function () { classVariable.refreshTable(); }, interval);
}
}
private populateMigrationTable(): void {
try {
const migrations = filterMigrations(this._model._migrations, (<azdata.CategoryValue>this._statusDropdown.value).name, this._searchBox.value!);
private registerCommands(): void {
vscode.commands.registerCommand(
'sqlmigration.cutover',
async (migrationId: string) => {
try {
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
if (this.canCutoverMigration(migration?.migrationContext.properties.migrationStatus)) {
const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
await cutoverDialogModel.fetchStatus();
const dialog = new ConfirmCutoverDialog(cutoverDialogModel);
await dialog.initialize();
} else {
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CUTOVER);
}
} catch (e) {
console.log(e);
}
});
const data: azdata.DeclarativeTableCellValue[][] = [];
vscode.commands.registerCommand(
'sqlmigration.view.database',
async (migrationId: string) => {
try {
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
const dialog = new MigrationCutoverDialog(migration!);
await dialog.initialize();
} catch (e) {
console.log(e);
}
});
vscode.commands.registerCommand(
'sqlmigration.view.target',
async (migrationId: string) => {
try {
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
const url = 'https://portal.azure.com/#resource/' + migration!.targetManagedInstance.id;
await vscode.env.openExternal(vscode.Uri.parse(url));
} catch (e) {
console.log(e);
}
});
vscode.commands.registerCommand(
'sqlmigration.view.service',
async (migrationId: string) => {
try {
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
const dialog = new SqlMigrationServiceDetailsDialog(migration!);
await dialog.initialize();
} catch (e) {
console.log(e);
}
});
vscode.commands.registerCommand(
'sqlmigration.copy.migration',
async (migrationId: string) => {
try {
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
await cutoverDialogModel.fetchStatus();
if (cutoverDialogModel.migrationOpStatus) {
await vscode.env.clipboard.writeText(JSON.stringify({
'async-operation-details': cutoverDialogModel.migrationOpStatus,
'details': cutoverDialogModel.migrationStatus
}, undefined, 2));
} else {
await vscode.env.clipboard.writeText(JSON.stringify(cutoverDialogModel.migrationStatus, undefined, 2));
}
await vscode.window.showInformationMessage(loc.DETAILS_COPIED);
} catch (e) {
console.log(e);
}
});
vscode.commands.registerCommand(
'sqlmigration.cancel.migration',
async (migrationId: string) => {
try {
const migration = this._model._migrations.find(migration => migration.migrationContext.id === migrationId);
if (this.canCancelMigration(migration?.migrationContext.properties.migrationStatus)) {
vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => {
if (v === loc.YES) {
const cutoverDialogModel = new MigrationCutoverDialogModel(migration!);
await cutoverDialogModel.fetchStatus();
await cutoverDialogModel.cancelMigration();
}
});
} else {
await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_CANCEL);
}
} catch (e) {
console.log(e);
}
});
}
private async populateMigrationTable(): Promise<void> {
try {
const migrations = filterMigrations(
this._model._migrations,
(<azdata.CategoryValue>this._statusDropdown.value).name,
this._searchBox.value!);
migrations.sort((m1, m2) => {
return new Date(m1.migrationContext.properties.startedOn) > new Date(m2.migrationContext.properties.startedOn) ? -1 : 1;
});
migrations.forEach((migration, index) => {
const migrationRow: azdata.DeclarativeTableCellValue[] = [];
const databaseHyperLink = this._view.modelBuilder.hyperlink().withProps({
label: migration.migrationContext.properties.sourceDatabaseName,
url: ''
}).component();
databaseHyperLink.onDidClick(async (e) => {
await (new MigrationCutoverDialog(migration)).initialize();
});
migrationRow.push({
value: databaseHyperLink,
});
migrationRow.push({
value: (migration.targetManagedInstance.type === 'microsoft.sql/managedinstances') ? loc.SQL_MANAGED_INSTANCE : loc.SQL_VIRTUAL_MACHINE
});
const sqlMigrationName = this._view.modelBuilder.hyperlink().withProps({
label: migration.targetManagedInstance.name,
url: ''
}).component();
sqlMigrationName.onDidClick((e) => {
vscode.window.showInformationMessage(loc.COMING_SOON);
});
migrationRow.push({
value: sqlMigrationName
});
const dms = this._view.modelBuilder.hyperlink().withProps({
label: migration.controller.name,
url: ''
}).component();
dms.onDidClick((e) => {
(new SqlMigrationServiceDetailsDialog(migration)).initialize();
});
migrationRow.push({
value: dms
});
migrationRow.push({
value: loc.ONLINE
});
let migrationStatus = migration.migrationContext.properties.migrationStatus ? migration.migrationContext.properties.migrationStatus : migration.migrationContext.properties.provisioningState;
let warningCount = 0;
if (migration.asyncOperationResult?.error?.message) {
warningCount++;
}
if (migration.migrationContext.properties.migrationFailureError?.message) {
warningCount++;
}
if (migration.migrationContext.properties.migrationStatusDetails?.fileUploadBlockingErrors) {
warningCount += migration.migrationContext.properties.migrationStatusDetails?.fileUploadBlockingErrors.length;
}
if (migration.migrationContext.properties.migrationStatusDetails?.restoreBlockingReason) {
warningCount++;
}
migrationRow.push({
value: loc.STATUS_WARNING_COUNT(migrationStatus, warningCount)
});
let duration;
if (migration.migrationContext.properties.endedOn) {
duration = convertTimeDifferenceToDuration(new Date(migration.migrationContext.properties.startedOn), new Date(migration.migrationContext.properties.endedOn));
} else {
duration = convertTimeDifferenceToDuration(new Date(migration.migrationContext.properties.startedOn), new Date());
}
migrationRow.push({
value: (migration.migrationContext.properties.startedOn) ? duration : '---'
});
migrationRow.push({
value: (migration.migrationContext.properties.startedOn) ? new Date(migration.migrationContext.properties.startedOn).toLocaleString() : '---'
});
migrationRow.push({
value: (migration.migrationContext.properties.endedOn) ? new Date(migration.migrationContext.properties.endedOn).toLocaleString() : '---'
});
data.push(migrationRow);
const data: azdata.DeclarativeTableCellValue[][] = migrations.map((migration, index) => {
return [
{ value: this._getDatabaserHyperLink(migration) },
{ value: this._getMigrationStatus(migration) },
{ value: loc.ONLINE },
{ value: this._getMigrationTargetType(migration) },
{ value: migration.targetManagedInstance.name },
{ value: migration.controller.name },
{
value: this._getMigrationDuration(
migration.migrationContext.properties.startedOn,
migration.migrationContext.properties.endedOn)
},
{ value: this._getMigrationTime(migration.migrationContext.properties.startedOn) },
{ value: this._getMigrationTime(migration.migrationContext.properties.endedOn) },
{
value: {
commands: [
'sqlmigration.cutover',
'sqlmigration.view.database',
'sqlmigration.view.target',
'sqlmigration.view.service',
'sqlmigration.copy.migration',
'sqlmigration.cancel.migration',
],
context: migration.migrationContext.id
},
}
];
});
this._statusTable.dataValues = data;
await this._statusTable.setDataValues(data);
} catch (e) {
console.log(e);
}
}
private _getDatabaserHyperLink(migration: MigrationContext): azdata.FlexContainer {
const imageControl = this._view.modelBuilder.image()
.withProps({
iconPath: IconPathHelper.sqlDatabaseLogo,
iconHeight: statusImageSize,
iconWidth: statusImageSize,
height: statusImageSize,
width: statusImageSize,
CSSStyles: imageCellStyles
})
.component();
const databaseHyperLink = this._view.modelBuilder
.hyperlink()
.withProps({
label: migration.migrationContext.properties.sourceDatabaseName,
url: '',
CSSStyles: statusCellStyles
}).component();
databaseHyperLink.onDidClick(
async (e) => await (new MigrationCutoverDialog(migration)).initialize());
return this._view.modelBuilder
.flexContainer()
.withItems([imageControl, databaseHyperLink])
.withProps({ CSSStyles: statusCellStyles, display: 'inline-flex' })
.component();
}
private _getMigrationTime(migrationTime: string): string {
return migrationTime
? new Date(migrationTime).toLocaleString()
: '---';
}
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 _getMigrationTargetType(migration: MigrationContext): string {
return migration.targetManagedInstance.type === 'microsoft.sql/managedinstances'
? loc.SQL_MANAGED_INSTANCE
: loc.SQL_VIRTUAL_MACHINE;
}
private _getMigrationStatus(migration: MigrationContext): azdata.FlexContainer {
const properties = migration.migrationContext.properties;
const migrationStatus = properties.migrationStatus ?? properties.provisioningState;
let warningCount = 0;
if (migration.asyncOperationResult?.error?.message) {
warningCount++;
}
if (properties.migrationFailureError?.message) {
warningCount++;
}
if (properties.migrationStatusDetails?.fileUploadBlockingErrors) {
warningCount += properties.migrationStatusDetails?.fileUploadBlockingErrors.length;
}
if (properties.migrationStatusDetails?.restoreBlockingReason) {
warningCount++;
}
return this._getStatusControl(migrationStatus, warningCount);
}
private _getStatusControl(status: string, count: number): azdata.FlexContainer {
const control = this._view.modelBuilder
.flexContainer()
.withItems([
// migration status icon
this._view.modelBuilder.image()
.withProps({
iconPath: this._statusImageMap(status),
iconHeight: statusImageSize,
iconWidth: statusImageSize,
height: statusImageSize,
width: statusImageSize,
CSSStyles: imageCellStyles
})
.component(),
// migration status text
this._view.modelBuilder.text().withProps({
value: loc.STATUS_VALUE(status, count),
height: statusImageSize,
CSSStyles: statusCellStyles,
}).component()
])
.withProps({ CSSStyles: statusCellStyles, display: 'inline-flex' })
.component();
if (count > 0) {
control.addItems([
// migration warning / error image
this._view.modelBuilder.image().withProps({
iconPath: this._statusInfoMap(status),
iconHeight: statusImageSize,
iconWidth: statusImageSize,
height: statusImageSize,
width: statusImageSize,
CSSStyles: imageCellStyles
}).component(),
// migration warning / error counts
this._view.modelBuilder.text().withProps({
value: loc.STATUS_WARNING_COUNT(status, count),
height: statusImageSize,
CSSStyles: statusCellStyles,
}).component()
]);
}
return control;
}
private async refreshTable(): Promise<void> {
this._refreshLoader.loading = true;
const currentConnection = await azdata.connection.getCurrentConnection();
this._model._migrations = await MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection, true);
this.populateMigrationTable();
await this.populateMigrationTable();
this._refreshLoader.loading = false;
}
@@ -298,25 +477,9 @@ export class MigrationStatusDialog {
headerCssStyles: headerCssStyles
},
{
displayName: loc.AZURE_SQL_TARGET,
valueType: azdata.DeclarativeDataType.string,
width: '140px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
displayName: loc.MIGRATION_STATUS,
valueType: azdata.DeclarativeDataType.component,
width: '130px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.DATABASE_MIGRATION_SERVICE,
valueType: azdata.DeclarativeDataType.component,
width: '150px',
width: '170px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
@@ -324,13 +487,29 @@ export class MigrationStatusDialog {
{
displayName: loc.MIGRATION_MODE,
valueType: azdata.DeclarativeDataType.string,
width: '100px',
width: '90px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.MIGRATION_STATUS,
displayName: loc.AZURE_SQL_TARGET,
valueType: azdata.DeclarativeDataType.string,
width: '130px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
valueType: azdata.DeclarativeDataType.string,
width: '130px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.DATABASE_MIGRATION_SERVICE,
valueType: azdata.DeclarativeDataType.string,
width: '150px',
isReadOnly: true,
@@ -348,7 +527,7 @@ export class MigrationStatusDialog {
{
displayName: loc.START_TIME,
valueType: azdata.DeclarativeDataType.string,
width: '120px',
width: '140px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
@@ -356,13 +535,50 @@ export class MigrationStatusDialog {
{
displayName: loc.FINISH_TIME,
valueType: azdata.DeclarativeDataType.string,
width: '120px',
width: '140px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: '',
valueType: azdata.DeclarativeDataType.menu,
width: '20px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles,
}
]
}).component();
return this._statusTable;
}
private _statusImageMap(status: string): azdata.IconPath {
switch (status) {
case 'InProgress':
return IconPathHelper.inProgressMigration;
case 'Succeeded':
return IconPathHelper.completedMigration;
case 'Creating':
return IconPathHelper.notStartedMigration;
case 'Completing':
return IconPathHelper.completingCutover;
case 'Cancelling':
return IconPathHelper.cancel;
case 'Failed':
default:
return IconPathHelper.error;
}
}
private _statusInfoMap(status: string): azdata.IconPath {
switch (status) {
case 'InProgress':
case 'Creating':
case 'Completing':
return IconPathHelper.warning;
default:
return IconPathHelper.error;
}
}
}