controller dropdown field to SQL MIAA and Postgres deployment. (#12217)

* saving first draft

* throw if no controllers

* cleanup

* bug fixes

* bug fixes and caching controller access

* pr comments and bug fixes.

* fixes

* fixes

* comment fix

* remove debug prints

* comment fixes

* remove debug logs

* inputValueTransformer returns string|Promise

* PR feedback

* pr fixes

* remove _ from protected fields

* anonymous to full methods

* small fixes
This commit is contained in:
Arvind Ranasaria
2020-09-15 14:47:49 -07:00
committed by GitHub
parent 92ed830564
commit 9cf80113fc
36 changed files with 754 additions and 190 deletions

View File

@@ -0,0 +1,83 @@
/*---------------------------------------------------------------------------------------------
* 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

@@ -0,0 +1,100 @@
/*---------------------------------------------------------------------------------------------
* 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 type OptionsSourceType = 'ArcControllersOptionsSource';
const OptionsSources = new Map<OptionsSourceType, new () => OptionsSource>();
export abstract class OptionsSource implements IOptionsSource {
private _variableNames!: { [index: string]: string; };
private _type!: OptionsSourceType;
get type(): OptionsSourceType { return this._type; }
get variableNames(): { [index: string]: string; } { return this._variableNames; }
abstract async getOptions(): Promise<string[] | CategoryValue[]>;
abstract async getVariableValue(variableName: string, input: string): Promise<string>;
abstract getIsPassword(variableName: string): boolean;
protected constructor() {
}
static construct(optionsSourceType: OptionsSourceType, variableNames: { [index: string]: string }): OptionsSource {
const sourceConstructor = OptionsSources.get(optionsSourceType);
throwUnless(sourceConstructor !== undefined, loc.noOptionsSourceDefined(optionsSourceType));
const obj = new sourceConstructor();
obj._type = optionsSourceType;
obj._variableNames = variableNames;
return obj;
}
}
export class ArcControllersOptionsSource extends OptionsSource {
private _cacheManager = new CacheManager<string, string>();
constructor() {
super();
}
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));
}
}
}
OptionsSources.set(<OptionsSourceType>ArcControllersOptionsSource.name, ArcControllersOptionsSource);

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* 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

@@ -5,6 +5,7 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { OptionsSource, OptionsSourceType } from './helpers/optionSources';
export const NoteBookEnvironmentVariablePrefix = 'AZDATA_NB_VAR_';
@@ -172,9 +173,18 @@ export type ComponentCSSStyles = {
[key: string]: string;
};
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;
}
export interface OptionsInfo {
values: string[] | azdata.CategoryValue[],
values?: string[] | azdata.CategoryValue[],
source?: OptionsSource,
defaultValue: string,
optionsType?: OptionsType
}

View File

@@ -4,6 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { FieldType, OptionsType } from './interfaces';
import { OptionsSourceType } from './helpers/optionSources';
const localize = nls.loadMessageBundle();
@@ -13,12 +15,22 @@ export const resourceGroup = localize('azure.account.resourceGroup', "Resource G
export const location = localize('azure.account.location', "Azure Location");
export const browse = localize('filePicker.browse', "Browse");
export const select = localize('filePicker.select', "Select");
export const kubeConfigFilePath = localize('kubeConfigClusterPicker.kubeConfigFilePatht', "Kube config file path");
export const kubeConfigFilePath = localize('kubeConfigClusterPicker.kubeConfigFilePath', "Kube config file path");
export const clusterContextNotFound = localize('kubeConfigClusterPicker.clusterContextNotFound', "No cluster context information found");
export const signIn = localize('azure.signin', "Sign in…");
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 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: OptionsSourceType) => 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");
export const optionsTypeRadioOrDropdown = localize('optionsTypeRadioOrDropdown', "When optionsType is not {0} then it must be {1}", OptionsType.Radio, OptionsType.Dropdown);

View File

@@ -5,26 +5,17 @@
import * as azurecore from 'azurecore';
import * as vscode from 'vscode';
import * as arc from 'arc';
export interface IApiService {
getAzurecoreApi(): Promise<azurecore.IExtension>;
readonly azurecoreApi: azurecore.IExtension;
readonly arcApi: arc.IExtension;
}
class ApiService implements IApiService {
private azurecoreApi: azurecore.IExtension | undefined;
constructor() { }
public async getAzurecoreApi(): Promise<azurecore.IExtension> {
if (!this.azurecoreApi) {
this.azurecoreApi = <azurecore.IExtension>(await vscode.extensions.getExtension(azurecore.extension.name)?.activate());
if (!this.azurecoreApi) {
throw new Error('Unable to retrieve azurecore API');
}
}
return this.azurecoreApi;
}
public get azurecoreApi() { return vscode.extensions.getExtension(azurecore.extension.name)?.exports; }
public get arcApi() { return vscode.extensions.getExtension(arc.extension.name)?.exports; }
}
export const apiService: IApiService = new ApiService();

View File

@@ -1,17 +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 * as vscode from 'vscode';
export class ArcService {
private _arcApi: arc.IExtension;
constructor() {
this._arcApi = vscode.extensions.getExtension(arc.extension.name)?.exports;
}
public async getRegisteredDataControllers(): Promise<arc.ControllerInfo[]> {
return await this._arcApi.getRegisteredDataControllers();
}
}

View File

@@ -9,8 +9,8 @@ import { apiService } from '../services/apiService';
suite('API Service Tests', function (): void {
test('getAzurecoreApi returns azure api', async () => {
const api = await apiService.getAzurecoreApi();
test('getAzurecoreApi returns azure api', () => {
const api = apiService.azurecoreApi;
assert(api !== undefined);
});
});

View File

@@ -176,11 +176,11 @@ export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
});
}
public onLeave(): void {
public async onLeave(): Promise<void> {
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
return true;
});
setModelValues(this.inputComponents, this.wizard.model);
await setModelValues(this.inputComponents, this.wizard.model);
Object.assign(this.wizard.inputComponents, this.inputComponents);
}
}

View File

@@ -289,8 +289,8 @@ export class ClusterSettingsPage extends WizardPageBase<DeployClusterWizard> {
});
}
public onLeave() {
setModelValues(this.inputComponents, this.wizard.model);
public async onLeave(): Promise<void> {
await setModelValues(this.inputComponents, this.wizard.model);
Object.assign(this.wizard.inputComponents, this.inputComponents);
if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) {
const variableDNSPrefixMapping: { [s: string]: string } = {};

View File

@@ -233,7 +233,7 @@ export class DeploymentProfilePage extends WizardPageBase<DeployClusterWizard> {
});
}
public onLeave() {
public async onLeave(): Promise<void> {
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
return true;
});

View File

@@ -400,8 +400,8 @@ export class ServiceSettingsPage extends WizardPageBase<DeployClusterWizard> {
});
}
public onLeave(): void {
setModelValues(this.inputComponents, this.wizard.model);
public async onLeave(): Promise<void> {
await setModelValues(this.inputComponents, this.wizard.model);
Object.assign(this.wizard.inputComponents, this.inputComponents);
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
return true;

View File

@@ -326,7 +326,7 @@ export class SummaryPage extends WizardPageBase<DeployClusterWizard> {
this.form.addFormItems(this.formItems);
}
public onLeave() {
public async onLeave(): Promise<void> {
this.wizard.hideCustomButtons();
this.wizard.wizardObject.message = { text: '' };
}

View File

@@ -74,7 +74,7 @@ export class TargetClusterContextPage extends WizardPageBase<DeployClusterWizard
});
}
public onLeave() {
public async onLeave(): Promise<void> {
this.wizard.wizardObject.registerNavigationValidator((e) => {
return true;
});

View File

@@ -94,9 +94,9 @@ export class DeploymentInputDialog extends DialogBase {
});
}
protected onComplete(): void {
protected async onComplete(): Promise<void> {
const model: Model = new Model();
setModelValues(this.inputComponents, model);
await setModelValues(this.inputComponents, model);
if (instanceOfNotebookBasedDialogInfo(this.dialogInfo)) {
model.setEnvironmentVariables();
if (this.dialogInfo.runNotebook) {
@@ -110,7 +110,7 @@ export class DeploymentInputDialog extends DialogBase {
});
}
} else {
vscode.commands.executeCommand(this.dialogInfo.command, model);
await vscode.commands.executeCommand(this.dialogInfo.command, model);
}
}

View File

@@ -27,12 +27,12 @@ export abstract class DialogBase {
this.dispose();
}
private onOkButtonClicked(): void {
this.onComplete();
private async onOkButtonClicked(): Promise<void> {
await this.onComplete();
this.dispose();
}
protected onComplete(): void {
protected async onComplete(): Promise<void> {
}
protected dispose(): void {

View File

@@ -3,27 +3,28 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { azureResource } from 'azureResource';
import * as fs from 'fs';
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 { azureResource } from 'azureResource';
import { OptionsSource } from '../helpers/optionSources';
import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, FilePickerFieldInfo, 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 { assert, getDateTimeString, getErrorMessage } from '../utils';
import { KubeCtlTool, KubeCtlToolName } from '../services/tools/kubeCtlTool';
import { IToolsService } from '../services/toolsService';
import { getDateTimeString, getErrorMessage, throwUnless } from '../utils';
import { WizardInfoBase } from './../interfaces';
import { Model } from './model';
import { RadioGroupLoadingComponentBuilder } from './radioGroupLoadingComponentBuilder';
import { IToolsService } from '../services/toolsService';
import { KubeCtlToolName, KubeCtlTool } from '../services/tools/kubeCtlTool';
const localize = nls.loadMessageBundle();
export type Validator = () => { valid: boolean, message: string };
export type InputValueTransformer = (inputValue: string) => string;
export type InputValueTransformer = (inputValue: string) => string | Promise<string>;
export type InputComponent = azdata.TextComponent | azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent | RadioGroupLoadingComponentBuilder;
export type InputComponentInfo = {
component: InputComponent;
@@ -353,7 +354,7 @@ function addLabelInputPairToContainer(view: azdata.ModelView, components: azdata
async function processField(context: FieldContext): Promise<void> {
switch (context.fieldInfo.type) {
case FieldType.Options:
processOptionsTypeField(context);
await processOptionsTypeField(context);
break;
case FieldType.DateTimeText:
processDateTimeTextField(context);
@@ -390,12 +391,23 @@ async function processField(context: FieldContext): Promise<void> {
await processKubeStorageClassField(context);
break;
default:
throw new Error(localize('UnknownFieldTypeError', "Unknown field type: \"{0}\"", context.fieldInfo.type));
throw new Error(loc.unknownFieldTypeError(context.fieldInfo.type));
}
}
function processOptionsTypeField(context: FieldContext): void {
assert(context.fieldInfo.options !== undefined, `FieldInfo.options must be defined for FieldType:${FieldType.Options}`);
function disableControlButtons(container: azdata.window.Dialog | azdata.window.Wizard): void {
if ('okButton' in container) {
container.okButton.enabled = false;
} else {
container.doneButton.enabled = false;
container.nextButton.enabled = false;
container.backButton.enabled = false;
container.customButtons.forEach(b => b.enabled = false);
}
}
async function processOptionsTypeField(context: FieldContext): Promise<void> {
throwUnless(context.fieldInfo.options !== undefined, loc.optionsNotDefined(context.fieldInfo.type));
if (Array.isArray(context.fieldInfo.options)) {
context.fieldInfo.options = <OptionsInfo>{
values: context.fieldInfo.options,
@@ -403,17 +415,62 @@ function processOptionsTypeField(context: FieldContext): void {
optionsType: OptionsType.Dropdown
};
}
assert(typeof context.fieldInfo.options === 'object', `FieldInfo.options must be an object if it is not an array`);
assert('optionsType' in context.fieldInfo.options, `When FieldInfo.options is an object it must have 'optionsType' property`);
throwUnless(typeof context.fieldInfo.options === 'object', loc.optionsNotObjectOrArray);
throwUnless('optionsType' in context.fieldInfo.options, loc.optionsTypeNotFound);
if (context.fieldInfo.options.source) {
try {
// if options.source still points to the IOptionsSource interface make it to point to the implementation
context.fieldInfo.options.source = OptionsSource.construct(context.fieldInfo.options.source.type, context.fieldInfo.options.source.variableNames);
context.fieldInfo.options.values = await context.fieldInfo.options.source.getOptions();
}
catch (e) {
disableControlButtons(context.container);
context.container.message = {
text: getErrorMessage(e),
description: '',
level: azdata.window.MessageLevel.Error
};
context.fieldInfo.options.values = [];
}
context.fieldInfo.subFields = context.fieldInfo.subFields || [];
}
let optionsComponent: InputComponent;
if (context.fieldInfo.options.optionsType === OptionsType.Radio) {
processRadioOptionsTypeField(context);
optionsComponent = await processRadioOptionsTypeField(context);
} else {
assert(context.fieldInfo.options.optionsType === OptionsType.Dropdown, `When optionsType is not ${OptionsType.Radio} then it must be ${OptionsType.Dropdown}`);
processDropdownOptionsTypeField(context);
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)
});
}
}
}
function processDropdownOptionsTypeField(context: FieldContext): void {
function processDropdownOptionsTypeField(context: FieldContext): azdata.DropDownComponent {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const options = context.fieldInfo.options as OptionsInfo;
const dropdown = createDropdown(context.view, {
@@ -427,6 +484,7 @@ function processDropdownOptionsTypeField(context: FieldContext): void {
dropdown.fireOnTextChange = true;
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: dropdown });
addLabelInputPairToContainer(context.view, context.components, label, dropdown, context.fieldInfo);
return dropdown;
}
function processDateTimeTextField(context: FieldContext): void {
@@ -579,8 +637,8 @@ function processEvaluatedTextField(context: FieldContext): ReadOnlyFieldInputs {
const readOnlyField = processReadonlyTextField(context, false /*allowEvaluation*/);
context.onNewInputComponentCreated(context.fieldInfo.variableName || context.fieldInfo.label, {
component: readOnlyField.text!,
inputValueTransformer: () => {
readOnlyField.text!.value = substituteVariableValues(context.inputComponents, context.fieldInfo.defaultValue);
inputValueTransformer: async () => {
readOnlyField.text!.value = await substituteVariableValues(context.inputComponents, context.fieldInfo.defaultValue);
return readOnlyField.text?.value!;
}
});
@@ -596,14 +654,15 @@ function processEvaluatedTextField(context: FieldContext): ReadOnlyFieldInputs {
* @param inputValue
* @param inputComponents
*/
function substituteVariableValues(inputComponents: InputComponents, inputValue?: string): string | undefined {
Object.keys(inputComponents)
async function substituteVariableValues(inputComponents: InputComponents, inputValue?: string): Promise<string | undefined> {
await Promise.all(Object.keys(inputComponents)
.filter(key => key.startsWith(NoteBookEnvironmentVariablePrefix))
.forEach(key => {
const value = getInputComponentValue(inputComponents, key) ?? '<undefined>';
.map(async key => {
const value = (await getInputComponentValue(inputComponents, key)) ?? '<undefined>';
const re: RegExp = new RegExp(`\\\$\\\(${key}\\\)`, 'gi');
inputValue = inputValue?.replace(re, value);
});
})
);
return inputValue;
}
@@ -984,7 +1043,7 @@ async function handleSelectedAccountChanged(
}
try {
const response = await (await apiService.getAzurecoreApi()).getSubscriptions(selectedAccount, true);
const response = await apiService.azurecoreApi.getSubscriptions(selectedAccount, true);
if (!response) {
return;
}
@@ -1099,7 +1158,7 @@ async function handleSelectedSubscriptionChanged(context: AzureAccountFieldConte
return;
}
try {
const response = await (await apiService.getAzurecoreApi()).getResourceGroups(selectedAccount, selectedSubscription, true);
const response = await apiService.azurecoreApi.getResourceGroups(selectedAccount, selectedSubscription, true);
if (!response) {
return;
}
@@ -1150,8 +1209,7 @@ async function processAzureLocationsField(context: AzureLocationsFieldContext):
width: context.fieldInfo.labelWidth,
cssStyles: context.fieldInfo.labelCSSStyles
});
const azurecoreApi = await apiService.getAzurecoreApi();
const locationValues = context.fieldInfo.locations?.map(l => { return { name: l, displayName: azurecoreApi.getRegionDisplayName(l) }; });
const locationValues = context.fieldInfo.locations?.map(l => { return { name: l, displayName: apiService.azurecoreApi.getRegionDisplayName(l) }; });
const locationDropdown = createDropdown(context.view, {
defaultValue: locationValues?.find(l => l.name === context.fieldInfo.defaultValue),
width: context.fieldInfo.inputWidth,
@@ -1174,7 +1232,7 @@ async function processAzureLocationsField(context: AzureLocationsFieldContext):
label: label.value!,
variableName: context.fieldInfo.displayLocationVariableName
});
context.onNewInputComponentCreated(context.fieldInfo.displayLocationVariableName, { component: locationDropdown, inputValueTransformer: (value => azurecoreApi.getRegionDisplayName(value)) });
context.onNewInputComponentCreated(context.fieldInfo.displayLocationVariableName, { component: locationDropdown, inputValueTransformer: (value => apiService.azurecoreApi.getRegionDisplayName(value)) });
}
addLabelInputPairToContainer(context.view, context.components, label, locationDropdown, context.fieldInfo);
return locationDropdown;
@@ -1207,14 +1265,14 @@ export function getPasswordMismatchMessage(fieldName: string): string {
return localize('passwordNotMatch', "{0} doesn't match the confirmation password", fieldName);
}
export function setModelValues(inputComponents: InputComponents, model: Model): void {
Object.keys(inputComponents).forEach(key => {
const value = getInputComponentValue(inputComponents, key);
export async function setModelValues(inputComponents: InputComponents, model: Model): Promise<void> {
await Promise.all(Object.keys(inputComponents).map(async key => {
const value = await getInputComponentValue(inputComponents, key);
model.setPropertyValue(key, value);
});
}));
}
function getInputComponentValue(inputComponents: InputComponents, key: string): string | undefined {
async function getInputComponentValue(inputComponents: InputComponents, key: string): Promise<string | undefined> {
const input = inputComponents[key].component;
if (input === undefined) {
return undefined;
@@ -1236,7 +1294,10 @@ function getInputComponentValue(inputComponents: InputComponents, key: string):
}
const inputValueTransformer = inputComponents[key].inputValueTransformer;
if (inputValueTransformer) {
value = inputValueTransformer(value || '');
value = inputValueTransformer(value ?? '');
if (typeof value !== 'string') {
value = await value;
}
}
return value;
}

View File

@@ -58,7 +58,7 @@ export class NotebookWizard extends WizardBase<NotebookWizard, NotebookWizardPag
}
protected async onOk(): Promise<void> {
setModelValues(this.inputComponents, this.model);
await setModelValues(this.inputComponents, this.model);
const env: NodeJS.ProcessEnv = {};
this.model.setEnvironmentVariables(env, (varName) => {
const isPassword = !!this.inputComponents[varName]?.isPassword;

View File

@@ -33,7 +33,7 @@ export class NotebookWizardAutoSummaryPage extends NotebookWizardPage {
});
}
public onLeave(): void {
public async onLeave(): Promise<void> {
this.wizard.wizardObject.message = { text: '' };
}

View File

@@ -57,7 +57,7 @@ export class NotebookWizardPage extends WizardPageBase<NotebookWizard> {
});
}
public onLeave(): void {
public async onLeave(): Promise<void> {
// The following callback registration clears previous navigation validators.
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
return true;
@@ -66,7 +66,7 @@ export class NotebookWizardPage extends WizardPageBase<NotebookWizard> {
public async onEnter(): Promise<void> {
if (this.pageInfo.isSummaryPage) {
setModelValues(this.wizard.inputComponents, this.wizard.model);
await setModelValues(this.wizard.inputComponents, this.wizard.model);
}
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {

View File

@@ -355,7 +355,7 @@ export class ResourceTypePickerDialog extends DialogBase {
return this._selectedResourceType.getProvider(options)!;
}
protected onComplete(): void {
protected async onComplete(): Promise<void> {
this.toolsService.toolsForCurrentProvider = this._tools;
this.resourceTypeService.startDeployment(this.getCurrentProvider());
}

View File

@@ -31,7 +31,7 @@ export abstract class WizardBase<T, P extends WizardPageBase<T>, M extends Model
this.toDispose.push(this.wizardObject.onPageChanged(async (e) => {
let previousPage = this.pages[e.lastPage];
let newPage = this.pages[e.newPage];
previousPage.onLeave();
await previousPage.onLeave();
await newPage.onEnter();
}));

View File

@@ -25,7 +25,7 @@ export abstract class WizardPageBase<T> {
public async onEnter(): Promise<void> { }
public onLeave(): void { }
public async onLeave(): Promise<void> { }
public abstract initialize(): void;

View File

@@ -40,7 +40,15 @@ export function setEnvironmentVariablesForInstallPaths(tools: ITool[], env: Node
}
}
export function assert(condition: boolean, message?: string): asserts condition {
/**
* Throws an Error with given {@link message} unless {@link condition} is true.
* This also tells the typescript compiler that the condition is 'truthy' in the remainder of the scope
* where this function was called.
*
* @param condition
* @param message
*/
export function throwUnless(condition: boolean, message?: string): asserts condition {
if (!condition) {
throw new Error(message);
}