diff --git a/extensions/arc/package.json b/extensions/arc/package.json index c3cb30183c..65f999b780 100644 --- a/extensions/arc/package.json +++ b/extensions/arc/package.json @@ -315,10 +315,10 @@ "type": "text", "required": true, "defaultValue": "", - "placeHolder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", - "enabled": { - "target": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE", - "value": "direct" + "enabled": false, + "valueProvider": { + "providerId": "subscription-id-to-tenant-id", + "triggerField": "AZDATA_NB_VAR_ARC_SUBSCRIPTION" }, "validations" : [{ "type": "regex_match", diff --git a/extensions/arc/src/providers/arcControllersOptionsSourceProvider.ts b/extensions/arc/src/providers/arcControllersOptionsSourceProvider.ts index 2623ab60fa..9ded9ed507 100644 --- a/extensions/arc/src/providers/arcControllersOptionsSourceProvider.ts +++ b/extensions/arc/src/providers/arcControllersOptionsSourceProvider.ts @@ -17,7 +17,7 @@ import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; */ export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourceProvider { private _cacheManager = new CacheManager(); - readonly optionsSourceId = 'arc.controllers'; + readonly id = 'arc.controllers'; constructor(private _treeProvider: AzureArcTreeDataProvider) { } async getOptions(): Promise { diff --git a/extensions/azdata/src/providers/arcControllerConfigProfilesOptionsSource.ts b/extensions/azdata/src/providers/arcControllerConfigProfilesOptionsSource.ts index d90ecd915d..b74d0e4d4b 100644 --- a/extensions/azdata/src/providers/arcControllerConfigProfilesOptionsSource.ts +++ b/extensions/azdata/src/providers/arcControllerConfigProfilesOptionsSource.ts @@ -10,7 +10,7 @@ import * as azdataExt from 'azdata-ext'; * Class that provides options sources for an Arc Data Controller */ export class ArcControllerConfigProfilesOptionsSource implements rd.IOptionsSourceProvider { - readonly optionsSourceId = 'arc.controller.config.profiles'; + readonly id = 'arc.controller.config.profiles'; constructor(private _azdataExtApi: azdataExt.IExtension) { } async getOptions(): Promise { const isEulaAccepted = await this._azdataExtApi.isEulaAccepted(); diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts index bac592f4f1..ebaedf0cd2 100644 --- a/extensions/azurecore/src/extension.ts +++ b/extensions/azurecore/src/extension.ts @@ -8,6 +8,7 @@ import * as vscode from 'vscode'; import { promises as fs } from 'fs'; import * as path from 'path'; import * as os from 'os'; +import * as resourceDeployment from 'resource-deployment'; import { AppContext } from './appContext'; import { AzureAccountProviderService } from './account-provider/azureAccountProviderService'; @@ -86,8 +87,8 @@ export async function activate(context: vscode.ExtensionContext): Promise onDidChangeConfiguration(e), this)); registerAzureResourceCommands(appContext, azureResourceTree, connectionDialogTree); azdata.dataprotocol.registerDataGridProvider(new AzureDataGridProvider(appContext)); @@ -105,6 +106,40 @@ export async function activate(context: vscode.ExtensionContext): Promise { + api.registerValueProvider({ + id: 'subscription-id-to-tenant-id', + getValue: async (triggerValue: string) => { + if (triggerValue === '') { + return ''; + } + let accounts: azdata.Account[] = []; + try { + accounts = await azdata.accounts.getAllAccounts(); + } catch (err) { + console.warn(`Error fetching accounts for subscription-id-to-tenant-id provider : ${err}`); + return ''; + } + + for (const account of accounts) { + // Ignore any errors - they'll be logged in the called function and we still want to look + // at any subscriptions that are returned - worst case we'll just return an empty string if we didn't + // find the matching subscription + const subs = await azureResourceUtils.getSubscriptions(appContext, account, true); + const sub = subs.subscriptions.find(sub => sub.id === triggerValue); + if (sub) { + return sub.tenant; + } + + } + console.error(`Unable to find subscription with ID ${triggerValue} when mapping subscription ID to tenant ID`); + return ''; + } + }); + }); + return { getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly: boolean = false): Thenable { return selectedOnly diff --git a/extensions/azurecore/src/typings/ref.d.ts b/extensions/azurecore/src/typings/ref.d.ts index 558e12bbe2..a86808d402 100644 --- a/extensions/azurecore/src/typings/ref.d.ts +++ b/extensions/azurecore/src/typings/ref.d.ts @@ -7,4 +7,5 @@ /// /// /// +/// /// diff --git a/extensions/resource-deployment/src/api.ts b/extensions/resource-deployment/src/api.ts index 79deef29a7..f7817d75b3 100644 --- a/extensions/resource-deployment/src/api.ts +++ b/extensions/resource-deployment/src/api.ts @@ -4,11 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as rd from 'resource-deployment'; +import { valueProviderService } from './services/valueProviderService'; import { optionsSourcesService } from './services/optionSourcesService'; export function getExtensionApi(): rd.IExtension { return { - registerOptionsSourceProvider: (provider: rd.IOptionsSourceProvider) => optionsSourcesService.registerOptionsSourceProvider(provider) + registerOptionsSourceProvider: (provider: rd.IOptionsSourceProvider) => optionsSourcesService.registerOptionsSourceProvider(provider), + registerValueProvider: (provider: rd.IValueProvider) => valueProviderService.registerValueProvider(provider) }; } diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index 67029ad98c..0befd281e4 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -261,6 +261,11 @@ export interface DynamicEnablementInfo { value: string } +export interface ValueProviderInfo { + providerId: string, + triggerField: string +} + export interface FieldInfoBase { labelWidth?: string; inputWidth?: string; @@ -307,9 +312,8 @@ export interface FieldInfo extends SubFieldInfo, FieldInfoBase { editable?: boolean; // for editable drop-down, enabled?: boolean | DynamicEnablementInfo; isEvaluated?: boolean; - valueLookup?: string; // for fetching dropdown options - validationLookup?: string // for fetching text field validations validations?: ValidationInfo[]; + valueProvider?: ValueProviderInfo; } export interface KubeClusterContextFieldInfo extends FieldInfo { diff --git a/extensions/resource-deployment/src/localizedConstants.ts b/extensions/resource-deployment/src/localizedConstants.ts index 5a1710e52a..b4f766f04b 100644 --- a/extensions/resource-deployment/src/localizedConstants.ts +++ b/extensions/resource-deployment/src/localizedConstants.ts @@ -28,7 +28,9 @@ export const NewResourceGroupAriaLabel = localize('azure.resourceGroup.NewResour export const realm = localize('deployCluster.Realm', "Realm"); export const unknownFieldTypeError = (type: FieldType) => localize('UnknownFieldTypeError', "Unknown field type: \"{0}\"", type); export const optionsSourceAlreadyDefined = (optionsSourceId: string) => localize('optionsSource.alreadyDefined', "Options Source with id:{0} is already defined", optionsSourceId); +export const valueProviderAlreadyDefined = (providerId: string) => localize('valueProvider.alreadyDefined', "Value Provider with id:{0} is already defined", providerId); export const noOptionsSourceDefined = (optionsSourceId: string) => localize('optionsSource.notDefined', "No Options Source defined for id: {0}", optionsSourceId); +export const noValueProviderDefined = (providerId: string) => localize('valueProvider.notDefined', "No Value Provider defined for id: {0}", providerId); export const variableValueFetchForUnsupportedVariable = (variableName: string) => localize('getVariableValue.unknownVariableName', "Attempt to get variable value for unknown variable:{0}", variableName); export const isPasswordFetchForUnsupportedVariable = (variableName: string) => localize('getIsPassword.unknownVariableName', "Attempt to get isPassword for unknown variable:{0}", variableName); export const optionsNotDefined = (fieldType: FieldType) => localize('optionsNotDefined', "FieldInfo.options was not defined for field type: {0}", fieldType); diff --git a/extensions/resource-deployment/src/services/optionSourcesService.ts b/extensions/resource-deployment/src/services/optionSourcesService.ts index 5d613eeb09..dc6cf77b6a 100644 --- a/extensions/resource-deployment/src/services/optionSourcesService.ts +++ b/extensions/resource-deployment/src/services/optionSourcesService.ts @@ -9,10 +9,10 @@ import * as loc from '../localizedConstants'; class OptionsSourcesService { private _optionsSourceStore = new Map(); registerOptionsSourceProvider(provider: rd.IOptionsSourceProvider): void { - if (this._optionsSourceStore.has(provider.optionsSourceId)) { - throw new Error(loc.optionsSourceAlreadyDefined(provider.optionsSourceId)); + if (this._optionsSourceStore.has(provider.id)) { + throw new Error(loc.optionsSourceAlreadyDefined(provider.id)); } - this._optionsSourceStore.set(provider.optionsSourceId, provider); + this._optionsSourceStore.set(provider.id, provider); } getOptionsSource(optionsSourceProviderId: string): rd.IOptionsSourceProvider { diff --git a/extensions/resource-deployment/src/services/valueProviderService.ts b/extensions/resource-deployment/src/services/valueProviderService.ts new file mode 100644 index 0000000000..152b9a89d6 --- /dev/null +++ b/extensions/resource-deployment/src/services/valueProviderService.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as rd from 'resource-deployment'; +import * as loc from '../localizedConstants'; + +class ValueProviderService { + private _valueProviderStore = new Map(); + registerValueProvider(provider: rd.IValueProvider): void { + if (this._valueProviderStore.has(provider.id)) { + throw new Error(loc.valueProviderAlreadyDefined(provider.id)); + } + this._valueProviderStore.set(provider.id, provider); + } + + getValueProvider(providerId: string): rd.IValueProvider { + const valueProvider = this._valueProviderStore.get(providerId); + if (valueProvider === undefined) { + throw new Error(loc.noValueProviderDefined(providerId)); + } + return valueProvider; + } +} + +export const valueProviderService = new ValueProviderService(); diff --git a/extensions/resource-deployment/src/typings/resource-deployment.d.ts b/extensions/resource-deployment/src/typings/resource-deployment.d.ts index cbefb687df..def6463787 100644 --- a/extensions/resource-deployment/src/typings/resource-deployment.d.ts +++ b/extensions/resource-deployment/src/typings/resource-deployment.d.ts @@ -17,12 +17,17 @@ declare module 'resource-deployment' { name = 'Microsoft.resource-deployment' } export interface IOptionsSourceProvider { - readonly optionsSourceId: string, + readonly id: string, getOptions(): Promise | string[] | azdata.CategoryValue[]; getVariableValue?: (variableName: string, input: string) => Promise | string; getIsPassword?: (variableName: string) => boolean | Promise; } + export interface IValueProvider { + readonly id: string, + getValue(triggerValue: string): Promise; + } + /** * Covers defining what the resource-deployment extension exports to other extensions * @@ -31,6 +36,7 @@ declare module 'resource-deployment' { */ export interface IExtension { - registerOptionsSourceProvider(provider: IOptionsSourceProvider): void + registerOptionsSourceProvider(provider: IOptionsSourceProvider): void, + registerValueProvider(provider: IValueProvider): void } } diff --git a/extensions/resource-deployment/src/ui/modelViewUtils.ts b/extensions/resource-deployment/src/ui/modelViewUtils.ts index 6d7c6cdcd8..f919f4ccb3 100644 --- a/extensions/resource-deployment/src/ui/modelViewUtils.ts +++ b/extensions/resource-deployment/src/ui/modelViewUtils.ts @@ -14,6 +14,7 @@ import { getDateTimeString, getErrorMessage, throwUnless } from '../common/utils 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 { valueProviderService } from '../services/valueProviderService'; import { getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../services/kubeService'; import { optionsSourcesService } from '../services/optionSourcesService'; import { KubeCtlTool, KubeCtlToolName } from '../services/tools/kubeCtlTool'; @@ -38,6 +39,7 @@ export type InputComponentInfo = { component: T; labelComponent?: azdata.TextComponent; getValue: () => Promise; + setValue: (value: InputValueType) => void; getDisplayValue?: () => Promise; onValueChanged: vscode.Event; isPassword?: boolean @@ -200,6 +202,7 @@ export function createInputBoxInputInfo(view: azdata.ModelView, inputInfo: Input return { component: component, getValue: async (): Promise => component.value, + setValue: (value: InputValueType) => component.value = value?.toString(), onValueChanged: component.onTextChanged }; } @@ -240,6 +243,7 @@ export function createCheckboxInputInfo(view: azdata.ModelView, info: { initialV return { component: checkbox, getValue: async () => checkbox.checked ? 'true' : 'false', + setValue: (value: InputValueType) => checkbox.checked = value?.toString().toLowerCase() === 'true' ? true : false, onValueChanged: checkbox.onChanged }; } @@ -265,6 +269,7 @@ export function createDropdownInputInfo(view: azdata.ModelView, info: { defaultV return { component: dropdown, getValue: async (): Promise => typeof dropdown.value === 'string' ? dropdown.value : dropdown.value?.name, + setValue: (value: InputValueType) => setDropdownValue(dropdown, value?.toString()), getDisplayValue: async (): Promise => (typeof dropdown.value === 'string' ? dropdown.value : dropdown.value?.displayName) || '', onValueChanged: dropdown.onValueChanged, }; @@ -331,6 +336,7 @@ export function initializeWizardPage(context: WizardPageContext): void { }); })); await hookUpDynamicEnablement(context); + await hookUpValueProviders(context); const formBuilder = view.modelBuilder.formContainer().withFormItems( sections.map(section => { return { title: '', component: section }; }), { @@ -397,6 +403,35 @@ 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 (field.valueProvider) { + const fieldKey = field.variableName || field.label; + const fieldComponent = context.inputComponents[fieldKey]; + const targetComponent = context.inputComponents[field.valueProvider.triggerField]; + if (!targetComponent) { + console.error(`Could not find target component ${field.valueProvider.triggerField} when hooking up value providers for ${field.label}`); + return; + } + const provider = valueProviderService.getValueProvider(field.valueProvider.providerId); + const updateFields = async () => { + const targetComponentValue = await targetComponent.getValue(); + const newFieldValue = await provider.getValue(targetComponentValue?.toString() ?? ''); + fieldComponent.setValue(newFieldValue); + }; + targetComponent.onValueChanged(() => { + updateFields(); + }); + await updateFields(); + } + })); + })); +} + export async function createSection(context: SectionContext): Promise { const components: azdata.Component[] = []; context.sectionInfo.inputWidth = context.sectionInfo.inputWidth || DefaultInputWidth; @@ -630,6 +665,7 @@ async function configureOptionsSourceSubfields(context: FieldContext, optionsSou throw e; } }, + setValue: (_value: InputValueType) => { throw new Error('Setting value of radio group isn\'t currently supported'); }, onValueChanged: optionsComponent.onValueChanged }); } @@ -666,6 +702,7 @@ function processNumberField(context: FieldContext): void { const value = await input.getValue(); return typeof value === 'string' && value.length > 0 ? parseFloat(value) : value; }, + setValue: (value: InputValueType) => input.component.value = value?.toString(), onValueChanged: input.onValueChanged }); } @@ -762,6 +799,7 @@ function processEvaluatedTextField(context: FieldContext): ReadOnlyFieldInputs { readOnlyField.text!.value = await substituteVariableValues(context.inputComponents, context.fieldInfo.defaultValue); return readOnlyField.text!.value; }, + setValue: (value: InputValueType) => readOnlyField.text!.value = value?.toString(), onValueChanged: onChangedEmitter.event, }); return readOnlyField; @@ -945,6 +983,7 @@ async function createRadioOptions(context: FieldContext, getRadioButtonInfo?: (( component: radioGroupLoadingComponentBuilder, labelComponent: label, getValue: async (): Promise => radioGroupLoadingComponentBuilder.value, + setValue: (value: InputValueType) => { throw new Error('Setting value of radio group isn\'t currently supported'); }, getDisplayValue: async (): Promise => radioGroupLoadingComponentBuilder.displayValue, onValueChanged: radioGroupLoadingComponentBuilder.onValueChanged, }); @@ -1140,6 +1179,7 @@ function createAzureSubscriptionDropdown( const inputValue = (await subscriptionDropdown.getValue())?.toString() || ''; return subscriptionValueToSubscriptionMap.get(inputValue)?.id || inputValue; }, + setValue: (value: InputValueType) => setDropdownValue(subscriptionDropdown.component, value?.toString()), getDisplayValue: subscriptionDropdown.getDisplayValue, onValueChanged: subscriptionDropdown.onValueChanged }); @@ -1401,3 +1441,18 @@ export function isInputBoxEmpty(input: azdata.InputBoxComponent): boolean { return input.value === undefined || input.value === ''; } +/** + * Sets the dropdown value to the corresponding value from the list of current values, converting + * into a CategoryValue if necessary (using the name field). + * @param dropdown The dropdown component to set the value for + * @param value The value to set - either the direct string value or the name of the CategoryValue to use + */ +function setDropdownValue(dropdown: azdata.DropDownComponent, value: string = ''): void { + const values = dropdown.values ?? []; + if (typeof values[0] === 'object') { + dropdown.value = (values).find(v => v.name === value); + } else { + dropdown.value = value; + } +} +