diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts index 2e3fb35b1c..a43525f9bb 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts @@ -6,7 +6,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { FieldType, SectionInfo } from '../../../interfaces'; -import { createFlexContainer, createGroupContainer, createLabel, createNumberInput, createSection, createInputBox, getCheckboxComponent, getDropdownComponent, getInputBoxComponent, InputComponentInfo, InputComponents, setModelValues, Validator, InputComponent } from '../../modelViewUtils'; +import { createFlexContainer, createGroupContainer, createLabel, createNumberInputBoxInputInfo, createSection, createInputBoxInputInfo, getCheckboxComponent, getDropdownComponent, getInputBoxComponent, InputComponentInfo, InputComponents, setModelValues, Validator, InputComponent } from '../../modelViewUtils'; import { ResourceTypePage } from '../../resourceTypePage'; import * as VariableNames from '../constants'; import { AuthenticationMode, DeployClusterWizardModel } from '../deployClusterWizardModel'; @@ -175,54 +175,54 @@ export class ServiceSettingsPage extends ResourceTypePage { this.endpointHeaderRow = createFlexContainer(view, [this.endpointNameColumnHeader, this.dnsColumnHeader, this.portColumnHeader]); this.controllerNameLabel = createLabel(view, { text: localize('deployCluster.ControllerText', "Controller"), width: labelWidth, required: true }); - const controllerDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.ControllerDNSName', "Controller DNS name"), required: false, width: inputWidth }); + const controllerDNSInput = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.ControllerDNSName', "Controller DNS name"), required: false, width: inputWidth }); this.controllerDNSInput = controllerDNSInput.component; - const controllerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ControllerPortName', "Controller port"), required: true, width: NumberInputWidth, min: 1 }); + const controllerPortInput = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.ControllerPortName', "Controller port"), required: true, width: NumberInputWidth, min: 1 }); this.controllerPortInput = controllerPortInput.component; this.controllerEndpointRow = createFlexContainer(view, [this.controllerNameLabel, this.controllerDNSInput, this.controllerPortInput]); this.onNewInputComponentCreated(VariableNames.ControllerDNSName_VariableName, controllerDNSInput); this.onNewInputComponentCreated(VariableNames.ControllerPort_VariableName, controllerPortInput); this.SqlServerNameLabel = createLabel(view, { text: localize('deployCluster.MasterSqlText', "SQL Server Master"), width: labelWidth, required: true }); - const sqlServerDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.MasterSQLServerDNSName', "SQL Server Master DNS name"), required: false, width: inputWidth }); + const sqlServerDNSInput = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.MasterSQLServerDNSName', "SQL Server Master DNS name"), required: false, width: inputWidth }); this.sqlServerDNSInput = sqlServerDNSInput.component; - const sqlServerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.MasterSQLServerPortName', "SQL Server Master port"), required: true, width: NumberInputWidth, min: 1 }); + const sqlServerPortInput = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.MasterSQLServerPortName', "SQL Server Master port"), required: true, width: NumberInputWidth, min: 1 }); this.sqlServerPortInput = sqlServerPortInput.component; this.sqlServerEndpointRow = createFlexContainer(view, [this.SqlServerNameLabel, this.sqlServerDNSInput, this.sqlServerPortInput]); this.onNewInputComponentCreated(VariableNames.SQLServerDNSName_VariableName, sqlServerDNSInput); this.onNewInputComponentCreated(VariableNames.SQLServerPort_VariableName, sqlServerPortInput); this.gatewayNameLabel = createLabel(view, { text: localize('deployCluster.GatewayText', "Gateway"), width: labelWidth, required: true }); - const gatewayDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.GatewayDNSName', "Gateway DNS name"), required: false, width: inputWidth }); + const gatewayDNSInput = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.GatewayDNSName', "Gateway DNS name"), required: false, width: inputWidth }); this.gatewayDNSInput = gatewayDNSInput.component; - const gatewayPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.GatewayPortName', "Gateway port"), required: true, width: NumberInputWidth, min: 1 }); + const gatewayPortInput = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.GatewayPortName', "Gateway port"), required: true, width: NumberInputWidth, min: 1 }); this.gatewayPortInput = gatewayPortInput.component; this.gatewayEndpointRow = createFlexContainer(view, [this.gatewayNameLabel, this.gatewayDNSInput, this.gatewayPortInput]); this.onNewInputComponentCreated(VariableNames.GatewayDNSName_VariableName, gatewayDNSInput); this.onNewInputComponentCreated(VariableNames.GateWayPort_VariableName, gatewayPortInput); this.serviceProxyNameLabel = createLabel(view, { text: localize('deployCluster.ServiceProxyText', "Management proxy"), width: labelWidth, required: true }); - const serviceProxyDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.ServiceProxyDNSName', "Management proxy DNS name"), required: false, width: inputWidth }); + const serviceProxyDNSInput = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.ServiceProxyDNSName', "Management proxy DNS name"), required: false, width: inputWidth }); this.serviceProxyDNSInput = serviceProxyDNSInput.component; - const serviceProxyPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ServiceProxyPortName', "Management proxy port"), required: true, width: NumberInputWidth, min: 1 }); + const serviceProxyPortInput = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.ServiceProxyPortName', "Management proxy port"), required: true, width: NumberInputWidth, min: 1 }); this.serviceProxyPortInput = serviceProxyPortInput.component; this.serviceProxyEndpointRow = createFlexContainer(view, [this.serviceProxyNameLabel, this.serviceProxyDNSInput, this.serviceProxyPortInput]); this.onNewInputComponentCreated(VariableNames.ServiceProxyDNSName_VariableName, serviceProxyDNSInput); this.onNewInputComponentCreated(VariableNames.ServiceProxyPort_VariableName, serviceProxyPortInput); this.appServiceProxyNameLabel = createLabel(view, { text: localize('deployCluster.AppServiceProxyText', "Application proxy"), width: labelWidth, required: true }); - const appServiceProxyDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.AppServiceProxyDNSName', "Application proxy DNS name"), required: false, width: inputWidth }); + const appServiceProxyDNSInput = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.AppServiceProxyDNSName', "Application proxy DNS name"), required: false, width: inputWidth }); this.appServiceProxyDNSInput = appServiceProxyDNSInput.component; - const appServiceProxyPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.AppServiceProxyPortName', "Application proxy port"), required: true, width: NumberInputWidth, min: 1 }); + const appServiceProxyPortInput = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.AppServiceProxyPortName', "Application proxy port"), required: true, width: NumberInputWidth, min: 1 }); this.appServiceProxyPortInput = appServiceProxyPortInput.component; this.appServiceProxyEndpointRow = createFlexContainer(view, [this.appServiceProxyNameLabel, this.appServiceProxyDNSInput, this.appServiceProxyPortInput]); this.onNewInputComponentCreated(VariableNames.AppServiceProxyDNSName_VariableName, appServiceProxyDNSInput); this.onNewInputComponentCreated(VariableNames.AppServiceProxyPort_VariableName, appServiceProxyPortInput); this.readableSecondaryNameLabel = createLabel(view, { text: localize('deployCluster.ReadableSecondaryText', "Readable secondary"), width: labelWidth, required: true }); - const readableSecondaryDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.ReadableSecondaryDNSName', "Readable secondary DNS name"), required: false, width: inputWidth }); + const readableSecondaryDNSInput = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.ReadableSecondaryDNSName', "Readable secondary DNS name"), required: false, width: inputWidth }); this.readableSecondaryDNSInput = readableSecondaryDNSInput.component; - const readableSecondaryPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ReadableSecondaryPortName', "Readable secondary port"), required: false, width: NumberInputWidth, min: 1 }); + const readableSecondaryPortInput = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.ReadableSecondaryPortName', "Readable secondary port"), required: false, width: NumberInputWidth, min: 1 }); this.readableSecondaryPortInput = readableSecondaryPortInput.component; this.readableSecondaryEndpointRow = createFlexContainer(view, [this.readableSecondaryNameLabel, this.readableSecondaryDNSInput, this.readableSecondaryPortInput]); this.onNewInputComponentCreated(VariableNames.ReadableSecondaryDNSName_VariableName, readableSecondaryDNSInput); @@ -243,10 +243,10 @@ export class ServiceSettingsPage extends ResourceTypePage { required: true, description: localize('deployCluster.AdvancedStorageDescription', "By default Controller storage settings will be applied to other services as well, you can expand the advanced storage settings to configure storage for other services.") }); - const controllerDataStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.controllerDataStorageClass', "Controller's data storage class"), width: inputWidth, required: true }); - const controllerDataStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.controllerDataStorageClaimSize', "Controller's data storage claim size"), width: inputWidth, required: true, min: 1 }); - const controllerLogsStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.controllerLogsStorageClass', "Controller's logs storage class"), width: inputWidth, required: true }); - const controllerLogsStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.controllerLogsStorageClaimSize', "Controller's logs storage claim size"), width: inputWidth, required: true, min: 1 }); + const controllerDataStorageClassInput = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerDataStorageClass', "Controller's data storage class"), width: inputWidth, required: true }); + const controllerDataStorageClaimSizeInput = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerDataStorageClaimSize', "Controller's data storage claim size"), width: inputWidth, required: true, min: 1 }); + const controllerLogsStorageClassInput = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerLogsStorageClass', "Controller's logs storage class"), width: inputWidth, required: true }); + const controllerLogsStorageClaimSizeInput = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.controllerLogsStorageClaimSize', "Controller's logs storage claim size"), width: inputWidth, required: true, min: 1 }); const storagePoolLabel = createLabel(view, { @@ -254,10 +254,10 @@ export class ServiceSettingsPage extends ResourceTypePage { width: inputWidth, required: false }); - const storagePoolDataStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.storagePoolDataStorageClass', "Storage pool's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields }); - const storagePoolDataStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.storagePoolDataStorageClaimSize', "Storage pool's data storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields }); - const storagePoolLogsStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.storagePoolLogsStorageClass', "Storage pool's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields }); - const storagePoolLogsStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.storagePoolLogsStorageClaimSize', "Storage pool's logs storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields }); + const storagePoolDataStorageClassInput = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolDataStorageClass', "Storage pool's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields }); + const storagePoolDataStorageClaimSizeInput = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolDataStorageClaimSize', "Storage pool's data storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields }); + const storagePoolLogsStorageClassInput = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolLogsStorageClass', "Storage pool's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields }); + const storagePoolLogsStorageClaimSizeInput = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.storagePoolLogsStorageClaimSize', "Storage pool's logs storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields }); const dataPoolLabel = createLabel(view, { @@ -265,10 +265,10 @@ export class ServiceSettingsPage extends ResourceTypePage { width: inputWidth, required: false }); - const dataPoolDataStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.dataPoolDataStorageClass', "Data pool's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields }); - const dataPoolDataStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.dataPoolDataStorageClaimSize', "Data pool's data storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields }); - const dataPoolLogsStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.dataPoolLogsStorageClass', "Data pool's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields }); - const dataPoolLogsStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.dataPoolLogsStorageClaimSize', "Data pool's logs storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields }); + const dataPoolDataStorageClassInput = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolDataStorageClass', "Data pool's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields }); + const dataPoolDataStorageClaimSizeInput = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolDataStorageClaimSize', "Data pool's data storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields }); + const dataPoolLogsStorageClassInput = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolLogsStorageClass', "Data pool's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields }); + const dataPoolLogsStorageClaimSizeInput = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.dataPoolLogsStorageClaimSize', "Data pool's logs storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields }); const sqlServerMasterLabel = createLabel(view, @@ -277,10 +277,10 @@ export class ServiceSettingsPage extends ResourceTypePage { width: inputWidth, required: false }); - const sqlServerMasterDataStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.sqlServerMasterDataStorageClass', "SQL Server master's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields }); - const sqlServerMasterDataStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.sqlServerMasterDataStorageClaimSize', "SQL Server master's data storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields }); - const sqlServerMasterLogsStorageClassInput = createInputBox(view, { ariaLabel: localize('deployCluster.sqlServerMasterLogsStorageClass', "SQL Server master's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields }); - const sqlServerMasterLogsStorageClaimSizeInput = createNumberInput(view, { ariaLabel: localize('deployCluster.sqlServerMasterLogsStorageClaimSize', "SQL Server master's logs storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields }); + const sqlServerMasterDataStorageClassInput = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterDataStorageClass', "SQL Server master's data storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields }); + const sqlServerMasterDataStorageClaimSizeInput = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterDataStorageClaimSize', "SQL Server master's data storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields }); + const sqlServerMasterLogsStorageClassInput = createInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterLogsStorageClass', "SQL Server master's logs storage class"), width: inputWidth, required: false, placeHolder: hintTextForStorageFields }); + const sqlServerMasterLogsStorageClaimSizeInput = createNumberInputBoxInputInfo(view, { ariaLabel: localize('deployCluster.sqlServerMasterLogsStorageClaimSize', "SQL Server master's logs storage claim size"), width: inputWidth, required: false, min: 1, placeHolder: hintTextForStorageFields }); this.onNewInputComponentCreated(VariableNames.ControllerDataStorageClassName_VariableName, controllerDataStorageClassInput); this.onNewInputComponentCreated(VariableNames.ControllerDataStorageSize_VariableName, controllerDataStorageClaimSizeInput); diff --git a/extensions/resource-deployment/src/ui/modelViewUtils.ts b/extensions/resource-deployment/src/ui/modelViewUtils.ts index 8389bbcab4..f55a909064 100644 --- a/extensions/resource-deployment/src/ui/modelViewUtils.ts +++ b/extensions/resource-deployment/src/ui/modelViewUtils.ts @@ -33,13 +33,11 @@ const localize = nls.loadMessageBundle(); export type Validator = () => { valid: boolean, message: string }; export type InputValueType = string | number | undefined; -export type InputValueTransformer = (inputValue: string) => InputValueType | Promise; export type InputComponent = azdata.TextComponent | azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent | RadioGroupLoadingComponentBuilder; export type InputComponentInfo = { component: T; getValue: () => Promise; onValueChanged: vscode.Event; - inputValueTransformer?: InputValueTransformer; isPassword?: boolean }; @@ -168,7 +166,7 @@ interface InputBoxInfo { */ function createInputBoxField({ context, inputBoxType = 'text' }: { context: FieldContext; inputBoxType?: azdata.InputBoxInputType; }) { const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles }); - const input = createInputBox(context.view, { + const input = createInputBoxInputInfo(context.view, { type: inputBoxType, defaultValue: context.fieldInfo.defaultValue, ariaLabel: context.fieldInfo.label, @@ -184,7 +182,7 @@ function createInputBoxField({ context, inputBoxType = 'text' }: { context: Fiel return input; } -export function createInputBox(view: azdata.ModelView, inputInfo: InputBoxInfo): InputComponentInfo { +export function createInputBoxInputInfo(view: azdata.ModelView, inputInfo: InputBoxInfo): InputComponentInfo { const component = view.modelBuilder.inputBox().withProperties({ value: inputInfo.defaultValue, ariaLabel: inputInfo.ariaLabel, @@ -198,7 +196,7 @@ export function createInputBox(view: azdata.ModelView, inputInfo: InputBoxInfo): }).withValidation(async (component) => await validateInputBoxComponent(component, inputInfo.validations)).component(); return { component: component, - getValue: async (): Promise => component.value || '', + getValue: async (): Promise => component.value, onValueChanged: component.onTextChanged }; } @@ -229,11 +227,19 @@ export function createLabel(view: azdata.ModelView, info: { text: string, descri * @param view - the ModelView object used to create the inputBox * @param info - an object to define the properties of the 'number' inputBox component. If the type property is set then it is overridden with 'number' type. */ -export function createNumberInput(view: azdata.ModelView, info: InputBoxInfo): InputComponentInfo { +export function createNumberInputBoxInputInfo(view: azdata.ModelView, info: InputBoxInfo): InputComponentInfo { info.type = 'number'; // for the type to be 'number' - return createInputBox(view, info); + return createInputBoxInputInfo(view, info); } +export function createCheckboxInputInfo(view: azdata.ModelView, info: { initialValue: boolean, label: string, required?: boolean }): InputComponentInfo { + const checkbox = createCheckbox(view, info); + return { + component: checkbox, + getValue: async () => checkbox.checked ? 'true' : 'false', + onValueChanged: checkbox.onChanged + }; +} export function createCheckbox(view: azdata.ModelView, info: { initialValue: boolean, label: string, required?: boolean }): azdata.CheckBoxComponent { return view.modelBuilder.checkBox().withProperties({ checked: info.initialValue, @@ -242,8 +248,8 @@ export function createCheckbox(view: azdata.ModelView, info: { initialValue: boo }).component(); } -export function createDropdown(view: azdata.ModelView, info: { defaultValue?: string | azdata.CategoryValue, values?: string[] | azdata.CategoryValue[], width?: string, editable?: boolean, required?: boolean, label: string }): azdata.DropDownComponent { - return view.modelBuilder.dropDown().withProperties({ +export function createDropdownInputInfo(view: azdata.ModelView, info: { defaultValue?: string | azdata.CategoryValue, values?: string[] | azdata.CategoryValue[], width?: string, editable?: boolean, required?: boolean, label: string }): InputComponentInfo { + const dropdown = view.modelBuilder.dropDown().withProperties({ values: info.values, value: info.defaultValue, width: info.width, @@ -252,6 +258,12 @@ export function createDropdown(view: azdata.ModelView, info: { defaultValue?: st required: info.required, ariaLabel: info.label }).component(); + + return { + component: dropdown, + getValue: async (): Promise => typeof dropdown.value === 'string' ? dropdown.value : dropdown.value?.name, + onValueChanged: dropdown.onValueChanged, + }; } export function initializeDialog(dialogContext: DialogContext): void { @@ -451,7 +463,7 @@ export function createGroupContainer(view: azdata.ModelView, items: azdata.Compo return view.modelBuilder.groupContainer().withItems(items).withLayout(layout).component(); } -function addLabelInputPairToContainer(view: azdata.ModelView, components: azdata.Component[], label: azdata.Component, input: azdata.Component | undefined, fieldInfo: FieldInfo, additionalComponents?: azdata.Component[]) { +function addLabelInputPairToContainer(view: azdata.ModelView, components: azdata.Component[], label: azdata.Component, input: azdata.Component | undefined, fieldInfo: FieldInfo, additionalComponents?: azdata.Component[]): void { const inputs: azdata.Component[] = [label]; if (input !== undefined) { inputs.push(input); @@ -479,7 +491,7 @@ async function processField(context: FieldContext): Promise { } }, () => context.inputComponents[context.fieldInfo.variableName || context.fieldInfo.label].getValue(), // callback to fetch the value of this field, and return the default value if the field value is undefined - (variable: string) => getInputComponentValue(context.inputComponents[variable]), // callback to fetch the value of a variable corresponding to any field already defined. + (variable: string) => context.inputComponents[variable].getValue(), // callback to fetch the value of a variable corresponding to any field already defined. (targetVariable: string) => (context.inputComponents[targetVariable].component).onValidityChanged, (disposable: vscode.Disposable) => context.onNewDisposableCreated(disposable) ))); @@ -581,16 +593,18 @@ async function processOptionsTypeField(context: FieldContext): Promise { } } -async function configureOptionsSourceSubfields(context: FieldContext, optionsSource: IOptionsSource, variableKey: string, optionsComponent: RadioGroupLoadingComponentBuilder | azdata.DropDownComponent, optionsSourceProvider: IOptionsSourceProvider) { +async function configureOptionsSourceSubfields(context: FieldContext, optionsSource: IOptionsSource, variableKey: string, optionsComponent: RadioGroupLoadingComponentBuilder | azdata.DropDownComponent, optionsSourceProvider: IOptionsSourceProvider): Promise { context.fieldInfo.subFields!.push({ label: context.fieldInfo.label, variableName: optionsSource.variableNames![variableKey] }); context.onNewInputComponentCreated(optionsSource.variableNames![variableKey], { component: optionsComponent, - inputValueTransformer: async (optionValue: string) => { + isPassword: await optionsSourceProvider.getIsPassword!(variableKey), + getValue: async (): Promise => { + const value = (typeof optionsComponent.value === 'string' ? optionsComponent.value : optionsComponent.value?.name) || ''; try { - return await optionsSourceProvider.getVariableValue!(variableKey, optionValue); + return await optionsSourceProvider.getVariableValue!(variableKey, value); } catch (e) { disableControlButtons(context.container); context.container.message = { @@ -601,8 +615,6 @@ async function configureOptionsSourceSubfields(context: FieldContext, optionsSou throw e; } }, - isPassword: await optionsSourceProvider.getIsPassword!(variableKey), - getValue: async (): Promise => (typeof optionsComponent.value === 'string' ? optionsComponent.value : optionsComponent.value?.displayName) || '', onValueChanged: optionsComponent.onValueChanged }); } @@ -610,7 +622,7 @@ async function configureOptionsSourceSubfields(context: FieldContext, optionsSou function processDropdownOptionsTypeField(context: FieldContext): azdata.DropDownComponent { const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles }); const options = context.fieldInfo.options as OptionsInfo; - const dropdown = createDropdown(context.view, { + const dropdown = createDropdownInputInfo(context.view, { values: options.values, defaultValue: options.defaultValue, width: context.fieldInfo.inputWidth, @@ -618,33 +630,27 @@ function processDropdownOptionsTypeField(context: FieldContext): azdata.DropDown required: context.fieldInfo.required, label: context.fieldInfo.label }); - dropdown.fireOnTextChange = true; - context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { - getValue: async (): Promise => typeof dropdown.value === 'string' ? dropdown.value : dropdown.value?.displayName || '', - onValueChanged: dropdown.onValueChanged, - component: dropdown - }); - addLabelInputPairToContainer(context.view, context.components, label, dropdown, context.fieldInfo); - return dropdown; + dropdown.component.fireOnTextChange = true; + context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, dropdown); + addLabelInputPairToContainer(context.view, context.components, label, dropdown.component, context.fieldInfo); + return dropdown.component; } function processDateTimeTextField(context: FieldContext): void { context.fieldInfo.defaultValue = context.fieldInfo.defaultValue + getDateTimeString(); const input = createInputBoxField({ context }); - context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { - component: input.component, - getValue: async (): Promise => input.component.value || '', - onValueChanged: input.component.onTextChanged - }); + context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, input); } function processNumberField(context: FieldContext): void { const input = createInputBoxField({ context, inputBoxType: 'number' }); context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: input.component, - getValue: input.getValue, - onValueChanged: input.onValueChanged, - inputValueTransformer: (value: string | number | undefined) => (typeof value === 'string') && value.length > 0 ? parseFloat(value) : value + getValue: async (): Promise => { + const value = await input.getValue(); + return typeof value === 'string' && value.length > 0 ? parseFloat(value) : value; + }, + onValueChanged: input.onValueChanged }); } @@ -655,8 +661,8 @@ function processTextField(context: FieldContext): InputComponentInfo(); // Stub event since we don't currently supp + const onChangedEmitter = new vscode.EventEmitter(); // Stub event since we don't currently support updating this when the dependent fields change context.onNewDisposableCreated(onChangedEmitter); context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: readOnlyField.text!, - getValue: async (): Promise => readOnlyField.text?.value || '', - onValueChanged: onChangedEmitter.event, - inputValueTransformer: async () => { + getValue: async (): Promise => { readOnlyField.text!.value = await substituteVariableValues(context.inputComponents, context.fieldInfo.defaultValue); - return readOnlyField.text?.value!; - } + return readOnlyField.text!.value; + }, + onValueChanged: onChangedEmitter.event, }); return readOnlyField; } @@ -759,7 +764,7 @@ async function substituteVariableValues(inputComponents: InputComponents, inputV await Promise.all(Object.keys(inputComponents) .filter(key => key.startsWith(NoteBookEnvironmentVariablePrefix)) .map(async key => { - const value = (await getInputComponentValue(inputComponents[key])) ?? ''; + const value = (await inputComponents[key].getValue()) ?? ''; const re: RegExp = new RegExp(`\\\$\\\(${key}\\\)`, 'gi'); inputValue = inputValue?.replace(re, value.toString()); }) @@ -768,17 +773,9 @@ async function substituteVariableValues(inputComponents: InputComponents, inputV } function processCheckboxField(context: FieldContext): void { - const checkbox = createCheckbox(context.view, { initialValue: context.fieldInfo.defaultValue! === 'true', label: context.fieldInfo.label, required: context.fieldInfo.required }); - context.components.push(checkbox); - const onChangedEmitter = new vscode.EventEmitter(); - context.onNewDisposableCreated(onChangedEmitter); - const onChangedEvent = checkbox.onChanged(() => onChangedEmitter.fire()); - context.onNewDisposableCreated(onChangedEvent); - context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { - getValue: async (): Promise => checkbox.checked?.toString() || '', - onValueChanged: onChangedEmitter.event, - component: checkbox - }); + const checkbox = createCheckboxInputInfo(context.view, { initialValue: context.fieldInfo.defaultValue! === 'true', label: context.fieldInfo.label, required: context.fieldInfo.required }); + context.components.push(checkbox.component); + context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, checkbox); } /** @@ -790,7 +787,7 @@ function processFilePickerField(context: FieldContext): FilePickerInputs { const buttonWidth = 100; const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles }); - const input = createInputBox(context.view, { + const input = createInputBoxInputInfo(context.view, { defaultValue: context.fieldInfo.defaultValue || '', ariaLabel: context.fieldInfo.label, required: context.fieldInfo.required, @@ -929,7 +926,7 @@ async function createRadioOptions(context: FieldContext, getRadioButtonInfo?: (( context.fieldInfo.labelPosition = LabelPosition.Left; context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: radioGroupLoadingComponentBuilder, - getValue: async (): Promise => radioGroupLoadingComponentBuilder.value || '', + getValue: async (): Promise => radioGroupLoadingComponentBuilder.value, onValueChanged: radioGroupLoadingComponentBuilder.onValueChanged, }); addLabelInputPairToContainer(context.view, context.components, label, radioGroupLoadingComponentBuilder.component(), context.fieldInfo); @@ -965,15 +962,11 @@ async function processAzureAccountField(context: AzureAccountFieldContext): Prom const subscriptionDropdown = createAzureSubscriptionDropdown(context, subscriptionValueToSubscriptionMap); const resourceGroupDropdown = createAzureResourceGroupsDropdown(context, accountDropdown, accountValueToAccountMap, subscriptionDropdown, subscriptionValueToSubscriptionMap); if (context.fieldInfo.allowNewResourceGroup) { - const newRGCheckbox = createCheckbox(context.view, { initialValue: false, label: loc.createNewResourceGroup }); - context.onNewInputComponentCreated(context.fieldInfo.newResourceGroupFlagVariableName!, { - component: newRGCheckbox, - getValue: async (): Promise => newRGCheckbox.checked?.toString() || '', - onValueChanged: newRGCheckbox.onChanged - }); - const newRGNameInput = createInputBox(context.view, { ariaLabel: loc.NewResourceGroupAriaLabel }); + const newRGCheckbox = createCheckboxInputInfo(context.view, { initialValue: false, label: loc.createNewResourceGroup }); + context.onNewInputComponentCreated(context.fieldInfo.newResourceGroupFlagVariableName!, newRGCheckbox); + const newRGNameInput = createInputBoxInputInfo(context.view, { ariaLabel: loc.NewResourceGroupAriaLabel }); context.onNewInputComponentCreated(context.fieldInfo.newResourceGroupNameVariableName!, newRGNameInput); - context.components.push(newRGCheckbox); + context.components.push(newRGCheckbox.component); context.components.push(newRGNameInput.component); const setRGStatus = (newRG: boolean) => { resourceGroupDropdown.required = !newRG; @@ -984,8 +977,8 @@ async function processAzureAccountField(context: AzureAccountFieldContext): Prom newRGNameInput.component.value = ''; } }; - context.onNewDisposableCreated(newRGCheckbox.onChanged(() => { - setRGStatus(newRGCheckbox.checked!); + context.onNewDisposableCreated(newRGCheckbox.onValueChanged(() => { + setRGStatus(newRGCheckbox.component.checked!); })); setRGStatus(false); } @@ -1049,7 +1042,7 @@ async function processKubeStorageClassField(context: FieldContext): Promise => (typeof storageClassDropdown.value === 'string' ? storageClassDropdown.value : storageClassDropdown.value?.displayName) || '', - onValueChanged: storageClassDropdown.onValueChanged - }); - addLabelInputPairToContainer(context.view, context.components, label, storageClassDropdown, context.fieldInfo); + storageClassDropdown.component.fireOnTextChange = true; + context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, storageClassDropdown); + addLabelInputPairToContainer(context.view, context.components, label, storageClassDropdown.component, context.fieldInfo); } @@ -1079,26 +1068,22 @@ function createAzureAccountDropdown(context: AzureAccountFieldContext): AzureAcc width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles }); - const accountDropdown = createDropdown(context.view, { + const accountDropdown = createDropdownInputInfo(context.view, { width: context.fieldInfo.inputWidth, editable: false, required: context.fieldInfo.required, label: loc.account }); - accountDropdown.fireOnTextChange = true; - context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { - component: accountDropdown, - getValue: async (): Promise => (typeof accountDropdown.value === 'string' ? accountDropdown.value : accountDropdown.value?.displayName) || '', - onValueChanged: accountDropdown.onValueChanged - }); + accountDropdown.component.fireOnTextChange = true; + context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, accountDropdown); const signInButton = context.view!.modelBuilder.button().withProperties({ label: loc.signIn, width: '100px' }).component(); const refreshButton = context.view!.modelBuilder.button().withProperties({ label: loc.refresh, width: '100px' }).component(); - addLabelInputPairToContainer(context.view, context.components, label, accountDropdown, context.fieldInfo); + addLabelInputPairToContainer(context.view, context.components, label, accountDropdown.component, context.fieldInfo); const buttons = createFlexContainer(context.view!, [signInButton, refreshButton], true, undefined, undefined, undefined, { 'margin-right': '10px' }); context.components.push(buttons); return { - accountDropdown: accountDropdown, + accountDropdown: accountDropdown.component, signInButton: signInButton, refreshAccountsButton: refreshButton }; @@ -1115,39 +1100,35 @@ function createAzureSubscriptionDropdown( width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles }); - const subscriptionDropdown = createDropdown(context.view, { + const subscriptionDropdown = createDropdownInputInfo(context.view, { defaultValue: (context.fieldInfo.required) ? undefined : '', width: context.fieldInfo.inputWidth, editable: false, required: context.fieldInfo.required, label: loc.subscription }); - subscriptionDropdown.fireOnTextChange = true; + subscriptionDropdown.component.fireOnTextChange = true; context.fieldInfo.subFields!.push({ label: label.value!, variableName: context.fieldInfo.subscriptionVariableName }); context.onNewInputComponentCreated(context.fieldInfo.subscriptionVariableName || context.fieldInfo.label, { - component: subscriptionDropdown, - getValue: async (): Promise => (typeof subscriptionDropdown.value === 'string' ? subscriptionDropdown.value : subscriptionDropdown.value?.displayName) || '', - onValueChanged: subscriptionDropdown.onValueChanged, - inputValueTransformer: (inputValue: string) => { + component: subscriptionDropdown.component, + getValue: async (): Promise => { + const inputValue = (await subscriptionDropdown.getValue())?.toString() || ''; return subscriptionValueToSubscriptionMap.get(inputValue)?.id || inputValue; - } + }, + onValueChanged: subscriptionDropdown.onValueChanged }); if (context.fieldInfo.displaySubscriptionVariableName) { context.fieldInfo.subFields!.push({ label: label.value!, variableName: context.fieldInfo.displaySubscriptionVariableName }); - context.onNewInputComponentCreated(context.fieldInfo.displaySubscriptionVariableName!, { - component: subscriptionDropdown, - getValue: async (): Promise => (typeof subscriptionDropdown.value === 'string' ? subscriptionDropdown.value : subscriptionDropdown.value?.displayName) || '', - onValueChanged: subscriptionDropdown.onValueChanged, - }); + context.onNewInputComponentCreated(context.fieldInfo.displaySubscriptionVariableName!, subscriptionDropdown); } - addLabelInputPairToContainer(context.view, context.components, label, subscriptionDropdown, context.fieldInfo); - return subscriptionDropdown; + addLabelInputPairToContainer(context.view, context.components, label, subscriptionDropdown.component, context.fieldInfo); + return subscriptionDropdown.component; } async function handleSelectedAccountChanged( @@ -1259,33 +1240,29 @@ function createAzureResourceGroupsDropdown( width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles }); - const resourceGroupDropdown = createDropdown(context.view, { + const resourceGroupDropdown = createDropdownInputInfo(context.view, { defaultValue: (context.fieldInfo.required) ? undefined : '', width: context.fieldInfo.inputWidth, editable: false, required: context.fieldInfo.required, label: loc.resourceGroup }); - resourceGroupDropdown.fireOnTextChange = true; + resourceGroupDropdown.component.fireOnTextChange = true; context.fieldInfo.subFields!.push({ label: label.value!, variableName: context.fieldInfo.resourceGroupVariableName }); const rgValueChangedEmitter = new vscode.EventEmitter(); resourceGroupDropdown.onValueChanged(() => rgValueChangedEmitter.fire()); - context.onNewInputComponentCreated(context.fieldInfo.resourceGroupVariableName || context.fieldInfo.label, { - component: resourceGroupDropdown, - getValue: async (): Promise => (typeof resourceGroupDropdown.value === 'string' ? resourceGroupDropdown.value : resourceGroupDropdown.value?.displayName) || '', - onValueChanged: resourceGroupDropdown.onValueChanged, - }); - addLabelInputPairToContainer(context.view, context.components, label, resourceGroupDropdown, context.fieldInfo); + context.onNewInputComponentCreated(context.fieldInfo.resourceGroupVariableName || context.fieldInfo.label, resourceGroupDropdown); + addLabelInputPairToContainer(context.view, context.components, label, resourceGroupDropdown.component, context.fieldInfo); subscriptionDropdown.onValueChanged(async selectedItem => { const selectedAccount = !accountDropdown || !accountDropdown.value ? undefined : accountValueToAccountMap.get(accountDropdown.value.toString()); const selectedSubscription = subscriptionValueToSubscriptionMap.get(selectedItem.selected); - await handleSelectedSubscriptionChanged(context, selectedAccount, selectedSubscription, resourceGroupDropdown); + await handleSelectedSubscriptionChanged(context, selectedAccount, selectedSubscription, resourceGroupDropdown.component); rgValueChangedEmitter.fire(); }); - return resourceGroupDropdown; + return resourceGroupDropdown.component; } async function handleSelectedSubscriptionChanged(context: AzureAccountFieldContext, selectedAccount: azdata.Account | undefined, selectedSubscription: azureResource.AzureResourceSubscription | undefined, resourceGroupDropdown: azdata.DropDownComponent): Promise { @@ -1347,7 +1324,7 @@ async function processAzureLocationsField(context: AzureLocationsFieldContext): cssStyles: context.fieldInfo.labelCSSStyles }); const locationValues = context.fieldInfo.locations?.map(l => { return { name: l, displayName: apiService.azurecoreApi.getRegionDisplayName(l) }; }); - const locationDropdown = createDropdown(context.view, { + const locationDropdown = createDropdownInputInfo(context.view, { defaultValue: locationValues?.find(l => l.name === context.fieldInfo.defaultValue), width: context.fieldInfo.inputWidth, editable: false, @@ -1355,33 +1332,33 @@ async function processAzureLocationsField(context: AzureLocationsFieldContext): label: loc.location, values: locationValues }); - locationDropdown.fireOnTextChange = true; + locationDropdown.component.fireOnTextChange = true; context.fieldInfo.subFields = context.fieldInfo.subFields || []; if (context.fieldInfo.locationVariableName) { context.fieldInfo.subFields!.push({ label: label.value!, variableName: context.fieldInfo.locationVariableName }); - context.onNewInputComponentCreated(context.fieldInfo.locationVariableName, { - component: locationDropdown, - getValue: async (): Promise => (typeof locationDropdown.value === 'string' ? locationDropdown.value : locationDropdown.value?.displayName) || '', - onValueChanged: locationDropdown.onValueChanged, - }); + context.onNewInputComponentCreated(context.fieldInfo.locationVariableName, locationDropdown); } if (context.fieldInfo.displayLocationVariableName) { context.fieldInfo.subFields!.push({ label: label.value!, variableName: context.fieldInfo.displayLocationVariableName }); + // Create a special input component that maps the dropdown to the display name for the location + // so that we have two variables - one for the value and one for the display name context.onNewInputComponentCreated(context.fieldInfo.displayLocationVariableName, { - component: locationDropdown, - inputValueTransformer: (value => apiService.azurecoreApi.getRegionDisplayName(value)), - getValue: async (): Promise => (typeof locationDropdown.value === 'string' ? locationDropdown.value : locationDropdown.value?.displayName) || '', + component: locationDropdown.component, + getValue: async (): Promise => { + const inputValue = (await locationDropdown.getValue())?.toString(); + return apiService.azurecoreApi.getRegionDisplayName(inputValue); + }, onValueChanged: locationDropdown.onValueChanged, }); } - addLabelInputPairToContainer(context.view, context.components, label, locationDropdown, context.fieldInfo); - return locationDropdown; + addLabelInputPairToContainer(context.view, context.components, label, locationDropdown.component, context.fieldInfo); + return locationDropdown.component; } export function isValidSQLPassword(password: string, userName: string = 'sa'): boolean { @@ -1413,34 +1390,11 @@ export function getPasswordMismatchMessage(fieldName: string): string { export async function setModelValues(inputComponents: InputComponents, model: Model): Promise { await Promise.all(Object.keys(inputComponents).map(async key => { - const value = await getInputComponentValue(inputComponents[key]); + const value = await inputComponents[key].getValue(); model.setPropertyValue(key, value); })); } -async function getInputComponentValue(inputComponentInfo: InputComponentInfo): Promise { - const input = inputComponentInfo.component; - if (input === undefined) { - return undefined; - } - let value: string | number | undefined; - if (input instanceof RadioGroupLoadingComponentBuilder) { - value = input.value; - } else if ('checked' in input) { // CheckBoxComponent - value = input.checked ? 'true' : 'false'; - } else if ('value' in input) { // InputBoxComponent or DropDownComponent - const inputValue = input.value; - if (typeof inputValue === 'string' || typeof inputValue === 'undefined' || typeof inputValue === 'number') { - value = inputValue; - } else { - value = inputValue.name; - } - } else { - throw new Error(`Unknown input type with ID ${input.id}`); - } - return inputComponentInfo.inputValueTransformer ? await inputComponentInfo.inputValueTransformer(value ?? '') : value; -} - export function isInputBoxEmpty(input: azdata.InputBoxComponent): boolean { return input.value === undefined || input.value === ''; }