allow registering options source providers to resource-deployment (#12712)

* first draft

* compile fixes

* uncomment code

* waitForAzdataToolDisovery added to azdata api

* missed change in last commit

* remove switchReturn

* contributeOptionsSource renamed

* remove switchReturn reference

* create optionSourceService

* azdataTool usage more reliable

* package.json fixes and cleanup

* cleanup

* revert 4831a6e6b8b08684488b2c9e18092fa252e3057f

* pr feedback

* pr feedback

* pr feedback

* cleanup

* cleanup

* fix eulaAccepted check

* fix whitespade in doc comments.
This commit is contained in:
Arvind Ranasaria
2020-10-05 19:29:40 -07:00
committed by GitHub
parent 362605cea0
commit c679d5e1f0
35 changed files with 534 additions and 377 deletions

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* 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 { optionsSourcesService } from './services/optionSourcesService';
export function getExtensionApi(): rd.IExtension {
return {
registerOptionsSourceProvider: (provider: rd.IOptionsSourceProvider) => optionsSourcesService.registerOptionsSourceProvider(provider)
};
}

View File

@@ -3,8 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { ToolsInstallPath } from './constants';
import { ITool, NoteBookEnvironmentVariablePrefix } from './interfaces';
import { ToolsInstallPath } from '../constants';
import { ITool, NoteBookEnvironmentVariablePrefix } from '../interfaces';
export function getErrorMessage(error: any): string {
return (error instanceof Error)

View File

@@ -1,83 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Deferred } from './promise';
const enum Status {
notStarted,
inProgress,
done
}
interface State<T> {
entry?: T,
error?: Error,
status: Status,
id: number,
pendingOperation: Deferred<void>
}
/**
* An implementation of Cache Manager which ensures that only one call to populate cache miss is pending at a given time.
* All remaining calls for retrieval are awaited until the one in progress finishes and then all awaited calls are resolved with the value
* from the cache.
*/
export class CacheManager<K, T> {
private _cache = new Map<K, State<T>>();
private _id = 0;
public async getCacheEntry(key: K, retrieveEntry: (key: K) => Promise<T>): Promise<T> {
const cacheHit: State<T> | undefined = this._cache.get(key);
// each branch either throws or returns the password.
if (cacheHit === undefined) {
// populate a new state entry and add it to the cache
const state: State<T> = {
status: Status.notStarted,
id: this._id++,
pendingOperation: new Deferred<void>()
};
this._cache.set(key, state);
// now that we have the state entry initialized, retry to fetch the cacheEntry
let returnValue: T = await this.getCacheEntry(key, retrieveEntry);
await state.pendingOperation;
return returnValue!;
} else {
switch (cacheHit.status) {
case Status.notStarted: {
cacheHit.status = Status.inProgress;
// retrieve and populate the missed cache hit.
try {
cacheHit.entry = await retrieveEntry(key);
} catch (error) {
cacheHit.error = error;
} finally {
cacheHit.status = Status.done;
// we do not reject here even in error case because we do not want our awaits on pendingOperation to throw
// We track our own error state and when all done we throw if an error had happened. This results
// in the rejection of the promised returned by this method.
cacheHit.pendingOperation.resolve();
}
return await this.getCacheEntry(key, retrieveEntry);
}
case Status.inProgress: {
await cacheHit.pendingOperation;
return await this.getCacheEntry(key, retrieveEntry);
}
case Status.done: {
if (cacheHit.error !== undefined) {
await cacheHit.pendingOperation;
throw cacheHit.error;
}
else {
await cacheHit.pendingOperation;
return cacheHit.entry!;
}
}
}
}
}
}

View File

@@ -1,102 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as arc from 'arc';
import { CategoryValue } from 'azdata';
import { IOptionsSource } from '../interfaces';
import * as loc from '../localizedConstants';
import { apiService } from '../services/apiService';
import { throwUnless } from '../utils';
import { CacheManager } from './cacheManager';
export const enum OptionsSourceType {
ArcControllersOptionsSource = 'ArcControllersOptionsSource',
ArcControllerConfigProfilesOptionsSource = 'ArcControllerConfigProfilesOptionsSource'
}
export abstract class OptionsSource implements IOptionsSource {
get type(): OptionsSourceType { return this._type; }
get variableNames(): { [index: string]: string; } { return this._variableNames; }
abstract getOptions(): Promise<string[] | CategoryValue[]>;
getVariableValue(variableName: string, controllerLabel: string): Promise<string> {
throw new Error(loc.variableValueFetchForUnsupportedVariable(variableName));
}
getIsPassword(variableName: string): boolean {
throw new Error(loc.isPasswordFetchForUnsupportedVariable(variableName));
}
constructor(private _variableNames: { [index: string]: string }, private _type: OptionsSourceType) {
}
}
/**
* Class that provides options sources for an Arc Data Controller
*/
export class ArcControllersOptionsSource extends OptionsSource {
private _cacheManager = new CacheManager<string, string>();
async getOptions(): Promise<string[] | CategoryValue[]> {
const controllers = await apiService.arcApi.getRegisteredDataControllers();
throwUnless(controllers !== undefined && controllers.length !== 0, loc.noControllersConnected);
return controllers.map(ci => {
return ci.label;
});
}
async getVariableValue(variableName: string, controllerLabel: string): Promise<string> {
const retrieveVariable = async (key: string) => {
const [variableName, controllerLabel] = JSON.parse(key);
const controllers = await apiService.arcApi.getRegisteredDataControllers();
const controller = controllers!.find(ci => ci.label === controllerLabel);
throwUnless(controller !== undefined, loc.noControllerInfoFound(controllerLabel));
switch (variableName) {
case 'endpoint':
return controller.info.url;
case 'username':
return controller.info.username;
case 'password':
const fetchedPassword = await this.getPassword(controller);
return fetchedPassword;
default:
throw new Error(loc.variableValueFetchForUnsupportedVariable(variableName));
}
};
const variableValue = await this._cacheManager.getCacheEntry(JSON.stringify([variableName, controllerLabel]), retrieveVariable);
return variableValue;
}
private async getPassword(controller: arc.DataController): Promise<string> {
let password = await apiService.arcApi.getControllerPassword(controller.info);
if (!password) {
password = await apiService.arcApi.reacquireControllerPassword(controller.info, password);
}
throwUnless(password !== undefined, loc.noPasswordFound(controller.label));
return password;
}
getIsPassword(variableName: string): boolean {
switch (variableName) {
case 'endpoint':
case 'username':
return false;
case 'password':
return true;
default:
throw new Error(loc.isPasswordFetchForUnsupportedVariable(variableName));
}
}
}
/**
* Class that provides options sources for an Arc Data Controller's Config Profiles
*/
export class ArcControllerConfigProfilesOptionsSource extends OptionsSource {
async getOptions(): Promise<string[]> {
return (await apiService.azdataApi.azdata.arc.dc.config.list()).result;
}
}

View File

@@ -1,25 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Deferred promise
*/
export class Deferred<T> {
promise: Promise<T>;
resolve!: (value?: T | PromiseLike<T>) => void;
reject!: (reason?: any) => void;
constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Thenable<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult> {
return this.promise.then(onfulfilled, onrejected);
}
}

View File

@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { IOptionsSourceProvider } from 'resource-deployment';
import * as vscode from 'vscode';
import { OptionsSourceType } from './helpers/optionSources';
export const NoteBookEnvironmentVariablePrefix = 'AZDATA_NB_VAR_';
@@ -219,11 +219,9 @@ export type ComponentCSSStyles = {
};
export interface IOptionsSource {
readonly type: OptionsSourceType;
readonly variableNames: { [index: string]: string; };
getOptions(): Promise<string[] | azdata.CategoryValue[]>;
getVariableValue(variableName: string, input: string): Promise<string>;
getIsPassword(variableName: string): boolean;
provider?: IOptionsSourceProvider
readonly variableNames?: { [index: string]: string; };
readonly providerId: string;
}

View File

@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { OptionsSourceType } from './helpers/optionSources';
import { FieldType, OptionsType } from './interfaces';
const localize = nls.loadMessageBundle();
@@ -23,14 +22,11 @@ export const refresh = localize('azure.refresh', "Refresh");
export const createNewResourceGroup = localize('azure.resourceGroup.createNewResourceGroup', "Create a new resource group");
export const NewResourceGroupAriaLabel = localize('azure.resourceGroup.NewResourceGroupAriaLabel', "New resource group name");
export const realm = localize('deployCluster.Realm', "Realm");
export const unexpectedOptionsSourceType = (type: OptionsSourceType) => localize('optionsSourceType.Invalid', "Invalid options source type:{0}", type);
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 noOptionsSourceDefined = (optionsSourceId: string) => localize('optionsSource.notDefined', "No Options Source defined for id: {0}", optionsSourceId);
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 noControllersConnected = localize('noControllersConnected', "No Azure Arc controllers are currently connected. Please run the command: 'Connect to Existing Azure Arc Controller' and then try again");
export const noOptionsSourceDefined = (optionsSourceType: string) => localize('noOptionsSourceDefined', "No OptionsSource defined for type: {0}", optionsSourceType);
export const noControllerInfoFound = (name: string) => localize('noControllerInfoFound', "controllerInfo could not be found with name: {0}", name);
export const noPasswordFound = (controllerName: string) => localize('noPasswordFound', "Password could not be retrieved for controller: {0} and user did not provide a password. Please retry later.", controllerName);
export const optionsNotDefined = (fieldType: FieldType) => localize('optionsNotDefined', "FieldInfo.options was not defined for field type: {0}", fieldType);
export const optionsNotObjectOrArray = localize('optionsNotObjectOrArray', "FieldInfo.options must be an object if it is not an array");
export const optionsTypeNotFound = localize('optionsTypeNotFound', "When FieldInfo.options is an object it must have 'optionsType' property");

View File

@@ -12,10 +12,12 @@ import { ResourceTypeService } from './services/resourceTypeService';
import { ToolsService } from './services/toolsService';
import { DeploymentInputDialog } from './ui/deploymentInputDialog';
import { ResourceTypePickerDialog } from './ui/resourceTypePickerDialog';
import * as rd from 'resource-deployment';
import { getExtensionApi } from './api';
const localize = nls.loadMessageBundle();
export async function activate(context: vscode.ExtensionContext): Promise<void> {
export async function activate(context: vscode.ExtensionContext): Promise<rd.IExtension> {
const platformService = new PlatformService(context.globalStoragePath);
await platformService.initialize();
const toolsService = new ToolsService(platformService);
@@ -27,7 +29,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
const errorMessage = localize('resourceDeployment.FailedToLoadExtension', "Failed to load extension: {0}, Error detected in the resource type definition in package.json, check debug console for details.", context.extensionPath);
vscode.window.showErrorMessage(errorMessage);
validationFailures.forEach(message => console.error(message));
return;
return <any>undefined;
}
/**
* Opens a new ResourceTypePickerDialog
@@ -73,6 +75,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
const dialog = new DeploymentInputDialog(notebookService, platformService, toolsService, dialogInfo);
dialog.open();
});
return getExtensionApi();
}
// this method is called when your extension is deactivated

View File

@@ -10,7 +10,7 @@ import { isString } from 'util';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { NotebookPathInfo } from '../interfaces';
import { getDateTimeString, getErrorMessage } from '../utils';
import { getDateTimeString, getErrorMessage } from '../common/utils';
import { IPlatformService } from './platformService';
const localize = nls.loadMessageBundle();

View File

@@ -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 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));
}
this._optionsSourceStore.set(provider.optionsSourceId, provider);
}
getOptionsSource(optionsSourceProviderId: string): rd.IOptionsSourceProvider {
const optionsSource = this._optionsSourceStore.get(optionsSourceProviderId);
if (optionsSource === undefined) {
throw new Error(loc.noOptionsSourceDefined(optionsSourceProviderId));
}
return optionsSource;
}
}
export const optionsSourcesService = new OptionsSourcesService();

View File

@@ -10,7 +10,7 @@ import * as sudo from 'sudo-prompt';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { OsDistribution, OsRelease } from '../interfaces';
import { getErrorMessage } from '../utils';
import { getErrorMessage } from '../common/utils';
const localize = nls.loadMessageBundle();
const extensionOutputChannel = localize('resourceDeployment.outputChannel', "Deployments");

View File

@@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdataExt from 'azdata-ext';
import { EOL } from 'os';
import * as path from 'path';
import { SemVer } from 'semver';
@@ -9,10 +10,9 @@ import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { AzdataInstallLocationKey, DeploymentConfigurationKey } from '../../constants';
import { Command, OsDistribution, ToolStatus, ToolType } from '../../interfaces';
import { apiService } from '../apiService';
import * as loc from '../../localizedConstants';
import { IPlatformService } from '../platformService';
import { dependencyType, ToolBase } from './toolBase';
import * as loc from '../../localizedConstants';
const localize = nls.loadMessageBundle();
export const AzdataToolName = 'azdata';
@@ -21,6 +21,7 @@ const macInstallationRoot = '/usr/local/bin';
const debianInstallationRoot = '/usr/local/bin';
export class AzdataTool extends ToolBase {
private azdataApi!: azdataExt.IExtension;
constructor(platformService: IPlatformService) {
super(platformService);
}
@@ -46,7 +47,7 @@ export class AzdataTool extends ToolBase {
}
public isEulaAccepted(): boolean {
if (apiService.azdataApi.isEulaAccepted()) {
if (this.azdataApi.isEulaAccepted()) {
return true;
} else {
this.setStatusDescription(loc.azdataEulaNotAccepted);
@@ -55,7 +56,7 @@ export class AzdataTool extends ToolBase {
}
public async promptForEula(): Promise<boolean> {
const eulaAccepted = await apiService.azdataApi.promptForEula();
const eulaAccepted = await this.azdataApi.promptForEula();
if (!eulaAccepted) {
this.setStatusDescription(loc.azdataEulaDeclined);
}
@@ -80,15 +81,16 @@ export class AzdataTool extends ToolBase {
* updates the version and status for the tool.
*/
protected async updateVersionAndStatus(): Promise<void> {
this.azdataApi = await vscode.extensions.getExtension(azdataExt.extension.name)?.activate();
this.setStatusDescription('');
await this.addInstallationSearchPathsToSystemPath();
const commandOutput = await apiService.azdataApi.azdata.version();
this.version = apiService.azdataApi.azdata.getSemVersion();
const commandOutput = await this.azdataApi.azdata.version();
this.version = await this.azdataApi.azdata.getSemVersion();
if (this.version) {
if (this.autoInstallSupported) {
// set the installationPath
this.setInstallationPathOrAdditionalInformation(apiService.azdataApi.azdata.getPath());
this.setInstallationPathOrAdditionalInformation(await this.azdataApi.azdata.getPath());
}
this.setStatus(ToolStatus.Installed);
}
@@ -99,8 +101,8 @@ export class AzdataTool extends ToolBase {
}
}
protected getVersionFromOutput(output: string): SemVer | undefined {
return apiService.azdataApi.azdata.getSemVersion();
protected getVersionFromOutput(output: string): SemVer | Promise<SemVer> | undefined {
return this.azdataApi.azdata.getSemVersion();
}

View File

@@ -4,11 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import { EOL } from 'os';
import * as path from 'path';
import { SemVer, compare as SemVerCompare } from 'semver';
import { compare as SemVerCompare, SemVer } from 'semver';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { getErrorMessage } from '../../common/utils';
import { Command, ITool, OsDistribution, ToolStatus, ToolType } from '../../interfaces';
import { getErrorMessage } from '../../utils';
import { IPlatformService } from '../platformService';
const localize = nls.loadMessageBundle();
@@ -52,7 +52,7 @@ export abstract class ToolBase implements ITool {
protected abstract readonly allInstallationCommands: Map<OsDistribution, Command[]>;
protected readonly dependenciesByOsType: Map<OsDistribution, dependencyType[]> = new Map<OsDistribution, dependencyType[]>();
protected abstract getVersionFromOutput(output: string): SemVer | undefined;
protected abstract getVersionFromOutput(output: string): SemVer | Promise<SemVer> | undefined;
protected readonly _onDidUpdateData = new vscode.EventEmitter<ITool>();
protected readonly uninstallCommand?: string;
@@ -274,7 +274,7 @@ export abstract class ToolBase implements ITool {
ignoreError: true
},
);
this.version = this.getVersionFromOutput(commandOutput);
this.version = await this.getVersionFromOutput(commandOutput);
if (this.version) {
if (this.autoInstallSupported) {
// discover and set the installationPath

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'resource-deployment' {
import * as azdata from 'azdata';
export const enum extension {
name = 'Microsoft.resource-deployment'
}
export interface IOptionsSourceProvider {
readonly optionsSourceId: string,
getOptions(): Promise<string[] | azdata.CategoryValue[]> | string[] | azdata.CategoryValue[];
getVariableValue?: (variableName: string, input: string) => Promise<string> | string;
getIsPassword?: (variableName: string) => boolean | Promise<boolean>;
}
/**
* Covers defining what the resource-deployment extension exports to other extensions
*
* IMPORTANT: THIS IS NOT A HARD DEFINITION unlike vscode; therefore no enums or classes should be defined here
* (const enums get evaluated when typescript -> javascript so those are fine)
*/
export interface IExtension {
registerOptionsSourceProvider(provider: IOptionsSourceProvider): void
}
}

View File

@@ -14,7 +14,7 @@ import { IAzdataService } from '../../services/azdataService';
import { IKubeService } from '../../services/kubeService';
import { INotebookService } from '../../services/notebookService';
import { IToolsService } from '../../services/toolsService';
import { getErrorMessage } from '../../utils';
import { getErrorMessage } from '../../common/utils';
import { InputComponents } from '../modelViewUtils';
import { WizardBase } from '../wizardBase';
import { WizardPageBase } from '../wizardPageBase';

View File

@@ -7,7 +7,7 @@ import { delimiter } from 'path';
import { BdcDeploymentType, ITool } from '../../interfaces';
import { BigDataClusterDeploymentProfile, DataResource, HdfsResource, SqlServerMasterResource } from '../../services/bigDataClusterDeploymentProfile';
import { KubeCtlToolName } from '../../services/tools/kubeCtlTool';
import { getRuntimeBinaryPathEnvironmentVariableName, setEnvironmentVariablesForInstallPaths } from '../../utils';
import { getRuntimeBinaryPathEnvironmentVariableName, setEnvironmentVariablesForInstallPaths } from '../../common/utils';
import { Model } from '../model';
import { ToolsInstallPath } from './../../constants';
import * as VariableNames from './constants';

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { EOL } from 'os';
import { ITool, NoteBookEnvironmentVariablePrefix } from '../interfaces';
import { setEnvironmentVariablesForInstallPaths, getRuntimeBinaryPathEnvironmentVariableName } from '../utils';
import { setEnvironmentVariablesForInstallPaths, getRuntimeBinaryPathEnvironmentVariableName } from '../common/utils';
import { ToolsInstallPath } from '../constants';
import { delimiter } from 'path';

View File

@@ -9,17 +9,18 @@ import { EOL, homedir as os_homedir } from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { ArcControllerConfigProfilesOptionsSource, ArcControllersOptionsSource, OptionsSourceType } from '../helpers/optionSources';
import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, FilePickerFieldInfo, 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';
import { KubeCtlTool, KubeCtlToolName } from '../services/tools/kubeCtlTool';
import { IToolsService } from '../services/toolsService';
import { getDateTimeString, getErrorMessage, throwUnless } from '../utils';
import { getDateTimeString, getErrorMessage, throwUnless } from '../common/utils';
import { WizardInfoBase } from './../interfaces';
import { Model } from './model';
import { RadioGroupLoadingComponentBuilder } from './radioGroupLoadingComponentBuilder';
import { optionsSourcesService } from '../services/optionSourcesService';
import { IOptionsSourceProvider } from 'resource-deployment';
const localize = nls.loadMessageBundle();
@@ -432,21 +433,10 @@ async function processOptionsTypeField(context: FieldContext): Promise<void> {
}
throwUnless(typeof context.fieldInfo.options === 'object', loc.optionsNotObjectOrArray);
throwUnless('optionsType' in context.fieldInfo.options, loc.optionsTypeNotFound);
if (context.fieldInfo.options.source) {
if (context.fieldInfo.options.source?.providerId) {
try {
let optionsSource: IOptionsSource;
switch (context.fieldInfo.options.source.type) {
case OptionsSourceType.ArcControllersOptionsSource:
optionsSource = new ArcControllersOptionsSource(context.fieldInfo.options.source.variableNames, context.fieldInfo.options.source.type);
break;
case OptionsSourceType.ArcControllerConfigProfilesOptionsSource:
optionsSource = new ArcControllerConfigProfilesOptionsSource(context.fieldInfo.options.source.variableNames, context.fieldInfo.options.source.type);
break;
default:
throw new Error(loc.noOptionsSourceDefined(context.fieldInfo.options.source.type));
}
context.fieldInfo.options.source = optionsSource;
context.fieldInfo.options.values = await context.fieldInfo.options.source.getOptions();
context.fieldInfo.options.source.provider = optionsSourcesService.getOptionsSource(context.fieldInfo.options.source.providerId);
context.fieldInfo.options.values = await context.fieldInfo.options.source.provider.getOptions();
}
catch (e) {
disableControlButtons(context.container);
@@ -466,35 +456,39 @@ async function processOptionsTypeField(context: FieldContext): Promise<void> {
throwUnless(context.fieldInfo.options.optionsType === OptionsType.Dropdown, loc.optionsTypeRadioOrDropdown);
optionsComponent = processDropdownOptionsTypeField(context);
}
if (context.fieldInfo.options.source) {
const optionsSource = context.fieldInfo.options.source;
for (const key of Object.keys(optionsSource.variableNames ?? {})) {
context.fieldInfo.subFields!.push({
label: context.fieldInfo.label,
variableName: optionsSource.variableNames[key]
});
context.onNewInputComponentCreated(optionsSource.variableNames[key], {
component: optionsComponent,
inputValueTransformer: async (controllerName: string) => {
try {
const variableValue = await optionsSource.getVariableValue(key, controllerName);
return variableValue;
} catch (e) {
disableControlButtons(context.container);
context.container.message = {
text: getErrorMessage(e),
description: '',
level: azdata.window.MessageLevel.Error
};
return '';
}
},
isPassword: optionsSource.getIsPassword(key)
});
}
const optionsSource = context.fieldInfo.options.source;
if (optionsSource?.provider) {
const optionsSourceProvider = optionsSource.provider;
await Promise.all(Object.keys(context.fieldInfo.options.source?.variableNames ?? {}).map(async key => {
await configureOptionsSourceSubfields(context, optionsSource, key, optionsComponent, optionsSourceProvider);
}));
}
}
async function configureOptionsSourceSubfields(context: FieldContext, optionsSource: IOptionsSource, variableKey: string, optionsComponent: InputComponent, optionsSourceProvider: IOptionsSourceProvider) {
context.fieldInfo.subFields!.push({
label: context.fieldInfo.label,
variableName: optionsSource.variableNames![variableKey]
});
context.onNewInputComponentCreated(optionsSource.variableNames![variableKey], {
component: optionsComponent,
inputValueTransformer: async (optionValue: string) => {
try {
return await optionsSourceProvider.getVariableValue!(variableKey, optionValue);
} catch (e) {
disableControlButtons(context.container);
context.container.message = {
text: getErrorMessage(e),
description: '',
level: azdata.window.MessageLevel.Error
};
return '';
}
},
isPassword: await optionsSourceProvider.getIsPassword!(variableKey)
});
}
function processDropdownOptionsTypeField(context: FieldContext): azdata.DropDownComponent {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const options = context.fieldInfo.options as OptionsInfo;
@@ -1304,7 +1298,7 @@ async function getInputComponentValue(inputComponents: InputComponents, key: str
if (input === undefined) {
return undefined;
}
let value;
let value: string | undefined;
if (input instanceof RadioGroupLoadingComponentBuilder) {
value = input.value;
} else if ('checked' in input) { // CheckBoxComponent
@@ -1319,14 +1313,7 @@ async function getInputComponentValue(inputComponents: InputComponents, key: str
} else {
throw new Error(`Unknown input type with ID ${input.id}`);
}
const inputValueTransformer = inputComponents[key].inputValueTransformer;
if (inputValueTransformer) {
value = inputValueTransformer(value ?? '');
if (typeof value !== 'string') {
value = await value;
}
}
return value;
return inputComponents[key].inputValueTransformer ? await inputComponents[key].inputValueTransformer!(value ?? '') : value;
}
export function isInputBoxEmpty(input: azdata.InputBoxComponent): boolean {

View File

@@ -5,7 +5,7 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { OptionsInfo, FieldInfo } from '../interfaces';
import { getErrorMessage } from '../utils';
import { getErrorMessage } from '../common/utils';
export class RadioGroupLoadingComponentBuilder implements azdata.ComponentBuilder<azdata.LoadingComponent, azdata.LoadingComponentProperties> {
private _optionsDivContainer!: azdata.DivContainer;

View File

@@ -9,7 +9,7 @@ import { AgreementInfo, DeploymentProvider, ITool, ResourceType, ToolStatus } fr
import { select } from '../localizedConstants';
import { IResourceTypeService } from '../services/resourceTypeService';
import { IToolsService } from '../services/toolsService';
import { getErrorMessage } from '../utils';
import { getErrorMessage } from '../common/utils';
import * as loc from './../localizedConstants';
import { DialogBase } from './dialogBase';
import { createFlexContainer } from './modelViewUtils';