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

@@ -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',