From bfcbd60d24afe56d9697bdfbe9e9c94d290d0a2c Mon Sep 17 00:00:00 2001 From: Raymond Truong Date: Fri, 26 Aug 2022 14:06:33 -0700 Subject: [PATCH] [SQL Migration] SKU recommendation improvements + SQL DB integration bug fixes (#20174) * WIP - show error message for failed SKU recommendation * WIP - run query to get correct instance name * WIP - integrate elastic model recommendation * Remove private endpoint restriction text * Add feature switch for elastic recommendation * Clean up * Clean up * Misc UI fixes * Update package.json with updated azdata dependency * Remove unused lines * Fix broken next button * Vbump extension to 1.0.6 * Update SQL DB card to show number of recommendations for correct model --- extensions/mssql/src/mssql.d.ts | 10 + extensions/sql-migration/package.json | 4 +- .../sql-migration/src/constants/strings.ts | 8 +- .../assessmentResultsDialog.ts | 24 +-- .../getAzureRecommendationDialog.ts | 2 - .../skuEditParametersDialog.ts | 72 ++++++- .../skuRecommendationResultsDialog.ts | 82 +++++--- .../sql-migration/src/models/stateMachine.ts | 176 ++++++++++-------- extensions/sql-migration/src/telemtery.ts | 2 +- .../src/wizard/databaseBackupPage.ts | 18 -- .../src/wizard/skuRecommendationPage.ts | 81 +++++--- .../src/wizard/targetSelectionPage.ts | 2 - 12 files changed, 300 insertions(+), 181 deletions(-) diff --git a/extensions/mssql/src/mssql.d.ts b/extensions/mssql/src/mssql.d.ts index 53b2e1f17b..53ac5fadb7 100644 --- a/extensions/mssql/src/mssql.d.ts +++ b/extensions/mssql/src/mssql.d.ts @@ -538,10 +538,20 @@ declare module 'mssql' { export interface SkuRecommendationResult { sqlDbRecommendationResults: PaaSSkuRecommendationResultItem[]; + sqlDbRecommendationDurationInMs: number; sqlMiRecommendationResults: PaaSSkuRecommendationResultItem[]; + sqlMiRecommendationDurationInMs: number; sqlVmRecommendationResults: IaaSSkuRecommendationResultItem[]; + sqlVmRecommendationDurationInMs: number; + elasticSqlDbRecommendationResults: PaaSSkuRecommendationResultItem[]; + elasticSqlDbRecommendationDurationInMs: number; + elasticSqlMiRecommendationResults: PaaSSkuRecommendationResultItem[]; + elasticSqlMiRecommendationDurationInMs: number; + elasticSqlVmRecommendationResults: IaaSSkuRecommendationResultItem[]; + elasticSqlVmRecommendationDurationInMs: number; instanceRequirements: SqlInstanceRequirements; skuRecommendationReportPaths: string[]; + elasticSkuRecommendationReportPaths: string[]; } // SKU recommendation enums, mirrored from Microsoft.SqlServer.Migration.SkuRecommendation diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index 07f42c6553..2b7a871219 100644 --- a/extensions/sql-migration/package.json +++ b/extensions/sql-migration/package.json @@ -2,7 +2,7 @@ "name": "sql-migration", "displayName": "%displayName%", "description": "%description%", - "version": "1.0.5", + "version": "1.0.6", "publisher": "Microsoft", "preview": false, "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", @@ -10,7 +10,7 @@ "aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e", "engines": { "vscode": "*", - "azdata": ">=1.37.0" + "azdata": ">=1.39.0" }, "activationEvents": [ "onDashboardOpen", diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index b9b2d08700..299c19af0d 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -70,7 +70,7 @@ export const SKU_RECOMMENDATION_ERROR = (serverName: string): string => { return localize('sql.migration.wizard.sku.error', "An error occurred while generating SKU recommendations for the server '{0}'.", serverName); }; export const SKU_RECOMMENDATION_NO_RECOMMENDATION = localize('sql.migration.wizard.sku.error.noRecommendation', 'No recommendation available'); -export const SKU_RECOMMENDATION_NO_RECOMMENDATION_REASON = localize('sql.migration.wizard.sku.error.noRecommendation.reason', 'No SKU recommendations were generated, as there were no SKUs which could satisfy the performance characteristics of your source. Try selecting a different target platform, adjusting recommendation parameters, or selecting a different set of databases to assess.'); +export const SKU_RECOMMENDATION_NO_RECOMMENDATION_REASON = localize('sql.migration.wizard.sku.error.noRecommendation.reason', 'No SKU recommendations were generated, as there were no SKUs which could satisfy the performance characteristics of your source. Try selecting a different target platform, adjusting recommendation parameters, selecting a different set of databases to assess, or changing the recommendation model.'); export const SKU_RECOMMENDATION_ASSESSMENT_ERROR = (serverName: string): string => { return localize('sql.migration.wizard.sku.assessment.error', "An error occurred while assessing the server '{0}'.", serverName); }; @@ -142,7 +142,7 @@ export const STOP_PERFORMANCE_COLLECTION = localize('sql.migration.sku.stop.perf export const RESTART_PERFORMANCE_COLLECTION = localize('sql.migration.sku.restart.performance.collection', "Restart data collection"); export const AZURE_RECOMMENDATION_CARD_NOT_ENABLED = localize('sql.migration.sku.card.azureRecommendation.notEnabled', "Azure recommendation is not available. Click “Get Azure recommendation” button below"); export const AZURE_RECOMMENDATION_CARD_IN_PROGRESS = localize('sql.migration.sku.card.azureRecommendation.inProgress', "Azure recommendation will be displayed once data collection is complete."); -export const AZURE_RECOMMENDATION_STATUS_NOT_ENABLED = localize('sql.migration.sku.azureRecommendation.status.notEnabled', "Azure recommendation collects and analyzes performance data and then recommends an appropriate sized database in Azure for your workload."); +export const AZURE_RECOMMENDATION_STATUS_NOT_ENABLED = localize('sql.migration.sku.azureRecommendation.status.notEnabled', "Azure recommendation collects and analyzes performance data and then recommends an appropriate sized target in Azure for your workload."); export const AZURE_RECOMMENDATION_STATUS_IN_PROGRESS = localize('sql.migration.sku.azureRecommendation.status.inProgress', "Data collection in progress. Generating initial recommendations..."); export const AZURE_RECOMMENDATION_STATUS_REFINING = localize('sql.migration.sku.azureRecommendation.status.refining', "Data collection still in progress. Refining existing recommendations..."); export const AZURE_RECOMMENDATION_STATUS_STOPPED = localize('sql.migration.sku.azureRecommendation.status.stopped', "Data collection for Azure recommendations has been stopped."); @@ -156,7 +156,6 @@ export const AZURE_RECOMMENDATION_TOOLTIP_IN_PROGRESS = localize('sql.migration. export const AZURE_RECOMMENDATION_START = localize('sql.migration.sku.azureRecommendation.start', "Start"); export const AZURE_RECOMMENDATION_START_POPUP = localize('sql.migration.sku.azureRecommendation.start.popup', "Starting performance data collection..."); -export const AZURE_RECOMMENDATION_OPEN_EXISTING_POPUP = localize('sql.migration.sku.azureRecommendation.openExisting.popup', "Generating Azure recommendations using provided performance data..."); export const AZURE_RECOMMENDATION_STOP_POPUP = localize('sql.migration.sku.azureRecommendation.stop.popup', "Stopping performance data collection..."); export const AZURE_RECOMMENDATION_DESCRIPTION = localize('sql.migration.sku.azureRecommendation.description', "Azure recommendation requires performance data of SQL server instance to provide target recommendation. Enable performance data collection to receive the target recommendation for the databases you want to migrate. The longer this will be enabled the better the recommendation. You can disable performance data collection at any time."); export const AZURE_RECOMMENDATION_DESCRIPTION2 = localize('sql.migration.sku.azureRecommendation.description2', "You can also choose to select this data from an existing folder, if you have already collected it previously."); @@ -266,6 +265,8 @@ export const SCALE_FACTOR_TOOLTIP = localize('sql.migration.sku.parameters.scale export const INVALID_SCALE_FACTOR = localize('sql.migration.sku.parameters.scale.factor.invalid', "Invalid scale factor. Enter a positive integer value."); export const PERCENTAGE_UTILIZATION = localize('sql.migration.sku.parameters.percentage.utilization', "Percentage utilization"); export const PERCENTAGE_UTILIZATION_TOOLTIP = localize('sql.migration.sku.parameters.percentage.utilization.tooltip', "Percentile of data points to be used during aggregation of the performance data."); +export const ELASTIC_RECOMMENDATION_LABEL = localize('sql.migration.sku.parameters.enable.elastic', "Enable elastic recommendation"); +export const ELASTIC_RECOMMENDATION_INFO = localize('sql.migration.sku.parameters.enable.elastic.info', "Elastic recommendation uses an alternate recommendation model which utilizes personalized price-performance profiling against existing on-cloud customers."); export function PERCENTAGE(val: number): string { return localize('sql.migration.sku.percentage', "{0}%", val); } @@ -424,7 +425,6 @@ export const DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL = localize('sql.migrat export const DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_PLACEHOLDER = localize('sql.migration.network.share.password.placeholder', "Enter password."); export const DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HEADER = localize('sql.migration.network.share.azure.header', "Storage account details"); export const DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP = localize('sql.migration.network.share.azure.help', "Provide the Azure Storage account where the backups will be uploaded to."); -export const DATABASE_BACKUP_PRIVATE_ENDPOINT_INFO_TEXT = localize('sql.migration.database.private.endpoint.info.text', "Ensure that the Azure Storage account does not use a private endpoint."); export const DUPLICATE_NAME_ERROR = localize('sql.migration.unique.name', "Select a unique name for this target database"); export function DATABASE_ALREADY_EXISTS_MI(dbName: string, targetName: string): string { return localize('sql.migration.database.already.exists', "Database '{0}' already exists on the target managed instance '{1}'.", dbName, targetName); diff --git a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts index 5b9a915e7a..c4fa3be8c7 100644 --- a/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts +++ b/extensions/sql-migration/src/dialog/assessmentResults/assessmentResultsDialog.ts @@ -83,17 +83,19 @@ export class AssessmentResultsDialog { this._disposables.push( this._saveButton.onClick(async () => { const folder = await utils.promptUserForFolder(); - const destinationFilePath = path.join(folder, AssessmentResultsDialog._assessmentReportName); - if (this.model._assessmentReportFilePath) { - fs.copyFile(this.model._assessmentReportFilePath, destinationFilePath, (err) => { - if (err) { - console.log(err); - } else { - void vscode.window.showInformationMessage(constants.SAVE_ASSESSMENT_REPORT_SUCCESS(destinationFilePath)); - } - }); - } else { - console.log('assessment report not found'); + if (folder) { + const destinationFilePath = path.join(folder, AssessmentResultsDialog._assessmentReportName); + if (this.model._assessmentReportFilePath) { + fs.copyFile(this.model._assessmentReportFilePath, destinationFilePath, (err) => { + if (err) { + console.log(err); + } else { + void vscode.window.showInformationMessage(constants.SAVE_ASSESSMENT_REPORT_SUCCESS(destinationFilePath)); + } + }); + } else { + console.log('assessment report not found'); + } } })); this.dialog.customButtons = [this._saveButton]; diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/getAzureRecommendationDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/getAzureRecommendationDialog.ts index c250089f29..3ad579d7e1 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/getAzureRecommendationDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/getAzureRecommendationDialog.ts @@ -333,8 +333,6 @@ export class GetAzureRecommendationDialog { const serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName; const errors: string[] = []; try { - void vscode.window.showInformationMessage(constants.AZURE_RECOMMENDATION_OPEN_EXISTING_POPUP); - await this.skuRecommendationPage.startCardLoading(); await this.migrationStateModel.getSkuRecommendations(); diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/skuEditParametersDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/skuEditParametersDialog.ts index 6ef0c1eea8..ecf98b1db9 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/skuEditParametersDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/skuEditParametersDialog.ts @@ -24,12 +24,11 @@ export class SkuEditParametersDialog { private _scaleFactorInput!: azdata.InputBoxComponent; private _targetPercentileDropdown!: azdata.DropDownComponent; private _enablePreviewValue!: boolean; + private _enableElasticRecommendation!: boolean; constructor( public skuRecommendationPage: SKURecommendationPage, public migrationStateModel: MigrationStateModel) { - - this._enablePreviewValue = true; } private async initializeDialog(dialog: azdata.window.Dialog): Promise { @@ -182,6 +181,70 @@ export class SkuEditParametersDialog { CSSStyles: { ...styles.BODY_CSS, } }).component(); + const enableElasticLabel = _view.modelBuilder.text().withProps({ + value: constants.ELASTIC_RECOMMENDATION_LABEL, + width: WIZARD_INPUT_COMPONENT_WIDTH, + requiredIndicator: true, + CSSStyles: { + ...styles.LABEL_CSS, + } + }).component(); + const elasticButtonGroup = 'enableElasticRecommendations'; + const enableElasticRadioButtonContainer = _view.modelBuilder.flexContainer() + .withProps({ + CSSStyles: { + 'flex-direction': 'row', + 'width': 'fit-content', + 'margin-top': '-1em', + 'margin-bottom': '8px', + } + }).component(); + const enableElasticButton = _view.modelBuilder.radioButton() + .withProps({ + name: elasticButtonGroup, + label: constants.YES, + checked: this._enableElasticRecommendation, + CSSStyles: { + ...styles.BODY_CSS, + 'width': 'fit-content', + 'margin': '0' + }, + }).component(); + this._disposables.push(enableElasticButton.onDidChangeCheckedState(async (e) => { + if (e) { + this._enableElasticRecommendation = true; + } + })); + const disableElasticButton = _view.modelBuilder.radioButton() + .withProps({ + name: elasticButtonGroup, + label: constants.NO, + checked: !this._enableElasticRecommendation, + CSSStyles: { + ...styles.BODY_CSS, + 'width': 'fit-content', + 'margin': '0 12px', + } + }).component(); + this._disposables.push(disableElasticButton.onDidChangeCheckedState(async (e) => { + if (e) { + this._enableElasticRecommendation = false; + } + })); + enableElasticRadioButtonContainer.addItems([ + enableElasticButton, + disableElasticButton + ]); + + const enableElasticInfoBox = _view.modelBuilder.infoBox() + .withProps({ + text: constants.ELASTIC_RECOMMENDATION_INFO, + style: 'information', + CSSStyles: { + ...styles.BODY_CSS, + } + }).component(); + container.addItems([ description, scaleFactorLabel, @@ -191,6 +254,9 @@ export class SkuEditParametersDialog { enablePreviewLabel, enablePreviewRadioButtonContainer, enablePreviewInfoBox, + enableElasticLabel, + enableElasticRadioButtonContainer, + enableElasticInfoBox, ]); return container; } @@ -219,6 +285,7 @@ export class SkuEditParametersDialog { this._scaleFactorInput.value = this.migrationStateModel._skuScalingFactor.toString(); this._enablePreviewValue = this.migrationStateModel._skuEnablePreview; + this._enableElasticRecommendation = this.migrationStateModel._skuEnableElastic; (this._targetPercentileDropdown.values)?.forEach((percentile, index) => { if ((percentile).name.toLowerCase() === this.migrationStateModel._skuTargetPercentile.toString()) { selectDropDownIndex(this._targetPercentileDropdown, index); @@ -232,6 +299,7 @@ export class SkuEditParametersDialog { this.migrationStateModel._skuScalingFactor = Number(this._scaleFactorInput.value!); this.migrationStateModel._skuTargetPercentile = Number((this._targetPercentileDropdown.value).name); this.migrationStateModel._skuEnablePreview = this._enablePreviewValue; + this.migrationStateModel._skuEnableElastic = this._enableElasticRecommendation; await this.skuRecommendationPage.refreshSkuParameters(); } diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts index f681406e58..149004692e 100644 --- a/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/skuRecommendationResultsDialog.ts @@ -20,6 +20,7 @@ export class SkuRecommendationResultsDialog { private _isOpen: boolean = false; private dialog: azdata.window.Dialog | undefined; + private migrationStateModel: MigrationStateModel; // Dialog Name for Telemetry public dialogName: string | undefined; @@ -45,6 +46,7 @@ export class SkuRecommendationResultsDialog { } this.title = constants.RECOMMENDATIONS_TITLE(this.targetName); + this.migrationStateModel = model; } private async initializeDialog(dialog: azdata.window.Dialog): Promise { @@ -158,17 +160,24 @@ export class SkuRecommendationResultsDialog { if (this._targetType === MigrationTargetType.SQLDB) { const databaseNameLabel = _view.modelBuilder.text() + .withProps({ + value: constants.SOURCE_DATABASE, + CSSStyles: { ...styles.LABEL_CSS, 'margin': '0', } + }).component(); + + const databaseNameValue = _view.modelBuilder.text() .withProps({ value: recommendation.databaseName!, - CSSStyles: { ...styles.SECTION_HEADER_CSS, } + CSSStyles: { ...styles.BODY_CSS, 'margin': '0', } }).component(); recommendationContainer.addItem(databaseNameLabel); + recommendationContainer.addItem(databaseNameValue); } const targetDeploymentTypeLabel = _view.modelBuilder.text() .withProps({ value: constants.TARGET_DEPLOYMENT_TYPE, - CSSStyles: { ...styles.LABEL_CSS, 'margin': '0', } + CSSStyles: { ...styles.LABEL_CSS, 'margin': '12px 0 0', } }).component(); const targetDeploymentTypeValue = _view.modelBuilder.text() .withProps({ @@ -456,15 +465,25 @@ export class SkuRecommendationResultsDialog { switch (this._targetType) { case MigrationTargetType.SQLMI: - this.targetRecommendations = recommendations?.sqlMiRecommendationResults; + if (this.migrationStateModel._skuEnableElastic) { + this.targetRecommendations = recommendations?.elasticSqlMiRecommendationResults; + } else { + this.targetRecommendations = recommendations?.sqlMiRecommendationResults; + } break; case MigrationTargetType.SQLVM: + // elastic model currently doesn't support SQL VM, so show the baseline model results regardless of user preference + // this.targetRecommendations = recommendations?.elasticModelResults.sqlDbRecommendationResults; this.targetRecommendations = recommendations?.sqlVmRecommendationResults; break; case MigrationTargetType.SQLDB: - this.targetRecommendations = recommendations?.sqlDbRecommendationResults; + if (this.migrationStateModel._skuEnableElastic) { + this.targetRecommendations = recommendations?.elasticSqlDbRecommendationResults; + } else { + this.targetRecommendations = recommendations?.sqlDbRecommendationResults; + } break; } @@ -484,38 +503,39 @@ export class SkuRecommendationResultsDialog { this._disposables.push( this._saveButton.onClick(async () => { const folder = await utils.promptUserForFolder(); + if (folder) { + if (this.model._skuRecommendationReportFilePaths) { - if (this.model._skuRecommendationReportFilePaths) { + let sourceFilePath: string | undefined; + let destinationFilePath: string | undefined; - let sourceFilePath: string | undefined; - let destinationFilePath: string | undefined; + switch (this._targetType) { + case MigrationTargetType.SQLMI: + sourceFilePath = this.model._skuRecommendationReportFilePaths.find(filePath => filePath.includes('SkuRecommendationReport-AzureSqlManagedInstance')); + destinationFilePath = path.join(folder, 'SkuRecommendationReport-AzureSqlManagedInstance.html'); + break; - switch (this._targetType) { - case MigrationTargetType.SQLMI: - sourceFilePath = this.model._skuRecommendationReportFilePaths.find(filePath => filePath.includes('SkuRecommendationReport-AzureSqlManagedInstance')); - destinationFilePath = path.join(folder, 'SkuRecommendationReport-AzureSqlManagedInstance.html'); - break; + case MigrationTargetType.SQLVM: + sourceFilePath = this.model._skuRecommendationReportFilePaths.find(filePath => filePath.includes('SkuRecommendationReport-AzureSqlVirtualMachine')); + destinationFilePath = path.join(folder, 'SkuRecommendationReport-AzureSqlVirtualMachine.html'); + break; - case MigrationTargetType.SQLVM: - sourceFilePath = this.model._skuRecommendationReportFilePaths.find(filePath => filePath.includes('SkuRecommendationReport-AzureSqlVirtualMachine')); - destinationFilePath = path.join(folder, 'SkuRecommendationReport-AzureSqlVirtualMachine.html'); - break; - - case MigrationTargetType.SQLDB: - sourceFilePath = this.model._skuRecommendationReportFilePaths.find(filePath => filePath.includes('SkuRecommendationReport-AzureSqlDatabase')); - destinationFilePath = path.join(folder, 'SkuRecommendationReport-AzureSqlDatabase.html'); - break; - } - - fs.copyFile(sourceFilePath!, destinationFilePath, (err) => { - if (err) { - console.log(err); - } else { - void vscode.window.showInformationMessage(constants.SAVE_RECOMMENDATION_REPORT_SUCCESS(destinationFilePath!)); + case MigrationTargetType.SQLDB: + sourceFilePath = this.model._skuRecommendationReportFilePaths.find(filePath => filePath.includes('SkuRecommendationReport-AzureSqlDatabase')); + destinationFilePath = path.join(folder, 'SkuRecommendationReport-AzureSqlDatabase.html'); + break; } - }); - } else { - console.log('recommendation report not found'); + + fs.copyFile(sourceFilePath!, destinationFilePath, (err) => { + if (err) { + console.log(err); + } else { + void vscode.window.showInformationMessage(constants.SAVE_RECOMMENDATION_REPORT_SUCCESS(destinationFilePath!)); + } + }); + } else { + console.log('recommendation report not found'); + } } })); this.dialog.customButtons = [this._saveButton]; diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 189d62a746..654c2fa1d6 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -225,6 +225,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { public _perfDataCollectionErrors!: string[]; public _perfDataCollectionIsCollecting!: boolean; + public readonly _refreshGetSkuRecommendationIntervalInMinutes = 10; public readonly _performanceDataQueryIntervalInSeconds = 30; public readonly _staticDataQueryIntervalInSeconds = 60; public readonly _numberOfPerformanceDataQueryIterations = 19; @@ -233,11 +234,12 @@ export class MigrationStateModel implements Model, vscode.Disposable { public readonly _recommendationTargetPlatforms = [MigrationTargetType.SQLDB, MigrationTargetType.SQLMI, MigrationTargetType.SQLVM]; public refreshPerfDataCollectionFrequency = this._performanceDataQueryIntervalInSeconds * 1000; - public refreshGetSkuRecommendationFrequency = constants.TIME_IN_MINUTES(10); + public refreshGetSkuRecommendationFrequency = constants.TIME_IN_MINUTES(this._refreshGetSkuRecommendationIntervalInMinutes); public _skuScalingFactor!: number; public _skuTargetPercentile!: number; public _skuEnablePreview!: boolean; + public _skuEnableElastic!: boolean; public refreshDatabaseBackupPage!: boolean; public retryMigration!: boolean; @@ -273,7 +275,8 @@ export class MigrationStateModel implements Model, vscode.Disposable { this._skuScalingFactor = 100; this._skuTargetPercentile = 95; - this._skuEnablePreview = true; + this._skuEnablePreview = false; + this._skuEnableElastic = false; } public get sourceConnectionId(): string { @@ -371,15 +374,28 @@ export class MigrationStateModel implements Model, vscode.Disposable { public async getSkuRecommendations(): Promise { try { - const serverInfo = await azdata.connection.getServerInfo(this.sourceConnectionId); - const machineName = (serverInfo)['machineName']; // contains the correct machine name but not necessarily the correct instance name - const instanceName = (await this.getSourceConnectionProfile()).serverName; // contains the correct instance name but not necessarily the correct machine name - let fullInstanceName: string; - if (instanceName.includes('\\')) { - fullInstanceName = machineName + '\\' + instanceName.substring(instanceName.indexOf('\\') + 1); + + // execute a query against the source to get the correct instance name + const connectionProfile = await this.getSourceConnectionProfile(); + const connectionUri = await azdata.connection.getUriForConnection(this._sourceConnectionId); + const queryProvider = azdata.dataprotocol.getProvider(connectionProfile.providerId, azdata.DataProviderType.QueryProvider); + const queryString = 'select @@servername;'; + const queryResult = await queryProvider.runQueryAndReturn(connectionUri, queryString); + + if (queryResult.rowCount > 0) { + fullInstanceName = queryResult.rows[0][0].displayValue; } else { - fullInstanceName = machineName; + // get the instance name from connection info in case querying for the instance name doesn't work for whatever reason + const serverInfo = await azdata.connection.getServerInfo(this.sourceConnectionId); + const machineName = (serverInfo)['machineName']; // contains the correct machine name but not necessarily the correct instance name + const instanceName = (await this.getSourceConnectionProfile()).serverName; // contains the correct instance name but not necessarily the correct machine name + + if (instanceName.includes('\\')) { + fullInstanceName = machineName + '\\' + instanceName.substring(instanceName.indexOf('\\') + 1); + } else { + fullInstanceName = machineName; + } } const response = (await this.migrationService.getSkuRecommendations( @@ -398,40 +414,18 @@ export class MigrationStateModel implements Model, vscode.Disposable { // clone list of databases currently being assessed and store them, so that if the user ever changes the list we can refresh new recommendations this._skuRecommendationRecommendedDatabaseList = this._databasesForAssessment.slice(); - if (response?.sqlDbRecommendationResults || response?.sqlMiRecommendationResults || response?.sqlVmRecommendationResults) { + if (response) { this._skuRecommendationResults = { - recommendations: { - sqlDbRecommendationResults: response?.sqlDbRecommendationResults ?? [], - sqlMiRecommendationResults: response?.sqlMiRecommendationResults ?? [], - sqlVmRecommendationResults: response?.sqlVmRecommendationResults ?? [], - instanceRequirements: response?.instanceRequirements, - skuRecommendationReportPaths: response?.skuRecommendationReportPaths - }, - }; - this._skuRecommendationReportFilePaths = response.skuRecommendationReportPaths; - } else { - this._skuRecommendationResults = { - recommendations: { - sqlDbRecommendationResults: [], - sqlMiRecommendationResults: [], - sqlVmRecommendationResults: [], - instanceRequirements: response?.instanceRequirements, - skuRecommendationReportPaths: response?.skuRecommendationReportPaths - }, + recommendations: response }; + this._skuRecommendationReportFilePaths = this._skuEnableElastic ? response.elasticSkuRecommendationReportPaths : response.skuRecommendationReportPaths; } } catch (error) { logError(TelemetryViews.SkuRecommendationWizard, 'GetSkuRecommendationFailed', error); this._skuRecommendationResults = { - recommendations: { - sqlDbRecommendationResults: this._skuRecommendationApiResponse?.sqlDbRecommendationResults ?? [], - sqlMiRecommendationResults: this._skuRecommendationApiResponse?.sqlMiRecommendationResults ?? [], - sqlVmRecommendationResults: this._skuRecommendationApiResponse?.sqlVmRecommendationResults ?? [], - instanceRequirements: this._skuRecommendationApiResponse?.instanceRequirements, - skuRecommendationReportPaths: this._skuRecommendationApiResponse?.skuRecommendationReportPaths - }, + recommendations: this._skuRecommendationApiResponse, recommendationError: error }; } // Generating all the telemetry asynchronously as we don't need to block the user for it. @@ -442,42 +436,59 @@ export class MigrationStateModel implements Model, vscode.Disposable { private async generateSkuRecommendationTelemetry(): Promise { try { + this._skuRecommendationResults?.recommendations?.sqlDbRecommendationResults + .map((e, i) => [e, this._skuRecommendationResults?.recommendations?.elasticSqlDbRecommendationResults[i]]) + .forEach(resultPair => { + // Send telemetry for recommended DB SKUs + sendSqlMigrationActionEvent( + TelemetryViews.SkuRecommendationWizard, + TelemetryAction.GetDBSkuRecommendation, + { + 'sessionId': this._sessionId, + 'recommendedSku': JSON.stringify(resultPair[0]?.targetSku), + 'elasticRecommendedSku': JSON.stringify(resultPair[1]?.targetSku), + 'recommendationDurationInMs': JSON.stringify(this._skuRecommendationResults?.recommendations?.sqlDbRecommendationDurationInMs), + 'elasticRecommendationDurationInMs': JSON.stringify(this._skuRecommendationResults?.recommendations?.elasticSqlDbRecommendationDurationInMs), + }, + {} + ); + }); - this._skuRecommendationResults?.recommendations?.sqlMiRecommendationResults?.forEach(resultItem => { - // Send telemetry for recommended MI SKU - sendSqlMigrationActionEvent( - TelemetryViews.SkuRecommendationWizard, - TelemetryAction.GetMISkuRecommendation, - { - 'sessionId': this._sessionId, - 'recommendedSku': JSON.stringify(resultItem?.targetSku) - }, - {}); - }); + this._skuRecommendationResults?.recommendations?.sqlMiRecommendationResults + .map((e, i) => [e, this._skuRecommendationResults?.recommendations?.elasticSqlMiRecommendationResults[i]]) + .forEach(resultPair => { + // Send telemetry for recommended MI SKUs + sendSqlMigrationActionEvent( + TelemetryViews.SkuRecommendationWizard, + TelemetryAction.GetMISkuRecommendation, + { + 'sessionId': this._sessionId, + 'recommendedSku': JSON.stringify(resultPair[0]?.targetSku), + 'elasticRecommendedSku': JSON.stringify(resultPair[1]?.targetSku), + 'recommendationDurationInMs': JSON.stringify(this._skuRecommendationResults?.recommendations?.sqlMiRecommendationDurationInMs), + 'elasticRecommendationDurationInMs': JSON.stringify(this._skuRecommendationResults?.recommendations?.elasticSqlMiRecommendationDurationInMs), + }, + {} + ); + }); - this._skuRecommendationResults?.recommendations?.sqlVmRecommendationResults?.forEach(resultItem => { - // Send telemetry for recommended VM SKU - sendSqlMigrationActionEvent( - TelemetryViews.SkuRecommendationWizard, - TelemetryAction.GetVMSkuRecommendation, - { - 'sessionId': this._sessionId, - 'recommendedSku': JSON.stringify(resultItem?.targetSku) - }, - {}); - }); - - this._skuRecommendationResults?.recommendations?.sqlDbRecommendationResults?.forEach(resultItem => { - // Send telemetry for recommended SQLDB SKU - sendSqlMigrationActionEvent( - TelemetryViews.SkuRecommendationWizard, - TelemetryAction.GetSqlDbSkuRecommendation, - { - 'sessionId': this._sessionId, - 'recommendedSku': JSON.stringify(resultItem?.targetSku) - }, - {}); - }); + this._skuRecommendationResults?.recommendations?.sqlVmRecommendationResults + .map((e, i) => [e, this._skuRecommendationResults?.recommendations?.elasticSqlVmRecommendationResults[i]]) + .forEach(resultPair => { + // Send telemetry for recommended VM SKUs + sendSqlMigrationActionEvent( + TelemetryViews.SkuRecommendationWizard, + TelemetryAction.GetVMSkuRecommendation, + { + 'sessionId': this._sessionId, + 'recommendedSku': JSON.stringify(resultPair[0]?.targetSku), + 'elasticRecommendedSku': JSON.stringify(resultPair[1]?.targetSku), + 'recommendationDurationInMs': JSON.stringify(this._skuRecommendationResults?.recommendations?.sqlVmRecommendationDurationInMs), + 'elasticRecommendationDurationInMs': JSON.stringify(this._skuRecommendationResults?.recommendations?.elasticSqlVmRecommendationDurationInMs), + }, + {} + ); + }); // Send Instance requirements used for calculating recommendations sendSqlMigrationActionEvent( @@ -517,18 +528,19 @@ export class MigrationStateModel implements Model, vscode.Disposable { })) }, { - 'cpuRequirementInCores': this._skuRecommendationResults?.recommendations?.instanceRequirements?.cpuRequirementInCores, - 'dataStorageRequirementInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.dataStorageRequirementInMB, - 'logStorageRequirementInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.logStorageRequirementInMB, - 'memoryRequirementInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.memoryRequirementInMB, - 'dataIOPSRequirement': this._skuRecommendationResults?.recommendations?.instanceRequirements?.dataIOPSRequirement, - 'logIOPSRequirement': this._skuRecommendationResults?.recommendations?.instanceRequirements?.logIOPSRequirement, - 'ioLatencyRequirementInMs': this._skuRecommendationResults?.recommendations?.instanceRequirements?.ioLatencyRequirementInMs, - 'ioThroughputRequirementInMBps': this._skuRecommendationResults?.recommendations?.instanceRequirements?.ioThroughputRequirementInMBps, - 'tempDBSizeInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.tempDBSizeInMB, - 'aggregationTargetPercentile': this._skuRecommendationResults?.recommendations?.instanceRequirements?.aggregationTargetPercentile, - 'numberOfDataPointsAnalyzed': this._skuRecommendationResults?.recommendations?.instanceRequirements?.numberOfDataPointsAnalyzed, - }); + 'cpuRequirementInCores': this._skuRecommendationResults?.recommendations?.instanceRequirements?.cpuRequirementInCores!, + 'dataStorageRequirementInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.dataStorageRequirementInMB!, + 'logStorageRequirementInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.logStorageRequirementInMB!, + 'memoryRequirementInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.memoryRequirementInMB!, + 'dataIOPSRequirement': this._skuRecommendationResults?.recommendations?.instanceRequirements?.dataIOPSRequirement!, + 'logIOPSRequirement': this._skuRecommendationResults?.recommendations?.instanceRequirements?.logIOPSRequirement!, + 'ioLatencyRequirementInMs': this._skuRecommendationResults?.recommendations?.instanceRequirements?.ioLatencyRequirementInMs!, + 'ioThroughputRequirementInMBps': this._skuRecommendationResults?.recommendations?.instanceRequirements?.ioThroughputRequirementInMBps!, + 'tempDBSizeInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.tempDBSizeInMB!, + 'aggregationTargetPercentile': this._skuRecommendationResults?.recommendations?.instanceRequirements?.aggregationTargetPercentile!, + 'numberOfDataPointsAnalyzed': this._skuRecommendationResults?.recommendations?.instanceRequirements?.numberOfDataPointsAnalyzed!, + } + ); } catch (e) { logError(TelemetryViews.SkuRecommendationWizard, 'GetSkuRecommendationTelemetryFailed', e); @@ -1191,6 +1203,6 @@ export interface ServerAssessment { } export interface SkuRecommendation { - recommendations: mssql.SkuRecommendationResult; + recommendations?: mssql.SkuRecommendationResult; recommendationError?: Error; } diff --git a/extensions/sql-migration/src/telemtery.ts b/extensions/sql-migration/src/telemtery.ts index 98dd4605c9..575055d4f9 100644 --- a/extensions/sql-migration/src/telemtery.ts +++ b/extensions/sql-migration/src/telemtery.ts @@ -57,7 +57,7 @@ export enum TelemetryAction { OnPageLeave = 'OnPageLeave', GetMISkuRecommendation = 'GetMISkuRecommendation', GetVMSkuRecommendation = 'GetVMSkuRecommendation', - GetSqlDbSkuRecommendation = 'GetSqlDbSkuRecommendation', + GetDBSkuRecommendation = 'GetDBSkuRecommendation', GetInstanceRequirements = 'GetInstanceRequirements', StartDataCollection = 'StartDataCollection', StopDataCollection = 'StopDataCollection' diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index 0e428b338c..4a8ebe1710 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -413,14 +413,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { CSSStyles: { ...styles.SECTION_HEADER_CSS } }).component(); - const azureStoragePrivateEndpointInfoBox = this._view.modelBuilder.infoBox() - .withProps({ - text: constants.DATABASE_BACKUP_PRIVATE_ENDPOINT_INFO_TEXT, - style: 'information', - width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { ...styles.BODY_CSS } - }).component(); - this._networkShareTargetDatabaseNamesTable = this._view.modelBuilder.declarativeTable() .withProps({ columns: [ @@ -523,7 +515,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { .withItems([ blobTableText, allFieldsRequiredLabel, - azureStoragePrivateEndpointInfoBox, this._blobContainerTargetDatabaseNamesTable]) .withProps({ CSSStyles: { 'display': 'none', } }) .component(); @@ -553,14 +544,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '12px' } }).component(); - const azureStoragePrivateEndpointInfoBox = this._view.modelBuilder.infoBox() - .withProps({ - text: constants.DATABASE_BACKUP_PRIVATE_ENDPOINT_INFO_TEXT, - style: 'information', - width: WIZARD_INPUT_COMPONENT_WIDTH, - CSSStyles: { ...styles.BODY_CSS } - }).component(); - const subscriptionLabel = this._view.modelBuilder.text() .withProps({ value: constants.SUBSCRIPTION, @@ -673,7 +656,6 @@ export class DatabaseBackupPage extends MigrationWizardPage { .withItems([ azureAccountHeader, azureAccountHelpText, - azureStoragePrivateEndpointInfoBox, subscriptionLabel, this._networkShareContainerSubscription, locationLabel, diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index 80c6cbd411..f6bd2db995 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -68,10 +68,12 @@ export class SKURecommendationPage extends MigrationWizardPage { private _skuScaleFactorText!: azdata.TextComponent; private _skuTargetPercentileText!: azdata.TextComponent; private _skuEnablePreviewSkuText!: azdata.TextComponent; + private _skuEnableElasticRecommendationsText!: azdata.TextComponent; private assessmentGroupContainer!: azdata.FlexContainer; private _disposables: vscode.Disposable[] = []; + private _serverName: string = ''; private _supportedProducts: Product[] = [ { type: MigrationTargetType.SQLMI, @@ -357,11 +359,11 @@ export class SKURecommendationPage extends MigrationWizardPage { CSSStyles: { 'margin': '12px 0' } }).component(); - const serverName = this.migrationStateModel.serverName || (await this.migrationStateModel.getSourceConnectionProfile()).serverName; + this._serverName = this.migrationStateModel.serverName || (await this.migrationStateModel.getSourceConnectionProfile()).serverName; - const miDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLMI); - const vmDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLVM); - const dbDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLDB); + const miDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(this._serverName), this, MigrationTargetType.SQLMI); + const vmDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(this._serverName), this, MigrationTargetType.SQLVM); + const dbDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(this._serverName), this, MigrationTargetType.SQLDB); this._disposables.push(button.onDidClick(async (e) => { switch (this._rbg.selectedCardId) { @@ -437,8 +439,6 @@ export class SKURecommendationPage extends MigrationWizardPage { level: azdata.window.MessageLevel.Error }; - const serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName; - if (this.migrationStateModel._runAssessments) { const errors: string[] = []; await this._setAssessmentState(true, false); @@ -453,22 +453,22 @@ export class SKURecommendationPage extends MigrationWizardPage { e => `message: ${e.message}${EOL}errorSummary: ${e.errorSummary}${EOL}possibleCauses: ${e.possibleCauses}${EOL}guidance: ${e.guidance}${EOL}errorId: ${e.errorId}`)!); } } catch (e) { - errors.push(constants.SKU_RECOMMENDATION_ASSESSMENT_UNEXPECTED_ERROR(serverName, e)); + errors.push(constants.SKU_RECOMMENDATION_ASSESSMENT_UNEXPECTED_ERROR(this._serverName, e)); logError(TelemetryViews.MigrationWizardSkuRecommendationPage, 'SkuRecommendationUnexpectedError', e); } finally { this.migrationStateModel._runAssessments = errors.length > 0; if (errors.length > 0) { this.wizard.message = { - text: constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR(serverName), + text: constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR(this._serverName), description: errors.join(EOL), level: azdata.window.MessageLevel.Error }; this._assessmentStatusIcon.iconPath = IconPathHelper.error; - this._igComponent.value = constants.ASSESSMENT_FAILED(serverName); - this._detailsComponent.value = constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR(serverName); + this._igComponent.value = constants.ASSESSMENT_FAILED(this._serverName); + this._detailsComponent.value = constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR(this._serverName); } else { this._assessmentStatusIcon.iconPath = IconPathHelper.completedMigration; - this._igComponent.value = constants.ASSESSMENT_COMPLETED(serverName); + this._igComponent.value = constants.ASSESSMENT_COMPLETED(this._serverName); this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL( this.migrationStateModel._assessmentResults?.databaseAssessments?.length); } @@ -476,7 +476,7 @@ export class SKURecommendationPage extends MigrationWizardPage { } else { // use prior assessment results this._assessmentStatusIcon.iconPath = IconPathHelper.completedMigration; - this._igComponent.value = constants.ASSESSMENT_COMPLETED(serverName); + this._igComponent.value = constants.ASSESSMENT_COMPLETED(this._serverName); this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL( this.migrationStateModel._assessmentResults?.databaseAssessments?.length); } @@ -529,20 +529,30 @@ export class SKURecommendationPage extends MigrationWizardPage { } private async _setAssessmentState(assessing: boolean, failedAssessment: boolean): Promise { - await utils.updateControlDisplay(this._assessmentComponent, assessing); - await utils.updateControlDisplay(this._skipAssessmentCheckbox, !assessing && failedAssessment); + await utils.updateControlDisplay( + this._assessmentComponent, + assessing, + 'block'); + await utils.updateControlDisplay( + this._skipAssessmentCheckbox, + !assessing && failedAssessment, + 'block'); await utils.updateControlDisplay( this._skipAssessmentSubText, !assessing && failedAssessment, 'block'); - await utils.updateControlDisplay(this._formContainer.component(), !assessing); + await utils.updateControlDisplay( + this._formContainer.component(), + !assessing, + 'block'); await utils.updateControlDisplay( this._chooseTargetComponent, - !failedAssessment || this._skipAssessmentCheckbox.checked === true); - + !failedAssessment || this._skipAssessmentCheckbox.checked === true, + 'block'); await utils.updateControlDisplay( this.assessmentGroupContainer, - this._rbg.selectedCardId !== undefined && (!failedAssessment || this._skipAssessmentCheckbox.checked === true)); + this._rbg.selectedCardId !== undefined && (!failedAssessment || this._skipAssessmentCheckbox.checked === true), + 'inline'); this._assessmentLoader.loading = assessing; } @@ -611,8 +621,6 @@ export class SKURecommendationPage extends MigrationWizardPage { if (!this.migrationStateModel._assessmentResults) { this._rbg.cards[index].descriptions[CardDescriptionIndex.ASSESSMENT_STATUS].textValue = ''; } else { - // TO-DO: add the assessed db counts - // this._rbg.cards[index].descriptions[5].textValue = constants.ASSESSED_DBS(dbCount); if (this.hasRecommendations()) { this._rbg.cards[index].descriptions[CardDescriptionIndex.VIEW_SKU_DETAILS].linkDisplayValue = constants.VIEW_DETAILS; this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textStyles = { @@ -641,7 +649,11 @@ export class SKURecommendationPage extends MigrationWizardPage { constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount); if (this.hasRecommendations()) { - recommendation = this.migrationStateModel._skuRecommendationResults.recommendations.sqlMiRecommendationResults[0]; + if (this.migrationStateModel._skuEnableElastic) { + recommendation = this.migrationStateModel._skuRecommendationResults.recommendations?.elasticSqlMiRecommendationResults[0]; + } else { + recommendation = this.migrationStateModel._skuRecommendationResults.recommendations?.sqlMiRecommendationResults[0]; + } // result returned but no SKU recommended if (!recommendation?.targetSku) { @@ -672,7 +684,8 @@ export class SKURecommendationPage extends MigrationWizardPage { constants.CAN_BE_MIGRATED(dbCount, dbCount); if (this.hasRecommendations()) { - recommendation = this.migrationStateModel._skuRecommendationResults.recommendations.sqlVmRecommendationResults[0]; + // elastic model currently doesn't support SQL VM, so show the baseline model results regardless of user preference + recommendation = this.migrationStateModel._skuRecommendationResults.recommendations?.sqlVmRecommendationResults[0]; // result returned but no SKU recommended if (!recommendation?.targetSku) { @@ -708,9 +721,10 @@ export class SKURecommendationPage extends MigrationWizardPage { constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount); if (this.hasRecommendations()) { - const successfulRecommendationsCount = - this.migrationStateModel._skuRecommendationResults.recommendations.sqlDbRecommendationResults - .filter(r => r.targetSku !== null).length; + const recommendations = this.migrationStateModel._skuEnableElastic + ? this.migrationStateModel._skuRecommendationResults.recommendations!.elasticSqlDbRecommendationResults + : this.migrationStateModel._skuRecommendationResults.recommendations!.sqlDbRecommendationResults; + const successfulRecommendationsCount = recommendations.filter(r => r.targetSku !== null).length; this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue = constants.RECOMMENDATIONS_AVAILABLE(successfulRecommendationsCount); } @@ -768,7 +782,7 @@ export class SKURecommendationPage extends MigrationWizardPage { private async createAssessmentInfo(): Promise { this._assessmentInfo = this._view.modelBuilder.text() .withProps({ - value: constants.ASSESSMENT_IN_PROGRESS_CONTENT((await this.migrationStateModel.getSourceConnectionProfile()).serverName), + value: constants.ASSESSMENT_IN_PROGRESS_CONTENT(this._serverName), CSSStyles: { ...styles.BODY_CSS, 'width': '660px' @@ -1070,6 +1084,9 @@ export class SKURecommendationPage extends MigrationWizardPage { this.migrationStateModel._skuEnablePreview ? constants.YES : constants.NO); this._skuEnablePreviewSkuText = skuEnablePreviewParameterGroup.text; + const skuEnableElasticRecommendationsParameterGroup = createParameterGroup(constants.ELASTIC_RECOMMENDATION_LABEL, this.migrationStateModel._skuEnableElastic ? constants.YES : constants.NO); + this._skuEnableElasticRecommendationsText = skuEnableElasticRecommendationsParameterGroup.text; + const parametersContainer = _view.modelBuilder.flexContainer() .withProps({ CSSStyles: { @@ -1082,6 +1099,7 @@ export class SKURecommendationPage extends MigrationWizardPage { scaleFactorParameterGroup.flexContainer, skuTargetPercentileParameterGroup.flexContainer, skuEnablePreviewParameterGroup.flexContainer, + skuEnableElasticRecommendationsParameterGroup.flexContainer ]); container.addItems([ @@ -1096,6 +1114,7 @@ export class SKURecommendationPage extends MigrationWizardPage { this._skuScaleFactorText.value = this.migrationStateModel._skuScalingFactor.toString(); 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(); } @@ -1103,6 +1122,16 @@ export class SKURecommendationPage extends MigrationWizardPage { await this.startCardLoading(); this._skuLastRefreshTimeText.value = constants.LAST_REFRESHED_TIME(); await this.migrationStateModel.getSkuRecommendations(); + + const skuRecommendationError = this.migrationStateModel._skuRecommendationResults?.recommendationError; + if (skuRecommendationError) { + this.wizard.message = { + text: constants.SKU_RECOMMENDATION_ERROR(this._serverName), + description: skuRecommendationError.message, + level: azdata.window.MessageLevel.Error + }; + } + await this.refreshSkuRecommendationComponents(); this._skuLastRefreshTimeText.value = constants.LAST_REFRESHED_TIME(new Date().toLocaleString()); } diff --git a/extensions/sql-migration/src/wizard/targetSelectionPage.ts b/extensions/sql-migration/src/wizard/targetSelectionPage.ts index e3d510e0ee..f2bea3c3dc 100644 --- a/extensions/sql-migration/src/wizard/targetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/targetSelectionPage.ts @@ -109,7 +109,6 @@ export class TargetSelectionPage extends MigrationWizardPage { // and came back, forcibly reload the location/resource group/resource values since they will now be different this._migrationTargetPlatform = this.migrationStateModel._targetType; - await this._azureResourceTable.setDataValues([]); this._targetPasswordInputBox.value = ''; this.migrationStateModel._sqlMigrationServices = undefined!; this.migrationStateModel._targetServerInstance = undefined!; @@ -119,7 +118,6 @@ export class TargetSelectionPage extends MigrationWizardPage { } if (this.migrationStateModel._didUpdateDatabasesForMigration) { - await this._azureResourceTable.setDataValues([]); this._initializeSourceTargetDatabaseMap(); this._updateConnectionButtonState(); }