mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-28 17:23:19 -05:00
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:
@@ -39,13 +39,11 @@ export class MigrationCutoverDialog {
|
||||
private _lastAppliedLSN!: azdata.TextComponent;
|
||||
private _lastAppliedBackupFile!: azdata.TextComponent;
|
||||
private _lastAppliedBackupTakenOn!: azdata.TextComponent;
|
||||
|
||||
private _fileCount!: azdata.TextComponent;
|
||||
|
||||
private fileTable!: azdata.TableComponent;
|
||||
private _autoRefreshHandle!: any;
|
||||
readonly _infoFieldWidth: string = '250px';
|
||||
|
||||
readonly _infoFieldWidth: string = '250px';
|
||||
|
||||
constructor(migration: MigrationContext) {
|
||||
this._model = new MigrationCutoverDialogModel(migration);
|
||||
@@ -55,244 +53,233 @@ export class MigrationCutoverDialog {
|
||||
async initialize(): Promise<void> {
|
||||
let tab = azdata.window.createTab('');
|
||||
tab.registerContent(async (view: azdata.ModelView) => {
|
||||
this._view = view;
|
||||
const sourceDatabase = this.createInfoField(loc.SOURCE_DATABASE, '');
|
||||
const sourceDetails = this.createInfoField(loc.SOURCE_SERVER, '');
|
||||
const sourceVersion = this.createInfoField(loc.SOURCE_VERSION, '');
|
||||
try {
|
||||
this._view = view;
|
||||
const sourceDatabase = this.createInfoField(loc.SOURCE_DATABASE, '');
|
||||
const sourceDetails = this.createInfoField(loc.SOURCE_SERVER, '');
|
||||
const sourceVersion = this.createInfoField(loc.SOURCE_VERSION, '');
|
||||
|
||||
this._sourceDatabase = sourceDatabase.text;
|
||||
this._serverName = sourceDetails.text;
|
||||
this._serverVersion = sourceVersion.text;
|
||||
this._sourceDatabase = sourceDatabase.text;
|
||||
this._serverName = sourceDetails.text;
|
||||
this._serverVersion = sourceVersion.text;
|
||||
|
||||
const flexServer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
const flexServer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
flexServer.addItem(sourceDatabase.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexServer.addItem(sourceDetails.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexServer.addItem(sourceVersion.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
const targetDatabase = this.createInfoField(loc.TARGET_DATABASE_NAME, '');
|
||||
const targetServer = this.createInfoField(loc.TARGET_SERVER, '');
|
||||
const targetVersion = this.createInfoField(loc.TARGET_VERSION, '');
|
||||
|
||||
this._targetDatabase = targetDatabase.text;
|
||||
this._targetServer = targetServer.text;
|
||||
this._targetVersion = targetVersion.text;
|
||||
|
||||
const flexTarget = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
flexTarget.addItem(targetDatabase.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexTarget.addItem(targetServer.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexTarget.addItem(targetVersion.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
const migrationStatus = this.createInfoField(loc.MIGRATION_STATUS, '');
|
||||
const fullBackupFileOn = this.createInfoField(loc.FULL_BACKUP_FILES, '');
|
||||
const backupLocation = this.createInfoField(loc.BACKUP_LOCATION, '');
|
||||
|
||||
|
||||
this._migrationStatus = migrationStatus.text;
|
||||
this._fullBackupFile = fullBackupFileOn.text;
|
||||
this._backupLocation = backupLocation.text;
|
||||
|
||||
const flexStatus = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
flexStatus.addItem(migrationStatus.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexStatus.addItem(fullBackupFileOn.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexStatus.addItem(backupLocation.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
const lastSSN = this.createInfoField(loc.LAST_APPLIED_LSN, '');
|
||||
const lastAppliedBackup = this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
|
||||
const lastAppliedBackupOn = this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '');
|
||||
|
||||
|
||||
this._lastAppliedLSN = lastSSN.text;
|
||||
this._lastAppliedBackupFile = lastAppliedBackup.text;
|
||||
this._lastAppliedBackupTakenOn = lastAppliedBackupOn.text;
|
||||
|
||||
const flexFile = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
flexFile.addItem(lastSSN.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexFile.addItem(lastAppliedBackup.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexFile.addItem(lastAppliedBackupOn.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
const flexInfo = view.modelBuilder.flexContainer().withProps({
|
||||
width: 1000
|
||||
}).component();
|
||||
|
||||
flexInfo.addItem(flexServer, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
flexInfo.addItem(flexTarget, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
flexInfo.addItem(flexStatus, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
flexInfo.addItem(flexFile, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
this._fileCount = view.modelBuilder.text().withProps({
|
||||
width: '500px',
|
||||
CSSStyles: {
|
||||
'font-size': '14px',
|
||||
'font-weight': 'bold'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this.fileTable = view.modelBuilder.table().withProps({
|
||||
columns: [
|
||||
{
|
||||
value: loc.ACTIVE_BACKUP_FILES,
|
||||
width: 230,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: loc.TYPE,
|
||||
width: 90,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.STATUS,
|
||||
width: 60,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.DATA_UPLOADED,
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.COPY_THROUGHPUT,
|
||||
width: 150,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.BACKUP_START_TIME,
|
||||
width: 130,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.FIRST_LSN,
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.LAST_LSN,
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text
|
||||
flexServer.addItem(sourceDatabase.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
],
|
||||
data: [],
|
||||
width: '1100px',
|
||||
height: '300px',
|
||||
fontSize: '12px'
|
||||
}).component();
|
||||
|
||||
const formBuilder = view.modelBuilder.formContainer().withFormItems(
|
||||
[
|
||||
{
|
||||
component: this.migrationContainerHeader()
|
||||
},
|
||||
{
|
||||
component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component()
|
||||
},
|
||||
{
|
||||
component: flexInfo
|
||||
},
|
||||
{
|
||||
component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component()
|
||||
},
|
||||
{
|
||||
component: this._fileCount
|
||||
},
|
||||
{
|
||||
component: this.fileTable
|
||||
});
|
||||
flexServer.addItem(sourceDetails.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
],
|
||||
{
|
||||
horizontal: false
|
||||
}
|
||||
);
|
||||
const form = formBuilder.withLayout({ width: '100%' }).component();
|
||||
this._view.onClosed(e => {
|
||||
clearInterval(this._autoRefreshHandle);
|
||||
});
|
||||
return view.initializeModel(form).then((value) => {
|
||||
this.refreshStatus();
|
||||
});
|
||||
});
|
||||
flexServer.addItem(sourceVersion.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
const targetDatabase = this.createInfoField(loc.TARGET_DATABASE_NAME, '');
|
||||
const targetServer = this.createInfoField(loc.TARGET_SERVER, '');
|
||||
const targetVersion = this.createInfoField(loc.TARGET_VERSION, '');
|
||||
|
||||
this._targetDatabase = targetDatabase.text;
|
||||
this._targetServer = targetServer.text;
|
||||
this._targetVersion = targetVersion.text;
|
||||
|
||||
const flexTarget = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
flexTarget.addItem(targetDatabase.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexTarget.addItem(targetServer.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexTarget.addItem(targetVersion.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
const migrationStatus = this.createInfoField(loc.MIGRATION_STATUS, '');
|
||||
const fullBackupFileOn = this.createInfoField(loc.FULL_BACKUP_FILES, '');
|
||||
const backupLocation = this.createInfoField(loc.BACKUP_LOCATION, '');
|
||||
|
||||
this._migrationStatus = migrationStatus.text;
|
||||
this._fullBackupFile = fullBackupFileOn.text;
|
||||
this._backupLocation = backupLocation.text;
|
||||
|
||||
const flexStatus = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
flexStatus.addItem(migrationStatus.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexStatus.addItem(fullBackupFileOn.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexStatus.addItem(backupLocation.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
const lastSSN = this.createInfoField(loc.LAST_APPLIED_LSN, '');
|
||||
const lastAppliedBackup = this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES, '');
|
||||
const lastAppliedBackupOn = this.createInfoField(loc.LAST_APPLIED_BACKUP_FILES_TAKEN_ON, '');
|
||||
|
||||
this._lastAppliedLSN = lastSSN.text;
|
||||
this._lastAppliedBackupFile = lastAppliedBackup.text;
|
||||
this._lastAppliedBackupTakenOn = lastAppliedBackupOn.text;
|
||||
|
||||
const flexFile = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
flexFile.addItem(lastSSN.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexFile.addItem(lastAppliedBackup.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
flexFile.addItem(lastAppliedBackupOn.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
const flexInfo = view.modelBuilder.flexContainer().withProps({
|
||||
width: 1000
|
||||
}).component();
|
||||
|
||||
flexInfo.addItem(flexServer, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
flexInfo.addItem(flexTarget, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
flexInfo.addItem(flexStatus, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
flexInfo.addItem(flexFile, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'flex': '0',
|
||||
'width': this._infoFieldWidth
|
||||
}
|
||||
});
|
||||
|
||||
this._fileCount = view.modelBuilder.text().withProps({
|
||||
width: '500px',
|
||||
CSSStyles: {
|
||||
'font-size': '14px',
|
||||
'font-weight': 'bold'
|
||||
}
|
||||
}).component();
|
||||
|
||||
this.fileTable = view.modelBuilder.table().withProps({
|
||||
columns: [
|
||||
{
|
||||
value: loc.ACTIVE_BACKUP_FILES,
|
||||
width: 230,
|
||||
type: azdata.ColumnType.text,
|
||||
},
|
||||
{
|
||||
value: loc.TYPE,
|
||||
width: 90,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.STATUS,
|
||||
width: 60,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.DATA_UPLOADED,
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.COPY_THROUGHPUT,
|
||||
width: 150,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.BACKUP_START_TIME,
|
||||
width: 130,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.FIRST_LSN,
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text
|
||||
},
|
||||
{
|
||||
value: loc.LAST_LSN,
|
||||
width: 120,
|
||||
type: azdata.ColumnType.text
|
||||
}
|
||||
],
|
||||
data: [],
|
||||
width: '1100px',
|
||||
height: '300px',
|
||||
fontSize: '12px'
|
||||
}).component();
|
||||
|
||||
const formBuilder = view.modelBuilder.formContainer().withFormItems(
|
||||
[
|
||||
{ component: this.migrationContainerHeader() },
|
||||
{ component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component() },
|
||||
{ component: flexInfo },
|
||||
{ component: this._view.modelBuilder.separator().withProps({ width: 1000 }).component() },
|
||||
{ component: this._fileCount },
|
||||
{ component: this.fileTable }
|
||||
],
|
||||
{ horizontal: false }
|
||||
);
|
||||
const form = formBuilder.withLayout({ width: '100%' }).component();
|
||||
this._view.onClosed(e => {
|
||||
clearInterval(this._autoRefreshHandle);
|
||||
});
|
||||
|
||||
return view.initializeModel(form).then((value) => {
|
||||
this.refreshStatus();
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
this._dialogObject.content = [tab];
|
||||
|
||||
@@ -305,7 +292,6 @@ export class MigrationCutoverDialog {
|
||||
azdata.window.openDialog(this._dialogObject);
|
||||
}
|
||||
|
||||
|
||||
private migrationContainerHeader(): azdata.FlexContainer {
|
||||
const sqlDatbaseLogo = this._view.modelBuilder.image().withProps({
|
||||
iconPath: IconPathHelper.sqlDatabaseLogo,
|
||||
@@ -404,7 +390,7 @@ export class MigrationCutoverDialog {
|
||||
this._cancelButton.onDidClick((e) => {
|
||||
vscode.window.showInformationMessage(loc.CANCEL_MIGRATION_CONFIRMATION, loc.YES, loc.NO).then(async (v) => {
|
||||
if (v === loc.YES) {
|
||||
await this.cancelMigration();
|
||||
await this._model.cancelMigration();
|
||||
await this.refreshStatus();
|
||||
}
|
||||
});
|
||||
@@ -427,9 +413,8 @@ export class MigrationCutoverDialog {
|
||||
}
|
||||
}).component();
|
||||
|
||||
this._refreshButton.onDidClick((e) => {
|
||||
this.refreshStatus();
|
||||
});
|
||||
this._refreshButton.onDidClick(
|
||||
async (e) => await this.refreshStatus());
|
||||
|
||||
headerActions.addItem(this._refreshButton, {
|
||||
flex: '0',
|
||||
@@ -597,7 +582,7 @@ export class MigrationCutoverDialog {
|
||||
|
||||
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
|
||||
|
||||
//Sorting files in descending order of backupStartTime
|
||||
// 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) => {
|
||||
@@ -664,11 +649,6 @@ export class MigrationCutoverDialog {
|
||||
text: textComponent
|
||||
};
|
||||
}
|
||||
|
||||
private async cancelMigration(): Promise<void> {
|
||||
await this._model.cancelMigration();
|
||||
await this.refreshStatus();
|
||||
}
|
||||
}
|
||||
|
||||
interface ActiveBackupFileSchema {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user