mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-14 03:58:33 -05:00
[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:
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user