From 175d46d508538a526b27166e6bb5eb1e9254968c Mon Sep 17 00:00:00 2001 From: Charles Gagnon Date: Thu, 19 Nov 2020 16:23:28 -0800 Subject: [PATCH] Add support for dynamic enablement of resource deployment components (#13464) * saving wip to merge main * temp fixes for textValidation* removal * save wip to switch tasks * save wip to switch tasks * save wip to switch tasks * code complete - with known bugs * missed test file * fix extHostModelView changes * validation module * missed test changes * missed change * pr feedback * pr feedback * revert inadvertent change * remove unneeded change * merge from bug/12082-2 * pr feedback * pr feedback * bdd -> tdd for validation tests * bdd -> tdd for validation tests * pr feedback * remove unneeded file * pr feedback * EOL instead of '\n' * pr feedback * pr feedback * minor fixes. * pr feedback * fix comment * comments and var renames * test fixes * working version after validation simplification * working version after validation simplification * remove inadvertent change * simplified validations * undo uneeded change * cleanup * working version after latest merge * comments and whitespace fixes * remove is_integer checks * sentence case field validation messages * Use generic strings in sample fields * minor fixes to sample extension strings * spaces to tabs for indentation * request fields before limit fields * reaarange request/limit fields * is_integer checks for PG Server Group number fields * Thenable to Promise * InputBoxInfo * pr feedback * pr feedback * isUndefinedOrEmpty to utils * include asde package.json * use ValidationValueType * ValidationValueType -> InputValueType * Add support for dynamic enablement of resource deployment components * use instanceof function * getValue returns InputValueType Co-authored-by: Arvind Ranasaria --- .../resource-deployment/src/interfaces.ts | 11 +- .../pages/azureSettingsPage.ts | 4 +- .../pages/clusterSettingsPage.ts | 8 +- .../pages/serviceSettingsPage.ts | 98 +++++---- .../src/ui/deploymentInputDialog.ts | 4 +- .../src/ui/modelViewUtils.ts | 205 ++++++++++++++---- .../ui/notebookWizard/notebookWizardPage.ts | 4 +- .../ui/radioGroupLoadingComponentBuilder.ts | 10 +- 8 files changed, 241 insertions(+), 103 deletions(-) diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index b6059651e4..36a90a8b52 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -211,6 +211,10 @@ export function instanceOfCommandBasedDialogInfo(obj: any): obj is CommandBasedD return obj && 'command' in obj; } +export function instanceOfDynamicEnablementInfo(obj: any): obj is DynamicEnablementInfo { + return (obj)?.target !== undefined && (obj)?.value !== undefined; +} + export interface DialogInfoBase { title: string; name: string; @@ -252,6 +256,11 @@ export interface OptionsInfo { optionsType?: OptionsType } +export interface DynamicEnablementInfo { + target: string, + value: string +} + export interface FieldInfoBase { labelWidth?: string; inputWidth?: string; @@ -296,7 +305,7 @@ export interface FieldInfo extends SubFieldInfo, FieldInfoBase { fontWeight?: FontWeight; links?: azdata.LinkArea[]; editable?: boolean; // for editable drop-down, - enabled?: boolean; + enabled?: boolean | DynamicEnablementInfo; isEvaluated?: boolean; valueLookup?: string; // for fetching dropdown options validationLookup?: string // for fetching text field validations diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts index b1947ef7d0..04e95f4169 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts @@ -7,7 +7,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { FieldType, LabelPosition, SectionInfo } from '../../../interfaces'; -import { createSection, getDropdownComponent, InputComponentInfo, InputComponents, setModelValues, Validator } from '../../modelViewUtils'; +import { createSection, getDropdownComponent, InputComponent, InputComponentInfo, InputComponents, setModelValues, Validator } from '../../modelViewUtils'; import { AksName_VariableName, Location_VariableName, ResourceGroup_VariableName, SubscriptionId_VariableName, VMCount_VariableName, VMSize_VariableName } from '../constants'; import { AzureRegion } from 'azurecore'; import { DeployClusterWizardModel } from '../deployClusterWizardModel'; @@ -132,7 +132,7 @@ export class AzureSettingsPage extends ResourceTypePage { onNewDisposableCreated: (disposable: vscode.Disposable): void => { self.wizard.registerDisposable(disposable); }, - onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { + onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { self.inputComponents[name] = inputComponentInfo; self._model.inputComponents[name] = inputComponentInfo; }, diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts index 455eaa2a6f..1f3a7f2f24 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts @@ -9,7 +9,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { FieldType, LabelPosition, SectionInfo } from '../../../interfaces'; import * as localizedConstants from '../../../localizedConstants'; -import { createSection, getInputBoxComponent, getInvalidSQLPasswordMessage, getPasswordMismatchMessage, InputComponentInfo, InputComponents, isValidSQLPassword, setModelValues, Validator } from '../../modelViewUtils'; +import { createSection, getInputBoxComponent, getInvalidSQLPasswordMessage, getPasswordMismatchMessage, InputComponent, InputComponentInfo, InputComponents, isValidSQLPassword, setModelValues, Validator } from '../../modelViewUtils'; import { ResourceTypePage } from '../../resourceTypePage'; import { ValidationType } from '../../validation/validations'; import * as VariableNames from '../constants'; @@ -219,7 +219,7 @@ export class ClusterSettingsPage extends ResourceTypePage { onNewDisposableCreated: (disposable: vscode.Disposable): void => { self.wizard.registerDisposable(disposable); }, - onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { + onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { self.inputComponents[name] = inputComponentInfo; self._model.inputComponents[name] = inputComponentInfo; }, @@ -236,7 +236,7 @@ export class ClusterSettingsPage extends ResourceTypePage { onNewDisposableCreated: (disposable: vscode.Disposable): void => { self.wizard.registerDisposable(disposable); }, - onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { + onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { self.inputComponents[name] = inputComponentInfo; self._model.inputComponents[name] = inputComponentInfo; }, @@ -253,7 +253,7 @@ export class ClusterSettingsPage extends ResourceTypePage { onNewDisposableCreated: (disposable: vscode.Disposable): void => { self.wizard.registerDisposable(disposable); }, - onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { + onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { self.inputComponents[name] = inputComponentInfo; self._model.inputComponents[name] = inputComponentInfo; }, diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts index 10827234e5..2e3fb35b1c 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 } from '../../modelViewUtils'; +import { createFlexContainer, createGroupContainer, createLabel, createNumberInput, createSection, createInputBox, getCheckboxComponent, getDropdownComponent, getInputBoxComponent, InputComponentInfo, InputComponents, setModelValues, Validator, InputComponent } from '../../modelViewUtils'; import { ResourceTypePage } from '../../resourceTypePage'; import * as VariableNames from '../constants'; import { AuthenticationMode, DeployClusterWizardModel } from '../deployClusterWizardModel'; @@ -124,7 +124,7 @@ export class ServiceSettingsPage extends ResourceTypePage { onNewDisposableCreated: (disposable: vscode.Disposable): void => { self.wizard.registerDisposable(disposable); }, - onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { + onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { self.onNewInputComponentCreated(name, inputComponentInfo); }, onNewValidatorCreated: (validator: Validator): void => { @@ -153,7 +153,7 @@ export class ServiceSettingsPage extends ResourceTypePage { }); } - private onNewInputComponentCreated(name: string, inputComponentInfo: InputComponentInfo) { + private onNewInputComponentCreated(name: string, inputComponentInfo: InputComponentInfo) { this.inputComponents[name] = inputComponentInfo; this._model.inputComponents[name] = inputComponentInfo; } @@ -175,46 +175,58 @@ 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 }); - this.controllerDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.ControllerDNSName', "Controller DNS name"), required: false, width: inputWidth }); - this.controllerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ControllerPortName', "Controller port"), required: true, width: NumberInputWidth, min: 1 }); + const controllerDNSInput = createInputBox(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 }); + this.controllerPortInput = controllerPortInput.component; this.controllerEndpointRow = createFlexContainer(view, [this.controllerNameLabel, this.controllerDNSInput, this.controllerPortInput]); - this.onNewInputComponentCreated(VariableNames.ControllerDNSName_VariableName, { component: this.controllerDNSInput }); - this.onNewInputComponentCreated(VariableNames.ControllerPort_VariableName, { component: 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 }); - this.sqlServerDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.MasterSQLServerDNSName', "SQL Server Master DNS name"), required: false, width: inputWidth }); - this.sqlServerPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.MasterSQLServerPortName', "SQL Server Master port"), required: true, width: NumberInputWidth, min: 1 }); + const sqlServerDNSInput = createInputBox(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 }); + this.sqlServerPortInput = sqlServerPortInput.component; this.sqlServerEndpointRow = createFlexContainer(view, [this.SqlServerNameLabel, this.sqlServerDNSInput, this.sqlServerPortInput]); - this.onNewInputComponentCreated(VariableNames.SQLServerDNSName_VariableName, { component: this.sqlServerDNSInput }); - this.onNewInputComponentCreated(VariableNames.SQLServerPort_VariableName, { component: 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 }); - this.gatewayDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.GatewayDNSName', "Gateway DNS name"), required: false, width: inputWidth }); - this.gatewayPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.GatewayPortName', "Gateway port"), required: true, width: NumberInputWidth, min: 1 }); + const gatewayDNSInput = createInputBox(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 }); + this.gatewayPortInput = gatewayPortInput.component; this.gatewayEndpointRow = createFlexContainer(view, [this.gatewayNameLabel, this.gatewayDNSInput, this.gatewayPortInput]); - this.onNewInputComponentCreated(VariableNames.GatewayDNSName_VariableName, { component: this.gatewayDNSInput }); - this.onNewInputComponentCreated(VariableNames.GateWayPort_VariableName, { component: 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 }); - this.serviceProxyDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.ServiceProxyDNSName', "Management proxy DNS name"), required: false, width: inputWidth }); - this.serviceProxyPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ServiceProxyPortName', "Management proxy port"), required: true, width: NumberInputWidth, min: 1 }); + const serviceProxyDNSInput = createInputBox(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 }); + this.serviceProxyPortInput = serviceProxyPortInput.component; this.serviceProxyEndpointRow = createFlexContainer(view, [this.serviceProxyNameLabel, this.serviceProxyDNSInput, this.serviceProxyPortInput]); - this.onNewInputComponentCreated(VariableNames.ServiceProxyDNSName_VariableName, { component: this.serviceProxyDNSInput }); - this.onNewInputComponentCreated(VariableNames.ServiceProxyPort_VariableName, { component: 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 }); - this.appServiceProxyDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.AppServiceProxyDNSName', "Application proxy DNS name"), required: false, width: inputWidth }); - this.appServiceProxyPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.AppServiceProxyPortName', "Application proxy port"), required: true, width: NumberInputWidth, min: 1 }); + const appServiceProxyDNSInput = createInputBox(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 }); + this.appServiceProxyPortInput = appServiceProxyPortInput.component; this.appServiceProxyEndpointRow = createFlexContainer(view, [this.appServiceProxyNameLabel, this.appServiceProxyDNSInput, this.appServiceProxyPortInput]); - this.onNewInputComponentCreated(VariableNames.AppServiceProxyDNSName_VariableName, { component: this.appServiceProxyDNSInput }); - this.onNewInputComponentCreated(VariableNames.AppServiceProxyPort_VariableName, { component: 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 }); - this.readableSecondaryDNSInput = createInputBox(view, { ariaLabel: localize('deployCluster.ReadableSecondaryDNSName', "Readable secondary DNS name"), required: false, width: inputWidth }); - this.readableSecondaryPortInput = createNumberInput(view, { ariaLabel: localize('deployCluster.ReadableSecondaryPortName', "Readable secondary port"), required: false, width: NumberInputWidth, min: 1 }); + const readableSecondaryDNSInput = createInputBox(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 }); + this.readableSecondaryPortInput = readableSecondaryPortInput.component; this.readableSecondaryEndpointRow = createFlexContainer(view, [this.readableSecondaryNameLabel, this.readableSecondaryDNSInput, this.readableSecondaryPortInput]); - this.onNewInputComponentCreated(VariableNames.ReadableSecondaryDNSName_VariableName, { component: this.readableSecondaryDNSInput }); - this.onNewInputComponentCreated(VariableNames.ReadableSecondaryPort_VariableName, { component: this.readableSecondaryPortInput }); + this.onNewInputComponentCreated(VariableNames.ReadableSecondaryDNSName_VariableName, readableSecondaryDNSInput); + this.onNewInputComponentCreated(VariableNames.ReadableSecondaryPort_VariableName, readableSecondaryPortInput); return createGroupContainer(view, [this.endpointHeaderRow, this.controllerEndpointRow, this.sqlServerEndpointRow, this.gatewayEndpointRow, this.serviceProxyEndpointRow, this.appServiceProxyEndpointRow, this.readableSecondaryEndpointRow], { header: localize('deployCluster.EndpointSettings', "Endpoint settings"), @@ -270,22 +282,22 @@ export class ServiceSettingsPage extends ResourceTypePage { 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 }); - this.onNewInputComponentCreated(VariableNames.ControllerDataStorageClassName_VariableName, { component: controllerDataStorageClassInput }); - this.onNewInputComponentCreated(VariableNames.ControllerDataStorageSize_VariableName, { component: controllerDataStorageClaimSizeInput }); - this.onNewInputComponentCreated(VariableNames.ControllerLogsStorageClassName_VariableName, { component: controllerLogsStorageClassInput }); - this.onNewInputComponentCreated(VariableNames.ControllerLogsStorageSize_VariableName, { component: controllerLogsStorageClaimSizeInput }); - this.onNewInputComponentCreated(VariableNames.HDFSDataStorageClassName_VariableName, { component: storagePoolDataStorageClassInput }); - this.onNewInputComponentCreated(VariableNames.HDFSDataStorageSize_VariableName, { component: storagePoolDataStorageClaimSizeInput }); - this.onNewInputComponentCreated(VariableNames.HDFSLogsStorageClassName_VariableName, { component: storagePoolLogsStorageClassInput }); - this.onNewInputComponentCreated(VariableNames.HDFSLogsStorageSize_VariableName, { component: storagePoolLogsStorageClaimSizeInput }); - this.onNewInputComponentCreated(VariableNames.DataPoolDataStorageClassName_VariableName, { component: dataPoolDataStorageClassInput }); - this.onNewInputComponentCreated(VariableNames.DataPoolDataStorageSize_VariableName, { component: dataPoolDataStorageClaimSizeInput }); - this.onNewInputComponentCreated(VariableNames.DataPoolLogsStorageClassName_VariableName, { component: dataPoolLogsStorageClassInput }); - this.onNewInputComponentCreated(VariableNames.DataPoolLogsStorageSize_VariableName, { component: dataPoolLogsStorageClaimSizeInput }); - this.onNewInputComponentCreated(VariableNames.SQLServerDataStorageClassName_VariableName, { component: sqlServerMasterDataStorageClassInput }); - this.onNewInputComponentCreated(VariableNames.SQLServerDataStorageSize_VariableName, { component: sqlServerMasterDataStorageClaimSizeInput }); - this.onNewInputComponentCreated(VariableNames.SQLServerLogsStorageClassName_VariableName, { component: sqlServerMasterLogsStorageClassInput }); - this.onNewInputComponentCreated(VariableNames.SQLServerLogsStorageSize_VariableName, { component: sqlServerMasterLogsStorageClaimSizeInput }); + this.onNewInputComponentCreated(VariableNames.ControllerDataStorageClassName_VariableName, controllerDataStorageClassInput); + this.onNewInputComponentCreated(VariableNames.ControllerDataStorageSize_VariableName, controllerDataStorageClaimSizeInput); + this.onNewInputComponentCreated(VariableNames.ControllerLogsStorageClassName_VariableName, controllerLogsStorageClassInput); + this.onNewInputComponentCreated(VariableNames.ControllerLogsStorageSize_VariableName, controllerLogsStorageClaimSizeInput); + this.onNewInputComponentCreated(VariableNames.HDFSDataStorageClassName_VariableName, storagePoolDataStorageClassInput); + this.onNewInputComponentCreated(VariableNames.HDFSDataStorageSize_VariableName, storagePoolDataStorageClaimSizeInput); + this.onNewInputComponentCreated(VariableNames.HDFSLogsStorageClassName_VariableName, storagePoolLogsStorageClassInput); + this.onNewInputComponentCreated(VariableNames.HDFSLogsStorageSize_VariableName, storagePoolLogsStorageClaimSizeInput); + this.onNewInputComponentCreated(VariableNames.DataPoolDataStorageClassName_VariableName, dataPoolDataStorageClassInput); + this.onNewInputComponentCreated(VariableNames.DataPoolDataStorageSize_VariableName, dataPoolDataStorageClaimSizeInput); + this.onNewInputComponentCreated(VariableNames.DataPoolLogsStorageClassName_VariableName, dataPoolLogsStorageClassInput); + this.onNewInputComponentCreated(VariableNames.DataPoolLogsStorageSize_VariableName, dataPoolLogsStorageClaimSizeInput); + this.onNewInputComponentCreated(VariableNames.SQLServerDataStorageClassName_VariableName, sqlServerMasterDataStorageClassInput); + this.onNewInputComponentCreated(VariableNames.SQLServerDataStorageSize_VariableName, sqlServerMasterDataStorageClaimSizeInput); + this.onNewInputComponentCreated(VariableNames.SQLServerLogsStorageClassName_VariableName, sqlServerMasterLogsStorageClassInput); + this.onNewInputComponentCreated(VariableNames.SQLServerLogsStorageSize_VariableName, sqlServerMasterLogsStorageClaimSizeInput); const storageSettingTable = view.modelBuilder.declarativeTable() .withProperties( diff --git a/extensions/resource-deployment/src/ui/deploymentInputDialog.ts b/extensions/resource-deployment/src/ui/deploymentInputDialog.ts index 003821413b..b6da25fafc 100644 --- a/extensions/resource-deployment/src/ui/deploymentInputDialog.ts +++ b/extensions/resource-deployment/src/ui/deploymentInputDialog.ts @@ -12,7 +12,7 @@ import { INotebookService } from '../services/notebookService'; import { IPlatformService } from '../services/platformService'; import { DialogBase } from './dialogBase'; import { Model } from './model'; -import { initializeDialog, InputComponentInfo, InputComponents, setModelValues, Validator } from './modelViewUtils'; +import { initializeDialog, InputComponent, InputComponentInfo, InputComponents, setModelValues, Validator } from './modelViewUtils'; import { IToolsService } from '../services/toolsService'; const localize = nls.loadMessageBundle(); @@ -69,7 +69,7 @@ export class DeploymentInputDialog extends DialogBase { onNewDisposableCreated: (disposable: vscode.Disposable): void => { this._toDispose.push(disposable); }, - onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { + onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { this.inputComponents[name] = inputComponentInfo; }, onNewValidatorCreated: (validator: Validator): void => { diff --git a/extensions/resource-deployment/src/ui/modelViewUtils.ts b/extensions/resource-deployment/src/ui/modelViewUtils.ts index b9686e5aca..8389bbcab4 100644 --- a/extensions/resource-deployment/src/ui/modelViewUtils.ts +++ b/extensions/resource-deployment/src/ui/modelViewUtils.ts @@ -11,7 +11,7 @@ import { IOptionsSourceProvider } from 'resource-deployment'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { getDateTimeString, getErrorMessage, throwUnless } from '../common/utils'; -import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, FilePickerFieldInfo, IOptionsSource, KubeClusterContextFieldInfo, LabelPosition, NoteBookEnvironmentVariablePrefix, OptionsInfo, OptionsType, PageInfoBase, RowInfo, SectionInfo, TextCSSStyles } from '../interfaces'; +import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, FilePickerFieldInfo, instanceOfDynamicEnablementInfo, IOptionsSource, KubeClusterContextFieldInfo, LabelPosition, NoteBookEnvironmentVariablePrefix, OptionsInfo, OptionsType, PageInfoBase, RowInfo, SectionInfo, TextCSSStyles } from '../interfaces'; import * as loc from '../localizedConstants'; import { apiService } from '../services/apiService'; import { getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../services/kubeService'; @@ -35,14 +35,16 @@ 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: InputComponent; +export type InputComponentInfo = { + component: T; + getValue: () => Promise; + onValueChanged: vscode.Event; inputValueTransformer?: InputValueTransformer; isPassword?: boolean }; export type InputComponents = { - [s: string]: InputComponentInfo + [s: string]: InputComponentInfo }; export function getInputBoxComponent(name: string, inputComponents: InputComponents): azdata.InputBoxComponent { @@ -124,7 +126,7 @@ interface ContextBase { inputComponents: InputComponents; onNewValidatorCreated: (validator: Validator) => void; onNewDisposableCreated: (disposable: vscode.Disposable) => void; - onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo) => void; + onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo) => void; } /** @@ -175,15 +177,15 @@ function createInputBoxField({ context, inputBoxType = 'text' }: { context: Fiel max: context.fieldInfo.max, placeHolder: context.fieldInfo.placeHolder, width: context.fieldInfo.inputWidth, - enabled: context.fieldInfo.enabled, + enabled: instanceOfDynamicEnablementInfo(context.fieldInfo.enabled) ? false : context.fieldInfo.enabled, // Dynamic enablement is initially set to false validations: context.fieldValidations }); - addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo); + addLabelInputPairToContainer(context.view, context.components, label, input.component, context.fieldInfo); return input; } -export function createInputBox(view: azdata.ModelView, inputInfo: InputBoxInfo): azdata.InputBoxComponent { - return view.modelBuilder.inputBox().withProperties({ +export function createInputBox(view: azdata.ModelView, inputInfo: InputBoxInfo): InputComponentInfo { + const component = view.modelBuilder.inputBox().withProperties({ value: inputInfo.defaultValue, ariaLabel: inputInfo.ariaLabel, inputType: inputInfo.type || 'text', @@ -194,6 +196,11 @@ export function createInputBox(view: azdata.ModelView, inputInfo: InputBoxInfo): width: inputInfo.width, enabled: inputInfo.enabled }).withValidation(async (component) => await validateInputBoxComponent(component, inputInfo.validations)).component(); + return { + component: component, + getValue: async (): Promise => component.value || '', + onValueChanged: component.onTextChanged + }; } export function createLabel(view: azdata.ModelView, info: { text: string, description?: string, required?: boolean, width?: string, links?: azdata.LinkArea[], cssStyles?: TextCSSStyles }): azdata.TextComponent { @@ -222,7 +229,7 @@ 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): azdata.InputBoxComponent { +export function createNumberInput(view: azdata.ModelView, info: InputBoxInfo): InputComponentInfo { info.type = 'number'; // for the type to be 'number' return createInputBox(view, info); } @@ -307,6 +314,7 @@ export function initializeWizardPage(context: WizardPageContext): void { sectionInfo: sectionInfo }); })); + await hookUpDynamicEnablement(context); const formBuilder = view.modelBuilder.formContainer().withFormItems( sections.map(section => { return { title: '', component: section }; }), { @@ -319,6 +327,49 @@ export function initializeWizardPage(context: WizardPageContext): void { }); } +/** + * Hooks up the dynamic enablement for fields which use that. This will attach a listener to the target component + * for when the value changes and update the enabled state of the source component based on the current value + * of the target component. + * + * Note that currently this is only supported for Notebook Wizard Pages and only supports direct equals comparison + * for the value currently selected. + * + * Additionally this only supports hooking up components that are on the same page. + * @param context The page context + */ +async function hookUpDynamicEnablement(context: WizardPageContext): Promise { + await Promise.all(context.pageInfo.sections.map(async section => { + if (!section.fields) { + return; + } + await Promise.all(section.fields.map(async field => { + if (instanceOfDynamicEnablementInfo(field.enabled)) { + const fieldKey = field.variableName || field.label; + const fieldComponent = context.inputComponents[fieldKey]; + const targetComponent = context.inputComponents[field.enabled.target]; + const targetValue = field.enabled.value; + if (!targetComponent) { + console.error(`Could not find target component ${field.enabled.target} when hooking up dynamic enablement for ${field.label}`); + return; + } + const updateFields = async () => { + const targetComponentValue = await targetComponent.getValue(); + fieldComponent.component.enabled = targetComponentValue === targetValue; + // We also need to update the required flag so that when the component is disabled it won't block the page from proceeding + if ('required' in fieldComponent.component) { + fieldComponent.component.required = fieldComponent.component.enabled === false ? false : field.required; + } + }; + targetComponent.onValueChanged(() => { + updateFields(); + }); + await updateFields(); + } + })); + })); +} + export async function createSection(context: SectionContext): Promise { const components: azdata.Component[] = []; context.sectionInfo.inputWidth = context.sectionInfo.inputWidth || DefaultInputWidth; @@ -427,7 +478,7 @@ async function processField(context: FieldContext): Promise { await inputBox.updateProperty('validationErrorMessage', validationMessage); } }, - () => getInputComponentValue(context.inputComponents[context.fieldInfo.variableName || context.fieldInfo.label]), // callback to fetch the value of this field, and return the default value if the field value is undefined + () => 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. (targetVariable: string) => (context.inputComponents[targetVariable].component).onValidityChanged, (disposable: vscode.Disposable) => context.onNewDisposableCreated(disposable) @@ -514,7 +565,7 @@ async function processOptionsTypeField(context: FieldContext): Promise { } context.fieldInfo.subFields = context.fieldInfo.subFields || []; } - let optionsComponent: InputComponent; + let optionsComponent: RadioGroupLoadingComponentBuilder | azdata.DropDownComponent; if (context.fieldInfo.options.optionsType === OptionsType.Radio) { optionsComponent = await processRadioOptionsTypeField(context); } else { @@ -530,7 +581,7 @@ async function processOptionsTypeField(context: FieldContext): Promise { } } -async function configureOptionsSourceSubfields(context: FieldContext, optionsSource: IOptionsSource, variableKey: string, optionsComponent: InputComponent, optionsSourceProvider: IOptionsSourceProvider) { +async function configureOptionsSourceSubfields(context: FieldContext, optionsSource: IOptionsSource, variableKey: string, optionsComponent: RadioGroupLoadingComponentBuilder | azdata.DropDownComponent, optionsSourceProvider: IOptionsSourceProvider) { context.fieldInfo.subFields!.push({ label: context.fieldInfo.label, variableName: optionsSource.variableNames![variableKey] @@ -550,7 +601,9 @@ async function configureOptionsSourceSubfields(context: FieldContext, optionsSou throw e; } }, - isPassword: await optionsSourceProvider.getIsPassword!(variableKey) + isPassword: await optionsSourceProvider.getIsPassword!(variableKey), + getValue: async (): Promise => (typeof optionsComponent.value === 'string' ? optionsComponent.value : optionsComponent.value?.displayName) || '', + onValueChanged: optionsComponent.onValueChanged }); } @@ -566,7 +619,11 @@ function processDropdownOptionsTypeField(context: FieldContext): azdata.DropDown label: context.fieldInfo.label }); dropdown.fireOnTextChange = true; - context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: dropdown }); + 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; } @@ -574,22 +631,29 @@ function processDropdownOptionsTypeField(context: FieldContext): azdata.DropDown 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 }); + context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { + component: input.component, + getValue: async (): Promise => input.component.value || '', + onValueChanged: input.component.onTextChanged + }); } function processNumberField(context: FieldContext): void { const input = createInputBoxField({ context, inputBoxType: 'number' }); context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { - component: input, + component: input.component, + getValue: input.getValue, + onValueChanged: input.onValueChanged, inputValueTransformer: (value: string | number | undefined) => (typeof value === 'string') && value.length > 0 ? parseFloat(value) : value }); } -function processTextField(context: FieldContext): azdata.InputBoxComponent { +function processTextField(context: FieldContext): InputComponentInfo { const isPasswordField = context.fieldInfo.type === FieldType.Password || context.fieldInfo.type === FieldType.SQLPassword; const inputBoxType = isPasswordField ? 'password' : 'text'; const input = createInputBoxField({ context, inputBoxType }); - context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: input, isPassword: isPasswordField }); + input.isPassword = isPasswordField; + context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, input); return input; } @@ -598,14 +662,14 @@ function processPasswordField(context: FieldContext): void { if (context.fieldInfo.type === FieldType.SQLPassword) { const invalidPasswordMessage = getInvalidSQLPasswordMessage(context.fieldInfo.label); - context.onNewDisposableCreated(passwordInput.onTextChanged(() => { - if (context.fieldInfo.type === FieldType.SQLPassword && isValidSQLPassword(passwordInput.value!, context.fieldInfo.userName)) { + context.onNewDisposableCreated(passwordInput.component.onTextChanged(() => { + if (context.fieldInfo.type === FieldType.SQLPassword && isValidSQLPassword(passwordInput.component.value!, context.fieldInfo.userName)) { removeValidationMessage(context.container, invalidPasswordMessage); } })); context.onNewValidatorCreated((): { valid: boolean, message: string } => { - return { valid: isValidSQLPassword(passwordInput.value!, context.fieldInfo.userName), message: invalidPasswordMessage }; + return { valid: isValidSQLPassword(passwordInput.component.value!, context.fieldInfo.userName), message: invalidPasswordMessage }; }); } @@ -621,17 +685,17 @@ function processPasswordField(context: FieldContext): void { addLabelInputPairToContainer(context.view, context.components, confirmPasswordLabel, confirmPasswordInput, context.fieldInfo); context.onNewValidatorCreated((): { valid: boolean, message: string } => { - const passwordMatches = passwordInput.value === confirmPasswordInput.value; + const passwordMatches = passwordInput.component.value === confirmPasswordInput.value; return { valid: passwordMatches, message: passwordNotMatchMessage }; }); const updatePasswordMismatchMessage = () => { - if (passwordInput.value === confirmPasswordInput.value) { + if (passwordInput.component.value === confirmPasswordInput.value) { removeValidationMessage(context.container, passwordNotMatchMessage); } }; - context.onNewDisposableCreated(passwordInput.onTextChanged(() => { + context.onNewDisposableCreated(passwordInput.component.onTextChanged(() => { updatePasswordMismatchMessage(); })); context.onNewDisposableCreated(confirmPasswordInput.onTextChanged(() => { @@ -668,8 +732,12 @@ function processHyperlinkedTextField(context: FieldContext): ReadOnlyFieldInputs function processEvaluatedTextField(context: FieldContext): ReadOnlyFieldInputs { const readOnlyField = processReadonlyTextField(context, false /*allowEvaluation*/); + const onChangedEmitter = new vscode.EventEmitter(); // Stub event since we don't currently supp + 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 () => { readOnlyField.text!.value = await substituteVariableValues(context.inputComponents, context.fieldInfo.defaultValue); return readOnlyField.text?.value!; @@ -702,7 +770,15 @@ 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); - context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: 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 + }); } /** @@ -720,11 +796,11 @@ function processFilePickerField(context: FieldContext): FilePickerInputs { required: context.fieldInfo.required, placeHolder: context.fieldInfo.placeHolder, width: `${inputWidth - buttonWidth}px`, - enabled: context.fieldInfo.enabled, + enabled: typeof context.fieldInfo.enabled === 'boolean' ? context.fieldInfo.enabled : false, validations: context.fieldValidations }); - context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: input }); - input.enabled = false; + context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, input); + input.component.enabled = false; const browseFileButton = context.view!.modelBuilder.button().withProperties({ label: loc.browse, width: buttonWidth }).component(); const fieldInfo = context.fieldInfo as FilePickerFieldInfo; let filter: { [name: string]: string[] } | undefined = undefined; @@ -738,7 +814,7 @@ function processFilePickerField(context: FieldContext): FilePickerInputs { canSelectFiles: true, canSelectFolders: false, canSelectMany: false, - defaultUri: input.value ? vscode.Uri.file(path.dirname(input.value)) : undefined, + defaultUri: input.component.value ? vscode.Uri.file(path.dirname(input.component.value)) : undefined, openLabel: loc.select, filters: filter }); @@ -746,11 +822,11 @@ function processFilePickerField(context: FieldContext): FilePickerInputs { return; } let fileUri = fileUris[0]; - input.value = fileUri.fsPath; + input.component.value = fileUri.fsPath; })); - const component = createFlexContainer(context.view, [input, browseFileButton], true, context.fieldInfo.inputWidth); + const component = createFlexContainer(context.view, [input.component, browseFileButton], true, context.fieldInfo.inputWidth); addLabelInputPairToContainer(context.view, context.components, label, component, context.fieldInfo); - return { input: input, browseButton: browseFileButton }; + return { input: input.component, browseButton: browseFileButton }; } /** @@ -851,7 +927,11 @@ async function createRadioOptions(context: FieldContext, getRadioButtonInfo?: (( 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 radioGroupLoadingComponentBuilder = new RadioGroupLoadingComponentBuilder(context.view, context.onNewDisposableCreated, context.fieldInfo); context.fieldInfo.labelPosition = LabelPosition.Left; - context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: radioGroupLoadingComponentBuilder }); + context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { + component: radioGroupLoadingComponentBuilder, + getValue: async (): Promise => radioGroupLoadingComponentBuilder.value || '', + onValueChanged: radioGroupLoadingComponentBuilder.onValueChanged, + }); addLabelInputPairToContainer(context.view, context.components, label, radioGroupLoadingComponentBuilder.component(), context.fieldInfo); const options = context.fieldInfo.options as OptionsInfo; await radioGroupLoadingComponentBuilder.loadOptions( @@ -886,18 +966,22 @@ async function processAzureAccountField(context: AzureAccountFieldContext): Prom 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 }); + context.onNewInputComponentCreated(context.fieldInfo.newResourceGroupFlagVariableName!, { + component: newRGCheckbox, + getValue: async (): Promise => newRGCheckbox.checked?.toString() || '', + onValueChanged: newRGCheckbox.onChanged + }); const newRGNameInput = createInputBox(context.view, { ariaLabel: loc.NewResourceGroupAriaLabel }); - context.onNewInputComponentCreated(context.fieldInfo.newResourceGroupNameVariableName!, { component: newRGNameInput }); + context.onNewInputComponentCreated(context.fieldInfo.newResourceGroupNameVariableName!, newRGNameInput); context.components.push(newRGCheckbox); - context.components.push(newRGNameInput); + context.components.push(newRGNameInput.component); const setRGStatus = (newRG: boolean) => { resourceGroupDropdown.required = !newRG; resourceGroupDropdown.enabled = !newRG; - newRGNameInput.required = newRG; - newRGNameInput.enabled = newRG; + newRGNameInput.component.required = newRG; + newRGNameInput.component.enabled = newRG; if (!newRG) { - newRGNameInput.value = ''; + newRGNameInput.component.value = ''; } }; context.onNewDisposableCreated(newRGCheckbox.onChanged(() => { @@ -974,7 +1058,11 @@ 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); } @@ -998,7 +1086,11 @@ function createAzureAccountDropdown(context: AzureAccountFieldContext): AzureAcc label: loc.account }); accountDropdown.fireOnTextChange = true; - context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, { component: accountDropdown }); + 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 + }); 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); @@ -1037,6 +1129,8 @@ function createAzureSubscriptionDropdown( }); 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) => { return subscriptionValueToSubscriptionMap.get(inputValue)?.id || inputValue; } @@ -1046,7 +1140,11 @@ function createAzureSubscriptionDropdown( label: label.value!, variableName: context.fieldInfo.displaySubscriptionVariableName }); - context.onNewInputComponentCreated(context.fieldInfo.displaySubscriptionVariableName!, { component: subscriptionDropdown }); + context.onNewInputComponentCreated(context.fieldInfo.displaySubscriptionVariableName!, { + component: subscriptionDropdown, + getValue: async (): Promise => (typeof subscriptionDropdown.value === 'string' ? subscriptionDropdown.value : subscriptionDropdown.value?.displayName) || '', + onValueChanged: subscriptionDropdown.onValueChanged, + }); } addLabelInputPairToContainer(context.view, context.components, label, subscriptionDropdown, context.fieldInfo); return subscriptionDropdown; @@ -1175,7 +1273,11 @@ function createAzureResourceGroupsDropdown( }); const rgValueChangedEmitter = new vscode.EventEmitter(); resourceGroupDropdown.onValueChanged(() => rgValueChangedEmitter.fire()); - context.onNewInputComponentCreated(context.fieldInfo.resourceGroupVariableName || context.fieldInfo.label, { component: resourceGroupDropdown }); + 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); subscriptionDropdown.onValueChanged(async selectedItem => { const selectedAccount = !accountDropdown || !accountDropdown.value ? undefined : accountValueToAccountMap.get(accountDropdown.value.toString()); @@ -1260,14 +1362,23 @@ async function processAzureLocationsField(context: AzureLocationsFieldContext): label: label.value!, variableName: context.fieldInfo.locationVariableName }); - context.onNewInputComponentCreated(context.fieldInfo.locationVariableName, { component: locationDropdown }); + context.onNewInputComponentCreated(context.fieldInfo.locationVariableName, { + component: locationDropdown, + getValue: async (): Promise => (typeof locationDropdown.value === 'string' ? locationDropdown.value : locationDropdown.value?.displayName) || '', + onValueChanged: locationDropdown.onValueChanged, + }); } if (context.fieldInfo.displayLocationVariableName) { context.fieldInfo.subFields!.push({ label: label.value!, variableName: context.fieldInfo.displayLocationVariableName }); - context.onNewInputComponentCreated(context.fieldInfo.displayLocationVariableName, { component: locationDropdown, inputValueTransformer: (value => apiService.azurecoreApi.getRegionDisplayName(value)) }); + 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) || '', + onValueChanged: locationDropdown.onValueChanged, + }); } addLabelInputPairToContainer(context.view, context.components, label, locationDropdown, context.fieldInfo); return locationDropdown; @@ -1307,7 +1418,7 @@ export async function setModelValues(inputComponents: InputComponents, model: Mo })); } -async function getInputComponentValue(inputComponentInfo: InputComponentInfo): Promise { +async function getInputComponentValue(inputComponentInfo: InputComponentInfo): Promise { const input = inputComponentInfo.component; if (input === undefined) { return undefined; diff --git a/extensions/resource-deployment/src/ui/notebookWizard/notebookWizardPage.ts b/extensions/resource-deployment/src/ui/notebookWizard/notebookWizardPage.ts index 4229ebfe16..baec09919f 100644 --- a/extensions/resource-deployment/src/ui/notebookWizard/notebookWizardPage.ts +++ b/extensions/resource-deployment/src/ui/notebookWizard/notebookWizardPage.ts @@ -7,7 +7,7 @@ import { EOL } from 'os'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { NotebookWizardPageInfo } from '../../interfaces'; -import { initializeWizardPage, InputComponentInfo, setModelValues, Validator } from '../modelViewUtils'; +import { initializeWizardPage, InputComponent, InputComponentInfo, setModelValues, Validator } from '../modelViewUtils'; import { ResourceTypePage } from '../resourceTypePage'; import { WizardPageInfo } from '../wizardPageInfo'; import { NotebookWizardModel } from './notebookWizardModel'; @@ -59,7 +59,7 @@ export class NotebookWizardPage extends ResourceTypePage { }, onNewInputComponentCreated: ( name: string, - inputComponentInfo: InputComponentInfo + inputComponentInfo: InputComponentInfo ): void => { if (name) { this._model.inputComponents[name] = inputComponentInfo; diff --git a/extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts b/extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts index e101d9d7d0..120f68f7d9 100644 --- a/extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts +++ b/extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; import * as vscode from 'vscode'; -import { OptionsInfo, FieldInfo } from '../interfaces'; +import { OptionsInfo, FieldInfo, instanceOfDynamicEnablementInfo } from '../interfaces'; import { getErrorMessage } from '../common/utils'; export class RadioGroupLoadingComponentBuilder implements azdata.ComponentBuilder { @@ -51,7 +51,7 @@ export class RadioGroupLoadingComponentBuilder implements azdata.ComponentBuilde label: option.displayName, checked: option.displayName === defaultValue, name: option.name, - enabled: this._fieldInfo.enabled + enabled: instanceOfDynamicEnablementInfo(this._fieldInfo.enabled) ? false : this._fieldInfo.enabled // Dynamic enablement is initially set to false }).component(); if (radioOption.checked) { this._currentRadioOption = radioOption; @@ -82,6 +82,12 @@ export class RadioGroupLoadingComponentBuilder implements azdata.ComponentBuilde return this._currentRadioOption; } + set enabled(enabled: boolean) { + this._optionsDivContainer.items.forEach(radioButton => { + radioButton.enabled = enabled; + }); + } + get onValueChanged(): vscode.Event { return this._onValueChangedEmitter.event; }