From 5f8d59708be8ac53e5f7e0046fbcb21bcd0ec3e4 Mon Sep 17 00:00:00 2001 From: Ritik Kumar Date: Thu, 5 Oct 2023 15:06:09 +0530 Subject: [PATCH] [New Migration Experience] Creating the new import performance dialog box (#24597) * added import performance dialog * updated open folder icon * updated alignment for the import performance dialog * added localized strings * updated skuRecommendationPage with openExisting option --- .../sql-migration/images/openFolder.svg | 3 + .../src/constants/iconPathHelper.ts | 6 +- .../sql-migration/src/constants/strings.ts | 4 + .../sql-migration/src/constants/styles.ts | 6 + .../importPerformanceDataDialog.ts | 279 ++++++++++++++++++ .../skuDataCollectionToolbar.ts | 11 +- .../skuRecommendationPage.ts | 20 +- 7 files changed, 318 insertions(+), 11 deletions(-) create mode 100644 extensions/sql-migration/images/openFolder.svg create mode 100644 extensions/sql-migration/src/dialog/skuRecommendationResults/importPerformanceDataDialog.ts diff --git a/extensions/sql-migration/images/openFolder.svg b/extensions/sql-migration/images/openFolder.svg new file mode 100644 index 000000000000..c95891b35335 --- /dev/null +++ b/extensions/sql-migration/images/openFolder.svg @@ -0,0 +1,3 @@ + + + diff --git a/extensions/sql-migration/src/constants/iconPathHelper.ts b/extensions/sql-migration/src/constants/iconPathHelper.ts index bebbf438d6bc..ccc84a4985d3 100644 --- a/extensions/sql-migration/src/constants/iconPathHelper.ts +++ b/extensions/sql-migration/src/constants/iconPathHelper.ts @@ -54,6 +54,7 @@ export class IconPathHelper { public static stopDataCollection: IconPath; public static import: IconPath; public static settings: IconPath; + public static openFolder: IconPath; public static setExtensionContext(context: vscode.ExtensionContext) { IconPathHelper.copy = { @@ -228,6 +229,9 @@ export class IconPathHelper { light: context.asAbsolutePath('images/settings.svg'), dark: context.asAbsolutePath('images/settings.svg') }; - + IconPathHelper.openFolder = { + light: context.asAbsolutePath('images/openFolder.svg'), + dark: context.asAbsolutePath('images/openFolder.svg') + }; } } diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 1b76c2584591..5c079f736c73 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -211,6 +211,9 @@ export const START_PERFORMANCE_COLLECTION = localize('sql.migration.sku.start.pe export const STOP_PERFORMANCE_COLLECTION = localize('sql.migration.sku.stop.performance.collection', "Stop data collection"); export const RESTART_PERFORMANCE_COLLECTION = localize('sql.migration.sku.restart.performance.collection', "Restart data collection"); export const IMPORT_PERFORMANCE_DATA = localize('sql.migration.sku.import.performance.data', "Import performance data"); +export const IMPORT_PERFORMANCE_DATA_DIALOG_DESCRIPTION = localize('sql.migration.sku.import.performance.data.dialog.description', "Import this data file from an existing folder, if you have already collected it using Data Migration Assistant."); +export const IMPORT_PERFORMANCE_DATA_DIALOG_HELPER_MESSAGE = localize('sql.migration.sku.import.performance.data.dialog.helper.message', "Select a folder on your local drive"); +export const IMPORT_PERFORMANCE_DATA_DIALOG_OPEN_FILE = localize('sql.migration.sku.import.performance.data.dialog.open.file', "Select a file"); // allow-any-unicode-next-line export const AZURE_RECOMMENDATION_CARD_NOT_ENABLED = localize('sql.migration.sku.card.azureRecommendation.notEnabled', "Azure recommendation is not available. Click “Start data collection” button above to get started."); export const AZURE_RECOMMENDATION_CARD_IN_PROGRESS = localize('sql.migration.sku.card.azureRecommendation.inProgress', "Azure recommendation will be displayed once data collection is complete."); @@ -961,6 +964,7 @@ export const NAME = localize('sql.migration.name', "Name"); export const LOCATION = localize('sql.migration.location', "Location"); export const REFRESH = localize('sql.migration.refresh', "Refresh"); export const CREATE = localize('sql.migration.create', "Create"); +export const IMPORT = localize('sql.migration.cancel', "Import"); export const CANCEL = localize('sql.migration.cancel', "Cancel"); export const TYPE = localize('sql.migration.type', "Type"); export const USER_ACCOUNT = localize('sql.migration.path.user.account', "User account"); diff --git a/extensions/sql-migration/src/constants/styles.ts b/extensions/sql-migration/src/constants/styles.ts index 9ae5e6e7e6f5..4c7677fdef8d 100644 --- a/extensions/sql-migration/src/constants/styles.ts +++ b/extensions/sql-migration/src/constants/styles.ts @@ -107,3 +107,9 @@ export const ASSESSMENT_SUMMARY_CARD_CSS = { 'font-weight': '700', 'margin': '0px', }; + +export const PERFORMANCE_DATA_DIALOG_CSS = { + 'font-size': '13px', + 'line-height': '18px', + 'font-weight': '600', +}; diff --git a/extensions/sql-migration/src/dialog/skuRecommendationResults/importPerformanceDataDialog.ts b/extensions/sql-migration/src/dialog/skuRecommendationResults/importPerformanceDataDialog.ts new file mode 100644 index 000000000000..ada1e5be31d5 --- /dev/null +++ b/extensions/sql-migration/src/dialog/skuRecommendationResults/importPerformanceDataDialog.ts @@ -0,0 +1,279 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; +import * as styles from '../../constants/styles'; +import * as utils from '../../api/utils'; +import * as constants from '../../constants/strings'; +import { IconPathHelper } from '../../constants/iconPathHelper'; +import { MigrationStateModel, PerformanceDataSourceOptions } from '../../models/stateMachine'; +import { SKURecommendationPage } from '../../wizard/skuRecommendation/skuRecommendationPage'; +import { getSourceConnectionProfile } from '../../api/sqlUtils'; +import { EOL } from 'os'; +import { EventEmitter } from 'stream'; + + +/*--------------------------------------------------------------------------------------------- +* For selecting the path which contains the data files. +* This file contains the code for import performance data dialog. +-----------------------------------------------------------------------------------------------*/ +export class ImportPerformanceDataDialog { + private dialog!: azdata.window.Dialog; + private _importButton!: azdata.ButtonComponent; + + private _disposables: vscode.Disposable[] = []; + + private _creationEvent: EventEmitter = new EventEmitter; + private _isOpen: boolean = false; + + private _performanceDataSource!: PerformanceDataSourceOptions; + private _openExistingContainer!: azdata.FlexContainer; + private _openExistingFolderInput!: azdata.InputBoxComponent; + + constructor(public skuRecommendationPage: SKURecommendationPage, public wizard: azdata.window.Wizard, public migrationStateModel: MigrationStateModel) { + this._performanceDataSource = PerformanceDataSourceOptions.OpenExisting; + } + + private async initializeDialog(dialog: azdata.window.Dialog): Promise { + return new Promise((resolve, reject) => { + dialog.registerContent(async (view) => { + try { + const flex = this.createContainer(view); + + this._disposables.push( + view.onClosed(e => + this._disposables.forEach( + d => { try { d.dispose(); } catch { } }))); + + await view.initializeModel(flex); + + this._creationEvent.once('done', async () => { + azdata.window.closeDialog(this.dialog); + resolve(); + }); + + } + catch (ex) { + reject(ex); + } + }); + }); + } + + private createContainer(_view: azdata.ModelView): azdata.FlexContainer { + const container = _view.modelBuilder.flexContainer().withProps({ + CSSStyles: { + 'margin': '0px', + 'flex-direction': 'column', + } + }).component(); + const headingLabel = _view.modelBuilder.text().withProps({ + value: constants.IMPORT_PERFORMANCE_DATA, + CSSStyles: { + ...styles.PAGE_TITLE_CSS, + 'margin-top': '5px', + } + }).component(); + const description = _view.modelBuilder.text().withProps({ + value: constants.IMPORT_PERFORMANCE_DATA_DIALOG_DESCRIPTION, + CSSStyles: { + ...styles.TOOLBAR_CSS, + } + }).component(); + + this._openExistingContainer = this.createOpenExistingFolderContainer(_view); + + this._importButton = _view.modelBuilder.button().withProps({ + label: constants.IMPORT, + width: '80px', + enabled: false, + CSSStyles: { + ...styles.PERFORMANCE_DATA_DIALOG_CSS, + 'padding': '3px 20px 3px 20px', + 'gap': '10px' + } + }).component(); + + this._disposables.push(this._importButton.onDidClick(async () => { + await this.execute(); + this._creationEvent.emit('done'); + })); + + const cancelButton = _view.modelBuilder.button().withProps({ + label: constants.CANCEL, + secondary: true, + width: '80px', + CSSStyles: { + ...styles.PERFORMANCE_DATA_DIALOG_CSS, + 'padding': '3px 20px 3px 20px', + 'gap': '10px' + } + }).component(); + + this._disposables.push(cancelButton.onDidClick(e => { + this._creationEvent.emit('done'); + })); + + const buttonContainer = _view.modelBuilder.flexContainer().withProps({ + CSSStyles: { + 'height': '48px', + 'margin-top': '20px', + 'margin-left': '70px', + } + }).component(); + + buttonContainer.addItem(this._importButton, { + flex: '0', + }); + + buttonContainer.addItem(cancelButton, { + flex: '0', + CSSStyles: { + 'margin-left': '2px', + 'width': '80px' + } + }); + + container.addItems([ + headingLabel, + description, + this._openExistingContainer, + buttonContainer + ]); + return container; + } + + public async openDialog(dialogName?: string) { + if (!this._isOpen) { + this._isOpen = true; + + this.dialog = azdata.window.createModelViewDialog( + '', + 'ImportPerformanceDataDialog', + 350, + 'callout', + 'below', + false, + false, + { + height: 51, + width: 0, + xPos: 0, + yPos: 0 + } + ); + + const promise = this.initializeDialog(this.dialog); + azdata.window.openDialog(this.dialog); + await promise; + } + } + + private createOpenExistingFolderContainer(_view: azdata.ModelView): azdata.FlexContainer { + const container = _view.modelBuilder.flexContainer() + .withProps({ + CSSStyles: { + 'flex-direction': 'column', + 'margin-top': '10px' + } + }) + .component(); + + const instructions = _view.modelBuilder.text().withProps({ + value: constants.IMPORT_PERFORMANCE_DATA_DIALOG_HELPER_MESSAGE, + CSSStyles: { + 'font-size': '13px', + 'line-height': '18px', + 'font-weight': '600', + } + }).component(); + + const selectFolderContainer = _view.modelBuilder.flexContainer() + .withProps({ + CSSStyles: { + 'flex-direction': 'row', + 'align-items': 'center', + 'marign-right': '30px' + } + }) + .component(); + + this._openExistingFolderInput = _view.modelBuilder.inputBox().withProps({ + placeHolder: constants.IMPORT_PERFORMANCE_DATA_DIALOG_OPEN_FILE, + readOnly: true, + height: 24, + width: 222, + CSSStyles: { + 'font-size': '13px', + 'line-height': '18px', + 'font-weight': '400', + 'margin': '0px', + }, + }).component(); + this._disposables.push( + this._openExistingFolderInput.onTextChanged(async (value) => { + if (value) { + this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim(); + this.dialog!.okButton.enabled = true; + this._importButton.enabled = true; + } + })); + + const openButton = _view.modelBuilder.button() + .withProps({ + iconHeight: 24, + iconWidth: 24, + iconPath: IconPathHelper.openFolder, + CSSStyles: { + 'margin': '0', + 'padding': '0', + } + }).component(); + this._disposables.push( + openButton.onDidClick( + async (e) => this._openExistingFolderInput.value = await utils.promptUserForFolder())); + + selectFolderContainer.addItems([ + this._openExistingFolderInput, + openButton]); + container.addItems([ + instructions, + selectFolderContainer]); + + return container; + } + + protected async execute() { + this._isOpen = false; + this.migrationStateModel._skuRecommendationPerformanceDataSource = this._performanceDataSource; + + const serverName = (await getSourceConnectionProfile()).serverName; + const errors: string[] = []; + try { + // TODO - ADD the card loading. + // await this.skuRecommendationPage.startCardLoading(); + await this.migrationStateModel.getSkuRecommendations(); + + const skuRecommendationError = this.migrationStateModel._skuRecommendationResults?.recommendationError; + if (skuRecommendationError) { + errors.push(`message: ${skuRecommendationError.message}`); + } + } catch (e) { + console.log(e); + errors.push(constants.SKU_RECOMMENDATION_ASSESSMENT_UNEXPECTED_ERROR(serverName, e)); + } finally { + if (errors.length > 0) { + this.wizard.message = { + text: constants.SKU_RECOMMENDATION_ERROR(serverName), + description: errors.join(EOL), + level: azdata.window.MessageLevel.Error + }; + } + } + + await this.skuRecommendationPage.refreshSkuRecommendationComponents(); + } +} diff --git a/extensions/sql-migration/src/wizard/skuRecommendation/skuDataCollectionToolbar.ts b/extensions/sql-migration/src/wizard/skuRecommendation/skuDataCollectionToolbar.ts index 9c8546caee43..41f2dacc2777 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendation/skuDataCollectionToolbar.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendation/skuDataCollectionToolbar.ts @@ -16,6 +16,7 @@ import * as styles from '../../constants/styles'; import * as constants from '../../constants/strings'; import { MigrationStateModel, PerformanceDataSourceOptions } from '../../models/stateMachine'; import { SKURecommendationPage } from './skuRecommendationPage'; +import { ImportPerformanceDataDialog } from '../../dialog/skuRecommendationResults/importPerformanceDataDialog'; // TODO - "Change this to actual default path once it is available" const DEFAULT_PATH_FOR_START_DATA_COLLECTION = "C:\DataPointsCollectionFolder"; @@ -31,7 +32,7 @@ export class SkuDataCollectionToolbar implements vscode.Disposable { private _disposables: vscode.Disposable[] = []; - constructor(private skuRecommendationPage: SKURecommendationPage, private migrationStateModel: MigrationStateModel) { + constructor(private skuRecommendationPage: SKURecommendationPage, public wizard: azdata.window.Wizard, private migrationStateModel: MigrationStateModel) { } public createToolbar(view: azdata.ModelView): azdata.ToolbarContainer { @@ -171,7 +172,13 @@ export class SkuDataCollectionToolbar implements vscode.Disposable { ...styles.TOOLBAR_CSS } }).component(); - // TODO - implement onDidClick and add to disposables + + this._disposables.push( + importPerformanceDataButton.onDidClick(async (e) => { + const importPerformanceDataDialog = new ImportPerformanceDataDialog(this.skuRecommendationPage, this.wizard, this.migrationStateModel); + await importPerformanceDataDialog.openDialog(); + }) + ); return importPerformanceDataButton; } diff --git a/extensions/sql-migration/src/wizard/skuRecommendation/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendation/skuRecommendationPage.ts index 41b80bca3ae5..df3ddd288129 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendation/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendation/skuRecommendationPage.ts @@ -59,7 +59,7 @@ export class SKURecommendationPage extends MigrationWizardPage { protected async registerContent(view: azdata.ModelView) { this._view = view; - this._skuDataCollectionToolbar = new SkuDataCollectionToolbar(this, this.migrationStateModel); + this._skuDataCollectionToolbar = new SkuDataCollectionToolbar(this, this.wizard, this.migrationStateModel); const toolbar = this._skuDataCollectionToolbar.createToolbar(view); this._assessmentStatusIcon = this._view.modelBuilder.image() @@ -314,11 +314,10 @@ export class SKURecommendationPage extends MigrationWizardPage { break; } - // TODO - Will be done once import performance dialog is implemented. - // case PerformanceDataSourceOptions.OpenExisting: { - // shouldGetSkuRecommendations = true; - // break; - // } + case PerformanceDataSourceOptions.OpenExisting: { + shouldGetSkuRecommendations = true; + break; + } } } @@ -564,8 +563,13 @@ export class SKURecommendationPage extends MigrationWizardPage { } // TODO - implement the import performance data functionality. - // case PerformanceDataSourceOptions.OpenExisting: { - // } + case PerformanceDataSourceOptions.OpenExisting: { + if (this.hasRecommendations()) { + // TODO - update the status container, text and icon. + // TODO - update the visibility of different button and status message. + } + break; + } // initial state before "Get Azure recommendation" dialog default: {