From 35f7736b9647b0114ab58d6af9bd218f75e06b8f Mon Sep 17 00:00:00 2001 From: Cheena Malhotra <13396919+cheenamalhotra@users.noreply.github.com> Date: Wed, 1 Mar 2023 11:01:50 -0800 Subject: [PATCH] Add Secure Enclaves dropdown with customizable Advanced options (#22019) --- extensions/mssql/package.json | 81 +++++++++++++++++-- extensions/mssql/package.nls.json | 3 + src/sql/azdata.proposed.d.ts | 6 ++ .../base/browser/ui/selectBox/selectBox.ts | 10 +-- .../workbench/browser/modal/optionsDialog.ts | 72 ++++++++++++++++- .../browser/modal/optionsDialogHelper.ts | 6 +- .../browser/advancedPropertiesController.ts | 1 + 7 files changed, 163 insertions(+), 16 deletions(-) diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 9b22c4835d..796dcddeff 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -928,15 +928,66 @@ "categoryValues": [ { "displayName": "%mssql.disabled%", - "name": "Disabled" + "name": "disabled" }, { "displayName": "%mssql.enabled%", - "name": "Enabled" + "name": "enabled" } ], "isRequired": false, - "isArray": false + "isArray": false, + "onSelectionChange": [ + { + "values": [ + "disabled", + "" + ], + "dependentOptionActions": [ + { + "optionName": "secureEnclaves", + "action": "hide" + } + ] + } + ] + }, + { + "specialValueType": null, + "isIdentity": false, + "name": "secureEnclaves", + "displayName": "%mssql.connectionOptions.secureEnclaves.displayName%", + "description": "%mssql.connectionOptions.secureEnclaves.description%", + "groupName": "%mssql.connectionOptions.groupName.security%", + "valueType": "category", + "defaultValue": null, + "objectType": null, + "categoryValues": [ + { + "displayName": "%mssql.disabled%", + "name": "disabled" + }, + { + "displayName": "%mssql.enabled%", + "name": "enabled" + } + ], + "isRequired": false, + "isArray": false, + "onSelectionChange": [ + { + "values": [ + "disabled", + "" + ], + "dependentOptionActions": [ + { + "optionName": "attestationProtocol", + "action": "hide" + } + ] + } + ] }, { "specialValueType": null, @@ -951,15 +1002,33 @@ "categoryValues": [ { "displayName": "%mssql.connectionOptions.enclaveAttestationProtocol.categoryValues.HGS%", - "name": "HGS" + "name": "hgs" }, { "displayName": "%mssql.connectionOptions.enclaveAttestationProtocol.categoryValues.AAS%", - "name": "AAS" + "name": "aas" + }, + { + "displayName": "%mssql.connectionOptions.enclaveAttestationProtocol.categoryValues.None%", + "name": "none" } ], "isRequired": false, - "isArray": false + "isArray": false, + "onSelectionChange": [ + { + "values": [ + "none", + "" + ], + "dependentOptionActions": [ + { + "optionName": "enclaveAttestationUrl", + "action": "hide" + } + ] + } + ] }, { "specialValueType": null, diff --git a/extensions/mssql/package.nls.json b/extensions/mssql/package.nls.json index 9bfc2521be..f57a726327 100644 --- a/extensions/mssql/package.nls.json +++ b/extensions/mssql/package.nls.json @@ -111,10 +111,13 @@ "mssql.connectionOptions.currentLanguage.description": "The SQL Server language record name", "mssql.connectionOptions.columnEncryptionSetting.displayName": "Always Encrypted", "mssql.connectionOptions.columnEncryptionSetting.description": "Enables or disables Always Encrypted for the connection", + "mssql.connectionOptions.secureEnclaves.displayName": "Secure Enclaves", + "mssql.connectionOptions.secureEnclaves.description": "Enables or disables Secure Enclaves for the connection", "mssql.connectionOptions.enclaveAttestationProtocol.displayName": "Attestation Protocol", "mssql.connectionOptions.enclaveAttestationProtocol.description": "Specifies a protocol for attesting a server-side enclave used with Always Encrypted with secure enclaves", "mssql.connectionOptions.enclaveAttestationProtocol.categoryValues.AAS": "Azure Attestation", "mssql.connectionOptions.enclaveAttestationProtocol.categoryValues.HGS": "Host Guardian Service", + "mssql.connectionOptions.enclaveAttestationProtocol.categoryValues.None": "None", "mssql.connectionOptions.enclaveAttestationUrl.displayName": "Enclave Attestation URL", "mssql.connectionOptions.enclaveAttestationUrl.description": "Specifies an endpoint for attesting a server-side enclave used with Always Encrypted with secure enclaves", "mssql.connectionOptions.encrypt.displayName": "Encrypt", diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 041459f793..c8413066c5 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -575,6 +575,12 @@ declare module 'azdata' { onSelectionChange?: SelectionChangeEvent[]; } + export interface ServiceOption { + /** + * Used to define list of values based on which another option is rendered visible/hidden. + */ + onSelectionChange?: SelectionChangeEvent[]; + } /** * This change event defines actions */ diff --git a/src/sql/base/browser/ui/selectBox/selectBox.ts b/src/sql/base/browser/ui/selectBox/selectBox.ts index f197b9fc4c..bd1748a90f 100644 --- a/src/sql/base/browser/ui/selectBox/selectBox.ts +++ b/src/sql/base/browser/ui/selectBox/selectBox.ts @@ -195,22 +195,22 @@ export class SelectBox extends vsSelectBox implements AdsWidget { this.applyStyles(); } - public selectWithOptionName(optionName?: string, selectFirstByDefault: boolean = true): void { + public selectWithOptionName(optionName?: string, selectFirstByDefault: boolean = true, forceSelectionEvent: boolean = false): void { let option: number | undefined; if (optionName !== undefined) { option = this._optionsDictionary.get(optionName); } if (option !== undefined) { - this.select(option); + this.select(option, forceSelectionEvent); } else if (selectFirstByDefault) { - this.select(0); + this.select(0, forceSelectionEvent); } } - public override select(index: number): void { + public override select(index: number, forceSelectionEvent: boolean = false): void { super.select(index); let selectedOptionIndex = this._optionsDictionary.get(this._selectedOption); - if (selectedOptionIndex === index) { // Not generating an event if the same value is selected. + if (!forceSelectionEvent && selectedOptionIndex === index) { // Not generating an event if the same value is selected. return; } if (this._dialogOptions !== undefined) { diff --git a/src/sql/workbench/browser/modal/optionsDialog.ts b/src/sql/workbench/browser/modal/optionsDialog.ts index cc6086e69b..58d841f56f 100644 --- a/src/sql/workbench/browser/modal/optionsDialog.ts +++ b/src/sql/workbench/browser/modal/optionsDialog.ts @@ -21,7 +21,7 @@ import * as styler from 'vs/platform/theme/common/styler'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { Widget } from 'vs/base/browser/ui/widget'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { append, $, clearNode } from 'vs/base/browser/dom'; +import { append, $, clearNode, createCSSRule } from 'vs/base/browser/dom'; import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeService'; import { ILogService } from 'vs/platform/log/common/log'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; @@ -30,6 +30,8 @@ import { ServiceOptionType } from 'sql/platform/connection/common/interfaces'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { GroupHeaderBackground } from 'sql/platform/theme/common/colorRegistry'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; +import { AdsWidget } from 'sql/base/browser/ui/adsWidget'; +import { Actions } from 'sql/platform/connection/common/constants'; export interface IOptionsDialogOptions extends IModalOptions { cancelLabel?: string; @@ -123,7 +125,7 @@ export class OptionsDialog extends Modal { private fillInOptions(container: HTMLElement, options: azdata.ServiceOption[]): void { for (let i = 0; i < options.length; i++) { let option: azdata.ServiceOption = options[i]; - let rowContainer = DialogHelper.appendRow(container, option.displayName, 'optionsDialog-label', 'optionsDialog-input'); + let rowContainer = DialogHelper.appendRow(container, option.displayName, 'optionsDialog-label', 'optionsDialog-input', `option-${option.name}`); const optionElement = OptionsDialogHelper.createOptionElement(option, rowContainer, this._optionValues, this._optionElements, this._contextViewService, (name) => this.onOptionLinkClicked(name)); this.disposableStore.add(optionElement.optionWidget); } @@ -204,6 +206,7 @@ export class OptionsDialog extends Modal { let bodyContainer = $('table.optionsDialog-table'); bodyContainer.setAttribute('role', 'presentation'); this.fillInOptions(bodyContainer, serviceOptions); + this.registerOnSelectionChangeEvents(optionValues, bodyContainer); append(this._optionGroupsContainer!, bodyContainer); } this.updateTheme(this._themeService.getColorTheme()); @@ -211,6 +214,71 @@ export class OptionsDialog extends Modal { this.show(); } + /** + * Registers on selection change event for connection options configured with 'onSelectionChange' property. + */ + private registerOnSelectionChangeEvents(options: { [name: string]: any }, container: HTMLElement): void { + //Register on selection change event for all advanced options + for (let optionName in this._optionElements) { + let widget: Widget = this._optionElements[optionName].optionWidget; + if (widget instanceof SelectBox) { + this._registerSelectionChangeEvents([this._optionElements], this._optionElements[optionName].option, widget, container); + } + } + } + + private _registerSelectionChangeEvents(collections: { [optionName: string]: OptionsDialogHelper.IOptionElement }[], option: azdata.ServiceOption, widget: SelectBox, container: HTMLElement) { + if (option?.onSelectionChange) { + option.onSelectionChange.forEach((event) => { + this._register(widget.onDidSelect(value => { + let selectedValue = value.selected; + event?.dependentOptionActions?.forEach((optionAction) => { + let defaultValue: string | undefined = collections[optionAction.optionName]?.option.defaultValue ?? ''; + let widget: AdsWidget | undefined = this._findWidget(collections, optionAction.optionName); + if (widget) { + createCSSRule(`.hide-${widget.id} .option-${widget.id}`, `display: none;`); + this._onValueChangeEvent(container, selectedValue, event.values, widget, defaultValue, optionAction.action); + } + }); + })); + }); + // Clear selection change actions once event is registered. + option.onSelectionChange = undefined; + if (this.optionValues[option.name]) { + widget.selectWithOptionName(this.optionValues[option.name], true); + } else { + widget.select(0, true); + } + } + } + + private _onValueChangeEvent(container: HTMLElement, selectedValue: string, acceptedValues: string[], + widget: AdsWidget, defaultValue: string, action: string): void { + if ((acceptedValues.includes(selectedValue.toLocaleLowerCase()) && action === Actions.Show) + || (!acceptedValues.includes(selectedValue.toLocaleLowerCase()) && action === Actions.Hide)) { + container.classList.remove(`hide-${widget.id}`); + } else { + // Support more Widget classes here as needed. + if (widget instanceof SelectBox) { + widget.select(widget.values.indexOf(defaultValue)); + } else if (widget instanceof InputBox) { + widget.value = defaultValue; + } + container.classList.add(`hide-${widget.id}`); + widget.hideMessage(); + } + } + + /** + * Finds Widget from provided collection of widgets using option name. + * @param collections collections of widgets to search for the widget with the widget Id + * @param id Widget Id + * @returns Widget if found, undefined otherwise + */ + private _findWidget(collections: { [optionName: string]: OptionsDialogHelper.IOptionElement }[], id: string): AdsWidget | undefined { + return collections.find(collection => !!collection[id].optionWidget)[id]?.optionWidget; + } + protected layout(height?: number): void { } diff --git a/src/sql/workbench/browser/modal/optionsDialogHelper.ts b/src/sql/workbench/browser/modal/optionsDialogHelper.ts index bc0a70b574..00eb4c5be6 100644 --- a/src/sql/workbench/browser/modal/optionsDialogHelper.ts +++ b/src/sql/workbench/browser/modal/optionsDialogHelper.ts @@ -42,11 +42,11 @@ export function createOptionElement(option: azdata.ServiceOption, rowContainer: } }, ariaLabel: option.displayName - }); + }, option.name); optionWidget.value = optionValue; inputElement = findElement(rowContainer, 'input'); } else if (option.valueType === ServiceOptionType.category || option.valueType === ServiceOptionType.boolean) { - optionWidget = new SelectBox(possibleInputs, optionValue.toString(), contextViewService, undefined, { ariaLabel: option.displayName }); + optionWidget = new SelectBox(possibleInputs, optionValue.toString(), contextViewService, undefined, { ariaLabel: option.displayName }, option.name); DialogHelper.appendInputSelectBox(rowContainer, optionWidget); inputElement = findElement(rowContainer, 'monaco-select-box'); } else if (option.valueType === ServiceOptionType.string || option.valueType === ServiceOptionType.password) { @@ -55,7 +55,7 @@ export function createOptionElement(option: azdata.ServiceOption, rowContainer: validation: (value: string) => (!value && option.isRequired) ? ({ type: MessageType.ERROR, content: option.displayName + missingErrorMessage }) : null }, ariaLabel: option.displayName - }); + }, option.name); optionWidget.value = optionValue; if (option.valueType === ServiceOptionType.password) { optionWidget.inputElement.type = 'password'; diff --git a/src/sql/workbench/services/connection/browser/advancedPropertiesController.ts b/src/sql/workbench/services/connection/browser/advancedPropertiesController.ts index 6beaca4621..06ebf64499 100644 --- a/src/sql/workbench/services/connection/browser/advancedPropertiesController.ts +++ b/src/sql/workbench/services/connection/browser/advancedPropertiesController.ts @@ -56,6 +56,7 @@ export class AdvancedPropertiesController { categoryValues: connectionOption.categoryValues, isRequired: connectionOption.isRequired, isArray: undefined, + onSelectionChange: connectionOption.onSelectionChange }; } }