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: {