mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-14 01:25:37 -05:00
Revamping cutover page based on new mockups (#16547)
* WIP * Fixing some table issues * updating package.json * Fixing readable time * fixing display string * Handling null case in get12hourtime util method
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<svg width="6" height="12" viewBox="0 0 6 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0.289062L5.71094 6L0 11.7109V0.289062ZM1 2.71094V9.28906L4.28906 6L1 2.71094Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 207 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="6" height="12" viewBox="0 0 6 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0.289062L5.71094 6L0 11.7109V0.289062ZM1 2.71094V9.28906L4.28906 6L1 2.71094Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 207 B |
3
extensions/sql-migration/images/expandButtonOpenDark.svg
Normal file
3
extensions/sql-migration/images/expandButtonOpenDark.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.289062 8L8 0.289062V8H0.289062Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 159 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.289062 8L8 0.289062V8H0.289062Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 159 B |
@@ -2,7 +2,7 @@
|
||||
"name": "sql-migration",
|
||||
"displayName": "%displayName%",
|
||||
"description": "%description%",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
|
||||
@@ -218,3 +218,11 @@ export function getMigrationStatusImage(status: string): IconPath {
|
||||
return IconPathHelper.error;
|
||||
}
|
||||
}
|
||||
|
||||
export function get12HourTime(date: Date | undefined): string {
|
||||
const localeTimeStringOptions: Intl.DateTimeFormatOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
};
|
||||
return (date ? date : new Date()).toLocaleTimeString([], localeTimeStringOptions);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ export class IconPathHelper {
|
||||
public static completingCutover: IconPath;
|
||||
public static migrationService: IconPath;
|
||||
public static sendFeedback: IconPath;
|
||||
public static expandButtonClosed: IconPath;
|
||||
public static expandButtonOpen: IconPath;
|
||||
public static newSupportRequest: IconPath;
|
||||
|
||||
public static setExtensionContext(context: vscode.ExtensionContext) {
|
||||
@@ -129,6 +131,14 @@ export class IconPathHelper {
|
||||
light: context.asAbsolutePath('images/sendFeedback.svg'),
|
||||
dark: context.asAbsolutePath('images/sendFeedback.svg')
|
||||
};
|
||||
IconPathHelper.expandButtonClosed = {
|
||||
light: context.asAbsolutePath('images/expandButtonClosedLight.svg'),
|
||||
dark: context.asAbsolutePath('images/expandButtonClosedDark.svg')
|
||||
};
|
||||
IconPathHelper.expandButtonOpen = {
|
||||
light: context.asAbsolutePath('images/expandButtonOpenLight.svg'),
|
||||
dark: context.asAbsolutePath('images/expandButtonOpenDark.svg')
|
||||
};
|
||||
IconPathHelper.newSupportRequest = {
|
||||
light: context.asAbsolutePath('images/newSupportRequest.svg'),
|
||||
dark: context.asAbsolutePath('images/newSupportRequest.svg')
|
||||
|
||||
@@ -384,12 +384,20 @@ export const NO = localize('sql.migration.no', "No");
|
||||
//Migration confirm cutover dialog
|
||||
export const COMPLETING_CUTOVER_WARNING = localize('sql.migration.completing.cutover.warning', "Completing cutover without restoring all the backup(s) may result in loss of data.");
|
||||
export const BUSINESS_CRITICAL_INFO = localize('sql.migration.bc.info', "Managed Instance migration cutover for Business Critical service tier can take significantly longer than General Purpose as three secondary replicas have to be seeded for Always On High Availability group. This operation duration depends on the size of data. Seeding speed in 90% of cases is 220 GB/hour or higher.");
|
||||
export const CUTOVER_HELP_MAIN = localize('sql.migration.cutover.help.main', "When you are ready to do the migration cutover, perform the following steps to complete the database migration. Please note that the database is ready for cutover only after a full backup has been restored on the target Azure SQL Database Managed Instance.");
|
||||
export const CUTOVER_HELP_MAIN = localize('sql.migration.cutover.help.main', "Perform the following steps before you complete cutover.");
|
||||
export const CUTOVER_HELP_STEP1 = localize('sql.migration.cutover.step.1', "1. Stop all the incoming transactions coming to the source database.");
|
||||
export const CUTOVER_HELP_STEP2 = localize('sql.migration.cutover.step.2', "2. Take final transaction log backup and provide it in the network share location.");
|
||||
export const CUTOVER_HELP_STEP3 = localize('sql.migration.cutover.step.3', "3. Make sure all the log backups are restored on target database. The \"Log backups(s) pending restore\" should be zero.");
|
||||
export const CUTOVER_HELP_STEP2_NETWORK_SHARE = localize('sql.migration.cutover.step.2.network.share', "2. Take final transaction log backup and provide it in the network share location.");
|
||||
export const CUTOVER_HELP_STEP2_BLOB_CONTAINER = localize('sql.migration.cutover.step.2.blob', "2. Take final differential or transaction log backup and provide it in the Azure Storage Blob Container.");
|
||||
export const CUTOVER_HELP_STEP3_NETWORK_SHARE = localize('sql.migration.cutover.step.3.network.share', "3. Make sure all the log backups are restored on target database. The \"Log backups(s) pending restore\" should be zero.");
|
||||
export const CUTOVER_HELP_STEP3_BLOB_CONTAINER = localize('sql.migration.cutover.step.3.blob', "3. Make sure all the log backups are restored on target database. The \"Log backups(s) pending restore\" should be zero.");
|
||||
export function LAST_FILE_RESTORED(fileName: string): string {
|
||||
return localize('sql.migration.cutover.last.file.restored', "Last file restored: {0}", fileName);
|
||||
}
|
||||
export function LAST_SCAN_COMPLETED(time: string): string {
|
||||
return localize('sql.migration.last.scan.completed', "Last scan completed: {0}", time);
|
||||
}
|
||||
export function PENDING_BACKUPS(count: number): string {
|
||||
return localize('sql.migartion.cutover.pending.backup', "Pending log backups: {0}", count);
|
||||
return localize('sql.migartion.cutover.pending.backup', "Log backups pending restore: {0}", count);
|
||||
}
|
||||
export const CONFIRM_CUTOVER_CHECKBOX = localize('sql.migration.confirm.checkbox.message', "I confirm there are no additional log backup(s) to provide and want to complete cutover.");
|
||||
export function CUTOVER_IN_PROGRESS(dbName: string): string {
|
||||
@@ -397,7 +405,9 @@ export function CUTOVER_IN_PROGRESS(dbName: string): string {
|
||||
}
|
||||
export const MIGRATION_CANNOT_CANCEL = localize('sql.migration.cannot.cancel', 'Migration is not in progress and cannot be cancelled.');
|
||||
export const MIGRATION_CANNOT_CUTOVER = localize('sql.migration.cannot.cutover', 'Migration is not in progress and cannot be cutover.');
|
||||
|
||||
export const FILE_NAME = localize('sql.migration.file.name', "File name");
|
||||
export const SIZE_COLUMN_HEADER = localize('sql.migration.size.column.header', "Size");
|
||||
export const NO_PENDING_BACKUPS = localize('sql.migration.no.pending.backups', "No pending backups. Click refresh to check current status.");
|
||||
//Migration status dialog
|
||||
export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migration', "Search for migrations");
|
||||
export const ONLINE = localize('sql.migration.online', "Online");
|
||||
|
||||
@@ -8,6 +8,8 @@ import * as vscode from 'vscode';
|
||||
import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
|
||||
import * as constants from '../../constants/strings';
|
||||
import { SqlManagedInstance } from '../../api/azure';
|
||||
import { IconPathHelper } from '../../constants/iconPathHelper';
|
||||
import { convertByteSizeToReadableUnit, get12HourTime } from '../../api/utils';
|
||||
|
||||
export class ConfirmCutoverDialog {
|
||||
private _dialogObject!: azdata.window.Dialog;
|
||||
@@ -19,6 +21,7 @@ export class ConfirmCutoverDialog {
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
|
||||
let tab = azdata.window.createTab('');
|
||||
tab.registerContent(async (view: azdata.ModelView) => {
|
||||
this._view = view;
|
||||
@@ -50,27 +53,19 @@ export class ConfirmCutoverDialog {
|
||||
}).component();
|
||||
|
||||
const helpStepsText = this._view.modelBuilder.text().withProps({
|
||||
value: `${constants.CUTOVER_HELP_STEP1}
|
||||
${constants.CUTOVER_HELP_STEP2}
|
||||
${constants.CUTOVER_HELP_STEP3}`,
|
||||
value: this.migrationCutoverModel.confirmCutoverStepsString(),
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
|
||||
const pendingBackupCount = this.migrationCutoverModel.migrationStatus.properties.migrationStatusDetails?.pendingLogBackupsCount ?? 0;
|
||||
const pendingText = this._view.modelBuilder.text().withProps({
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'font-weight': 'bold'
|
||||
},
|
||||
value: constants.PENDING_BACKUPS(pendingBackupCount!)
|
||||
}).component();
|
||||
const fileContainer = this.migrationCutoverModel.isBlobMigration() ? this.createBlobFileContainer() : this.createNewtorkShareFileContainer();
|
||||
|
||||
const confirmCheckbox = this._view.modelBuilder.checkBox().withProps({
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'margin-bottom': '8px'
|
||||
},
|
||||
label: constants.CONFIRM_CUTOVER_CHECKBOX,
|
||||
}).component();
|
||||
@@ -111,7 +106,7 @@ export class ConfirmCutoverDialog {
|
||||
separator,
|
||||
helpMainText,
|
||||
helpStepsText,
|
||||
pendingText,
|
||||
fileContainer,
|
||||
confirmCheckbox,
|
||||
cutoverWarning,
|
||||
businessCriticalinfoBox
|
||||
@@ -147,4 +142,219 @@ export class ConfirmCutoverDialog {
|
||||
this._dialogObject.content = [tab];
|
||||
azdata.window.openDialog(this._dialogObject);
|
||||
}
|
||||
|
||||
private createBlobFileContainer(): azdata.FlexContainer {
|
||||
const container = this._view.modelBuilder.flexContainer().component();
|
||||
|
||||
const containerHeading = this._view.modelBuilder.text().withProps({
|
||||
value: constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0),
|
||||
width: 250,
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'line-height': '18px',
|
||||
'font-weight': 'bold'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const refreshButton = this._view.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.refresh,
|
||||
iconHeight: 16,
|
||||
iconWidth: 16,
|
||||
width: 70,
|
||||
height: 20,
|
||||
label: constants.REFRESH,
|
||||
CSSStyles: {
|
||||
'margin-top': '13px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
|
||||
container.addItem(containerHeading, {
|
||||
flex: '0'
|
||||
});
|
||||
|
||||
refreshButton.onDidClick(async e => {
|
||||
refreshLoader.loading = true;
|
||||
try {
|
||||
await this.migrationCutoverModel.fetchStatus();
|
||||
containerHeading.value = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
|
||||
} catch (e) {
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: e.toString()
|
||||
};
|
||||
} finally {
|
||||
refreshLoader.loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
container.addItem(refreshButton, {
|
||||
flex: '0'
|
||||
});
|
||||
|
||||
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
|
||||
loading: false,
|
||||
CSSStyles: {
|
||||
'margin-top': '8px',
|
||||
'margin-left': '5px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
container.addItem(refreshLoader, {
|
||||
flex: '0'
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
private createNewtorkShareFileContainer(): azdata.FlexContainer {
|
||||
const container = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
const headingRow = this._view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'row'
|
||||
}).component();
|
||||
|
||||
let expanded: boolean = false;
|
||||
const containerHeading = this._view.modelBuilder.button().withProps({
|
||||
label: constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0),
|
||||
width: 220,
|
||||
height: 14,
|
||||
iconHeight: 12,
|
||||
iconWidth: 8,
|
||||
iconPath: IconPathHelper.expandButtonClosed,
|
||||
CSSStyles: {
|
||||
'font-size': '13px',
|
||||
'line-height': '18px',
|
||||
'font-weight': 'bold',
|
||||
'margin': '16px 10px 0px 0px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
containerHeading.onDidClick(async e => {
|
||||
if (expanded) {
|
||||
containerHeading.iconPath = IconPathHelper.expandButtonClosed;
|
||||
containerHeading.iconHeight = 12;
|
||||
fileTable.updateCssStyles({
|
||||
'display': 'none'
|
||||
});
|
||||
|
||||
} else {
|
||||
containerHeading.iconPath = IconPathHelper.expandButtonOpen;
|
||||
containerHeading.iconHeight = 8;
|
||||
fileTable.updateCssStyles({
|
||||
'display': 'inline'
|
||||
});
|
||||
}
|
||||
expanded = !expanded;
|
||||
});
|
||||
|
||||
const refreshButton = this._view.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.refresh,
|
||||
iconHeight: 16,
|
||||
iconWidth: 16,
|
||||
width: 70,
|
||||
height: 20,
|
||||
label: constants.REFRESH,
|
||||
CSSStyles: {
|
||||
'margin-top': '13px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
headingRow.addItem(containerHeading, {
|
||||
flex: '0'
|
||||
});
|
||||
|
||||
refreshButton.onDidClick(async e => {
|
||||
refreshLoader.loading = true;
|
||||
try {
|
||||
await this.migrationCutoverModel.fetchStatus();
|
||||
containerHeading.label = constants.PENDING_BACKUPS(this.migrationCutoverModel.getPendingLogBackupsCount() ?? 0);
|
||||
lastScanCompleted.value = constants.LAST_SCAN_COMPLETED(get12HourTime(new Date()));
|
||||
this.refreshFileTable(fileTable);
|
||||
} catch (e) {
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: e.toString()
|
||||
};
|
||||
} finally {
|
||||
refreshLoader.loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
headingRow.addItem(refreshButton, {
|
||||
flex: '0'
|
||||
});
|
||||
|
||||
const refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
|
||||
loading: false,
|
||||
CSSStyles: {
|
||||
'margin-top': '15px',
|
||||
'margin-left': '5px',
|
||||
'height': '13px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
headingRow.addItem(refreshLoader, {
|
||||
flex: '0'
|
||||
});
|
||||
|
||||
container.addItem(headingRow);
|
||||
|
||||
const lastScanCompleted = this._view.modelBuilder.text().withProps({
|
||||
value: constants.LAST_SCAN_COMPLETED(get12HourTime(new Date())),
|
||||
CSSStyles: {
|
||||
'font-size': '12px',
|
||||
}
|
||||
}).component();
|
||||
|
||||
container.addItem(lastScanCompleted);
|
||||
|
||||
const fileTable = this._view.modelBuilder.table().withProps({
|
||||
columns: [
|
||||
{
|
||||
value: constants.FILE_NAME,
|
||||
type: azdata.ColumnType.text,
|
||||
width: 250
|
||||
},
|
||||
{
|
||||
value: constants.STATUS,
|
||||
type: azdata.ColumnType.text,
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
value: constants.SIZE_COLUMN_HEADER,
|
||||
type: azdata.ColumnType.text,
|
||||
width: 70
|
||||
}
|
||||
],
|
||||
data: [],
|
||||
width: 400,
|
||||
height: 150,
|
||||
CSSStyles: {
|
||||
'display': 'none'
|
||||
}
|
||||
}).component();
|
||||
container.addItem(fileTable);
|
||||
this.refreshFileTable(fileTable);
|
||||
return container;
|
||||
}
|
||||
|
||||
private refreshFileTable(filetable: azdata.TableComponent) {
|
||||
const pendingFiles = this.migrationCutoverModel.getPendingfiles();
|
||||
if (pendingFiles.length > 0) {
|
||||
filetable.data = pendingFiles.map(f => {
|
||||
return [
|
||||
f.fileName,
|
||||
f.status,
|
||||
convertByteSizeToReadableUnit(f.totalSize)
|
||||
];
|
||||
});
|
||||
} else {
|
||||
filetable.data = [
|
||||
[constants.NO_PENDING_BACKUPS]
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,7 +344,10 @@ export class MigrationCutoverDialog {
|
||||
|
||||
this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
|
||||
loading: false,
|
||||
height: '15px'
|
||||
CSSStyles: {
|
||||
'height': '8px',
|
||||
'margin-top': '4px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
headerActions.addItem(this._refreshLoader, {
|
||||
@@ -549,7 +552,7 @@ export class MigrationCutoverDialog {
|
||||
this.showInfoField(this._fullBackupFileOnInfoField);
|
||||
|
||||
let backupLocation;
|
||||
const isBlobMigration = this._isBlobMigration();
|
||||
const isBlobMigration = this._model.isBlobMigration();
|
||||
// Displaying storage accounts and blob container for azure blob backups.
|
||||
if (isBlobMigration) {
|
||||
backupLocation = `${this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob?.storageAccountResourceId.split('/').pop()} - ${this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob?.blobContainerName}`;
|
||||
@@ -704,12 +707,8 @@ export class MigrationCutoverDialog {
|
||||
return migrationMode === MigrationMode.ONLINE;
|
||||
}
|
||||
|
||||
private _isBlobMigration(): boolean {
|
||||
return this._model._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob !== undefined;
|
||||
}
|
||||
|
||||
private _shouldDisplayBackupFileTable(): boolean {
|
||||
return this._isProvisioned() && this._isOnlineMigration() && !this._isBlobMigration();
|
||||
return this._isProvisioned() && this._isOnlineMigration() && !this._model.isBlobMigration();
|
||||
}
|
||||
|
||||
private getMigrationStatus(): string {
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { getMigrationStatus, DatabaseMigration, startMigrationCutover, stopMigration, getMigrationAsyncOperationDetails, AzureAsyncOperationResource } from '../../api/azure';
|
||||
import { getMigrationStatus, DatabaseMigration, startMigrationCutover, stopMigration, getMigrationAsyncOperationDetails, AzureAsyncOperationResource, BackupFileInfo } from '../../api/azure';
|
||||
import { MigrationContext } from '../../models/migrationLocalStorage';
|
||||
import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews } from '../../telemtery';
|
||||
import * as constants from '../../constants/strings';
|
||||
|
||||
export enum MigrationStatus {
|
||||
Failed = 'Failed',
|
||||
@@ -102,4 +103,40 @@ export class MigrationCutoverDialogModel {
|
||||
}
|
||||
return undefined!;
|
||||
}
|
||||
|
||||
public isBlobMigration(): boolean {
|
||||
return this._migration.migrationContext.properties.backupConfiguration.sourceLocation?.azureBlob !== undefined;
|
||||
}
|
||||
|
||||
public confirmCutoverStepsString(): string {
|
||||
if (this.isBlobMigration()) {
|
||||
return `${constants.CUTOVER_HELP_STEP1}
|
||||
${constants.CUTOVER_HELP_STEP2_BLOB_CONTAINER}
|
||||
${constants.CUTOVER_HELP_STEP3_BLOB_CONTAINER}`;
|
||||
} else {
|
||||
return `${constants.CUTOVER_HELP_STEP1}
|
||||
${constants.CUTOVER_HELP_STEP2_NETWORK_SHARE}
|
||||
${constants.CUTOVER_HELP_STEP3_NETWORK_SHARE}`;
|
||||
}
|
||||
}
|
||||
|
||||
public getLastBackupFileRestoredName(): string | undefined {
|
||||
return this.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename;
|
||||
}
|
||||
|
||||
public getPendingLogBackupsCount(): number | undefined {
|
||||
return this.migrationStatus.properties.migrationStatusDetails?.pendingLogBackupsCount;
|
||||
}
|
||||
|
||||
public getPendingfiles(): BackupFileInfo[] {
|
||||
const files: BackupFileInfo[] = [];
|
||||
this.migrationStatus.properties.migrationStatusDetails?.activeBackupSets?.forEach(abs => {
|
||||
abs.listOfBackupFiles.forEach(f => {
|
||||
if (f.status !== 'Restored') {
|
||||
files.push(f);
|
||||
}
|
||||
});
|
||||
});
|
||||
return files;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user