Surfacing migration errors in dashboard (#14956)

* vbumping migration

* Adding 2 new icons cancel and warning

* Fixed help link display text in assessments

* Adding summary page redesign and resource name validations

* Made headings bold

* Fixed sku recommendation page styling
Added check item for assessment

* Validating account dropdown after token refresh

* Renamed cutover to mode

* cutover to mode renaming changes.

* Converting to details api for more warnings

* Added target database name and fixed cancel icon

* Surfacing warning info in dashboard.

* Consolidated fetch migrations logic
Localilzed some strings
Surface migration errors in dashboard and status page
Table redesign in status dialog
Fixed a major bug that happens when multiple dashboards are opened due to class variable sharing

* removing console count

* Fixing regex for SQL MI database names

* Allowing spaces in regex
This commit is contained in:
Aasim Khan
2021-04-02 18:49:34 -07:00
committed by GitHub
parent fde5caa9a4
commit 684dfc9760
19 changed files with 433 additions and 151 deletions

View File

@@ -27,6 +27,8 @@ export class IconPathHelper {
public static sqlServerLogo: IconPath;
public static sqlDatabaseLogo: IconPath;
public static sqlDatabaseWarningLogo: IconPath;
public static cancel: IconPath;
public static warning: IconPath;
public static setExtensionContext(context: vscode.ExtensionContext) {
IconPathHelper.copy = {
@@ -93,5 +95,13 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/sqlDatabaseWarning.svg'),
dark: context.asAbsolutePath('images/sqlDatabaseWarning.svg')
};
IconPathHelper.cancel = {
light: context.asAbsolutePath('images/cancel.svg'),
dark: context.asAbsolutePath('images/cancel.svg')
};
IconPathHelper.warning = {
light: context.asAbsolutePath('images/warning.svg'),
dark: context.asAbsolutePath('images/warning.svg')
};
}
}

View File

@@ -134,6 +134,7 @@ export function TARGET_BLOB_CONTAINER(dbName: string): string {
export const ENTER_NETWORK_SHARE_INFORMATION = localize('sql.migration.enter.network.share.information', "Enter target names for selected source database(s)");
export const ENTER_BLOB_CONTAINER_INFORMATION = localize('sql.migration.blob.container.information', "Enter the target name and select the blob container location for selected databases");
export const ENTER_FILE_SHARE_INFORMATION = localize('sql.migration.enter.file.share.information', "Enter the target name and select the file share location of selected databases");
export const INVALID_TARGET_NAME_ERROR = localize('sql.migration.invalid.target.name.error', "Please enter a valid name for the target database.");
// integration runtime page
export const IR_PAGE_TITLE = localize('sql.migration.ir.page.title', "Azure Database Migration Service");
@@ -212,8 +213,8 @@ export const SUMMARY_PAGE_TITLE = localize('sql.migration.summary.page.title', "
export const AZURE_ACCOUNT_LINKED = localize('sql.migration.summary.azure.account.linked', "Azure account linked");
export const MIGRATION_TARGET = localize('sql.migration.summary.migration.target', "Migration target");
export const SUMMARY_MI_TYPE = localize('sql.migration.summary.mi.type', "Azure SQL Managed Instance");
export const SUMMARY_VM_TYPE = localize('sql.migration.summary.vm.type', "Azure SQL Virtual Machine");
export const SUMMARY_DATABASE_COUNT_LABEL = localize('sql.migration.summary.database.count', "Number of database to be migrated");
export const SUMMARY_VM_TYPE = localize('sql.migration.summary.vm.type', "SQL Server on Azure Virtual Machine");
export const SUMMARY_DATABASE_COUNT_LABEL = localize('sql.migration.summary.database.count', "Database(s) to be migrated");
export const SUMMARY_AZURE_STORAGE_SUBSCRIPTION = localize('sql.migration.summary.azure.storage.subscription', "Azure storage subscription");
export const SUMMARY_AZURE_STORAGE = localize('sql.migration.summary.azure.storage', "Azure storage");
export const SUMMARY_IR_NODE = localize('sql.migration.ir.node', "Integration Runtime node");
@@ -221,6 +222,11 @@ export const NETWORK_SHARE = localize('sql.migration.network.share', "Network Sh
export const BLOB_CONTAINER = localize('sql.migration.blob.container.title', "Blob Container");
export const FILE_SHARE = localize('sql.migration.file.share.title', "File Share");
export const MIGRATION_STARTED = localize('sql.migration.started.notification', "Migration in progress");
export const SOURCE_DATABASES = localize('sql.migration.source.databases', "Source Database(s)");
export const MODE = localize('sql.migration.mode', "Mode");
export const BACKUP_LOCATION = localize('sql.migration.backup.location', "Backup Location");
export const AZURE_STORAGE_ACCOUNT_TO_UPLOAD_BACKUPS = localize('sql.migration.azure.storage.account.to.upload.backups', "Azure Storage Account to Upload Backups");
export const SHIR = localize('sql.migration.shir', "Self-hosted Integration Runtime node");
// Open notebook quick pick string
export const NOTEBOOK_QUICK_PICK_PLACEHOLDER = localize('sql.migration.quick.pick.placeholder', "Select the operation you'd like to perform");
@@ -243,13 +249,21 @@ export const PRE_REQ_TITLE = localize('sql.migration.pre.req.title', "Things you
export const PRE_REQ_1 = localize('sql.migration.pre.req.1', "Azure account details");
export const PRE_REQ_2 = localize('sql.migration.pre.req.2', "Azure SQL Managed Instance or SQL Server on Azure Virtual Machine");
export const PRE_REQ_3 = localize('sql.migration.pre.req.3', "Backup location details");
export const MIGRATION_IN_PROGRESS = localize('sql.migration.migration.in.progress', "Migration in progress");
export const MIGRATION_IN_PROGRESS = localize('sql.migration.migration.in.progress', "Database migration in progress");
export const LOG_SHIPPING_IN_PROGRESS = localize('sql.migration.log.shipping.in.progress', "Log shipping in progress");
export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Migration completed");
export const MIGRATION_COMPLETED = localize('sql.migration.migration.completed', "Database migration completed");
export const SUCCESSFULLY_MIGRATED_TO_AZURE_SQL = localize('sql.migration.successfully.migrated.to.azure.sql', "Successfully migrated to Azure SQL");
export const MIGRATION_NOT_STARTED = localize('sql.migration.migration.not.started', "Migration not started");
export const CHOOSE_TO_MIGRATE_TO_AZURE_SQL = localize('sql.migration.choose.to.migrate.to.azure.sql', "Choose to migrate to Azure SQL");
export const COMING_SOON = localize('sql.migration.coming.soon', "Coming soon");
export function MIGRATION_INPROGRESS_WARNING(count: number) {
switch (count) {
case 1:
return localize('sql.migration.inprogress.warning.single', "{0} database has warnings", count);
default:
return localize('sql.migration.inprogress.warning.multiple', "{0} databases have warnings", count);
}
}
// Azure APIs
export const EASTUS2EUAP = localize('sql.migration.eastus2euap', 'East US 2 EUAP');
@@ -259,6 +273,7 @@ export const MIGRATION_CUTOVER = localize('sql.migration.cutover', "Migration cu
export const SOURCE_DATABASE = localize('sql.migration.source.database', "Source database");
export const SOURCE_SERVER = localize('sql.migration.source.server', "Source server");
export const SOURCE_VERSION = localize('sql.migration.source.version', "Source version");
export const TARGET_DATABASE_NAME = localize('sql.migration.target.database.name', "Target database name");
export const TARGET_SERVER = localize('sql.migration.target.server', "Target server");
export const TARGET_VERSION = localize('sql.migration.target.version', "Target version");
export const MIGRATION_STATUS = localize('sql.migration.migration.status', "Migration status");
@@ -295,6 +310,28 @@ export const TARGET_AZURE_SQL_INSTANCE_NAME = localize('sql.migration.target.azu
export const MIGRATION_MODE = localize('sql.migration.cutover.type', "Migration Mode");
export const START_TIME = localize('sql.migration.start.time', "Start Time");
export const FINISH_TIME = localize('sql.migration.finish.time', "Finish Time");
export function STATUS_WARNING_COUNT(status: string, count: number): string {
if (status === 'InProgress' || status === 'Creating' || status === 'Completing' || status === 'Creating') {
switch (count) {
case 0:
return localize('sql.migration.status.warning.count.none', "{0}", status);
case 1:
return localize('sql.migration.status.warning.count.single', "{0} ({1} Warning)", status, count);
default:
return localize('sql.migration.status.warning.count.multiple', "{0} ({1} Warnings)", status, count);
}
} else {
switch (count) {
case 0:
return localize('sql.migration.status.error.count.none', "{0}", status);
case 1:
return localize('sql.migration.status.error.count.single', "{0} ({1} Error)", status, count);
default:
return localize('sql.migration.status.error.count.multiple', "{0} ({1} Errors)", status, count);
}
}
}
//Source Credentials page.
export const SOURCE_CONFIGURATION = localize('sql.migration.source.configuration', "Source Configuration");

View File

@@ -23,7 +23,10 @@ const maxWidth = 800;
interface StatusCard {
container: azdata.DivContainer;
count: azdata.TextComponent
count: azdata.TextComponent,
textContainer?: azdata.FlexContainer,
warningContainer?: azdata.FlexContainer,
warningText?: azdata.TextComponent,
}
export class DashboardWidget {
@@ -34,10 +37,10 @@ export class DashboardWidget {
private _inProgressMigrationButton!: StatusCard;
private _inProgressWarningMigrationButton!: StatusCard;
private _successfulMigrationButton!: StatusCard;
private _notStartedMigrationCard!: StatusCard;
private _migrationStatus!: MigrationContext[];
private _migrationStatusMap: Map<string, MigrationContext[]> = new Map();
private _viewAllMigrationsButton!: azdata.ButtonComponent;
constructor() {
@@ -46,6 +49,16 @@ export class DashboardWidget {
});
}
private async getCurrentMigrations(): Promise<MigrationContext[]> {
const connectionId = (await azdata.connection.getCurrentConnection()).connectionId;
return this._migrationStatusMap.get(connectionId)!;
}
private async setCurrentMigrations(migrations: MigrationContext[]): Promise<void> {
const connectionId = (await azdata.connection.getCurrentConnection()).connectionId;
this._migrationStatusMap.set(connectionId, migrations);
}
public register(): void {
azdata.ui.registerModelViewProvider('migration.dashboard', async (view) => {
this._view = view;
@@ -199,7 +212,7 @@ export class DashboardWidget {
height: maxHeight,
iconHeight: 32,
iconPath: taskMetaData.iconPath,
iconWidth: 32,
iconWidth: 36,
label: taskMetaData.title,
title: taskMetaData.title,
width: maxWidth,
@@ -219,26 +232,47 @@ export class DashboardWidget {
this._viewAllMigrationsButton.enabled = false;
this._migrationStatusCardLoadingContainer.loading = true;
try {
this._migrationStatus = await this.getMigrations();
const inProgressMigrations = this._migrationStatus.filter((value) => {
this.setCurrentMigrations(await this.getMigrations());
const migrationStatus = await this.getCurrentMigrations();
const inProgressMigrations = migrationStatus.filter((value) => {
const status = value.migrationContext.properties.migrationStatus;
const provisioning = value.migrationContext.properties.provisioningState;
return status === 'InProgress' || status === 'Creating' || status === 'Completing' || provisioning === 'Creating';
});
this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
let warningCount = 0;
const successfulMigration = this._migrationStatus.filter((value) => {
for (let i = 0; i < inProgressMigrations.length; i++) {
if (
inProgressMigrations[i].asyncOperationResult?.error?.message ||
inProgressMigrations[i].migrationContext.properties.migrationFailureError?.message ||
inProgressMigrations[i].migrationContext.properties.migrationStatusDetails?.fileUploadBlockingErrors ||
inProgressMigrations[i].migrationContext.properties.migrationStatusDetails?.restoreBlockingReason
) {
warningCount += 1;
}
}
if (warningCount > 0) {
this._inProgressWarningMigrationButton.warningText!.value = loc.MIGRATION_INPROGRESS_WARNING(warningCount);
this._inProgressMigrationButton.container.display = 'none';
this._inProgressWarningMigrationButton.container.display = 'inline';
} else {
this._inProgressMigrationButton.container.display = 'inline';
this._inProgressWarningMigrationButton.container.display = 'none';
}
this._inProgressMigrationButton.count.value = inProgressMigrations.length.toString();
this._inProgressWarningMigrationButton.count.value = inProgressMigrations.length.toString();
const successfulMigration = migrationStatus.filter((value) => {
const status = value.migrationContext.properties.migrationStatus;
return status === 'Succeeded';
});
this._successfulMigrationButton.count.value = successfulMigration.length.toString();
const currentConnection = (await azdata.connection.getCurrentConnection());
const migrationDatabases = new Set(
this._migrationStatus.map((value) => {
migrationStatus.map((value) => {
return value.migrationContext.properties.sourceDatabaseName;
}));
const serverDatabases = await azdata.connection.listDatabases(currentConnection.connectionId);
@@ -260,26 +294,18 @@ export class DashboardWidget {
private createStatusCard(
cardIconPath: IconPath,
cardTitle: string,
cardDescription: string
): StatusCard {
const cardTitleText = this._view.modelBuilder.text().withProps({ value: cardTitle }).withProps({
CSSStyles: {
'height': '40px',
'height': '23px',
'margin-top': '15px',
'margin-bottom': '0px',
'width': '300px',
'font-size': '14px',
}
}).component();
const cardDescriptionText = this._view.modelBuilder.text().withProps({ value: cardDescription }).withProps({
CSSStyles: {
'height': '0px',
'margin-top': '0px',
'margin-bottom': '0px',
'width': '300px'
}
}).component();
const cardCount = this._view.modelBuilder.text().withProps({
value: '0',
CSSStyles: {
@@ -289,22 +315,123 @@ export class DashboardWidget {
}
}).component();
const flex = this._view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'width': '400px',
'height': '50px',
'margin-top': '10px',
'border': '1px solid',
}
}).component();
const img = this._view.modelBuilder.image().withProps({
iconPath: cardIconPath!.light,
iconHeight: 24,
iconWidth: 24,
width: 64,
height: 30,
CSSStyles: {
'margin-top': '10px'
}
}).component();
flex.addItem(img, {
flex: '0'
});
flex.addItem(cardTitleText, {
flex: '0',
CSSStyles: {
'width': '300px'
}
});
flex.addItem(cardCount, {
flex: '0'
});
const compositeButton = this._view.modelBuilder.divContainer().withItems([flex]).withProps({
ariaRole: 'button',
ariaLabel: 'show status',
clickable: true
}).component();
return {
container: compositeButton,
count: cardCount
};
}
private createStatusWithSubtextCard(
cardIconPath: IconPath,
cardTitle: string,
cardDescription: string
): StatusCard {
const cardTitleText = this._view.modelBuilder.text().withProps({ value: cardTitle }).withProps({
CSSStyles: {
'height': '23px',
'margin-top': '15px',
'margin-bottom': '0px',
'width': '300px',
'font-size': '14px',
}
}).component();
const cardDescriptionWarning = this._view.modelBuilder.image().withProps({
iconPath: IconPathHelper.warning,
iconWidth: 12,
iconHeight: 12,
width: 12,
height: 17
}).component();
const cardDescriptionText = this._view.modelBuilder.text().withProps({ value: cardDescription }).withProps({
CSSStyles: {
'height': '13px',
'margin-top': '0px',
'margin-bottom': '0px',
'width': '250px',
'font-height': '13px',
'margin': '0 0 0 4px'
}
}).component();
const subTextContainer = this._view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'justify-content': 'left',
}
}).component();
subTextContainer.addItem(cardDescriptionWarning, {
flex: '0 0 auto'
});
subTextContainer.addItem(cardDescriptionText, {
flex: '0 0 auto'
});
const cardCount = this._view.modelBuilder.text().withProps({
value: '0',
CSSStyles: {
'font-size': '28px',
'line-height': '28px',
'margin-top': '15px'
}
}).component();
const flexContainer = this._view.modelBuilder.flexContainer().withItems([
cardTitleText,
cardDescriptionText
subTextContainer
]).withLayout({
flexFlow: 'column'
}).withProps({
CSSStyles: {
'width': '300px',
'height': '50px'
}
}).component();
const flex = this._view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'width': '400px',
'height': '50px',
'height': '70px',
'margin-top': '10px',
'border': '1px solid'
}
@@ -312,10 +439,13 @@ export class DashboardWidget {
const img = this._view.modelBuilder.image().withProps({
iconPath: cardIconPath!.light,
iconHeight: 16,
iconWidth: 16,
iconHeight: 24,
iconWidth: 24,
width: 64,
height: 50
height: 30,
CSSStyles: {
'margin-top': '20px'
}
}).component();
flex.addItem(img, {
@@ -338,7 +468,10 @@ export class DashboardWidget {
}).component();
return {
container: compositeButton,
count: cardCount
count: cardCount,
textContainer: flexContainer,
warningContainer: subTextContainer,
warningText: cardDescriptionText
};
}
@@ -377,7 +510,7 @@ export class DashboardWidget {
const statusContainerTitle = view.modelBuilder.text().withProps({
value: loc.DATABASE_MIGRATION_STATUS,
CSSStyles: {
'font-size': '18px',
'font-size': '14px',
'font-weight': 'bold',
'margin': '0px',
'width': '290px'
@@ -393,7 +526,8 @@ export class DashboardWidget {
}).component();
this._viewAllMigrationsButton.onDidClick(async (e) => {
new MigrationStatusDialog(this._migrationStatus ? this._migrationStatus : await this.getMigrations(), MigrationCategory.ALL).initialize();
const migrationStatus = await this.getCurrentMigrations();
new MigrationStatusDialog(migrationStatus ? migrationStatus : await this.getMigrations(), MigrationCategory.ALL).initialize();
});
const refreshButton = view.modelBuilder.hyperlink().withProps({
@@ -444,24 +578,37 @@ export class DashboardWidget {
this._inProgressMigrationButton = this.createStatusCard(
IconPathHelper.inProgressMigration,
loc.MIGRATION_IN_PROGRESS,
''
loc.MIGRATION_IN_PROGRESS
);
this._inProgressMigrationButton.container.onDidClick((e) => {
const dialog = new MigrationStatusDialog(this._migrationStatus, MigrationCategory.ONGOING);
this._inProgressMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), MigrationCategory.ONGOING);
dialog.initialize();
});
this._migrationStatusCardsContainer.addItem(
this._inProgressMigrationButton.container
);
this._successfulMigrationButton = this.createStatusCard(
IconPathHelper.completedMigration,
loc.MIGRATION_COMPLETED,
this._inProgressWarningMigrationButton = this.createStatusWithSubtextCard(
IconPathHelper.inProgressMigration,
loc.MIGRATION_IN_PROGRESS,
''
);
this._successfulMigrationButton.container.onDidClick((e) => {
const dialog = new MigrationStatusDialog(this._migrationStatus, MigrationCategory.SUCCEEDED);
this._inProgressWarningMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), MigrationCategory.ONGOING);
dialog.initialize();
});
this._migrationStatusCardsContainer.addItem(
this._inProgressWarningMigrationButton.container
);
this._successfulMigrationButton = this.createStatusCard(
IconPathHelper.completedMigration,
loc.MIGRATION_COMPLETED
);
this._successfulMigrationButton.container.onDidClick(async (e) => {
const dialog = new MigrationStatusDialog(await this.getCurrentMigrations(), MigrationCategory.SUCCEEDED);
dialog.initialize();
});
this._migrationStatusCardsContainer.addItem(
@@ -470,8 +617,7 @@ export class DashboardWidget {
this._notStartedMigrationCard = this.createStatusCard(
IconPathHelper.notStartedMigration,
loc.MIGRATION_NOT_STARTED,
loc.CHOOSE_TO_MIGRATE_TO_AZURE_SQL
loc.MIGRATION_NOT_STARTED
);
this._notStartedMigrationCard.container.onDidClick((e) => {
vscode.window.showInformationMessage('Feature coming soon');
@@ -546,16 +692,6 @@ export class DashboardWidget {
});
const videosContainer = this.createVideoLinkContainers(view, [
{
iconPath: IconPathHelper.sqlMiVideoThumbnail,
description: loc.HELP_VIDEO1_TITLE,
link: 'https://www.youtube.com/watch?v=sE99cSoFOHs' //TODO: Fix Video link
},
{
iconPath: IconPathHelper.sqlVmVideoThumbnail,
description: loc.HELP_VIDEO2_TITLE,
link: 'https://www.youtube.com/watch?v=R4GCBoxADyQ' //TODO: Fix video link
}
]);
const viewPanelStyle = {
'padding': '10px 5px 10px 10px',

View File

@@ -681,7 +681,7 @@ export class SqlDatabaseTree {
this._assessmentTitle.value = this._selectedIssue.checkId;
this._descriptionText.value = this._selectedIssue.description;
this._moreInfo.url = this._selectedIssue.helpLink;
this._moreInfo.label = this._selectedIssue.helpLink;
this._moreInfo.label = this._selectedIssue.message;
this._impactedObjects = this._selectedIssue.impactedObjects;
this._recommendationText.value = this._selectedIssue.message; //TODO: Expose correct property for recommendation.
this._impactedObjectsTable.dataValues = this._selectedIssue.impactedObjects.map((object) => {

View File

@@ -218,7 +218,7 @@ export class CreateSqlMigrationServiceDialog {
if (!location) {
errors.push(constants.INVALID_REGION_ERROR);
}
if (!migrationServiceName || migrationServiceName.length === 0) {
if (!migrationServiceName || migrationServiceName.length < 3 || migrationServiceName.length > 63 || !/^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/.test(migrationServiceName)) {
errors.push(constants.INVALID_SERVICE_NAME_ERROR);
}
return errors.join(os.EOL);

View File

@@ -27,6 +27,7 @@ export class MigrationCutoverDialog {
private _serverName!: azdata.TextComponent;
private _serverVersion!: azdata.TextComponent;
private _sourceDatabase!: azdata.TextComponent;
private _targetDatabase!: azdata.TextComponent;
private _targetServer!: azdata.TextComponent;
private _targetVersion!: azdata.TextComponent;
private _migrationStatus!: azdata.TextComponent;
@@ -78,9 +79,11 @@ export class MigrationCutoverDialog {
}
});
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;
@@ -88,6 +91,11 @@ export class MigrationCutoverDialog {
flexFlow: 'column'
}).component();
flexTarget.addItem(targetDatabase.flexContainer, {
CSSStyles: {
'width': '230px'
}
});
flexTarget.addItem(targetServer.flexContainer, {
CSSStyles: {
'width': '230px'
@@ -198,7 +206,7 @@ export class MigrationCutoverDialog {
{
value: loc.ACTIVE_BACKUP_FILES,
width: 280,
type: azdata.ColumnType.text
type: azdata.ColumnType.text,
},
{
value: loc.TYPE,
@@ -226,7 +234,7 @@ export class MigrationCutoverDialog {
],
data: [],
width: '800px',
height: '600px',
height: '300px',
}).component();
const formBuilder = view.modelBuilder.formContainer().withFormItems(
@@ -307,7 +315,7 @@ export class MigrationCutoverDialog {
});
this._cancelButton = this._view.modelBuilder.button().withProps({
iconPath: IconPathHelper.discard,
iconPath: IconPathHelper.cancel,
iconHeight: '16px',
iconWidth: '16px',
label: loc.CANCEL_MIGRATION,
@@ -383,7 +391,10 @@ export class MigrationCutoverDialog {
}).component();
header.addItem(this._refreshLoader, {
flex: '0'
flex: '0',
CSSStyles: {
'margin-top': '15px'
}
});
return header;
@@ -397,19 +408,20 @@ export class MigrationCutoverDialog {
this._cancelButton.enabled = false;
await this._model.fetchStatus();
const errors = [];
errors.push(this._model.migrationOpStatus.error.message);
errors.push(this._model.migrationOpStatus.error?.message);
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 = {
text: errors.filter(e => e !== undefined).join(EOL),
level: this._model.migrationStatus.properties.migrationStatus === MigrationStatus.InProgress ? azdata.window.MessageLevel.Warning : azdata.window.MessageLevel.Error
level: (this._model.migrationStatus.properties.migrationStatus === MigrationStatus.InProgress || this._model.migrationStatus.properties.migrationStatus === 'Completing') ? azdata.window.MessageLevel.Warning : azdata.window.MessageLevel.Error
};
const sqlServerInfo = await azdata.connection.getServerInfo((await azdata.connection.getCurrentConnection()).connectionId);
const sqlServerName = this._model._migration.sourceConnectionProfile.serverName;
const sourceDatabaseName = this._model._migration.migrationContext.properties.sourceDatabaseName;
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
const targetDatabaseName = this._model._migration.migrationContext.name;
const targetServerName = this._model._migration.targetManagedInstance.name;
let targetServerVersion;
if (this._model.migrationStatus.id.includes('managedInstances')) {
@@ -452,6 +464,7 @@ export class MigrationCutoverDialog {
this._serverVersion.value = `${sqlServerVersion}
${sqlServerInfo.serverVersion}`;
this._targetDatabase.value = targetDatabaseName;
this._targetServer.value = targetServerName;
this._targetVersion.value = targetServerVersion;

View File

@@ -6,11 +6,10 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { IconPathHelper } from '../../constants/iconPathHelper';
import { MigrationContext } from '../../models/migrationLocalStorage';
import { MigrationContext, MigrationLocalStorage } from '../../models/migrationLocalStorage';
import { MigrationCutoverDialog } from '../migrationCutover/migrationCutoverDialog';
import { MigrationCategory, MigrationStatusDialogModel } from './migrationStatusDialogModel';
import * as loc from '../../constants/strings';
import { getDatabaseMigration } from '../../api/azure';
export class MigrationStatusDialog {
private _model: MigrationStatusDialogModel;
private _dialogObject!: azdata.window.Dialog;
@@ -81,7 +80,7 @@ export class MigrationStatusDialog {
dark: IconPathHelper.refresh.dark
},
iconHeight: '16px',
iconWidth: '16px',
iconWidth: '20px',
height: '30px',
label: 'Refresh',
}).component();
@@ -90,7 +89,11 @@ export class MigrationStatusDialog {
this.refreshTable();
});
const flexContainer = this._view.modelBuilder.flexContainer().component();
const flexContainer = this._view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'justify-content': 'left'
}
}).component();
flexContainer.addItem(this._searchBox, {
flex: '0'
@@ -109,7 +112,10 @@ export class MigrationStatusDialog {
}).component();
flexContainer.addItem(this._refreshLoader, {
flex: '0'
flex: '0 0 auto',
CSSStyles: {
'margin-left': '20px'
}
});
return flexContainer;
@@ -128,7 +134,7 @@ export class MigrationStatusDialog {
return new Date(m1.migrationContext.properties.startedOn) > new Date(m2.migrationContext.properties.startedOn) ? -1 : 1;
});
migrations.forEach((migration) => {
migrations.forEach((migration, index) => {
const migrationRow: azdata.DeclarativeTableCellValue[] = [];
const databaseHyperLink = this._view.modelBuilder.hyperlink().withProps({
@@ -142,10 +148,6 @@ export class MigrationStatusDialog {
value: databaseHyperLink,
});
migrationRow.push({
value: migration.migrationContext.properties.migrationStatus ? migration.migrationContext.properties.migrationStatus : migration.migrationContext.properties.provisioningState
});
const targetMigrationIcon = this._view.modelBuilder.image().withProps({
iconPath: (migration.targetManagedInstance.type === 'microsoft.sql/managedinstances') ? IconPathHelper.sqlMiLogo : IconPathHelper.sqlVmLogo,
iconWidth: '16px',
@@ -163,7 +165,7 @@ export class MigrationStatusDialog {
const sqlMigrationContainer = this._view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'justify-content': 'center'
'justify-content': 'left'
}
}).component();
sqlMigrationContainer.addItem(targetMigrationIcon, {
@@ -186,6 +188,27 @@ export class MigrationStatusDialog {
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)
});
migrationRow.push({
value: (migration.migrationContext.properties.startedOn) ? new Date(migration.migrationContext.properties.startedOn).toLocaleString() : '---'
});
@@ -202,21 +225,28 @@ export class MigrationStatusDialog {
}
}
private refreshTable(): void {
private async refreshTable(): Promise<void> {
this._refreshLoader.loading = true;
this._model._migrations.forEach(async (migration) => {
migration.migrationContext = await getDatabaseMigration(
migration.azureAccount,
migration.subscription,
migration.targetManagedInstance.location,
migration.migrationContext.id
);
});
const currentConnection = await azdata.connection.getCurrentConnection();
this._model._migrations = await MigrationLocalStorage.getMigrationsBySourceConnections(currentConnection, true);
this.populateMigrationTable();
this._refreshLoader.loading = false;
}
private createStatusTable(): azdata.DeclarativeTableComponent {
const rowCssStyle: azdata.CssStyles = {
'border': 'none',
'text-align': 'left',
'border-bottom': '1px solid'
};
const headerCssStyles: azdata.CssStyles = {
'border': 'none',
'text-align': 'left',
'border-bottom': '1px solid',
'font-weight': 'bold'
};
this._statusTable = this._view.modelBuilder.declarativeTable().withProps({
columns: [
{
@@ -224,54 +254,48 @@ export class MigrationStatusDialog {
valueType: azdata.DeclarativeDataType.component,
width: '100px',
isReadOnly: true,
rowCssStyles: {
'text-align': 'center'
}
},
{
displayName: loc.MIGRATION_STATUS,
valueType: azdata.DeclarativeDataType.string,
width: '150px',
isReadOnly: true,
rowCssStyles: {
'text-align': 'center'
}
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.TARGET_AZURE_SQL_INSTANCE_NAME,
valueType: azdata.DeclarativeDataType.component,
width: '300px',
width: '170px',
isReadOnly: true,
rowCssStyles: {
'text-align': 'center'
}
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.MIGRATION_MODE,
valueType: azdata.DeclarativeDataType.string,
width: '100px',
isReadOnly: true,
rowCssStyles: {
'text-align': 'center'
}
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.MIGRATION_STATUS,
valueType: azdata.DeclarativeDataType.string,
width: '150px',
isReadOnly: true,
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.START_TIME,
valueType: azdata.DeclarativeDataType.string,
width: '150px',
width: '120px',
isReadOnly: true,
rowCssStyles: {
'text-align': 'center'
}
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
},
{
displayName: loc.FINISH_TIME,
valueType: azdata.DeclarativeDataType.string,
width: '150px',
width: '120px',
isReadOnly: true,
rowCssStyles: {
'text-align': 'center'
}
rowCssStyles: rowCssStyle,
headerCssStyles: headerCssStyles
}
]
}).component();

View File

@@ -7,7 +7,6 @@ import * as azdata from 'azdata';
import { MigrationContext } from '../../models/migrationLocalStorage';
export class MigrationStatusDialogModel {
public statusDropdownValues: azdata.CategoryValue[] = [
{
displayName: 'Status: All',

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { azureResource } from 'azureResource';
import { DatabaseMigration, SqlMigrationService, SqlManagedInstance, getDatabaseMigration } from '../api/azure';
import { DatabaseMigration, SqlMigrationService, SqlManagedInstance, getMigrationStatus, AzureAsyncOperationResource, getMigrationAsyncOperationDetails } from '../api/azure';
import * as azdata from 'azdata';
@@ -27,12 +27,18 @@ export class MigrationLocalStorage {
if (migration.sourceConnectionProfile.serverName === connectionProfile.serverName) {
if (refreshStatus) {
try {
migration.migrationContext = await getDatabaseMigration(
migration.migrationContext = await getMigrationStatus(
migration.azureAccount,
migration.subscription,
migration.targetManagedInstance.location,
migration.migrationContext.id
migration.migrationContext
);
if (migration.asyncUrl) {
migration.asyncOperationResult = await getMigrationAsyncOperationDetails(
migration.azureAccount,
migration.subscription,
migration.asyncUrl
);
}
}
catch (e) {
// Keeping only valid migrations in cache. Clearing all the migrations which return ResourceDoesNotExit error.
@@ -89,5 +95,6 @@ export interface MigrationContext {
azureAccount: azdata.Account,
subscription: azureResource.AzureResourceSubscription,
controller: SqlMigrationService,
asyncUrl: string
asyncUrl: string,
asyncOperationResult?: AzureAsyncOperationResource
}

View File

@@ -44,7 +44,7 @@ export enum MigrationSourceAuthenticationType {
Sql = 'SqlAuthentication'
}
export enum MigrationCutover {
export enum MigrationMode {
ONLINE,
OFFLINE
}
@@ -62,7 +62,7 @@ export interface NetworkShare {
}
export interface DatabaseBackupModel {
migrationCutover: MigrationCutover;
migrationMode: MigrationMode;
networkContainerType: NetworkContainerType;
networkShareLocation: string;
windowsUser: string;

View File

@@ -103,6 +103,7 @@ export class AccountsSelectionPage extends MigrationWizardPage {
this.wizard.message = {
text: ''
};
this._azureAccountsDropdown.validate();
});
const flexContainer = view.modelBuilder.flexContainer()

View File

@@ -271,7 +271,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
}).withValidation((component) => {
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
if (component.value) {
if (!/(?<=\\\\)[^\\]*/.test(component.value)) {
if (!/^[\\\/]{2,}[^\\\/]+[\\\/]+[^\\\/]+/.test(component.value)) {
return false;
}
}
@@ -304,7 +304,7 @@ export class DatabaseBackupPage extends MigrationWizardPage {
.withValidation((component) => {
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
if (component.value) {
if (!/(?<=\\).*$/.test(component.value)) {
if (!/^[A-Za-z0-9\\\._-]{7,}$/.test(component.value)) {
return false;
}
}
@@ -512,10 +512,14 @@ export class DatabaseBackupPage extends MigrationWizardPage {
c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(this.migrationStateModel._targetServerInstance.name);
return false;
}
if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) {
c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR;
return false;
}
return true;
}).component();
targetNameNetworkInputBox.onTextChanged((value) => {
this.migrationStateModel._targetDatabaseNames[index] = value;
this.migrationStateModel._targetDatabaseNames[index] = value.trim();
});
this._targetDatabaseNames.push(targetNameNetworkInputBox);

View File

@@ -6,7 +6,7 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationCutover, MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import { MigrationMode, MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings';
export class MigrationModePage extends MigrationWizardPage {
@@ -57,11 +57,11 @@ export class MigrationModePage extends MigrationWizardPage {
}
}).component();
this.migrationStateModel._databaseBackup.migrationCutover = MigrationCutover.ONLINE;
this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.ONLINE;
onlineButton.onDidChangeCheckedState((e) => {
if (e) {
this.migrationStateModel._databaseBackup.migrationCutover = MigrationCutover.ONLINE;
this.migrationStateModel._databaseBackup.migrationMode = MigrationMode.ONLINE;
}
});

View File

@@ -23,6 +23,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
private _view!: azdata.ModelView;
private _igComponent!: azdata.TextComponent;
private _assessmentStatusIcon!: azdata.ImageComponent;
private _detailsComponent!: azdata.TextComponent;
private _chooseTargetComponent!: azdata.DivContainer;
private _azureSubscriptionText!: azdata.TextComponent;
@@ -63,7 +64,31 @@ export class SKURecommendationPage extends MigrationWizardPage {
protected async registerContent(view: azdata.ModelView) {
this._view = view;
this._igComponent = this.createStatusComponent(view); // The first component giving basic information
this._assessmentStatusIcon = this._view.modelBuilder.image().withProps({
iconPath: IconPathHelper.completedMigration,
iconHeight: 17,
iconWidth: 17,
width: 17,
height: 20
}).component();
const igContainer = this._view.modelBuilder.flexContainer().component();
igContainer.addItem(this._assessmentStatusIcon, {
flex: '0 0 auto'
});
igContainer.addItem(this._igComponent, {
flex: '0 0 auto'
});
this._detailsComponent = this.createDetailsComponent(view); // The details of what can be moved
const statusContainer = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).withItems(
[
igContainer,
this._detailsComponent
]
).component();
this._chooseTargetComponent = await this.createChooseTargetComponent(view);
this._azureSubscriptionText = this.createAzureSubscriptionText(view);
@@ -164,11 +189,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
[
{
title: '',
component: this._igComponent
},
{
title: '',
component: this._detailsComponent
component: statusContainer
},
{
title: constants.SKU_RECOMMENDATION_CHOOSE_A_TARGET,
@@ -212,14 +233,20 @@ export class SKURecommendationPage extends MigrationWizardPage {
private createStatusComponent(view: azdata.ModelView): azdata.TextComponent {
const component = view.modelBuilder.text().withProps({
CSSStyles: {
'font-size': '14px'
'font-size': '14px',
'margin': '0 0 0 8px',
'line-height': '20px'
}
}).component();
return component;
}
private createDetailsComponent(view: azdata.ModelView): azdata.TextComponent {
const component = view.modelBuilder.text().component();
const component = view.modelBuilder.text().withProps({
CSSStyles: {
'font-size': '13px'
}
}).component();
return component;
}

View File

@@ -5,7 +5,7 @@
import * as azdata from 'azdata';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
import { MigrationMode, MigrationStateModel, NetworkContainerType, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../constants/strings';
import { createHeadingTextComponent, createInformationRow } from './wizardController';
@@ -36,19 +36,32 @@ export class SummaryPage extends MigrationWizardPage {
public async onPageEnter(): Promise<void> {
this._flexContainer.addItems(
[
createHeadingTextComponent(this._view, constants.AZURE_ACCOUNT_LINKED),
createHeadingTextComponent(this._view, this.migrationStateModel._azureAccount.displayInfo.displayName),
createHeadingTextComponent(this._view, constants.MIGRATION_TARGET),
createInformationRow(this._view, constants.TYPE, (this.migrationStateModel._targetServerInstance.type === 'microsoft.compute/virtualmachines') ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE),
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name),
createInformationRow(this._view, constants.SUMMARY_MI_TYPE, this.migrationStateModel._targetServerInstance.name),
createInformationRow(this._view, constants.SUMMARY_DATABASE_COUNT_LABEL, this.migrationStateModel._migrationDbs.length.toString()),
createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_PAGE_TITLE),
this.createNetworkContainerRows(),
createHeadingTextComponent(this._view, constants.IR_PAGE_TITLE),
createInformationRow(this._view, constants.IR_PAGE_TITLE, this.migrationStateModel._sqlMigrationService?.name!),
createInformationRow(this._view, constants.SUMMARY_IR_NODE, this.migrationStateModel._nodeNames.join(', ')),
createHeadingTextComponent(this._view, constants.ACCOUNTS_SELECTION_PAGE_TITLE),
createInformationRow(this._view, constants.ACCOUNTS_SELECTION_PAGE_TITLE, this.migrationStateModel._azureAccount.displayInfo.displayName),
createHeadingTextComponent(this._view, constants.SOURCE_DATABASES),
createInformationRow(this._view, constants.SUMMARY_DATABASE_COUNT_LABEL, this.migrationStateModel._migrationDbs.length.toString()),
createHeadingTextComponent(this._view, constants.SKU_RECOMMENDATION_PAGE_TITLE),
createInformationRow(this._view, constants.SKU_RECOMMENDATION_PAGE_TITLE, (this.migrationStateModel._targetServerInstance.type === 'microsoft.compute/virtualmachines') ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE),
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name),
createInformationRow(this._view, constants.LOCATION, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.location)),
createInformationRow(this._view, constants.RESOURCE_GROUP, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.resourceGroup!)),
createInformationRow(this._view, (this.migrationStateModel._targetServerInstance.type === 'microsoft.compute/virtualmachines') ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE, await this.migrationStateModel.getLocationDisplayName(this.migrationStateModel._targetServerInstance.name!)),
createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_MIGRATION_MODE_LABEL),
createInformationRow(this._view, constants.MODE, this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.ONLINE ? constants.DATABASE_BACKUP_MIGRATION_MODE_ONLINE_LABEL : constants.DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL),
createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_PAGE_TITLE),
await this.createNetworkContainerRows(),
createHeadingTextComponent(this._view, constants.IR_PAGE_TITLE),
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name),
createInformationRow(this._view, constants.LOCATION, this.migrationStateModel._sqlMigrationService.location),
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._sqlMigrationService.properties.resourceGroup),
createInformationRow(this._view, constants.IR_PAGE_TITLE, this.migrationStateModel._targetSubscription.name),
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._sqlMigrationService.name),
createInformationRow(this._view, constants.SHIR, this.migrationStateModel._nodeNames[0]),
]
);
}
@@ -63,7 +76,7 @@ export class SummaryPage extends MigrationWizardPage {
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
}
private createNetworkContainerRows(): azdata.FlexContainer {
private async createNetworkContainerRows(): Promise<azdata.FlexContainer> {
const flexContainer = this._view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).component();
@@ -71,11 +84,14 @@ export class SummaryPage extends MigrationWizardPage {
case NetworkContainerType.NETWORK_SHARE:
flexContainer.addItems(
[
createInformationRow(this._view, constants.TYPE, constants.NETWORK_SHARE),
createInformationRow(this._view, constants.DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL, this.migrationStateModel._databaseBackup.networkShareLocation),
createInformationRow(this._view, constants.BACKUP_LOCATION, constants.NETWORK_SHARE),
createInformationRow(this._view, constants.NETWORK_SHARE, this.migrationStateModel._databaseBackup.networkShareLocation),
createInformationRow(this._view, constants.USER_ACCOUNT, this.migrationStateModel._databaseBackup.windowsUser),
createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name),
createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE, this.migrationStateModel._databaseBackup.storageAccount.name),
createHeadingTextComponent(this._view, constants.AZURE_STORAGE_ACCOUNT_TO_UPLOAD_BACKUPS),
createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name),
createInformationRow(this._view, constants.LOCATION, this.migrationStateModel._databaseBackup.storageAccount.location),
createInformationRow(this._view, constants.RESOURCE_GROUP, this.migrationStateModel._databaseBackup.storageAccount.resourceGroup!),
createInformationRow(this._view, constants.STORAGE_ACCOUNT, this.migrationStateModel._databaseBackup.storageAccount.name!),
createHeadingTextComponent(this._view, 'Target Databases:')
]
);

View File

@@ -110,7 +110,7 @@ export function createHeadingTextComponent(view: azdata.ModelView, value: string
const component = createTextCompononent(view, value);
component.updateCssStyles({
'font-size': '13px',
'font-weight': 'bold'
'font-weight': 'bold',
});
return component;
}