mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-22 01:25:38 -05:00
* - Added coming soon message for learn more. - Potential fix for learn more message * Renaming of controller to sqlMigrationService * Surfacing some errors -Azure account is stale error -Migration Service creation error. * Adding refresh azure token validation. * Fixing some errors pointed during PR -Fixing property names -Fixing count * Fixing migration status - Adding special error handling for resource not found error - Deleting unfound migrations from local cache - Using prefetched migration status for view all Misc fixes: - Using SQL server version name instead of number - Fixing Icons on sku recommendation page - Fixing table column width in cutover dialog - Adding spinner button to refresh. * Fixing all strings in migration service page and dialog * fixed a string error in create service dialog
492 lines
13 KiB
TypeScript
492 lines
13 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* 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 { IconPathHelper } from '../../constants/iconPathHelper';
|
|
import { MigrationContext } from '../../models/migrationLocalStorage';
|
|
import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
|
|
import * as loc from '../../constants/strings';
|
|
import { getSqlServerName } from '../../api/utils';
|
|
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 _serverName!: azdata.TextComponent;
|
|
private _serverVersion!: azdata.TextComponent;
|
|
private _targetServer!: azdata.TextComponent;
|
|
private _targetVersion!: azdata.TextComponent;
|
|
private _migrationStatus!: azdata.TextComponent;
|
|
private _fullBackupFile!: azdata.TextComponent;
|
|
private _lastAppliedLSN!: azdata.TextComponent;
|
|
private _lastAppliedBackupFile!: azdata.TextComponent;
|
|
private _lastAppliedBackupTakenOn!: azdata.TextComponent;
|
|
|
|
private _fileCount!: azdata.TextComponent;
|
|
|
|
private fileTable!: azdata.TableComponent;
|
|
|
|
private _startCutover!: boolean;
|
|
|
|
constructor(migration: MigrationContext) {
|
|
this._model = new MigrationCutoverDialogModel(migration);
|
|
this._dialogObject = azdata.window.createModelViewDialog(loc.MIGRATION_CUTOVER, 'MigrationCutoverDialog', 1000);
|
|
}
|
|
|
|
async initialize(): Promise<void> {
|
|
let tab = azdata.window.createTab('');
|
|
tab.registerContent(async (view: azdata.ModelView) => {
|
|
this._view = view;
|
|
const sourceDetails = this.createInfoField(loc.SOURCE_SERVER, '');
|
|
const sourceVersion = this.createInfoField(loc.SOURCE_VERSION, '');
|
|
|
|
this._serverName = sourceDetails.text;
|
|
this._serverVersion = sourceVersion.text;
|
|
|
|
const flexServer = view.modelBuilder.flexContainer().withLayout({
|
|
flexFlow: 'column'
|
|
}).component();
|
|
|
|
flexServer.addItem(sourceDetails.flexContainer, {
|
|
CSSStyles: {
|
|
'width': '150px'
|
|
}
|
|
});
|
|
flexServer.addItem(sourceVersion.flexContainer, {
|
|
CSSStyles: {
|
|
'width': '150px'
|
|
}
|
|
});
|
|
|
|
const targetServer = this.createInfoField(loc.TARGET_SERVER, '');
|
|
const targetVersion = this.createInfoField(loc.TARGET_VERSION, '');
|
|
|
|
this._targetServer = targetServer.text;
|
|
this._targetVersion = targetVersion.text;
|
|
|
|
const flexTarget = view.modelBuilder.flexContainer().withLayout({
|
|
flexFlow: 'column'
|
|
}).component();
|
|
|
|
flexTarget.addItem(targetServer.flexContainer, {
|
|
CSSStyles: {
|
|
'width': '230px'
|
|
}
|
|
});
|
|
flexTarget.addItem(targetVersion.flexContainer, {
|
|
CSSStyles: {
|
|
'width': '230px'
|
|
}
|
|
});
|
|
|
|
const migrationStatus = this.createInfoField(loc.MIGRATION_STATUS, '');
|
|
const fullBackupFileOn = this.createInfoField(loc.FULL_BACKUP_FILES, '');
|
|
|
|
|
|
this._migrationStatus = migrationStatus.text;
|
|
this._fullBackupFile = fullBackupFileOn.text;
|
|
|
|
const flexStatus = view.modelBuilder.flexContainer().withLayout({
|
|
flexFlow: 'column'
|
|
}).component();
|
|
|
|
flexStatus.addItem(migrationStatus.flexContainer, {
|
|
CSSStyles: {
|
|
'width': '180px'
|
|
}
|
|
});
|
|
flexStatus.addItem(fullBackupFileOn.flexContainer, {
|
|
CSSStyles: {
|
|
'width': '180px'
|
|
}
|
|
});
|
|
|
|
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': '230px'
|
|
}
|
|
});
|
|
flexFile.addItem(lastAppliedBackup.flexContainer, {
|
|
CSSStyles: {
|
|
'width': '230px'
|
|
}
|
|
});
|
|
flexFile.addItem(lastAppliedBackupOn.flexContainer, {
|
|
CSSStyles: {
|
|
'width': '230px'
|
|
}
|
|
});
|
|
const flexInfo = view.modelBuilder.flexContainer().withProps({
|
|
CSSStyles: {
|
|
'width': '700px'
|
|
}
|
|
}).component();
|
|
|
|
flexInfo.addItem(flexServer, {
|
|
flex: '0',
|
|
CSSStyles: {
|
|
'flex': '0',
|
|
'width': '150px'
|
|
}
|
|
});
|
|
|
|
flexInfo.addItem(flexTarget, {
|
|
flex: '0',
|
|
CSSStyles: {
|
|
'flex': '0',
|
|
'width': '230px'
|
|
}
|
|
});
|
|
|
|
flexInfo.addItem(flexStatus, {
|
|
flex: '0',
|
|
CSSStyles: {
|
|
'flex': '0',
|
|
'width': '180px'
|
|
}
|
|
});
|
|
|
|
flexInfo.addItem(flexFile, {
|
|
flex: '0',
|
|
CSSStyles: {
|
|
'flex': '0',
|
|
'width': '200px'
|
|
}
|
|
});
|
|
|
|
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: 280,
|
|
type: azdata.ColumnType.text
|
|
},
|
|
{
|
|
value: loc.TYPE,
|
|
width: 90,
|
|
type: azdata.ColumnType.text
|
|
},
|
|
{
|
|
value: loc.STATUS,
|
|
width: 60,
|
|
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: '800px',
|
|
height: '600px',
|
|
}).component();
|
|
|
|
const formBuilder = view.modelBuilder.formContainer().withFormItems(
|
|
[
|
|
{
|
|
component: await this.migrationContainerHeader()
|
|
},
|
|
{
|
|
component: flexInfo
|
|
},
|
|
{
|
|
component: this._fileCount
|
|
},
|
|
{
|
|
component: this.fileTable
|
|
}
|
|
],
|
|
{
|
|
horizontal: false
|
|
}
|
|
);
|
|
const form = formBuilder.withLayout({ width: '100%' }).component();
|
|
return view.initializeModel(form).then((value) => {
|
|
this.refreshStatus();
|
|
});
|
|
});
|
|
this._dialogObject.content = [tab];
|
|
azdata.window.openDialog(this._dialogObject);
|
|
}
|
|
|
|
|
|
private migrationContainerHeader(): azdata.FlexContainer {
|
|
const header = this._view.modelBuilder.flexContainer().withLayout({
|
|
}).component();
|
|
|
|
this._databaseTitleName = this._view.modelBuilder.text().withProps({
|
|
CSSStyles: {
|
|
'font-size': 'large',
|
|
'width': '400px'
|
|
},
|
|
value: this._model._migration.migrationContext.name
|
|
}).component();
|
|
|
|
header.addItem(this._databaseTitleName, {
|
|
flex: '0',
|
|
CSSStyles: {
|
|
'width': '500px'
|
|
}
|
|
});
|
|
|
|
this._cutoverButton = this._view.modelBuilder.button().withProps({
|
|
iconPath: IconPathHelper.cutover,
|
|
iconHeight: '14px',
|
|
iconWidth: '12px',
|
|
label: 'Start Cutover',
|
|
height: '55px',
|
|
width: '100px',
|
|
enabled: false
|
|
}).component();
|
|
|
|
this._cutoverButton.onDidClick(async (e) => {
|
|
if (this._startCutover) {
|
|
await this._model.startCutover();
|
|
this.refreshStatus();
|
|
} else {
|
|
this._dialogObject.message = {
|
|
text: loc.CANNOT_START_CUTOVER_ERROR,
|
|
level: azdata.window.MessageLevel.Error
|
|
};
|
|
}
|
|
});
|
|
|
|
header.addItem(this._cutoverButton, {
|
|
flex: '0',
|
|
CSSStyles: {
|
|
'width': '100px'
|
|
}
|
|
});
|
|
|
|
this._cancelButton = this._view.modelBuilder.button().withProps({
|
|
iconPath: IconPathHelper.discard,
|
|
iconHeight: '16px',
|
|
iconWidth: '16px',
|
|
label: loc.CANCEL_MIGRATION,
|
|
height: '55px',
|
|
width: '130px'
|
|
}).component();
|
|
|
|
this._cancelButton.onDidClick((e) => {
|
|
this.cancelMigration();
|
|
});
|
|
|
|
header.addItem(this._cancelButton, {
|
|
flex: '0',
|
|
CSSStyles: {
|
|
'width': '130px'
|
|
}
|
|
});
|
|
|
|
|
|
this._refreshButton = this._view.modelBuilder.button().withProps({
|
|
iconPath: IconPathHelper.refresh,
|
|
iconHeight: '16px',
|
|
iconWidth: '16px',
|
|
label: 'Refresh',
|
|
height: '55px',
|
|
width: '100px'
|
|
}).component();
|
|
|
|
this._refreshButton.onDidClick((e) => {
|
|
this.refreshStatus();
|
|
});
|
|
|
|
header.addItem(this._refreshButton, {
|
|
flex: '0',
|
|
CSSStyles: {
|
|
'width': '100px'
|
|
}
|
|
});
|
|
|
|
this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
|
|
loading: false,
|
|
height: '55px'
|
|
}).component();
|
|
|
|
header.addItem(this._refreshLoader, {
|
|
flex: '0'
|
|
});
|
|
return header;
|
|
}
|
|
|
|
|
|
private async refreshStatus(): Promise<void> {
|
|
try {
|
|
this._refreshLoader.loading = true;
|
|
this._cutoverButton.enabled = false;
|
|
this._cancelButton.enabled = false;
|
|
await this._model.fetchStatus();
|
|
const sqlServerInfo = await azdata.connection.getServerInfo(this._model._migration.sourceConnectionProfile.connectionId);
|
|
const sqlServerName = this._model._migration.sourceConnectionProfile.serverName;
|
|
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
|
|
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
|
|
const targetServerName = this._model._migration.targetManagedInstance.name;
|
|
let targetServerVersion;
|
|
if (this._model.migrationStatus.id.includes('managedInstances')) {
|
|
targetServerVersion = loc.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
|
|
} else {
|
|
targetServerVersion = loc.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
|
|
}
|
|
|
|
const migrationStatusTextValue = this._model.migrationStatus.properties.migrationStatus;
|
|
|
|
let fullBackupFileName: string;
|
|
let lastAppliedSSN: string;
|
|
let lastAppliedBackupFileTakenOn: string;
|
|
|
|
|
|
const tableData: ActiveBackupFileSchema[] = [];
|
|
|
|
this._model.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach((activeBackupSet) => {
|
|
tableData.push(
|
|
{
|
|
fileName: activeBackupSet.listOfBackupFiles[0].fileName,
|
|
type: activeBackupSet.backupType,
|
|
status: activeBackupSet.listOfBackupFiles[0].status,
|
|
backupStartTime: activeBackupSet.backupStartDate,
|
|
firstLSN: activeBackupSet.firstLSN,
|
|
lastLSN: activeBackupSet.lastLSN
|
|
}
|
|
);
|
|
if (activeBackupSet.listOfBackupFiles[0].fileName.substr(activeBackupSet.listOfBackupFiles[0].fileName.lastIndexOf('.') + 1) === 'bak') {
|
|
fullBackupFileName = activeBackupSet.listOfBackupFiles[0].fileName;
|
|
}
|
|
if (activeBackupSet.listOfBackupFiles[0].fileName === this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename) {
|
|
lastAppliedSSN = activeBackupSet.lastLSN;
|
|
lastAppliedBackupFileTakenOn = activeBackupSet.backupFinishDate;
|
|
}
|
|
});
|
|
|
|
this._serverName.value = sqlServerName;
|
|
this._serverVersion.value = `${sqlServerVersion}
|
|
${sqlServerInfo.serverVersion}`;
|
|
|
|
this._targetServer.value = targetServerName;
|
|
this._targetVersion.value = targetServerVersion;
|
|
|
|
this._migrationStatus.value = migrationStatusTextValue;
|
|
this._fullBackupFile.value = fullBackupFileName!;
|
|
|
|
this._lastAppliedLSN.value = lastAppliedSSN!;
|
|
this._lastAppliedBackupFile.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename;
|
|
this._lastAppliedBackupTakenOn.value = new Date(lastAppliedBackupFileTakenOn!).toLocaleString();
|
|
|
|
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
|
|
|
|
//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,
|
|
new Date(row.backupStartTime).toLocaleString(),
|
|
row.firstLSN,
|
|
row.lastLSN
|
|
];
|
|
});
|
|
if (this._model.migrationStatus.properties.migrationStatusDetails?.isFullBackupRestored) {
|
|
this._startCutover = true;
|
|
}
|
|
|
|
if (migrationStatusTextValue === 'InProgress') {
|
|
const fileNotRestored = await tableData.some(file => file.status !== 'Restored');
|
|
this._cutoverButton.enabled = !fileNotRestored;
|
|
this._cancelButton.enabled = true;
|
|
} else {
|
|
this._cutoverButton.enabled = false;
|
|
this._cancelButton.enabled = false;
|
|
}
|
|
} catch (e) {
|
|
console.log(e);
|
|
}
|
|
this._refreshLoader.loading = false;
|
|
}
|
|
|
|
private createInfoField(label: string, value: string): {
|
|
flexContainer: azdata.FlexContainer,
|
|
text: azdata.TextComponent
|
|
} {
|
|
const flexContainer = this._view.modelBuilder.flexContainer().withLayout({
|
|
flexFlow: 'column'
|
|
}).component();
|
|
|
|
const labelComponent = this._view.modelBuilder.text().withProps({
|
|
value: label,
|
|
CSSStyles: {
|
|
'font-weight': 'bold',
|
|
'margin-bottom': '0'
|
|
}
|
|
}).component();
|
|
flexContainer.addItem(labelComponent);
|
|
|
|
const textComponent = this._view.modelBuilder.text().withProps({
|
|
value: value,
|
|
CSSStyles: {
|
|
'margin-top': '5px',
|
|
'margin-bottom': '0',
|
|
'width': '100%',
|
|
'overflow': 'hidden',
|
|
'text-overflow': 'ellipses'
|
|
}
|
|
}).component();
|
|
flexContainer.addItem(textComponent);
|
|
return {
|
|
flexContainer: flexContainer,
|
|
text: textComponent
|
|
};
|
|
}
|
|
|
|
private async cancelMigration(): Promise<void> {
|
|
await this._model.cancelMigration();
|
|
await this.refreshStatus();
|
|
}
|
|
}
|
|
|
|
interface ActiveBackupFileSchema {
|
|
fileName: string,
|
|
type: string,
|
|
status: string,
|
|
backupStartTime: string,
|
|
firstLSN: string,
|
|
lastLSN: string
|
|
}
|