[Feature] SKU recommendations in SQL migration extension (#18252)

* Initial check in for SQL migration SKU recommendation feature (#18116)

Co-authored-by: Raymond Truong <ratruong@microsoft.com>

* add TargetSelectionPage, remove AccountSelectionPage, fix saveAndClose bugs (#18092)

* update sku interfaces (#18150)

* create the skuRecommendationResultsDialog (#18151)

* add TargetSelectionPage, remove AccountSelectionPage, fix saveAndClose bugs

* create skuRecommendationResultsDialog

* Replace placeholder SKU recommendation results with actual results (#18153)

* Replace placeholder SKU recommendation results with actual backend call results

* Remove skuRecommendationExample

* Replace number fields in interfaces with correct enums, and update UI text

* add getAzureRecommendationDialog for performance collection (#18159)

* add getAzureRecommendationDialog when there are no recommendations available

* update 'get azure rec' / 'view details' link values

* add condition to check if recommendations are available

* Implement start/stop perf data collection + import perf data into new UI (#18149)

* Implement start/stop perf data collection

* add getAzureRecommendationDialog when there are no recommendations available

* update 'get azure rec' / 'view details' link values

* add condition to check if recommendations are available

* Implement import existing data + start/stop perf collection with new UI

Co-authored-by: Rachel Kim <rackim@microsoft.com>

* Expose SqlInstanceRequirements in SKU recommendation results (#18207)

* Expose SqlInstanceRequirements

* Move string literals to constants file

* Fix formatting in mssql.d.ts

* create storage properties table (#18215)

* Edit sku recommendation parameters (#18244)

* Edit sku recommendation parameters

* make _targetPercentileDropdown not editable; styling updates

* Azure recommendation section exposes data collection status and stop option (#18246)

* Edit sku recommendation parameters

* create azure recommendation details section on sku page

* Improve error handling + add auto refresh + other small changes (#18228)

* Update source properties table

* WIP - refresh perf data collection

* Add auto refresh logic

* Address comments

Co-authored-by: Rachel Kim <rackim@microsoft.com>

* Show/hide azure recommendation components based on data collection source and status (#18254)

* add refresh recommendation button; show/hide content based on perf collection status

* show/hide azure rec content based on perf data source scenarios

* add popups for start/stop; allow user to restart data collection; add perf collection to save close; add info tooltips (#18278)

* Update SKU recommendation timer logic (#18281)

* Update timer logic

* Fix misc UI bugs

* update sql migration extension readme (#18295)

* Remove empty constant, as this may have broken the build

* Fix 'save and close' behavior for SKU recommendation (#18301)

* Update timer logic

* Fix misc UI bugs

* 'WIP'

* Add logic to restore correct SKU recommendation state when reloading

* SKU UX enhancements - status info, button validations, savedInfo logic (#18320)

* add stop/inprogress status icons to perf collection status text, update restart icon

* refactor savedInfo as an interface, edit parameter recommednations are saved, add open folder inputbox validation, handle no recommendations available scenario

* fix getazureredialog bug, cleanup cold

* nit card styling

* Update recommendations whenever user changes list of databases to assess + misc clean up (#18323)

* Consolidate constants, clean up redundant functions, misc clean up

* Remove old SKU recommendation interfaces

* Update some more strings

* Telemetry events for SKU Recommendation (#18282)

* Telemetry events for SKU Recommendation

* Addressing comments -
1) fixed camel casing
2) removed extra logging to console
3) added telemetry for subid, rg, tenantid on targetselectionpage

* Resolving conflicts

* Addressing comments - 1) removing filename 2) moving all numbers to measurements.

* Resolving comment - Fixing telemetry value for tenant id.

* removing warning 'logError' is declared but its value is never read (#18333)

* Stop existing data collection when leaving and starting a new migration + update timers (#18339)

* Refresh recommendations when pressing stop data collection button

* Fix orphaned data collection when save and closing and starting a new
migration

* Revert "Refresh recommendations when pressing stop data collection button"

This reverts commit e6fb2ade8f8a41952adb81cb0ee852414dfa4ef2.

* Update timers to use production values

* Remove unused import

* address bug bash issues: add learn more link, add last refreshed time, fix vm card view detail open issue, remember last selected folder, remove strings, refactor refresh logic on sku page (#18340)

* Address comments

* Update to sqltoolsservice 3.0.0-release.204

Co-authored-by: Rachel Kim <rackim@microsoft.com>
Co-authored-by: Neetu Singh <23.neetu@gmail.com>
This commit is contained in:
Raymond Truong
2022-02-11 21:23:49 -08:00
committed by GitHub
parent 08815a3c0f
commit 3cfd1d23da
22 changed files with 3052 additions and 671 deletions

View File

@@ -0,0 +1,402 @@
/*---------------------------------------------------------------------------------------------
* 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 { MigrationStateModel, PerformanceDataSourceOptions } from '../../models/stateMachine';
import * as constants from '../../constants/strings';
import * as styles from '../../constants/styles';
import * as utils from '../../api/utils';
import { SKURecommendationPage } from '../../wizard/skuRecommendationPage';
import { EOL } from 'os';
export class GetAzureRecommendationDialog {
private static readonly StartButtonText: string = constants.AZURE_RECOMMENDATION_START;
private dialog: azdata.window.Dialog | undefined;
private _isOpen: boolean = false;
private _disposables: vscode.Disposable[] = [];
private _performanceDataSource!: PerformanceDataSourceOptions;
private _collectDataContainer!: azdata.FlexContainer;
private _collectDataFolderInput!: azdata.InputBoxComponent;
private _openExistingContainer!: azdata.FlexContainer;
private _openExistingFolderInput!: azdata.InputBoxComponent;
constructor(public skuRecommendationPage: SKURecommendationPage, public wizard: azdata.window.Wizard, public migrationStateModel: MigrationStateModel) {
this._performanceDataSource = PerformanceDataSourceOptions.CollectData;
}
private async initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
return new Promise<void>((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);
resolve();
} catch (ex) {
reject(ex);
}
});
});
}
private createContainer(_view: azdata.ModelView): azdata.FlexContainer {
const container = _view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'margin': '8px 16px',
'flex-direction': 'column',
}
}).component();
const description1 = _view.modelBuilder.text().withProps({
value: constants.AZURE_RECOMMENDATION_DESCRIPTION,
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
const description2 = _view.modelBuilder.text().withProps({
value: constants.AZURE_RECOMMENDATION_DESCRIPTION2,
CSSStyles: {
...styles.BODY_CSS,
'margin-top': '8px',
}
}).component();
const selectDataSourceRadioButtons = this.createDataSourceContainer(_view);
container.addItems([
description1,
description2,
selectDataSourceRadioButtons,
]);
return container;
}
private createDataSourceContainer(_view: azdata.ModelView): azdata.FlexContainer {
const chooseMethodText = _view.modelBuilder.text().withProps({
value: constants.AZURE_RECOMMENDATION_CHOOSE_METHOD,
CSSStyles: {
...styles.LABEL_CSS,
'margin-top': '16px',
}
}).component();
const buttonGroup = 'dataSourceContainer';
const radioButtonContainer = _view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'flex-direction': 'row',
'width': 'fit-content',
'margin': '4px 0 16px',
}
}).component();
const collectDataButton = _view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
label: constants.AZURE_RECOMMENDATION_COLLECT_DATA,
checked: this._performanceDataSource === PerformanceDataSourceOptions.CollectData,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0'
},
}).component();
this._disposables.push(collectDataButton.onDidChangeCheckedState(async (e) => {
if (e) {
await this.switchDataSourceContainerFields(PerformanceDataSourceOptions.CollectData);
}
}));
const openExistingButton = _view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
label: constants.AZURE_RECOMMENDATION_OPEN_EXISTING,
checked: this._performanceDataSource === PerformanceDataSourceOptions.OpenExisting,
CSSStyles: {
...styles.BODY_CSS,
'margin': '0 12px',
}
}).component();
this._disposables.push(openExistingButton.onDidChangeCheckedState(async (e) => {
if (e) {
await this.switchDataSourceContainerFields(PerformanceDataSourceOptions.OpenExisting);
}
}));
radioButtonContainer.addItems([
collectDataButton,
openExistingButton
]);
this._collectDataContainer = this.createCollectDataContainer(_view);
this._openExistingContainer = this.createOpenExistingContainer(_view);
const container = _view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).withItems([
chooseMethodText,
radioButtonContainer,
this._openExistingContainer,
this._collectDataContainer,
]).component();
return container;
}
private createCollectDataContainer(_view: azdata.ModelView): azdata.FlexContainer {
const container = _view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'flex-direction': 'column',
'display': 'inline',
}
}).component();
const instructions = _view.modelBuilder.text().withProps({
value: constants.AZURE_RECOMMENDATION_COLLECT_DATA_FOLDER,
CSSStyles: {
...styles.LABEL_CSS,
'margin-bottom': '8px',
}
}).component();
const selectFolderContainer = _view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'flex-direction': 'row',
'align-items': 'center',
}
}).component();
this._collectDataFolderInput = _view.modelBuilder.inputBox().withProps({
placeHolder: constants.FOLDER_NAME,
readOnly: true,
width: 320,
CSSStyles: {
'margin-right': '12px'
},
}).component();
this._disposables.push(this._collectDataFolderInput.onTextChanged(async (value) => {
if (value) {
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
this.dialog!.okButton.enabled = true;
}
}));
const browseButton = _view.modelBuilder.button().withProps({
label: constants.BROWSE,
width: 100,
CSSStyles: {
'margin': '0'
}
}).component();
this._disposables.push(browseButton.onDidClick(async (e) => {
let folder = await this.handleBrowse();
this._collectDataFolderInput.value = folder;
}));
selectFolderContainer.addItems([
this._collectDataFolderInput,
browseButton,
]);
container.addItems([
instructions,
selectFolderContainer,
]);
return container;
}
private createOpenExistingContainer(_view: azdata.ModelView): azdata.FlexContainer {
const container = _view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'flex-direction': 'column',
'display': 'none',
}
}).component();
const instructions = _view.modelBuilder.text().withProps({
value: constants.AZURE_RECOMMENDATION_OPEN_EXISTING_FOLDER,
CSSStyles: {
...styles.LABEL_CSS,
'margin-bottom': '8px',
}
}).component();
const selectFolderContainer = _view.modelBuilder.flexContainer().withProps({
CSSStyles: {
'flex-direction': 'row',
'align-items': 'center',
}
}).component();
this._openExistingFolderInput = _view.modelBuilder.inputBox().withProps({
placeHolder: constants.FOLDER_NAME,
readOnly: true,
width: 320,
CSSStyles: {
'margin-right': '12px'
},
}).component();
this._disposables.push(this._openExistingFolderInput.onTextChanged(async (value) => {
if (value) {
this.migrationStateModel._skuRecommendationPerformanceLocation = value.trim();
this.dialog!.okButton.enabled = true;
}
}));
const openButton = _view.modelBuilder.button().withProps({
label: constants.OPEN,
width: 100,
CSSStyles: {
'margin': '0'
}
}).component();
this._disposables.push(openButton.onDidClick(async (e) => {
let folder = await this.handleBrowse();
this._openExistingFolderInput.value = folder;
}));
selectFolderContainer.addItems([
this._openExistingFolderInput,
openButton,
]);
container.addItems([
instructions,
selectFolderContainer,
]);
return container;
}
private async switchDataSourceContainerFields(containerType: PerformanceDataSourceOptions): Promise<void> {
this._performanceDataSource = containerType;
let okButtonEnabled = false;
switch (containerType) {
case PerformanceDataSourceOptions.CollectData: {
await this._collectDataContainer.updateCssStyles({ 'display': 'inline' });
await this._openExistingContainer.updateCssStyles({ 'display': 'none' });
if (this._collectDataFolderInput.value) {
okButtonEnabled = true;
}
break;
}
case PerformanceDataSourceOptions.OpenExisting: {
await this._collectDataContainer.updateCssStyles({ 'display': 'none' });
await this._openExistingContainer.updateCssStyles({ 'display': 'inline' });
if (this._openExistingFolderInput.value) {
okButtonEnabled = true;
}
break;
}
}
this.dialog!.okButton.enabled = okButtonEnabled;
}
public async openDialog(dialogName?: string) {
if (!this._isOpen) {
this._isOpen = true;
this.dialog = azdata.window.createModelViewDialog(constants.GET_AZURE_RECOMMENDATION, 'GetAzureRecommendationsDialog', 'narrow');
this.dialog.okButton.label = GetAzureRecommendationDialog.StartButtonText;
this._disposables.push(this.dialog.okButton.onClick(async () => await this.execute()));
this.dialog.cancelButton.onClick(() => this._isOpen = false);
const dialogSetupPromises: Thenable<void>[] = [];
dialogSetupPromises.push(this.initializeDialog(this.dialog));
azdata.window.openDialog(this.dialog);
await Promise.all(dialogSetupPromises);
// if data source was previously selected, default folder value to previously selected
switch (this.migrationStateModel._skuRecommendationPerformanceDataSource) {
case PerformanceDataSourceOptions.CollectData: {
this._collectDataFolderInput.value = this.migrationStateModel._skuRecommendationPerformanceLocation;
break;
}
case PerformanceDataSourceOptions.OpenExisting: {
this._openExistingFolderInput.value = this.migrationStateModel._skuRecommendationPerformanceLocation;
break;
}
}
await this.switchDataSourceContainerFields(this._performanceDataSource);
}
}
protected async execute() {
this._isOpen = false;
this.migrationStateModel._skuRecommendationPerformanceDataSource = this._performanceDataSource;
switch (this.migrationStateModel._skuRecommendationPerformanceDataSource) {
case PerformanceDataSourceOptions.CollectData: {
await this.migrationStateModel.startPerfDataCollection(
this.migrationStateModel._skuRecommendationPerformanceLocation,
this.migrationStateModel._performanceDataQueryIntervalInSeconds,
this.migrationStateModel._staticDataQueryIntervalInSeconds,
this.migrationStateModel._numberOfPerformanceDataQueryIterations,
this.skuRecommendationPage
);
break;
}
case PerformanceDataSourceOptions.OpenExisting: {
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();
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();
}
public get isOpen(): boolean {
return this._isOpen;
}
// TO-DO: add validation
private async handleBrowse(): Promise<string> {
let path = '';
let options: vscode.OpenDialogOptions = {
defaultUri: vscode.Uri.file(utils.getUserHome()!),
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
};
let fileUris = await vscode.window.showOpenDialog(options);
if (fileUris && fileUris?.length > 0 && fileUris[0]) {
path = fileUris[0].fsPath;
}
return path;
}
}