mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-22 09:35:37 -05:00
[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:
@@ -8,7 +8,7 @@ import * as vscode from 'vscode';
|
||||
import * as mssql from '../../../../mssql';
|
||||
import { azureResource } from 'azureResource';
|
||||
import { getLocations, getResourceGroupFromId, getBlobContainerId, getFullResourceGroupFromId, getResourceName } from '../../api/azure';
|
||||
import { MigrationMode, MigrationStateModel, NetworkContainerType, SavedInfo, Page } from '../../models/stateMachine';
|
||||
import { MigrationMode, MigrationStateModel, NetworkContainerType, SavedInfo } from '../../models/stateMachine';
|
||||
import { MigrationContext } from '../../models/migrationLocalStorage';
|
||||
import { WizardController } from '../../wizard/wizardController';
|
||||
import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper';
|
||||
@@ -28,7 +28,7 @@ export class RetryMigrationDialog {
|
||||
const sourceDatabaseName = migration.migrationContext.properties.sourceDatabaseName;
|
||||
let savedInfo: SavedInfo;
|
||||
savedInfo = {
|
||||
closedPage: Page.AzureAccount,
|
||||
closedPage: 0,
|
||||
|
||||
// AzureAccount
|
||||
azureAccount: migration.azureAccount,
|
||||
@@ -42,6 +42,7 @@ export class RetryMigrationDialog {
|
||||
databaseList: [sourceDatabaseName],
|
||||
migrationDatabases: [],
|
||||
serverAssessment: null,
|
||||
skuRecommendation: null,
|
||||
|
||||
migrationTargetType: getMigrationTargetTypeEnum(migration)!,
|
||||
subscription: migration.subscription,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 } from '../../models/stateMachine';
|
||||
import * as constants from '../../constants/strings';
|
||||
import * as styles from '../../constants/styles';
|
||||
import { selectDropDownIndex } from '../../api/utils';
|
||||
import { SKURecommendationPage } from '../../wizard/skuRecommendationPage';
|
||||
|
||||
export const TARGET_PERCENTILE_VALUES = [99, 97, 95, 90, 75, 50];
|
||||
|
||||
export class SkuEditParametersDialog {
|
||||
private static readonly UpdateButtonText: string = constants.UPDATE;
|
||||
|
||||
private dialog: azdata.window.Dialog | undefined;
|
||||
private _isOpen: boolean = false;
|
||||
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
private _scaleFactorInput!: azdata.InputBoxComponent;
|
||||
private _targetPercentileDropdown!: azdata.DropDownComponent;
|
||||
private _enablePreviewValue!: boolean;
|
||||
|
||||
constructor(public skuRecommendationPage: SKURecommendationPage, public migrationStateModel: MigrationStateModel) {
|
||||
this._enablePreviewValue = true;
|
||||
}
|
||||
|
||||
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 description = _view.modelBuilder.text().withProps({
|
||||
value: constants.EDIT_PARAMETERS_TEXT,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
|
||||
const WIZARD_INPUT_COMPONENT_WIDTH = '300px';
|
||||
const scaleFactorLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.SCALE_FACTOR,
|
||||
description: constants.SCALE_FACTOR_TOOLTIP,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS
|
||||
}
|
||||
}).component();
|
||||
this._scaleFactorInput = _view.modelBuilder.inputBox().withProps({
|
||||
required: true,
|
||||
validationErrorMessage: constants.INVALID_SCALE_FACTOR,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
CSSStyles: {
|
||||
'margin-top': '-1em',
|
||||
'margin-bottom': '8px',
|
||||
},
|
||||
}).withValidation(c => {
|
||||
if (Number(c.value) && Number(c.value) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).component();
|
||||
|
||||
const targetPercentileLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.PERCENTAGE_UTILIZATION,
|
||||
description: constants.PERCENTAGE_UTILIZATION_TOOLTIP,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
}
|
||||
}).component();
|
||||
const createPercentageValues = () => {
|
||||
let values: azdata.CategoryValue[] = [];
|
||||
TARGET_PERCENTILE_VALUES.forEach(n => {
|
||||
const val = n.toString();
|
||||
values.push({
|
||||
displayName: constants.PERCENTILE(val),
|
||||
name: val,
|
||||
});
|
||||
});
|
||||
return values;
|
||||
};
|
||||
this._targetPercentileDropdown = _view.modelBuilder.dropDown().withProps({
|
||||
values: createPercentageValues(),
|
||||
ariaLabel: constants.PERCENTAGE_UTILIZATION,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
editable: false,
|
||||
required: true,
|
||||
fireOnTextChange: true,
|
||||
CSSStyles: {
|
||||
'margin-top': '-1em',
|
||||
'margin-bottom': '8px',
|
||||
},
|
||||
}).component();
|
||||
|
||||
const enablePreviewLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.ENABLE_PREVIEW_SKU,
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
||||
requiredIndicator: true,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
}
|
||||
}).component();
|
||||
const buttonGroup = 'enablePreviewSKUs';
|
||||
const enablePreviewRadioButtonContainer = _view.modelBuilder.flexContainer()
|
||||
.withProps({
|
||||
CSSStyles: {
|
||||
'flex-direction': 'row',
|
||||
'width': 'fit-content',
|
||||
'margin-top': '-1em',
|
||||
'margin-bottom': '8px',
|
||||
}
|
||||
}).component();
|
||||
const enablePreviewButton = _view.modelBuilder.radioButton()
|
||||
.withProps({
|
||||
name: buttonGroup,
|
||||
label: constants.YES,
|
||||
checked: this._enablePreviewValue,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'width': 'fit-content',
|
||||
'margin': '0'
|
||||
},
|
||||
}).component();
|
||||
this._disposables.push(enablePreviewButton.onDidChangeCheckedState(async (e) => {
|
||||
if (e) {
|
||||
this._enablePreviewValue = true;
|
||||
}
|
||||
}));
|
||||
const disablePreviewButton = _view.modelBuilder.radioButton()
|
||||
.withProps({
|
||||
name: buttonGroup,
|
||||
label: constants.NO,
|
||||
checked: !this._enablePreviewValue,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'width': 'fit-content',
|
||||
'margin': '0 12px',
|
||||
}
|
||||
}).component();
|
||||
this._disposables.push(disablePreviewButton.onDidChangeCheckedState(async (e) => {
|
||||
if (e) {
|
||||
this._enablePreviewValue = false;
|
||||
}
|
||||
}));
|
||||
enablePreviewRadioButtonContainer.addItems([
|
||||
enablePreviewButton,
|
||||
disablePreviewButton
|
||||
]);
|
||||
|
||||
const enablePreviewInfoBox = _view.modelBuilder.infoBox()
|
||||
.withProps({
|
||||
text: constants.ENABLE_PREVIEW_SKU_INFO,
|
||||
style: 'information',
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
|
||||
container.addItems([
|
||||
description,
|
||||
scaleFactorLabel,
|
||||
this._scaleFactorInput,
|
||||
targetPercentileLabel,
|
||||
this._targetPercentileDropdown,
|
||||
enablePreviewLabel,
|
||||
enablePreviewRadioButtonContainer,
|
||||
enablePreviewInfoBox,
|
||||
]);
|
||||
return container;
|
||||
}
|
||||
|
||||
public async openDialog(dialogName?: string) {
|
||||
if (!this._isOpen) {
|
||||
this._isOpen = true;
|
||||
this.dialog = azdata.window.createModelViewDialog(constants.EDIT_RECOMMENDATION_PARAMETERS, 'SkuEditParametersDialog', 'narrow');
|
||||
|
||||
this.dialog.okButton.label = SkuEditParametersDialog.UpdateButtonText;
|
||||
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);
|
||||
|
||||
this._scaleFactorInput.value = this.migrationStateModel._skuScalingFactor.toString();
|
||||
this._enablePreviewValue = this.migrationStateModel._skuEnablePreview;
|
||||
(<azdata.CategoryValue[]>this._targetPercentileDropdown.values)?.forEach((percentile, index) => {
|
||||
if ((<azdata.CategoryValue>percentile).name.toLowerCase() === this.migrationStateModel._skuTargetPercentile.toString()) {
|
||||
selectDropDownIndex(this._targetPercentileDropdown, index);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected async execute() {
|
||||
this._isOpen = false;
|
||||
this.migrationStateModel._skuScalingFactor = Number(this._scaleFactorInput.value!);
|
||||
this.migrationStateModel._skuTargetPercentile = Number((<azdata.CategoryValue>this._targetPercentileDropdown.value).name);
|
||||
this.migrationStateModel._skuEnablePreview = this._enablePreviewValue;
|
||||
await this.skuRecommendationPage.refreshSkuParameters();
|
||||
}
|
||||
|
||||
public get isOpen(): boolean {
|
||||
return this._isOpen;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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, MigrationTargetType } from '../../models/stateMachine';
|
||||
import * as constants from '../../constants/strings';
|
||||
import * as styles from '../../constants/styles';
|
||||
import * as mssql from '../../../../mssql';
|
||||
|
||||
export class SkuRecommendationResultsDialog {
|
||||
|
||||
private static readonly OpenButtonText: string = 'Close';
|
||||
// private static readonly CreateTargetButtonText: string = 'Create target in portal';
|
||||
|
||||
private _isOpen: boolean = false;
|
||||
private dialog: azdata.window.Dialog | undefined;
|
||||
|
||||
// Dialog Name for Telemetry
|
||||
public dialogName: string | undefined;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
public title?: string;
|
||||
public targetName?: string;
|
||||
|
||||
public targetRecommendations?: mssql.SkuRecommendationResultItem[];
|
||||
public instanceRequirements?: mssql.SqlInstanceRequirements;
|
||||
|
||||
constructor(public model: MigrationStateModel, public _targetType: MigrationTargetType) {
|
||||
switch (this._targetType) {
|
||||
case MigrationTargetType.SQLMI:
|
||||
this.targetName = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE;
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLVM:
|
||||
this.targetName = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE;
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLDB:
|
||||
this.targetName = constants.AZURE_SQL_DATABASE;
|
||||
break;
|
||||
}
|
||||
|
||||
this.title = constants.RECOMMENDATIONS_TITLE(this.targetName);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
this.targetRecommendations?.forEach((recommendation, index) => {
|
||||
if (index > 0) {
|
||||
const separator = _view.modelBuilder.separator().withProps({ width: 750 }).component();
|
||||
container.addItem(separator);
|
||||
}
|
||||
|
||||
container.addItem(this.createRecommendation(_view, recommendation));
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
private createRecommendation(_view: azdata.ModelView, recommendationItem: mssql.SkuRecommendationResultItem): azdata.FlexContainer {
|
||||
|
||||
let recommendation: mssql.IaaSSkuRecommendationResultItem | mssql.PaaSSkuRecommendationResultItem;
|
||||
|
||||
let configuration = constants.NA;
|
||||
let storageSection = _view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
switch (this._targetType) {
|
||||
case MigrationTargetType.SQLVM:
|
||||
recommendation = <mssql.IaaSSkuRecommendationResultItem>recommendationItem;
|
||||
|
||||
if (recommendation.targetSku) {
|
||||
configuration = constants.VM_CONFIGURATION(recommendation.targetSku.virtualMachineSize!.azureSkuName, recommendation.targetSku.virtualMachineSize!.vCPUsAvailable);
|
||||
|
||||
storageSection = this.createSqlVmTargetStorageSection(_view, recommendation);
|
||||
}
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLMI:
|
||||
case MigrationTargetType.SQLDB:
|
||||
recommendation = <mssql.PaaSSkuRecommendationResultItem>recommendationItem;
|
||||
|
||||
if (recommendation.targetSku) {
|
||||
const serviceTier = recommendation.targetSku.category?.sqlServiceTier === mssql.AzureSqlPaaSServiceTier.GeneralPurpose
|
||||
? constants.GENERAL_PURPOSE
|
||||
: constants.BUSINESS_CRITICAL;
|
||||
|
||||
const hardwareType = recommendation.targetSku.category?.hardwareType === mssql.AzureSqlPaaSHardwareType.Gen5
|
||||
? constants.GEN5
|
||||
: recommendation.targetSku.category?.hardwareType === mssql.AzureSqlPaaSHardwareType.PremiumSeries
|
||||
? constants.PREMIUM_SERIES
|
||||
: constants.PREMIUM_SERIES_MEMORY_OPTIMIZED;
|
||||
|
||||
configuration = this._targetType === MigrationTargetType.SQLDB
|
||||
? constants.DB_CONFIGURATION(serviceTier, recommendation.targetSku.computeSize!)
|
||||
: constants.MI_CONFIGURATION(hardwareType, serviceTier, recommendation.targetSku.computeSize!);
|
||||
|
||||
const storageLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.STORAGE_HEADER,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin': '12px 0 0',
|
||||
}
|
||||
}).component();
|
||||
const storageValue = _view.modelBuilder.text().withProps({
|
||||
value: constants.STORAGE_GB(recommendation.targetSku.storageMaxSizeInMb! / 1024),
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
|
||||
storageSection.addItems([
|
||||
storageLabel,
|
||||
storageValue,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
const recommendationContainer = _view.modelBuilder.flexContainer().withProps({
|
||||
CSSStyles: {
|
||||
'margin-bottom': '20px',
|
||||
'flex-direction': 'column',
|
||||
}
|
||||
}).component();
|
||||
|
||||
if (this._targetType === MigrationTargetType.SQLDB) {
|
||||
const databaseNameLabel = _view.modelBuilder.text().withProps({
|
||||
value: recommendation.databaseName!,
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
}
|
||||
}).component();
|
||||
recommendationContainer.addItem(databaseNameLabel);
|
||||
}
|
||||
|
||||
const targetDeploymentTypeLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.TARGET_DEPLOYMENT_TYPE,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin': '0',
|
||||
}
|
||||
}).component();
|
||||
const targetDeploymentTypeValue = _view.modelBuilder.text().withProps({
|
||||
value: this.targetName,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0',
|
||||
}
|
||||
}).component();
|
||||
|
||||
const azureConfigurationLabel = _view.modelBuilder.text().withProps({
|
||||
value: constants.AZURE_CONFIGURATION,
|
||||
CSSStyles: {
|
||||
...styles.LABEL_CSS,
|
||||
'margin': '12px 0 0',
|
||||
}
|
||||
}).component();
|
||||
const azureConfigurationValue = _view.modelBuilder.text().withProps({
|
||||
value: configuration,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
'margin': '0',
|
||||
}
|
||||
}).component();
|
||||
|
||||
recommendationContainer.addItems([
|
||||
targetDeploymentTypeLabel,
|
||||
targetDeploymentTypeValue,
|
||||
|
||||
targetDeploymentTypeLabel,
|
||||
targetDeploymentTypeValue,
|
||||
|
||||
azureConfigurationLabel,
|
||||
azureConfigurationValue,
|
||||
|
||||
storageSection
|
||||
]);
|
||||
|
||||
const recommendationsReasonSection = _view.modelBuilder.text().withProps({
|
||||
value: constants.RECOMMENDATION_REASON,
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'margin': '12px 0 0'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const reasonsContainer = _view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
const justifications: string[] = recommendation?.positiveJustifications?.concat(recommendation?.negativeJustifications) || [constants.SKU_RECOMMENDATION_NO_RECOMMENDATION_REASON];
|
||||
justifications?.forEach(text => {
|
||||
reasonsContainer.addItem(
|
||||
_view.modelBuilder.text().withProps({
|
||||
value: text,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component()
|
||||
);
|
||||
});
|
||||
|
||||
const storagePropertiesContainer = this.createStoragePropertiesTable(_view, recommendation?.databaseName);
|
||||
|
||||
recommendationContainer.addItems([
|
||||
recommendationsReasonSection,
|
||||
reasonsContainer,
|
||||
storagePropertiesContainer,
|
||||
]);
|
||||
|
||||
return recommendationContainer;
|
||||
}
|
||||
|
||||
private createSqlVmTargetStorageSection(_view: azdata.ModelView, recommendation: mssql.IaaSSkuRecommendationResultItem): azdata.FlexContainer {
|
||||
const recommendedTargetStorageSection = _view.modelBuilder.text().withProps({
|
||||
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'margin-top': '12px'
|
||||
}
|
||||
}).component();
|
||||
const recommendedTargetStorageInfo = _view.modelBuilder.text().withProps({
|
||||
value: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION_INFO,
|
||||
CSSStyles: {
|
||||
...styles.BODY_CSS,
|
||||
}
|
||||
}).component();
|
||||
|
||||
const headerCssStyle = {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
'white-space': 'nowrap',
|
||||
'text-overflow': 'ellipsis',
|
||||
'overflow': 'hidden',
|
||||
'border-bottom': '1px solid'
|
||||
};
|
||||
|
||||
const rowCssStyle = {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
'white-space': 'nowrap',
|
||||
'text-overflow': 'ellipsis',
|
||||
'overflow': 'hidden',
|
||||
};
|
||||
|
||||
const columnWidth = 150;
|
||||
let columns: azdata.DeclarativeTableColumn[] = [
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.STORAGE_HEADER,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
},
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.RECOMMENDED_STORAGE_CONFIGURATION,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
},
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.CACHING,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
}
|
||||
];
|
||||
|
||||
const tempTableRow: azdata.DeclarativeTableCellValue[] = [
|
||||
{ value: constants.SQL_TEMPDB },
|
||||
{
|
||||
value: recommendation.targetSku.tempDbDiskSizes!.length > 0
|
||||
? constants.STORAGE_CONFIGURATION(recommendation.targetSku.logDiskSizes![0].size, recommendation.targetSku.logDiskSizes!.length)
|
||||
: constants.EPHEMERAL_TEMPDB
|
||||
},
|
||||
{
|
||||
value: recommendation.targetSku.tempDbDiskSizes!.length > 0
|
||||
? this.getCachingText(recommendation.targetSku.logDiskSizes![0].caching)
|
||||
: constants.CACHING_NA
|
||||
}
|
||||
];
|
||||
|
||||
const dataDiskTableRow: azdata.DeclarativeTableCellValue[] = [
|
||||
{ value: constants.SQL_DATA_FILES },
|
||||
{ value: constants.STORAGE_CONFIGURATION(recommendation.targetSku.dataDiskSizes![0].size, recommendation.targetSku.dataDiskSizes!.length) },
|
||||
{ value: this.getCachingText(recommendation.targetSku.dataDiskSizes![0].caching) }
|
||||
];
|
||||
|
||||
const logDiskTableRow: azdata.DeclarativeTableCellValue[] = [
|
||||
{ value: constants.SQL_LOG_FILES },
|
||||
{ value: constants.STORAGE_CONFIGURATION(recommendation.targetSku.logDiskSizes![0].size, recommendation.targetSku.logDiskSizes!.length) },
|
||||
{ value: this.getCachingText(recommendation.targetSku.logDiskSizes![0].caching) }
|
||||
];
|
||||
|
||||
let storageConfigurationTableRows: azdata.DeclarativeTableCellValue[][] = [
|
||||
tempTableRow,
|
||||
dataDiskTableRow,
|
||||
logDiskTableRow,
|
||||
];
|
||||
|
||||
const storageConfigurationTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable().withProps({
|
||||
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
|
||||
columns: columns,
|
||||
dataValues: storageConfigurationTableRows,
|
||||
width: 700
|
||||
}).component();
|
||||
|
||||
const container = _view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).withItems([
|
||||
recommendedTargetStorageSection,
|
||||
recommendedTargetStorageInfo,
|
||||
storageConfigurationTable,
|
||||
]).component();
|
||||
return container;
|
||||
}
|
||||
|
||||
private getCachingText(caching: mssql.AzureManagedDiskCaching): string {
|
||||
switch (caching) {
|
||||
case mssql.AzureManagedDiskCaching.NotApplicable:
|
||||
return constants.CACHING_NA;
|
||||
|
||||
case mssql.AzureManagedDiskCaching.None:
|
||||
return constants.CACHING_NONE;
|
||||
|
||||
case mssql.AzureManagedDiskCaching.ReadOnly:
|
||||
return constants.CACHING_READ_ONLY;
|
||||
|
||||
case mssql.AzureManagedDiskCaching.ReadWrite:
|
||||
return constants.CACHING_READ_WRITE;
|
||||
}
|
||||
}
|
||||
|
||||
private createStoragePropertiesTable(_view: azdata.ModelView, databaseName?: string): azdata.FlexContainer {
|
||||
let instanceRequirements;
|
||||
switch (this._targetType) {
|
||||
case MigrationTargetType.SQLVM:
|
||||
case MigrationTargetType.SQLMI:
|
||||
instanceRequirements = this.instanceRequirements;
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLDB:
|
||||
instanceRequirements = this.instanceRequirements?.databaseLevelRequirements.filter(d => {
|
||||
return databaseName === d.databaseName;
|
||||
})[0]!;
|
||||
break;
|
||||
}
|
||||
|
||||
const storagePropertiesSection = _view.modelBuilder.text().withProps({
|
||||
value: constants.SOURCE_PROPERTIES,
|
||||
CSSStyles: {
|
||||
...styles.SECTION_HEADER_CSS,
|
||||
'margin-top': '12px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
const headerCssStyle = {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
'white-space': 'nowrap',
|
||||
'text-overflow': 'ellipsis',
|
||||
'overflow': 'hidden',
|
||||
'border-bottom': '1px solid'
|
||||
};
|
||||
|
||||
const rowCssStyle = {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
'white-space': 'nowrap',
|
||||
'text-overflow': 'ellipsis',
|
||||
'overflow': 'hidden',
|
||||
};
|
||||
|
||||
const columnWidth = 80;
|
||||
let columns: azdata.DeclarativeTableColumn[] = [
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.DIMENSION,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
},
|
||||
{
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
displayName: constants.VALUE,
|
||||
isReadOnly: true,
|
||||
width: columnWidth,
|
||||
rowCssStyles: rowCssStyle,
|
||||
headerCssStyles: headerCssStyle
|
||||
}
|
||||
];
|
||||
|
||||
const createRow = (dimension: string, value: string) => {
|
||||
const row: azdata.DeclarativeTableCellValue[] = [
|
||||
{ value: dimension },
|
||||
{ value: value }
|
||||
];
|
||||
return row;
|
||||
};
|
||||
const cpuRow = createRow(constants.CPU_REQUIREMENT, constants.CPU_CORES(instanceRequirements?.cpuRequirementInCores!));
|
||||
const memoryRow = createRow(constants.MEMORY_REQUIREMENT, constants.GB(instanceRequirements?.memoryRequirementInMB! / 1024));
|
||||
const dataStorageRow = createRow(constants.DATA_STORAGE_REQUIREMENT, constants.GB(instanceRequirements?.dataStorageRequirementInMB! / 1024));
|
||||
const logStorageRow = createRow(constants.LOG_STORAGE_REQUIREMENT, constants.GB(instanceRequirements?.logStorageRequirementInMB! / 1024));
|
||||
const dataIOPSRow = createRow(constants.DATA_IOPS_REQUIREMENT, constants.IOPS(instanceRequirements?.dataIOPSRequirement!));
|
||||
const logsIOPSRow = createRow(constants.LOGS_IOPS_REQUIREMENT, constants.IOPS(instanceRequirements?.logIOPSRequirement!));
|
||||
const ioLatencyRow = createRow(constants.IO_LATENCY_REQUIREMENT, instanceRequirements?.ioThroughputRequirementInMBps! < 5 ? constants.NA : constants.MS(instanceRequirements?.ioLatencyRequirementInMs!));
|
||||
const storagePropertiesTableRows: azdata.DeclarativeTableCellValue[][] = [
|
||||
cpuRow,
|
||||
memoryRow,
|
||||
dataStorageRow,
|
||||
logStorageRow,
|
||||
dataIOPSRow,
|
||||
logsIOPSRow,
|
||||
ioLatencyRow,
|
||||
];
|
||||
|
||||
const storagePropertiesTable: azdata.DeclarativeTableComponent = _view.modelBuilder.declarativeTable().withProps({
|
||||
ariaLabel: constants.RECOMMENDED_TARGET_STORAGE_CONFIGURATION,
|
||||
columns: columns,
|
||||
dataValues: storagePropertiesTableRows,
|
||||
width: 300
|
||||
}).component();
|
||||
|
||||
const container = _view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).withItems([
|
||||
storagePropertiesSection,
|
||||
storagePropertiesTable,
|
||||
]).component();
|
||||
return container;
|
||||
}
|
||||
|
||||
public async openDialog(dialogName?: string, recommendations?: mssql.SkuRecommendationResult) {
|
||||
if (!this._isOpen) {
|
||||
this._isOpen = true;
|
||||
this.instanceRequirements = recommendations?.instanceRequirements;
|
||||
|
||||
switch (this._targetType) {
|
||||
case MigrationTargetType.SQLMI:
|
||||
this.targetRecommendations = recommendations?.sqlMiRecommendationResults;
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLVM:
|
||||
this.targetRecommendations = recommendations?.sqlVmRecommendationResults;
|
||||
break;
|
||||
|
||||
case MigrationTargetType.SQLDB:
|
||||
this.targetRecommendations = recommendations?.sqlDbRecommendationResults;
|
||||
break;
|
||||
}
|
||||
|
||||
this.dialog = azdata.window.createModelViewDialog(this.title!, 'SkuRecommendationResultsDialog', 'medium');
|
||||
|
||||
this.dialog.okButton.label = SkuRecommendationResultsDialog.OpenButtonText;
|
||||
this._disposables.push(this.dialog.okButton.onClick(async () => await this.execute()));
|
||||
|
||||
this.dialog.cancelButton.hidden = true;
|
||||
// TO-DO: When "Create target in Portal" feature is ready, unhide cancel button and use cancelButton to direct user to Portal
|
||||
// this.dialog.cancelButton.label = SkuRecommendationResultsDialog.CreateTargetButtonText;
|
||||
// this._disposables.push(this.dialog.cancelButton.onClick(async () => console.log(SkuRecommendationResultsDialog.CreateTargetButtonText)));
|
||||
|
||||
const dialogSetupPromises: Thenable<void>[] = [];
|
||||
dialogSetupPromises.push(this.initializeDialog(this.dialog));
|
||||
azdata.window.openDialog(this.dialog);
|
||||
await Promise.all(dialogSetupPromises);
|
||||
}
|
||||
}
|
||||
|
||||
protected async execute() {
|
||||
this._isOpen = false;
|
||||
}
|
||||
|
||||
public get isOpen(): boolean {
|
||||
return this._isOpen;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user