diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index c24cf69994..5617e1f60c 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -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', diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts index 86668dad7b..bdcf26f890 100644 --- a/extensions/sql-migration/src/api/utils.ts +++ b/extensions/sql-migration/src/api/utils.ts @@ -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 { diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index a6b93b59f4..d9e01dea80 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -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); } } } diff --git a/extensions/sql-migration/src/dashboard/migrationsListTab.ts b/extensions/sql-migration/src/dashboard/migrationsListTab.ts index d05d720096..909d1ce134 100644 --- a/extensions/sql-migration/src/dashboard/migrationsListTab.ts +++ b/extensions/sql-migration/src/dashboard/migrationsListTab.ts @@ -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 { 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 { headerCssClass: headerCssStyles, name: loc.SRC_SERVER, value: 'sourceServer', - width: 190, + width: 170, type: azdata.ColumnType.text, }, { @@ -484,7 +484,7 @@ export class MigrationsListTab extends TabBase { 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 { const buttonState = 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 { statusMessage, errors); break; + // "Source database" column case 0: await this._openMigrationDetails(migration); break; diff --git a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts index b84c7ea40c..c4fa3be8c7 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts @@ -45,14 +45,9 @@ export class AssessmentResultsDialog { return new Promise((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' }); diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResult.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResult.ts deleted file mode 100644 index 7c40b7997f..0000000000 --- a/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResult.ts +++ /dev/null @@ -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 { - - 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(); - } -} diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResultsList.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResultsList.ts deleted file mode 100644 index b6b97cca23..0000000000 --- a/extensions/sql-migration/src/dialog/assessmentResults/sqlAssessmentResultsList.ts +++ /dev/null @@ -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 { - - 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(); - } -} diff --git a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts index be77079870..3adab7d043 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/sqlDatabasesTree.ts @@ -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 }, diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/skuEditParametersDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/skuEditParametersDialog.ts index ecf98b1db9..7840040afa 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/skuEditParametersDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/skuEditParametersDialog.ts @@ -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 { diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 0b98a2fb17..38e84145a8 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -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, diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index 6aeabbdd8d..ba5db618bc 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -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' }) diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index 999e7dd155..2ea89d9a36 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -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 { @@ -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