[SQL Migration] Misc UI improvements (#20723)

* Add TrustServerCertificate to request body

* Preselect all dbs for migration

* Make disabled checkbox not auto selected

* Clarify assessment results in card

* Fix incorrect number of dbs ready for migration without issues to MI

* Fix SQL DB assessment results incorrectly greying out dbs with MI blocking issues

* Revert "Clarify assessment results in card"

This reverts commit e8b83f3c19a20ba29133aaa68c4644b04d4154fe.

* Revert "Fix incorrect number of dbs ready for migration without issues to MI"

This reverts commit d4e10875d132dd218d95be91ae7d46672e247706.

* Revert "Fix SQL DB assessment results incorrectly greying out dbs with MI blocking issues"

This reverts commit e2a7dcd7352d1c215052a2f6f3f6130fc710eff8.

* Add new fields

* Fix null reference exception in SKU rec with save and close

* Remove unused files

* Warnings for in progress migrations in list view

* Fix unscrollable assessment results

* Fix updating SKU parameters before recommendations are ready

* Remove checksum info box

* Address PR feedback
This commit is contained in:
Raymond Truong
2022-10-28 10:59:53 -07:00
committed by GitHub
parent 968f4c7aa4
commit a734c9d244
12 changed files with 40 additions and 149 deletions

View File

@@ -657,6 +657,7 @@ export interface DatabaseMigrationProperties {
provisioningError: string;
migrationStatus: 'InProgress' | 'Failed' | 'Succeeded' | 'Creating' | 'Completing' | 'Canceling';
migrationStatusDetails?: MigrationStatusDetails;
migrationStatusWarnings?: MigrationStatusWarnings;
startedOn: string;
endedOn: string;
sourceDatabaseName: string;
@@ -689,6 +690,12 @@ export interface MigrationStatusDetails {
sqlDataCopyErrors: string[];
}
export interface MigrationStatusWarnings {
restoreBlockingReason?: string;
completeRestoreErrorMessage?: string;
fileUploadBlockingErrorCount?: number;
}
export interface CopyProgressDetail {
tableName: string;
status: 'PreparingForCopy' | 'Copying' | 'CopyFinished' | 'RebuildingIndexes' | 'Succeeded' | 'Failed' | 'Canceled',

View File

@@ -279,16 +279,15 @@ export function getMigrationStatusWithErrors(migration: azure.DatabaseMigration)
warningCount += properties.migrationFailureError?.message?.length > 0 ? 1 : 0;
// file upload blocking errors
warningCount += properties.migrationStatusDetails?.fileUploadBlockingErrors?.length ?? 0;
warningCount += properties.migrationStatusWarnings?.fileUploadBlockingErrorCount ?? 0;
// restore blocking reason
warningCount += properties.migrationStatusDetails?.restoreBlockingReason ? 1 : 0;
warningCount += (properties.migrationStatusWarnings?.restoreBlockingReason ?? '').length > 0 ? 1 : 0;
// sql data copy errors
warningCount += properties.migrationStatusDetails?.sqlDataCopyErrors?.length ?? 0;
// complete restore error message
warningCount += (properties.migrationStatusWarnings?.completeRestoreErrorMessage ?? '').length > 0 ? 1 : 0;
return constants.STATUS_VALUE(migrationStatus, warningCount)
+ (constants.STATUS_WARNING_COUNT(migrationStatus, warningCount) ?? '');
return constants.STATUS_VALUE(migrationStatus) + (constants.STATUS_WARNING_COUNT(migrationStatus, warningCount) ?? '');
}
export function getPipelineStatusImage(status: string | undefined): IconPath {

View File

@@ -417,7 +417,6 @@ export const SELECT_SERVICE_PLACEHOLDER = localize('sql.migration.select.service
// database backup page
export const DATA_SOURCE_CONFIGURATION_PAGE_TITLE = localize('sql.migration.data.source.configuration.page.title', "Data source configuration");
export const DATABASE_BACKUP_PAGE_DESCRIPTION = localize('sql.migration.database.page.description', "Select the location of the database backups to use during migration.");
export const DATABASE_BACKUP_CHECKSUM_INFO_TEXT = localize('sql.migration.database.checksum.info.text', "Ensure that your backups were taken with the WITH CHECKSUM option.");
export const DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL = localize('sql.migration.nc.network.share.radio.label', "My database backups are on a network share");
export const DATABASE_BACKUP_NC_BLOB_STORAGE_RADIO_LABEL = localize('sql.migration.nc.blob.storage.radio.label', "My database backups are in an Azure Storage Blob Container");
export const DATABASE_BACKUP_NETWORK_SHARE_HEADER_TEXT = localize('sql.migration.network.share.header.text', "Network share details");
@@ -807,11 +806,8 @@ export const MIGRATION_MODE = localize('sql.migration.cutover.type', "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_VALUE(status: string, count: number): string {
if (count > 0) {
return localize('sql.migration.status.error.count.some', "{0} (", StatusLookup[status] ?? status);
}
return localize('sql.migration.status.error.count.none', "{0}", StatusLookup[status] ?? status);
export function STATUS_VALUE(status: string): string {
return localize('sql.migration.status.value', "{0}", StatusLookup[status] ?? status);
}
export const MIGRATION_ERROR_DETAILS_TITLE = localize('sql.migration.error.details.title', "Migration error details");
@@ -869,18 +865,18 @@ export function STATUS_WARNING_COUNT(status: string, count: number): string | un
case 0:
return undefined;
case 1:
return localize('sql.migration.status.warning.count.single', "{0} Warning)", count);
return localize('sql.migration.status.warning.count.single', " ({0} warning)", count);
default:
return localize('sql.migration.status.warning.count.multiple', "{0} Warnings)", count);
return localize('sql.migration.status.warning.count.multiple', " ({0} warnings)", count);
}
} else {
switch (count) {
case 0:
return undefined;
case 1:
return localize('sql.migration.status.error.count.single', "{0} Error)", count);
return localize('sql.migration.status.error.count.single', " ({0} error)", count);
default:
return localize('sql.migration.status.error.count.multiple', "{0} Errors)", count);
return localize('sql.migration.status.error.count.multiple', " ({0} errors)", count);
}
}
}

View File

@@ -9,7 +9,7 @@ import { IconPathHelper } from '../constants/iconPathHelper';
import { getCurrentMigrations, getSelectedServiceStatus } from '../models/migrationLocalStorage';
import * as loc from '../constants/strings';
import { filterMigrations, getMigrationDuration, getMigrationStatusImage, getMigrationStatusWithErrors, getMigrationTime, MenuCommands } from '../api/utils';
import { getMigrationTargetType, getMigrationMode, getMigrationModeEnum, canCancelMigration, canCutoverMigration, getMigrationStatus } from '../constants/helper';
import { getMigrationTargetType, getMigrationMode, getMigrationModeEnum, canCancelMigration, canCutoverMigration } from '../constants/helper';
import { DatabaseMigration, getResourceName } from '../api/azure';
import { logError, TelemetryViews } from '../telemtery';
import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog';
@@ -468,7 +468,7 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
headerCssClass: headerCssStyles,
name: loc.SRC_DATABASE,
value: 'sourceDatabase',
width: 190,
width: 170,
type: azdata.ColumnType.hyperlink,
},
{
@@ -476,7 +476,7 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
headerCssClass: headerCssStyles,
name: loc.SRC_SERVER,
value: 'sourceServer',
width: 190,
width: 170,
type: azdata.ColumnType.text,
},
<azdata.HyperlinkColumn>{
@@ -484,7 +484,7 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
headerCssClass: headerCssStyles,
name: loc.STATUS_COLUMN,
value: 'status',
width: 120,
width: 160,
type: azdata.ColumnType.hyperlink,
},
{
@@ -559,9 +559,9 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
const buttonState = <azdata.ICellActionEventArgs>rowState;
const migration = this._filteredMigrations[rowState.row];
switch (buttonState?.column) {
// "Migration status" column
case 2:
const status = getMigrationStatus(migration);
const statusMessage = loc.DATABASE_MIGRATION_STATUS_LABEL(status);
const statusMessage = loc.DATABASE_MIGRATION_STATUS_LABEL(getMigrationStatusWithErrors(migration));
const errors = this.getMigrationErrors(migration!);
this.showDialogMessage(
@@ -569,6 +569,7 @@ export class MigrationsListTab extends TabBase<MigrationsListTab> {
statusMessage,
errors);
break;
// "Source database" column
case 0:
await this._openMigrationDetails(migration);
break;

View File

@@ -45,14 +45,9 @@ export class AssessmentResultsDialog {
return new Promise<void>((resolve, reject) => {
dialog.registerContent(async (view) => {
try {
/**
* When using 100% height in the dialog, the container extends beyond the screen.
* This causes a vertical scrollbar to appear. To fix that, 33px needs to be
* subtracted from 100%.
*/
const flex = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row',
height: 'calc( 100% - 33px )',
height: '100%',
width: '100%'
}).component();
flex.addItem(await this._tree.createRootContainer(dialog, view), { flex: '1 1 auto' });

View File

@@ -1,75 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 * as loc from '../../constants/strings';
export class SqlAssessmentResult {
async createComponent(view: azdata.ModelView): Promise<azdata.Component> {
const title = this.createTitleComponent(view);
const impact = this.createImpactComponent(view);
const recommendation = this.createRecommendationComponent(view);
const moreInfo = this.createMoreInfoComponent(view);
const impactedObjects = this.createImpactedObjectsComponent(view);
return view.modelBuilder.divContainer().withItems([title, impact, recommendation, moreInfo, impactedObjects]).component();
}
private createTitleComponent(view: azdata.ModelView): azdata.TextComponent {
const title = view.modelBuilder.text().withProps({
value: 'Azure SQL Managed Instance does not support multiple log files', // TODO: Get this string from the actual results
});
return title.component();
}
private createImpactComponent(view: azdata.ModelView): azdata.TextComponent {
const impact = view.modelBuilder.text().withProps({
title: loc.IMPACT,
value: 'SQL Server allows a database to log transactions across multiple files. This databases uses multiple log files' // TODO: Get this string from the actual results
});
return impact.component();
}
private createRecommendationComponent(view: azdata.ModelView): azdata.TextComponent {
const recommendation = view.modelBuilder.text().withProps({
title: loc.RECOMMENDATION,
value: 'Azure SQL Managed Instance allows a single log file per database only. Please delete all but one of the log files before migrating this database.' // TODO: Get this string from the actual results
});
return recommendation.component();
}
private createMoreInfoComponent(view: azdata.ModelView): azdata.TextComponent {
const moreInfo = view.modelBuilder.text().withProps({
title: loc.MORE_INFO,
value: '{0}',
links: [
{
text: 'Managed instance T-SQL differences - Azure SQL Database', // TODO: Get this string from the actual results
url: 'https://microsoft.com' // TODO: Get this string from the actual results
}
]
});
return moreInfo.component();
}
private createImpactedObjectsComponent(view: azdata.ModelView): azdata.TableComponent {
const impactedObjects = view.modelBuilder.table().withProps({
title: 'Impacted Objects',
columns: [
loc.TYPE,
loc.NAME
],
data: [
['Database', 'AAAW2008P7'] // TODO: Get this string from the actual results
]
});
return impactedObjects.component();
}
}

View File

@@ -1,26 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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';
export class SqlAssessmentResultList {
async createComponent(view: azdata.ModelView): Promise<azdata.Component> {
return view.modelBuilder.divContainer().withItems([
this.createListComponent(view)
]
).component();
}
private createListComponent(view: azdata.ModelView): azdata.ListBoxComponent {
const list = view.modelBuilder.listBox().withProps({
values: [
'Filestream not supported in Azure SQL Managed Instance',
'Number of Log files per database something something',
]
});
return list.component();
}
}

View File

@@ -835,14 +835,11 @@ export class SqlDatabaseTree {
let instanceTableValues: azdata.DeclarativeTableCellValue[][] = [];
this._databaseTableValues = [];
this._dbNames = this._model._databasesForAssessment;
const selectedDbs = (this._targetType === MigrationTargetType.SQLVM)
? this._model._vmDbs
: (this._targetType === MigrationTargetType.SQLMI)
? this._model._miDbs
: this._model._sqldbDbs;
this._serverName = (await this._model.getSourceConnectionProfile()).serverName;
// pre-select the entire list
const selectedDbs = this._dbNames.filter(db => this._model._databasesForAssessment.includes(db));
if (this._targetType === MigrationTargetType.SQLVM || !this._model._assessmentResults) {
instanceTableValues = [[
{
@@ -893,7 +890,7 @@ export class SqlDatabaseTree {
}
this._databaseTableValues.push([
{
value: selectedDbs.includes(db.name),
value: selectedDbs.includes(db.name) && selectable,
style: styleLeft,
enabled: selectable
},

View File

@@ -301,6 +301,9 @@ export class SkuEditParametersDialog {
this.migrationStateModel._skuEnablePreview = this._enablePreviewValue;
this.migrationStateModel._skuEnableElastic = this._enableElasticRecommendation;
await this.skuRecommendationPage.refreshSkuParameters();
if (this.skuRecommendationPage.hasRecommendations()) {
await this.skuRecommendationPage.refreshAzureRecommendation();
}
}
public get isOpen(): boolean {

View File

@@ -889,7 +889,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
dataSource: currentConnection?.serverName!,
authentication: this._authenticationType,
userName: this._sqlServerUsername,
password: this._sqlServerPassword
password: this._sqlServerPassword,
trustServerCertificate: currentConnection?.options.trustServerCertificate ?? false
},
scope: this._targetServerInstance.id,
offlineConfiguration: {
@@ -969,7 +970,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
userName: this._sqlServerUsername,
password: this._sqlServerPassword,
encryptConnection: true,
trustServerCertificate: false,
trustServerCertificate: currentConnection?.options.trustServerCertificate ?? false,
};
requestBody.properties.targetSqlConnection = {
dataSource: sqlDbTarget.properties.fullyQualifiedDomainName,

View File

@@ -117,14 +117,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
CSSStyles: { ...styles.BODY_CSS }
}).component();
const backupChecksumInfoBox = this._view.modelBuilder.infoBox()
.withProps({
text: constants.DATABASE_BACKUP_CHECKSUM_INFO_TEXT,
style: 'information',
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: { ...styles.BODY_CSS }
}).component();
this._networkShareButton = this._view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
@@ -158,7 +150,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
const flexContainer = this._view.modelBuilder.flexContainer()
.withItems([
selectLocationText,
backupChecksumInfoBox,
this._networkShareButton,
this._blobContainerButton])
.withLayout({ flexFlow: 'column' })

View File

@@ -486,6 +486,9 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
let shouldGetSkuRecommendations = false;
// recommendations were already generated, then the user went back and changed the list of databases
// so recommendations should be re-generated
if (this.hasRecommendations() && this.migrationStateModel.hasRecommendedDatabaseListChanged()) {
shouldGetSkuRecommendations = true;
}
@@ -1112,7 +1115,6 @@ export class SKURecommendationPage extends MigrationWizardPage {
this._skuTargetPercentileText.value = constants.PERCENTAGE(this.migrationStateModel._skuTargetPercentile);
this._skuEnablePreviewSkuText.value = this.migrationStateModel._skuEnablePreview ? constants.YES : constants.NO;
this._skuEnableElasticRecommendationsText.value = this.migrationStateModel._skuEnableElastic ? constants.YES : constants.NO;
await this.refreshAzureRecommendation();
}
public async refreshAzureRecommendation(): Promise<void> {
@@ -1214,7 +1216,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
await this.refreshCardText(false);
}
private hasRecommendations(): boolean {
public hasRecommendations(): boolean {
return this.migrationStateModel._skuRecommendationResults?.recommendations
&& !this.migrationStateModel._skuRecommendationResults?.recommendationError
? true