From 987aed3b922c7b46882ab205154bc2177569b31b Mon Sep 17 00:00:00 2001 From: Cheena Malhotra <13396919+cheenamalhotra@users.noreply.github.com> Date: Tue, 25 Oct 2022 12:19:40 -0700 Subject: [PATCH] Add custom option support on Connection dialog + move Encrypt to connection dialog (#20959) --- extensions/cms/package.json | 3 +- extensions/mssql/package.json | 3 +- resources/xlf/en/sql.xlf | 8 ++- src/sql/azdata.proposed.d.ts | 6 ++ .../common/providerConnectionInfo.ts | 2 +- .../common/providerConnectionInfo.test.ts | 10 ++- .../browser/modal/optionsDialogHelper.ts | 4 +- .../browser/connectionController.ts | 4 +- .../connection/browser/connectionWidget.ts | 66 ++++++++++++++++++- .../browser/media/connectionDialog.css | 4 ++ .../browser/objectExplorerService.test.ts | 1 + 11 files changed, 98 insertions(+), 13 deletions(-) diff --git a/extensions/cms/package.json b/extensions/cms/package.json index dc732c5249..eb99baf9f5 100644 --- a/extensions/cms/package.json +++ b/extensions/cms/package.json @@ -228,7 +228,8 @@ "objectType": null, "categoryValues": null, "isRequired": false, - "isArray": false + "isArray": false, + "showOnConnectionDialog": true }, { "specialValueType": null, diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 68d0fe46df..a979a9d048 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -1046,7 +1046,8 @@ "objectType": null, "categoryValues": null, "isRequired": false, - "isArray": false + "isArray": false, + "showOnConnectionDialog": true }, { "specialValueType": null, diff --git a/resources/xlf/en/sql.xlf b/resources/xlf/en/sql.xlf index 7760c7cc26..fc5b612de4 100644 --- a/resources/xlf/en/sql.xlf +++ b/resources/xlf/en/sql.xlf @@ -5367,6 +5367,12 @@ Error: {1} Server group + + True + + + False + @@ -6544,4 +6550,4 @@ Error: {1} Show Getting Started - \ No newline at end of file + diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 7c3f4da427..8d7a8888fc 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -469,6 +469,12 @@ declare module 'azdata' { export interface ConnectionOption { defaultValueOsOverrides?: DefaultValueOsOverride[]; + + /** + * When set to true, the respective connection option will be rendered on the main connection dialog + * and not the Advanced Options window. + */ + showOnConnectionDialog?: boolean; } export interface TaskInfo { diff --git a/src/sql/platform/connection/common/providerConnectionInfo.ts b/src/sql/platform/connection/common/providerConnectionInfo.ts index efc192d38c..e62e765d30 100644 --- a/src/sql/platform/connection/common/providerConnectionInfo.ts +++ b/src/sql/platform/connection/common/providerConnectionInfo.ts @@ -215,7 +215,7 @@ export class ProviderConnectionInfo extends Disposable implements azdata.Connect let idNames = []; if (this.serverCapabilities) { idNames = this.serverCapabilities.connectionOptions.map(o => { - if ((o.specialValueType || o.isIdentity) + if ((o.specialValueType || o.isIdentity || o.showOnConnectionDialog) && o.specialValueType !== ConnectionOptionSpecialType.password && o.specialValueType !== ConnectionOptionSpecialType.connectionName) { return o.name; diff --git a/src/sql/platform/connection/test/common/providerConnectionInfo.test.ts b/src/sql/platform/connection/test/common/providerConnectionInfo.test.ts index 51fafd3e95..88c526bd5c 100644 --- a/src/sql/platform/connection/test/common/providerConnectionInfo.test.ts +++ b/src/sql/platform/connection/test/common/providerConnectionInfo.test.ts @@ -116,6 +116,7 @@ suite('SQL ProviderConnectionInfo tests', () => { defaultValue: undefined!, isIdentity: false, isRequired: false, + showOnConnectionDialog: true, specialValueType: undefined!, valueType: ServiceOptionType.string } @@ -200,7 +201,7 @@ suite('SQL ProviderConnectionInfo tests', () => { test('constructor should initialize the options given a valid model with options', () => { let options: { [key: string]: string } = {}; - options['encrypt'] = 'test value'; + options['encrypt'] = 'true'; let conn2 = Object.assign({}, connectionProfile, { options: options }); let conn = new ProviderConnectionInfo(capabilitiesService, conn2); @@ -210,12 +211,15 @@ suite('SQL ProviderConnectionInfo tests', () => { assert.strictEqual(conn.authenticationType, conn2.authenticationType); assert.strictEqual(conn.password, conn2.password); assert.strictEqual(conn.userName, conn2.userName); - assert.strictEqual(conn.options['encrypt'], 'test value'); + assert.strictEqual(conn.options['encrypt'], 'true'); }); test('getOptionsKey should create a valid unique id', () => { + let options: { [key: string]: string } = {}; + options['encrypt'] = 'true'; + connectionProfile.options = options; let conn = new ProviderConnectionInfo(capabilitiesService, connectionProfile); - let expectedId = 'providerName:MSSQL|authenticationType:|databaseName:database|serverName:new server|userName:user'; + let expectedId = 'providerName:MSSQL|authenticationType:|databaseName:database|encrypt:true|serverName:new server|userName:user'; let id = conn.getOptionsKey(); assert.strictEqual(id, expectedId); }); diff --git a/src/sql/workbench/browser/modal/optionsDialogHelper.ts b/src/sql/workbench/browser/modal/optionsDialogHelper.ts index 7d6d9bc706..bc0a70b574 100644 --- a/src/sql/workbench/browser/modal/optionsDialogHelper.ts +++ b/src/sql/workbench/browser/modal/optionsDialogHelper.ts @@ -147,8 +147,8 @@ export function updateOptions(options: { [optionName: string]: any }, optionsMap } } -export let trueInputValue: string = 'True'; -export let falseInputValue: string = 'False'; +export let trueInputValue: string = localize('boolean.true', 'True'); +export let falseInputValue: string = localize('boolean.false', 'False'); export function findElement(container: HTMLElement, className: string): HTMLElement { let elementBuilder = container; diff --git a/src/sql/workbench/services/connection/browser/connectionController.ts b/src/sql/workbench/services/connection/browser/connectionController.ts index 2098b302c4..17f45c2ec6 100644 --- a/src/sql/workbench/services/connection/browser/connectionController.ts +++ b/src/sql/workbench/services/connection/browser/connectionController.ts @@ -40,7 +40,7 @@ export class ConnectionController implements IConnectionComponentController { this._callback = callback; this._providerOptions = connectionProperties.connectionOptions; let specialOptions = this._providerOptions.filter( - (property) => (property.specialValueType !== null && property.specialValueType !== undefined)); + (property) => (property.specialValueType !== null && property.specialValueType !== undefined || property.showOnConnectionDialog)); this._connectionWidget = this._instantiationService.createInstance(ConnectionWidget, specialOptions, { onSetConnectButton: (enable: boolean) => this._callback.onSetConnectButton(enable), onCreateNewServerGroup: () => this.onCreateNewServerGroup(), @@ -123,7 +123,7 @@ export class ConnectionController implements IConnectionComponentController { this._advancedController = this._instantiationService.createInstance(AdvancedPropertiesController, () => this._connectionWidget.focusOnAdvancedButton()); } let advancedOption = this._providerOptions.filter( - (property) => (property.specialValueType === undefined || property.specialValueType === null)); + (property) => (property.specialValueType === undefined || property.specialValueType === null && !property.showOnConnectionDialog)); this._advancedController.showDialog(advancedOption, this._model.options); } diff --git a/src/sql/workbench/services/connection/browser/connectionWidget.ts b/src/sql/workbench/services/connection/browser/connectionWidget.ts index cb8889b047..19f85982d8 100644 --- a/src/sql/workbench/services/connection/browser/connectionWidget.ts +++ b/src/sql/workbench/services/connection/browser/connectionWidget.ts @@ -11,7 +11,7 @@ import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox'; import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox'; import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper'; import { IConnectionComponentCallbacks } from 'sql/workbench/services/connection/browser/connectionDialogService'; -import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; +import { IConnectionProfile, ServiceOptionType } from 'sql/platform/connection/common/interfaces'; import { ConnectionOptionSpecialType } from 'sql/workbench/api/common/sqlExtHostTypes'; import { ConnectionProfileGroup, IConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; @@ -68,11 +68,13 @@ export class ConnectionWidget extends lifecycle.Disposable { protected _container: HTMLElement; protected _serverGroupSelectBox: SelectBox; protected _authTypeSelectBox: SelectBox; + protected _customOptions: azdata.ConnectionOption[]; protected _optionsMaps: { [optionType: number]: azdata.ConnectionOption }; protected _tableContainer: HTMLElement; protected _providerName: string; protected _connectionNameInputBox: InputBox; protected _databaseNameInputBox: Dropdown; + protected _customOptionWidgets: (InputBox | SelectBox)[]; protected _advancedButton: Button; private static readonly _authTypes: AuthenticationType[] = [AuthenticationType.AzureMFA, AuthenticationType.AzureMFAAndUser, AuthenticationType.Integrated, AuthenticationType.SqlLogin, AuthenticationType.DSTSAuth, AuthenticationType.None]; @@ -115,6 +117,7 @@ export class ConnectionWidget extends lifecycle.Disposable { ) { super(); this._callbacks = callbacks; + this._customOptions = options.filter(a => a.showOnConnectionDialog === true); this._optionsMaps = {}; for (let i = 0; i < options.length; i++) { let option = options[i]; @@ -177,6 +180,7 @@ export class ConnectionWidget extends lifecycle.Disposable { this.addAuthenticationTypeOption(authTypeChanged); this.addLoginOptions(); this.addDatabaseOption(); + this.addCustomConnectionOptions(); this.addServerGroupOption(); this.addConnectionNameOptions(); this.addAdvancedOptions(); @@ -234,6 +238,12 @@ export class ConnectionWidget extends lifecycle.Disposable { const userNameOption: azdata.ConnectionOption = this._optionsMaps[ConnectionOptionSpecialType.userName]; this._serverNameInputBox.required = !this.useConnectionString; this._userNameInputBox.required = (!this.useConnectionString) && userNameOption?.isRequired; + this._userNameInputBox.value = ''; + if (this.useConnectionString) { + this._tableContainer.classList.add('hide-customOptions'); + } else { + this._tableContainer.classList.remove('hide-customOptions'); + } } protected addAuthenticationTypeOption(authTypeChanged: boolean = false): void { @@ -243,6 +253,32 @@ export class ConnectionWidget extends lifecycle.Disposable { } } + protected addCustomConnectionOptions(): void { + if (this._customOptions.length > 0) { + this._customOptionWidgets = []; + this._customOptions.forEach((option, i) => { + let customOptionsContainer = DialogHelper.appendRow(this._tableContainer, option.displayName, 'connection-label', 'connection-input', 'custom-connection-options'); + switch (option.valueType) { + case ServiceOptionType.boolean: + this._customOptionWidgets[i] = new SelectBox([localize('boolean.true', 'True'), localize('boolean.false', 'False')], option.defaultValue, this._contextViewService, customOptionsContainer, { ariaLabel: option.displayName }); + DialogHelper.appendInputSelectBox(customOptionsContainer, this._customOptionWidgets[i] as SelectBox); + this._register(styler.attachSelectBoxStyler(this._customOptionWidgets[i] as SelectBox, this._themeService)); + break; + case ServiceOptionType.category: + this._customOptionWidgets[i] = new SelectBox(option.categoryValues.map(c => c.displayName), option.defaultValue, this._contextViewService, customOptionsContainer, { ariaLabel: option.displayName }); + DialogHelper.appendInputSelectBox(customOptionsContainer, this._customOptionWidgets[i] as SelectBox); + this._register(styler.attachSelectBoxStyler(this._customOptionWidgets[i] as SelectBox, this._themeService)); + break; + default: + this._customOptionWidgets[i] = new InputBox(customOptionsContainer, this._contextViewService, { ariaLabel: option.displayName }); + this._register(styler.attachInputBoxStyler(this._customOptionWidgets[i] as InputBox, this._themeService)); + break; + } + this._register(this._customOptionWidgets[i]); + }); + } + } + protected addServerNameOption(): void { // Server name let serverNameOption = this._optionsMaps[ConnectionOptionSpecialType.serverName]; @@ -491,6 +527,8 @@ export class ConnectionWidget extends lifecycle.Disposable { protected onAuthTypeSelected(selectedAuthType: string) { let currentAuthType = this.getMatchingAuthType(selectedAuthType); + this._userNameInputBox.value === ''; + this._passwordInputBox.value === ''; this._userNameInputBox.hideMessage(); this._passwordInputBox.hideMessage(); this._azureAccountDropdown.hideMessage(); @@ -734,6 +772,16 @@ export class ConnectionWidget extends lifecycle.Disposable { this._tableContainer.classList.add('hide-azure-accounts'); } + if (this._customOptionWidgets) { + this._customOptionWidgets.forEach((widget, i) => { + if (widget instanceof SelectBox) { + widget.selectWithOptionName(this.getModelValue(connectionInfo.options[this._customOptions[i].name])); + } else { + widget.value = this.getModelValue(connectionInfo.options[this._customOptions[i].name]); + } + }); + } + if (this.authType === AuthenticationType.AzureMFA || this.authType === AuthenticationType.AzureMFAAndUser) { this.fillInAzureAccountOptions().then(async () => { let tenantId = connectionInfo.azureTenantId; @@ -802,7 +850,11 @@ export class ConnectionWidget extends lifecycle.Disposable { if (this._authTypeSelectBox) { this._authTypeSelectBox.disable(); } - + if (this._customOptionWidgets) { + this._customOptionWidgets.forEach(widget => { + widget.disable(); + }); + } if (this._connectionStringOptions.isEnabled) { this._connectionStringInputBox.disable(); this._defaultInputOptionRadioButton.enabled = false; @@ -840,6 +892,11 @@ export class ConnectionWidget extends lifecycle.Disposable { if (this._databaseNameInputBox) { this._databaseNameInputBox.enabled = true; } + if (this._customOptionWidgets) { + this._customOptionWidgets.forEach(widget => { + widget.enable(); + }); + } if (this._connectionStringOptions.isEnabled) { this._connectionStringInputBox.enable(); this._defaultInputOptionRadioButton.enabled = true; @@ -964,6 +1021,11 @@ export class ConnectionWidget extends lifecycle.Disposable { model.savePassword = this._rememberPasswordCheckBox.checked; model.connectionName = this.connectionName; model.databaseName = this.databaseName; + if (this._customOptionWidgets) { + this._customOptionWidgets.forEach((widget, i) => { + model.options[this._customOptions[i].name] = widget.value; + }); + } if (this._serverGroupSelectBox) { if (this._serverGroupSelectBox.value === this.DefaultServerGroup.name) { model.groupFullName = ''; diff --git a/src/sql/workbench/services/connection/browser/media/connectionDialog.css b/src/sql/workbench/services/connection/browser/media/connectionDialog.css index 1ac9fa466c..a3fb18d637 100644 --- a/src/sql/workbench/services/connection/browser/media/connectionDialog.css +++ b/src/sql/workbench/services/connection/browser/media/connectionDialog.css @@ -123,6 +123,10 @@ display: none; } +.hide-customOptions .custom-connection-options { + display: none; +} + .hide-refresh-link .azure-account-row.refresh-credentials-link { display: none; } diff --git a/src/sql/workbench/services/objectExplorer/test/browser/objectExplorerService.test.ts b/src/sql/workbench/services/objectExplorer/test/browser/objectExplorerService.test.ts index a3d97ee81e..9fb15eef53 100644 --- a/src/sql/workbench/services/objectExplorer/test/browser/objectExplorerService.test.ts +++ b/src/sql/workbench/services/objectExplorer/test/browser/objectExplorerService.test.ts @@ -215,6 +215,7 @@ suite('SQL Object Explorer Service tests', () => { defaultValue: undefined, isIdentity: false, isRequired: false, + showOnConnectionDialog: true, specialValueType: undefined, valueType: ServiceOptionType.string }