mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Add dependent field provider to resource deployment (#13664)
* Add dependent field provider to resource deployment * Change name to value provider service * Add error handling * providerId -> id * Set dropdown value correctly * missed id * back to providerId * fix updating missed id * remove placeholder
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -17,7 +17,7 @@ import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||
*/
|
||||
export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourceProvider {
|
||||
private _cacheManager = new CacheManager<string, string>();
|
||||
readonly optionsSourceId = 'arc.controllers';
|
||||
readonly id = 'arc.controllers';
|
||||
constructor(private _treeProvider: AzureArcTreeDataProvider) { }
|
||||
|
||||
async getOptions(): Promise<string[] | azdata.CategoryValue[]> {
|
||||
|
||||
@@ -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<string[]> {
|
||||
const isEulaAccepted = await this._azdataExtApi.isEulaAccepted();
|
||||
|
||||
@@ -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<azurec
|
||||
registerAzureServices(appContext);
|
||||
const azureResourceTree = new AzureResourceTreeProvider(appContext);
|
||||
const connectionDialogTree = new ConnectionDialogTreeProvider(appContext);
|
||||
pushDisposable(vscode.window.registerTreeDataProvider('connectionDialog/azureResourceExplorer', connectionDialogTree));
|
||||
pushDisposable(vscode.window.registerTreeDataProvider('azureResourceExplorer', azureResourceTree));
|
||||
pushDisposable(vscode.window.registerTreeDataProvider('connectionDialog/azureResourceExplorer', connectionDialogTree));
|
||||
pushDisposable(vscode.workspace.onDidChangeConfiguration(e => 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<azurec
|
||||
}
|
||||
});
|
||||
|
||||
// Don't block on this since there's a bit of a circular dependency here with the extension activation since resource deployment
|
||||
// depends on this extension too. It's fine to wait a bit for that to finish before registering the provider
|
||||
vscode.extensions.getExtension(resourceDeployment.extension.name).activate().then((api: resourceDeployment.IExtension) => {
|
||||
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<azurecore.GetSubscriptionsResult> {
|
||||
return selectedOnly
|
||||
|
||||
1
extensions/azurecore/src/typings/ref.d.ts
vendored
1
extensions/azurecore/src/typings/ref.d.ts
vendored
@@ -7,4 +7,5 @@
|
||||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
||||
/// <reference path='../../../../src/sql/azdata.d.ts'/>
|
||||
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
|
||||
/// <reference path='../../../resource-deployment/src/typings/resource-deployment.d.ts'/>
|
||||
/// <reference types='@types/node'/>
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -9,10 +9,10 @@ import * as loc from '../localizedConstants';
|
||||
class OptionsSourcesService {
|
||||
private _optionsSourceStore = new Map<string, rd.IOptionsSourceProvider>();
|
||||
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 {
|
||||
|
||||
@@ -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<string, rd.IValueProvider>();
|
||||
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();
|
||||
@@ -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[]> | string[] | azdata.CategoryValue[];
|
||||
getVariableValue?: (variableName: string, input: string) => Promise<string> | string;
|
||||
getIsPassword?: (variableName: string) => boolean | Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface IValueProvider {
|
||||
readonly id: string,
|
||||
getValue(triggerValue: string): Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T extends InputComponent> = {
|
||||
component: T;
|
||||
labelComponent?: azdata.TextComponent;
|
||||
getValue: () => Promise<InputValueType>;
|
||||
setValue: (value: InputValueType) => void;
|
||||
getDisplayValue?: () => Promise<string>;
|
||||
onValueChanged: vscode.Event<void>;
|
||||
isPassword?: boolean
|
||||
@@ -200,6 +202,7 @@ export function createInputBoxInputInfo(view: azdata.ModelView, inputInfo: Input
|
||||
return {
|
||||
component: component,
|
||||
getValue: async (): Promise<InputValueType> => 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<InputValueType> => typeof dropdown.value === 'string' ? dropdown.value : dropdown.value?.name,
|
||||
setValue: (value: InputValueType) => setDropdownValue(dropdown, value?.toString()),
|
||||
getDisplayValue: async (): Promise<string> => (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<void
|
||||
}));
|
||||
}
|
||||
|
||||
async function hookUpValueProviders(context: WizardPageContext): Promise<void> {
|
||||
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<azdata.GroupContainer> {
|
||||
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<InputValueType> => radioGroupLoadingComponentBuilder.value,
|
||||
setValue: (value: InputValueType) => { throw new Error('Setting value of radio group isn\'t currently supported'); },
|
||||
getDisplayValue: async (): Promise<string> => 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 = (<azdata.CategoryValue[]>values).find(v => v.name === value);
|
||||
} else {
|
||||
dropdown.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user