Custom Summary Page for NotebookWizard and notebook Cell with wizard variables (#10297)

* save not yet tested work

* Merge from master.

* Screeens Shared for Feeedback

* Code complete

* remove unneeded changes

* remove unnecessary comma

* remov wss

* remove dead code

* PR feedback

* checkpoint fixes

* PR & minor fixes

* minor fix for feature of  resourceType options being optional.

* reverting experimental change

* separating out changes for future featurework.

* revert unneeded change

* review feedback fixes

* review feedback

* rename InputFieldComponent to InputComponent

* working version of custom summary page

* add option to align items in a flex- container.

* changes to support labelColor

* save work , still pending issue with labelCSSStyles

* Summary page and setting variabless in notebook.

* minor fixes.

* pr feedbck

* fix formatting issues

* pr feedback

* pr feedback

* pr feedback

* fixing docs

* summary page value setting fix

* rename children of RowInfo to items

* rename a method

* rename summary_text to evaluated_text

* rename properties of fieldInfo

* revert inadvertent change

* rename linked_texttext to hyperlinked_text and removing linking facility from readonly_text

* pr feedback

* fix setting tools variables in env and notebook

* removing saving of originalValues for EvaluatedText

* await on launchNotebookWithEdits

* await on launchNotebookWithContent

* merge RadioOptions & Options into 1

* merge ReadOnlyText, links  & evaluatedText

* Samples for new generic wizard features

* fix comment

* fix assertions

* return type and comment for getClusterContext

* fix inadvertent change

* increase minimum required azdata version

* remove unneeded environment variable settings

* not leaking passwords in notebooks
This commit is contained in:
Arvind Ranasaria
2020-06-01 11:14:59 -07:00
committed by GitHub
parent 84492049e8
commit 678bbe3142
23 changed files with 1190 additions and 473 deletions

View File

@@ -9,19 +9,28 @@ import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as azurecore from '../../../azurecore/src/azurecore';
import { azureResource } from '../../../azurecore/src/azureResource/azure-resource';
import { AzureAccountFieldInfo, AzureLocationsFieldInfo, DialogInfoBase, FieldInfo, FieldType, FontStyle, FontWeight, LabelPosition, PageInfoBase, SectionInfo, KubeClusterContextFieldInfo } from '../interfaces';
import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, KubeClusterContextFieldInfo, LabelPosition, NoteBookEnvironmentVariablePrefix, OptionsInfo, OptionsType, PageInfoBase, RowInfo, SectionInfo, TextCSSStyles } from '../interfaces';
import * as loc from '../localizedConstants';
import { getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../services/kubeService';
import { getDateTimeString, getErrorMessage } from '../utils';
import { assert, getDateTimeString, getErrorMessage } from '../utils';
import { WizardInfoBase } from './../interfaces';
import { Model } from './model';
import { RadioGroupLoadingComponentBuilder } from './radioGroupLoadingComponentBuilder';
const localize = nls.loadMessageBundle();
export type Validator = () => { valid: boolean, message: string };
export type InputValueTransformer = (inputValue: string) => string;
export type InputComponent = azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent | azdata.RadioButtonComponent;
export type InputComponents = { [s: string]: { component: InputComponent; inputValueTransformer?: InputValueTransformer } };
export type InputComponent = azdata.TextComponent | azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent | RadioGroupLoadingComponentBuilder;
export type InputComponentInfo = {
component: InputComponent;
inputValueTransformer?: InputValueTransformer;
isPassword?: boolean
};
export type InputComponents = {
[s: string]: InputComponentInfo
};
export function getInputBoxComponent(name: string, inputComponents: InputComponents): azdata.InputBoxComponent {
return <azdata.InputBoxComponent>inputComponents[name].component;
@@ -39,40 +48,42 @@ export function getTextComponent(name: string, inputComponents: InputComponents)
return <azdata.TextComponent>inputComponents[name].component;
}
export const DefaultInputComponentWidth = '400px';
export const DefaultLabelComponentWidth = '200px';
export const DefaultInputWidth = '400px';
export const DefaultLabelWidth = '200px';
export const DefaultFieldAlignItems = undefined;
export const DefaultFieldWidth = undefined;
export const DefaultFieldHeight = undefined;
export interface DialogContext extends CreateContext {
export interface DialogContext extends ContextBase {
dialogInfo: DialogInfoBase;
container: azdata.window.Dialog;
}
export interface WizardPageContext extends CreateContext {
export interface WizardPageContext extends ContextBase {
wizardInfo: WizardInfoBase;
pageInfo: PageInfoBase;
page: azdata.window.WizardPage;
container: azdata.window.Wizard;
}
export interface SectionContext extends CreateContext {
export interface SectionContext extends ContextBase {
sectionInfo: SectionInfo;
view: azdata.ModelView;
}
interface FieldContext extends CreateContext {
export interface FieldContext extends ContextBase {
fieldInfo: FieldInfo;
components: azdata.Component[];
view: azdata.ModelView;
}
interface FilePickerInputs {
export interface FilePickerInputs {
input: azdata.InputBoxComponent;
browseButton: azdata.ButtonComponent;
}
interface RadioOptionsInputs {
optionsList: azdata.DivContainer;
loader: azdata.LoadingComponent;
interface ReadOnlyFieldInputs {
label: azdata.TextComponent;
text?: azdata.TextComponent;
}
interface KubeClusterContextFieldContext extends FieldContext {
@@ -87,11 +98,12 @@ interface AzureAccountFieldContext extends FieldContext {
fieldInfo: AzureAccountFieldInfo;
}
interface CreateContext {
interface ContextBase {
container: azdata.window.Dialog | azdata.window.Wizard;
inputComponents: InputComponents;
onNewValidatorCreated: (validator: Validator) => void;
onNewDisposableCreated: (disposable: vscode.Disposable) => void;
onNewInputComponentCreated: (name: string, component: InputComponent, inputValueTransformer?: InputValueTransformer) => void;
onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo) => void;
}
export function createTextInput(view: azdata.ModelView, inputInfo: { defaultValue?: string, ariaLabel: string, required?: boolean, placeHolder?: string, width?: string, enabled?: boolean }): azdata.InputBoxComponent {
@@ -106,12 +118,20 @@ export function createTextInput(view: azdata.ModelView, inputInfo: { defaultValu
}).component();
}
export function createLabel(view: azdata.ModelView, info: { text: string, description?: string, required?: boolean, width?: string, fontStyle?: FontStyle, fontWeight?: FontWeight, links?: azdata.LinkArea[] }): azdata.TextComponent {
export function createLabel(view: azdata.ModelView, info: { text: string, description?: string, required?: boolean, width?: string, links?: azdata.LinkArea[], cssStyles?: TextCSSStyles }): azdata.TextComponent {
let cssStyles: { [key: string]: string } = {};
if (info.cssStyles !== undefined) {
cssStyles = Object.assign(cssStyles, info.cssStyles, { 'font-style': info.cssStyles.fontStyle || 'normal', 'font-weight': info.cssStyles.fontWeight || 'normal' });
if (info.cssStyles.color !== undefined) {
cssStyles['color'] = info.cssStyles.color;
}
}
const text = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: info.text,
description: info.description,
requiredIndicator: info.required,
CSSStyles: { 'font-style': info.fontStyle || 'normal', 'font-weight': info.fontWeight || 'normal' },
CSSStyles: cssStyles,
links: info.links
}).component();
text.width = info.width;
@@ -157,8 +177,11 @@ export function initializeDialog(dialogContext: DialogContext): void {
const tab = azdata.window.createTab(tabInfo.title);
tab.registerContent((view: azdata.ModelView) => {
const sections = tabInfo.sections.map(sectionInfo => {
sectionInfo.inputWidth = sectionInfo.inputWidth || tabInfo.inputWidth || DefaultInputComponentWidth;
sectionInfo.labelWidth = sectionInfo.labelWidth || tabInfo.labelWidth || DefaultLabelComponentWidth;
sectionInfo.inputWidth = sectionInfo.inputWidth || tabInfo.inputWidth || DefaultInputWidth;
sectionInfo.labelWidth = sectionInfo.labelWidth || tabInfo.labelWidth || DefaultLabelWidth;
sectionInfo.fieldAlignItems = sectionInfo.fieldAlignItems || tabInfo.fieldAlignItems || DefaultFieldAlignItems;
sectionInfo.fieldWidth = sectionInfo.fieldWidth || tabInfo.fieldWidth || DefaultFieldWidth;
sectionInfo.fieldHeight = sectionInfo.fieldHeight || tabInfo.fieldHeight || DefaultFieldHeight;
sectionInfo.labelPosition = sectionInfo.labelPosition || tabInfo.labelPosition;
return createSection({
sectionInfo: sectionInfo,
@@ -166,7 +189,8 @@ export function initializeDialog(dialogContext: DialogContext): void {
onNewDisposableCreated: dialogContext.onNewDisposableCreated,
onNewInputComponentCreated: dialogContext.onNewInputComponentCreated,
onNewValidatorCreated: dialogContext.onNewValidatorCreated,
container: dialogContext.container
container: dialogContext.container,
inputComponents: dialogContext.inputComponents
});
});
const formBuilder = view.modelBuilder.formContainer().withFormItems(
@@ -189,12 +213,16 @@ export function initializeDialog(dialogContext: DialogContext): void {
export function initializeWizardPage(context: WizardPageContext): void {
context.page.registerContent((view: azdata.ModelView) => {
const sections = context.pageInfo.sections.map(sectionInfo => {
sectionInfo.inputWidth = sectionInfo.inputWidth || context.pageInfo.inputWidth || context.wizardInfo.inputWidth || DefaultInputComponentWidth;
sectionInfo.labelWidth = sectionInfo.labelWidth || context.pageInfo.labelWidth || context.wizardInfo.labelWidth || DefaultLabelComponentWidth;
sectionInfo.inputWidth = sectionInfo.inputWidth || context.pageInfo.inputWidth || context.wizardInfo.inputWidth || DefaultInputWidth;
sectionInfo.labelWidth = sectionInfo.labelWidth || context.pageInfo.labelWidth || context.wizardInfo.labelWidth || DefaultLabelWidth;
sectionInfo.fieldAlignItems = sectionInfo.fieldAlignItems || context.pageInfo.fieldAlignItems || DefaultFieldAlignItems;
sectionInfo.fieldWidth = sectionInfo.fieldWidth || context.pageInfo.fieldWidth || context.wizardInfo.fieldWidth || DefaultFieldWidth;
sectionInfo.fieldHeight = sectionInfo.fieldHeight || context.pageInfo.fieldHeight || context.wizardInfo.fieldHeight || DefaultFieldHeight;
sectionInfo.labelPosition = sectionInfo.labelPosition || context.pageInfo.labelPosition || context.wizardInfo.labelPosition;
return createSection({
view: view,
container: context.container,
inputComponents: context.inputComponents,
onNewDisposableCreated: context.onNewDisposableCreated,
onNewInputComponentCreated: context.onNewInputComponentCreated,
onNewValidatorCreated: context.onNewValidatorCreated,
@@ -215,16 +243,16 @@ export function initializeWizardPage(context: WizardPageContext): void {
export function createSection(context: SectionContext): azdata.GroupContainer {
const components: azdata.Component[] = [];
context.sectionInfo.inputWidth = context.sectionInfo.inputWidth || DefaultInputComponentWidth;
context.sectionInfo.labelWidth = context.sectionInfo.labelWidth || DefaultLabelComponentWidth;
context.sectionInfo.inputWidth = context.sectionInfo.inputWidth || DefaultInputWidth;
context.sectionInfo.labelWidth = context.sectionInfo.labelWidth || DefaultLabelWidth;
context.sectionInfo.fieldAlignItems = context.sectionInfo.fieldAlignItems || DefaultFieldAlignItems;
context.sectionInfo.fieldWidth = context.sectionInfo.fieldWidth || DefaultFieldWidth;
context.sectionInfo.fieldHeight = context.sectionInfo.fieldHeight || DefaultFieldHeight;
if (context.sectionInfo.fields) {
processFields(context.sectionInfo.fields, components, context);
} else if (context.sectionInfo.rows) {
context.sectionInfo.rows.forEach(rowInfo => {
const rowItems: azdata.Component[] = [];
processFields(rowInfo.fields, rowItems, context, context.sectionInfo.spaceBetweenFields || '50px');
const row = createFlexContainer(context.view, rowItems);
components.push(row);
components.push(processRow(rowInfo, context));
});
}
@@ -235,11 +263,26 @@ export function createSection(context: SectionContext): azdata.GroupContainer {
});
}
function processRow(rowInfo: RowInfo, context: SectionContext): azdata.Component {
const items: azdata.Component[] = [];
if ('items' in rowInfo.items[0]) { // rowInfo.items is RowInfo[]
const rowItems = rowInfo.items as RowInfo[];
items.push(...rowItems.map(rowInfo => processRow(rowInfo, context)));
} else { // rowInfo.items is FieldInfo[]
const fieldItems = rowInfo.items as FieldInfo[];
processFields(fieldItems, items, context, context.sectionInfo.spaceBetweenFields === undefined ? '50px' : context.sectionInfo.spaceBetweenFields);
}
return createFlexContainer(context.view, items, true, context.sectionInfo.fieldWidth, context.sectionInfo.fieldHeight, context.sectionInfo.fieldAlignItems, rowInfo.cssStyles);
}
function processFields(fieldInfoArray: FieldInfo[], components: azdata.Component[], context: SectionContext, spaceBetweenFields?: string): void {
for (let i = 0; i < fieldInfoArray.length; i++) {
const fieldInfo = fieldInfoArray[i];
fieldInfo.labelWidth = fieldInfo.labelWidth || context.sectionInfo.labelWidth;
fieldInfo.inputWidth = fieldInfo.inputWidth || context.sectionInfo.inputWidth;
fieldInfo.fieldAlignItems = fieldInfo.fieldAlignItems || context.sectionInfo.fieldAlignItems;
fieldInfo.fieldWidth = fieldInfo.fieldWidth || context.sectionInfo.fieldWidth;
fieldInfo.fieldHeight = fieldInfo.fieldHeight || context.sectionInfo.fieldHeight;
fieldInfo.labelPosition = fieldInfo.labelPosition === undefined ? context.sectionInfo.labelPosition : fieldInfo.labelPosition;
processField({
view: context.view,
@@ -248,6 +291,7 @@ function processFields(fieldInfoArray: FieldInfo[], components: azdata.Component
onNewValidatorCreated: context.onNewValidatorCreated,
fieldInfo: fieldInfo,
container: context.container,
inputComponents: context.inputComponents,
components: components
});
if (spaceBetweenFields && i < fieldInfoArray.length - 1) {
@@ -256,24 +300,37 @@ function processFields(fieldInfoArray: FieldInfo[], components: azdata.Component
}
}
export function createFlexContainer(view: azdata.ModelView, items: azdata.Component[], rowLayout: boolean = true): azdata.FlexContainer {
export function createFlexContainer(view: azdata.ModelView, items: azdata.Component[], rowLayout: boolean = true, width?: string | number, height?: string | number, alignItems?: azdata.AlignItemsType, cssStyles?: ComponentCSSStyles): azdata.FlexContainer {
const flexFlow = rowLayout ? 'row' : 'column';
const alignItems = rowLayout ? 'center' : undefined;
const itemsStyle = rowLayout ? { CSSStyles: { 'margin-right': '5px' } } : {};
return view.modelBuilder.flexContainer().withItems(items, itemsStyle).withLayout({ flexFlow: flexFlow, alignItems: alignItems }).component();
alignItems = alignItems || (rowLayout ? 'center' : undefined);
const itemsStyle = rowLayout ? { CSSStyles: { 'margin-right': '5px', } } : {};
const flexLayout: azdata.FlexLayout = { flexFlow: flexFlow };
if (height) {
flexLayout.height = height;
}
if (width) {
flexLayout.width = width;
}
if (alignItems) {
flexLayout.alignItems = alignItems;
}
return view.modelBuilder.flexContainer().withItems(items, itemsStyle).withLayout(flexLayout).withProperties<azdata.ComponentProperties>({ CSSStyles: cssStyles || {} }).component();
}
export function createGroupContainer(view: azdata.ModelView, items: azdata.Component[], layout: azdata.GroupLayout): azdata.GroupContainer {
return view.modelBuilder.groupContainer().withItems(items).withLayout(layout).component();
}
function addLabelInputPairToContainer(view: azdata.ModelView, components: azdata.Component[], label: azdata.Component, input: azdata.Component, labelPosition?: LabelPosition, additionalComponents?: azdata.Component[]) {
const inputs = [label, input];
function addLabelInputPairToContainer(view: azdata.ModelView, components: azdata.Component[], label: azdata.Component, input: azdata.Component | undefined, fieldInfo: FieldInfo, additionalComponents?: azdata.Component[]) {
const inputs: azdata.Component[] = [label];
if (input !== undefined) {
inputs.push(input);
}
if (additionalComponents && additionalComponents.length > 0) {
inputs.push(...additionalComponents);
}
if (labelPosition && labelPosition === LabelPosition.Left) {
const row = createFlexContainer(view, inputs);
if (fieldInfo.labelPosition === LabelPosition.Left) {
const row = createFlexContainer(view, inputs, true, fieldInfo.fieldWidth, fieldInfo.fieldHeight, fieldInfo.fieldAlignItems);
components.push(row);
} else {
components.push(...inputs);
@@ -285,9 +342,6 @@ function processField(context: FieldContext): void {
case FieldType.Options:
processOptionsTypeField(context);
break;
case FieldType.RadioOptions:
processRadioOptionsTypeField(context);
break;
case FieldType.DateTimeText:
processDateTimeTextField(context);
break;
@@ -325,21 +379,42 @@ function processField(context: FieldContext): void {
}
function processOptionsTypeField(context: FieldContext): void {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight });
assert(context.fieldInfo.options !== undefined, `FieldInfo.options must be defined for FieldType:${FieldType.Options}`);
if (Array.isArray(context.fieldInfo.options)) {
context.fieldInfo.options = <OptionsInfo>{
values: context.fieldInfo.options,
defaultValue: context.fieldInfo.defaultValue,
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`);
if (context.fieldInfo.options.optionsType === OptionsType.Radio) {
processRadioOptionsTypeField(context);
} else {
assert(context.fieldInfo.options.optionsType === OptionsType.Dropdown, `When optionsType is not ${OptionsType.Radio} then it must be ${OptionsType.Dropdown}`);
processDropdownOptionsTypeField(context);
}
}
function processDropdownOptionsTypeField(context: FieldContext): void {
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, {
values: context.fieldInfo.options,
defaultValue: context.fieldInfo.defaultValue,
values: options.values,
defaultValue: options.defaultValue,
width: context.fieldInfo.inputWidth,
editable: context.fieldInfo.editable,
required: context.fieldInfo.required,
label: context.fieldInfo.label
});
context.onNewInputComponentCreated(context.fieldInfo.variableName!, dropdown);
addLabelInputPairToContainer(context.view, context.components, label, dropdown, context.fieldInfo.labelPosition);
dropdown.fireOnTextChange = true;
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: dropdown });
addLabelInputPairToContainer(context.view, context.components, label, dropdown, context.fieldInfo);
}
function processDateTimeTextField(context: FieldContext): void {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight });
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 defaultValue = context.fieldInfo.defaultValue + getDateTimeString();
const input = context.view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
value: defaultValue,
@@ -349,12 +424,12 @@ function processDateTimeTextField(context: FieldContext): void {
placeHolder: context.fieldInfo.placeHolder
}).component();
input.width = context.fieldInfo.inputWidth;
context.onNewInputComponentCreated(context.fieldInfo.variableName!, input);
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo.labelPosition);
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: input });
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo);
}
function processNumberField(context: FieldContext): void {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight });
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 input = createNumberInput(context.view, {
defaultValue: context.fieldInfo.defaultValue,
ariaLabel: context.fieldInfo.label,
@@ -364,12 +439,12 @@ function processNumberField(context: FieldContext): void {
width: context.fieldInfo.inputWidth,
placeHolder: context.fieldInfo.placeHolder
});
context.onNewInputComponentCreated(context.fieldInfo.variableName!, input);
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo.labelPosition);
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: input });
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo);
}
function processTextField(context: FieldContext): void {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight });
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 input = createTextInput(context.view, {
defaultValue: context.fieldInfo.defaultValue,
ariaLabel: context.fieldInfo.label,
@@ -378,8 +453,8 @@ function processTextField(context: FieldContext): void {
width: context.fieldInfo.inputWidth,
enabled: context.fieldInfo.enabled
});
context.onNewInputComponentCreated(context.fieldInfo.variableName!, input);
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo.labelPosition);
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: input });
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo);
if (context.fieldInfo.textValidationRequired) {
let validationRegex: RegExp = new RegExp(context.fieldInfo.textValidationRegex!);
@@ -399,12 +474,11 @@ function processTextField(context: FieldContext): void {
return { valid: inputIsValid, message: context.fieldInfo.textValidationDescription! };
};
context.onNewValidatorCreated(inputValidator);
}
}
function processPasswordField(context: FieldContext): void {
const passwordLabel = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight });
const passwordLabel = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const passwordInput = context.view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: context.fieldInfo.label,
inputType: 'password',
@@ -412,8 +486,8 @@ function processPasswordField(context: FieldContext): void {
placeHolder: context.fieldInfo.placeHolder,
width: context.fieldInfo.inputWidth
}).component();
context.onNewInputComponentCreated(context.fieldInfo.variableName!, passwordInput);
addLabelInputPairToContainer(context.view, context.components, passwordLabel, passwordInput, context.fieldInfo.labelPosition);
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: passwordInput, isPassword: true });
addLabelInputPairToContainer(context.view, context.components, passwordLabel, passwordInput, context.fieldInfo);
if (context.fieldInfo.type === FieldType.SQLPassword) {
const invalidPasswordMessage = getInvalidSQLPasswordMessage(context.fieldInfo.label);
@@ -430,7 +504,7 @@ function processPasswordField(context: FieldContext): void {
if (context.fieldInfo.confirmationRequired) {
const passwordNotMatchMessage = getPasswordMismatchMessage(context.fieldInfo.label);
const confirmPasswordLabel = createLabel(context.view, { text: context.fieldInfo.confirmationLabel!, required: true, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight });
const confirmPasswordLabel = createLabel(context.view, { text: context.fieldInfo.confirmationLabel!, required: true, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const confirmPasswordInput = context.view.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
ariaLabel: context.fieldInfo.confirmationLabel,
inputType: 'password',
@@ -438,7 +512,7 @@ function processPasswordField(context: FieldContext): void {
width: context.fieldInfo.inputWidth
}).component();
addLabelInputPairToContainer(context.view, context.components, confirmPasswordLabel, confirmPasswordInput, context.fieldInfo.labelPosition);
addLabelInputPairToContainer(context.view, context.components, confirmPasswordLabel, confirmPasswordInput, context.fieldInfo);
context.onNewValidatorCreated((): { valid: boolean, message: string } => {
const passwordMatches = passwordInput.value === confirmPasswordInput.value;
return { valid: passwordMatches, message: passwordNotMatchMessage };
@@ -459,34 +533,85 @@ function processPasswordField(context: FieldContext): void {
}
}
function processReadonlyTextField(context: FieldContext): void {
let defaultValue = context.fieldInfo.defaultValue || '';
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: false, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight });
const text = createLabel(context.view, { text: defaultValue, description: '', required: false, width: context.fieldInfo.inputWidth, fontWeight: context.fieldInfo.textFontWeight, fontStyle: context.fieldInfo.fontStyle, links: context.fieldInfo.links });
addLabelInputPairToContainer(context.view, context.components, label, text, context.fieldInfo.labelPosition);
function processReadonlyTextField(context: FieldContext, allowEvaluation: boolean = true): ReadOnlyFieldInputs {
if ((context.fieldInfo.links?.length ?? 0) > 0) {
return processHyperlinkedTextField(context);
} else if (context.fieldInfo.isEvaluated && allowEvaluation) {
return processEvaluatedTextField(context);
}
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: false, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const text = context.fieldInfo.defaultValue !== undefined
? createLabel(context.view, { text: context.fieldInfo.defaultValue, description: '', required: false, width: context.fieldInfo.inputWidth })
: undefined;
addLabelInputPairToContainer(context.view, context.components, label, text, context.fieldInfo);
return { label: label, text: text };
}
/**
* creates a text component that has text that contains hyperlinks. The context.fieldInfo.label contains {0},{1} ...
* placeholder(s) where contents of link array object are placed with that portion interpolated as a clickable link.
*
* @param context - the FieldContext object using which the field gets created
*/
function processHyperlinkedTextField(context: FieldContext): ReadOnlyFieldInputs {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: false, width: context.fieldInfo.labelWidth, links: context.fieldInfo.links, cssStyles: context.fieldInfo.labelCSSStyles });
context.components.push(label);
return { label: label };
}
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);
return readOnlyField.text?.value!;
}
});
return readOnlyField;
}
/**
* Returns a string that interpolates all variable names in the {@param inputValue} string de-marked as $(VariableName)
* substituted with their corresponding values.
*
* Only variables in the current model starting with {@see NoteBookEnvironmentVariablePrefix} are replaced.
*
* @param inputValue
* @param inputComponents
*/
function substituteVariableValues(inputComponents: InputComponents, inputValue?: string): string | undefined {
Object.keys(inputComponents)
.filter(key => key.startsWith(NoteBookEnvironmentVariablePrefix))
.forEach(key => {
const value = getInputComponentValue(inputComponents, key) ?? '<undefined>';
const re: RegExp = new RegExp(`\\\$\\\(${key}\\\)`, 'gi');
inputValue = inputValue?.replace(re, value);
});
return inputValue;
}
function processCheckboxField(context: FieldContext): void {
const checkbox = createCheckbox(context.view, { initialValue: context.fieldInfo.defaultValue! === 'true', label: context.fieldInfo.label, required: context.fieldInfo.required });
context.components.push(checkbox);
context.onNewInputComponentCreated(context.fieldInfo.variableName!, checkbox);
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: checkbox });
}
/**
* A File Picker field consists of a text field and a browse button that allows a user to pick a file system file.
* @param context The context to use to create the field
*/
function processFilePickerField(context: FieldContext, defaultFilePath?: string): FilePickerInputs {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight });
function processFilePickerField(context: FieldContext): FilePickerInputs {
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 input = createTextInput(context.view, {
defaultValue: defaultFilePath || context.fieldInfo.defaultValue || '',
defaultValue: context.fieldInfo.defaultValue || '',
ariaLabel: context.fieldInfo.label,
required: context.fieldInfo.required,
placeHolder: context.fieldInfo.placeHolder,
width: context.fieldInfo.inputWidth,
enabled: context.fieldInfo.enabled
});
context.onNewInputComponentCreated(context.fieldInfo.variableName!, input);
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: input });
input.enabled = false;
const browseFileButton = context.view!.modelBuilder.button().withProperties({ label: loc.browse }).component();
context.onNewDisposableCreated(browseFileButton.onDidClick(async () => {
@@ -506,42 +631,22 @@ function processFilePickerField(context: FieldContext, defaultFilePath?: string)
let fileUri = fileUris[0];
input.value = fileUri.fsPath;
}));
addLabelInputPairToContainer(context.view, context.components, label, input, LabelPosition.Left, [browseFileButton]);
context.fieldInfo.labelPosition = LabelPosition.Left;
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo, [browseFileButton]);
return { input: input, browseButton: browseFileButton };
}
/**
* An Kube Config Cluster picker field consists of a file system filee picker and radio button selector for cluster contexts defined in the config filed picked using the file picker.
* @param context The context to use to create the field
* This function returns a method that reads the cluster context from the {@param file}. This method then returns the cluster contexts
* read as an OptionsInfo object asynchronously.
*
* @param file - the file from which to fetch the cluster contexts
*/
async function processKubeConfigClusterPickerField(context: KubeClusterContextFieldContext): Promise<void> {
const kubeConfigFilePathVariableName = context.fieldInfo.configFileVariableName || 'AZDATA_NB_VAR_KUBECONFIG_PATH';
const filePickerContext: FieldContext = {
container: context.container,
components: context.components,
view: context.view,
onNewValidatorCreated: context.onNewValidatorCreated,
onNewDisposableCreated: context.onNewDisposableCreated,
onNewInputComponentCreated: context.onNewInputComponentCreated,
fieldInfo: {
label: loc.kubeConfigFilePath,
type: FieldType.FilePicker,
labelWidth: context.fieldInfo.labelWidth,
variableName: kubeConfigFilePathVariableName,
required: true
}
};
const filePicker = processFilePickerField(filePickerContext, getDefaultKubeConfigPath());
context.fieldInfo.subFields = context.fieldInfo.subFields || [];
context.fieldInfo.subFields!.push({
label: filePickerContext.fieldInfo.label,
variableName: kubeConfigFilePathVariableName
});
context.onNewInputComponentCreated(kubeConfigFilePathVariableName, filePicker.input);
const getClusterContexts = async () => {
function getClusterContexts(file: string): (() => Promise<OptionsInfo>) {
return async () => {
try {
let currentClusterContext = '';
const clusterContexts: string[] = (await getKubeConfigClusterContexts(filePicker.input.value!)).map(kubeClusterContext => {
const clusterContexts: string[] = (await getKubeConfigClusterContexts(file)).map(kubeClusterContext => {
if (kubeClusterContext.isCurrentContext) {
currentClusterContext = kubeClusterContext.name;
}
@@ -551,76 +656,78 @@ async function processKubeConfigClusterPickerField(context: KubeClusterContextFi
throw Error(loc.clusterContextNotFound);
}
return { values: clusterContexts, defaultValue: currentClusterContext };
} catch (e) {
throw Error(localize('kubeConfigClusterPicker.errorLoadingClusters', "An error ocurred while loading or parsing the config file:{0}, error is:{1}", filePicker.input.value, getErrorMessage(e)));
}
catch (e) {
throw Error(localize('getClusterContexts.errorFetchingClusters', "An error ocurred while loading or parsing the config file:{0}, error is:{1}", file, getErrorMessage(e)));
}
};
createRadioOptions(context, getClusterContexts)
.then(clusterContextOptions => {
filePicker.input.onTextChanged(async () => {
await loadOrReloadRadioOptions(context, clusterContextOptions.optionsList, clusterContextOptions.loader, getClusterContexts);
});
}).catch(error => {
console.log(`failed to create radio options, Error: ${error}`);
});
}
async function processRadioOptionsTypeField(context: FieldContext): Promise<RadioOptionsInputs> {
/**
* A Kube Config Cluster picker field consists of a file system file picker and radio button selector for cluster contexts defined in the config filed picked using the file picker.
* @param context The context to use to create the field
*/
async function processKubeConfigClusterPickerField(context: KubeClusterContextFieldContext): Promise<void> {
const kubeConfigFilePathVariableName = context.fieldInfo.configFileVariableName || 'AZDATA_NB_VAR_KUBECONFIG_PATH';
const filePickerContext: FieldContext = {
container: context.container,
inputComponents: context.inputComponents,
components: context.components,
view: context.view,
onNewValidatorCreated: context.onNewValidatorCreated,
onNewDisposableCreated: context.onNewDisposableCreated,
onNewInputComponentCreated: context.onNewInputComponentCreated,
fieldInfo: {
label: loc.kubeConfigFilePath,
type: FieldType.FilePicker,
defaultValue: getDefaultKubeConfigPath(),
inputWidth: context.fieldInfo.inputWidth,
labelWidth: context.fieldInfo.labelWidth,
variableName: kubeConfigFilePathVariableName,
required: true
}
};
const filePicker = processFilePickerField(filePickerContext);
context.fieldInfo.subFields = context.fieldInfo.subFields || [];
context.fieldInfo.subFields.push({
label: filePickerContext.fieldInfo.label,
variableName: kubeConfigFilePathVariableName
});
const radioOptionsGroup = await createRadioOptions(context, getClusterContexts(filePicker.input.value!));
context.onNewDisposableCreated(filePicker.input.onTextChanged(async () =>
await radioOptionsGroup.loadOptions(getClusterContexts(filePicker.input.value!))
));
}
async function processRadioOptionsTypeField(context: FieldContext): Promise<RadioGroupLoadingComponentBuilder> {
return await createRadioOptions(context);
}
async function createRadioOptions(context: FieldContext, getRadioButtonInfo?: (() => Promise<{ values: string[] | azdata.CategoryValue[], defaultValue: string }>))
: Promise<RadioOptionsInputs> {
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, fontWeight: context.fieldInfo.labelFontWeight });
const optionsList = context.view!.modelBuilder.divContainer().withProperties<azdata.DivContainerProperties>({ clickable: false }).component();
const radioOptionsLoadingComponent = context.view!.modelBuilder.loadingComponent().withItem(optionsList).component();
addLabelInputPairToContainer(context.view, context.components, label, radioOptionsLoadingComponent, LabelPosition.Left);
await loadOrReloadRadioOptions(context, optionsList, radioOptionsLoadingComponent, getRadioButtonInfo);
return { optionsList: optionsList, loader: radioOptionsLoadingComponent };
async function createRadioOptions(context: FieldContext, getRadioButtonInfo?: (() => Promise<OptionsInfo>))
: Promise<RadioGroupLoadingComponentBuilder> {
if (context.fieldInfo.fieldAlignItems === undefined) {
context.fieldInfo.fieldAlignItems = 'flex-start'; // by default align the items to the top.
}
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
const radioGroupLoadingComponentBuilder = new RadioGroupLoadingComponentBuilder(context.view, context.onNewDisposableCreated);
context.fieldInfo.labelPosition = LabelPosition.Left;
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: radioGroupLoadingComponentBuilder });
addLabelInputPairToContainer(context.view, context.components, label, radioGroupLoadingComponentBuilder.component(), context.fieldInfo);
const options = context.fieldInfo.options as OptionsInfo;
await radioGroupLoadingComponentBuilder.loadOptions(
getRadioButtonInfo || options); // wait for the radioGroup to be fully initialized
return radioGroupLoadingComponentBuilder;
}
async function loadOrReloadRadioOptions(context: FieldContext, optionsList: azdata.DivContainer, radioOptionsLoadingComponent: azdata.LoadingComponent, getRadioButtonInfo: (() => Promise<{ values: string[] | azdata.CategoryValue[]; defaultValue: string; }>) | undefined): Promise<void> {
radioOptionsLoadingComponent.loading = true;
optionsList.clearItems();
let options: (string[] | azdata.CategoryValue[]) = context.fieldInfo.options!;
let defaultValue: string = context.fieldInfo.defaultValue!;
try {
if (getRadioButtonInfo) {
const radioButtonsInfo = await getRadioButtonInfo();
options = radioButtonsInfo.values;
defaultValue = radioButtonsInfo.defaultValue;
}
options.forEach((op: string | azdata.CategoryValue) => {
const option: azdata.CategoryValue = (typeof op === 'string') ? { name: op, displayName: op } : op as azdata.CategoryValue;
const radioOption = context.view!.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
label: option.displayName,
checked: option.displayName === defaultValue,
name: option.name,
}).component();
if (radioOption.checked) {
context.onNewInputComponentCreated(context.fieldInfo.variableName!, radioOption);
}
context.onNewDisposableCreated(radioOption.onDidClick(() => {
// reset checked status of all remaining radioButtons
optionsList.items.filter(otherOption => otherOption !== radioOption).forEach(otherOption => (otherOption as azdata.RadioButtonComponent).checked = false);
context.onNewInputComponentCreated(context.fieldInfo.variableName!, radioOption!);
}));
optionsList.addItem(radioOption);
});
}
catch (e) {
const errorLoadingRadioOptionsLabel = context.view!.modelBuilder.text().withProperties({ value: getErrorMessage(e) }).component();
optionsList.addItem(errorLoadingRadioOptionsLabel);
}
radioOptionsLoadingComponent.loading = false;
}
/**
* An Azure Account field consists of 3 separate dropdown fields - Account, Subscription and Resource Group
* @param context The context to use to create the field
*/
function processAzureAccountField(context: AzureAccountFieldContext): void {
async function processAzureAccountField(context: AzureAccountFieldContext): Promise<void> {
context.fieldInfo.subFields = [];
const accountValueToAccountMap = new Map<string, azdata.Account>();
const subscriptionValueToSubscriptionMap = new Map<string, azureResource.AzureResourceSubscription>();
@@ -628,11 +735,12 @@ function processAzureAccountField(context: AzureAccountFieldContext): void {
const subscriptionDropdown = createAzureSubscriptionDropdown(context, subscriptionValueToSubscriptionMap);
const resourceGroupDropdown = createAzureResourceGroupsDropdown(context, accountDropdown, accountValueToAccountMap, subscriptionDropdown, subscriptionValueToSubscriptionMap);
const locationDropdown = context.fieldInfo.locations && processAzureLocationsField(context);
accountDropdown.onValueChanged(selectedItem => {
accountDropdown.onValueChanged(async selectedItem => {
const selectedAccount = accountValueToAccountMap.get(selectedItem.selected)!;
handleSelectedAccountChanged(context, selectedAccount, subscriptionDropdown, subscriptionValueToSubscriptionMap, resourceGroupDropdown, locationDropdown);
await handleSelectedAccountChanged(context, selectedAccount, subscriptionDropdown, subscriptionValueToSubscriptionMap, resourceGroupDropdown, locationDropdown);
});
azdata.accounts.getAllAccounts().then((accounts: azdata.Account[]) => {
try {
const accounts = await azdata.accounts.getAllAccounts();
// Append a blank value for the "default" option if the field isn't required, context will clear all the dropdowns when selected
const dropdownValues = context.fieldInfo.required ? [] : [''];
accountDropdown.values = dropdownValues.concat(accounts.map(account => {
@@ -641,8 +749,10 @@ function processAzureAccountField(context: AzureAccountFieldContext): void {
return displayName;
}));
const selectedAccount = accountDropdown.value ? accountValueToAccountMap.get(accountDropdown.value.toString()) : undefined;
handleSelectedAccountChanged(context, selectedAccount, subscriptionDropdown, subscriptionValueToSubscriptionMap, resourceGroupDropdown, locationDropdown);
}, (err: any) => console.log(`Unexpected error fetching accounts: ${err}`));
await handleSelectedAccountChanged(context, selectedAccount, subscriptionDropdown, subscriptionValueToSubscriptionMap, resourceGroupDropdown, locationDropdown);
} catch (error) {
vscode.window.showErrorMessage(localize('azure.accounts.unexpectedAccountsError', 'Unexpected error fetching accounts: ${0}', getErrorMessage(error)));
}
}
function createAzureAccountDropdown(context: AzureAccountFieldContext): azdata.DropDownComponent {
@@ -651,7 +761,7 @@ function createAzureAccountDropdown(context: AzureAccountFieldContext): azdata.D
description: context.fieldInfo.description,
required: context.fieldInfo.required,
width: context.fieldInfo.labelWidth,
fontWeight: context.fieldInfo.labelFontWeight
cssStyles: context.fieldInfo.labelCSSStyles
});
const accountDropdown = createDropdown(context.view, {
width: context.fieldInfo.inputWidth,
@@ -659,8 +769,9 @@ function createAzureAccountDropdown(context: AzureAccountFieldContext): azdata.D
required: context.fieldInfo.required,
label: loc.account
});
context.onNewInputComponentCreated(context.fieldInfo.variableName!, accountDropdown);
addLabelInputPairToContainer(context.view, context.components, label, accountDropdown, context.fieldInfo.labelPosition);
accountDropdown.fireOnTextChange = true;
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: accountDropdown });
addLabelInputPairToContainer(context.view, context.components, label, accountDropdown, context.fieldInfo);
return accountDropdown;
}
@@ -671,36 +782,48 @@ function createAzureSubscriptionDropdown(
text: loc.subscription,
required: context.fieldInfo.required,
width: context.fieldInfo.labelWidth,
fontWeight: context.fieldInfo.labelFontWeight
cssStyles: context.fieldInfo.labelCSSStyles
});
const subscriptionDropdown = createDropdown(context.view, {
defaultValue: (context.fieldInfo.required) ? undefined : '',
width: context.fieldInfo.inputWidth,
editable: false,
required: context.fieldInfo.required,
label: loc.subscription
});
subscriptionDropdown.fireOnTextChange = true;
context.fieldInfo.subFields!.push({
label: label.value!,
variableName: context.fieldInfo.subscriptionVariableName
});
context.onNewInputComponentCreated(context.fieldInfo.subscriptionVariableName!, subscriptionDropdown, (inputValue: string) => {
return subscriptionValueToSubscriptionMap.get(inputValue)?.id || inputValue;
context.onNewInputComponentCreated(context.fieldInfo.subscriptionVariableName!, {
component: subscriptionDropdown,
inputValueTransformer: (inputValue: string) => {
return subscriptionValueToSubscriptionMap.get(inputValue)?.id || inputValue;
}
});
addLabelInputPairToContainer(context.view, context.components, label, subscriptionDropdown, context.fieldInfo.labelPosition);
if (context.fieldInfo.displaySubscriptionVariableName) {
context.fieldInfo.subFields!.push({
label: label.value!,
variableName: context.fieldInfo.displaySubscriptionVariableName
});
context.onNewInputComponentCreated(context.fieldInfo.displaySubscriptionVariableName, { component: subscriptionDropdown });
}
addLabelInputPairToContainer(context.view, context.components, label, subscriptionDropdown, context.fieldInfo);
return subscriptionDropdown;
}
function handleSelectedAccountChanged(
async function handleSelectedAccountChanged(
context: AzureAccountFieldContext,
selectedAccount: azdata.Account | undefined,
subscriptionDropdown: azdata.DropDownComponent,
subscriptionValueToSubscriptionMap: Map<string, azureResource.AzureResourceSubscription>,
resourceGroupDropdown: azdata.DropDownComponent,
locationDropdown?: azdata.DropDownComponent
): void {
): Promise<void> {
subscriptionValueToSubscriptionMap.clear();
subscriptionDropdown.values = [];
handleSelectedSubscriptionChanged(context, selectedAccount, undefined, resourceGroupDropdown);
await handleSelectedSubscriptionChanged(context, selectedAccount, undefined, resourceGroupDropdown);
if (!selectedAccount) {
subscriptionDropdown.values = [''];
if (locationDropdown) {
@@ -715,7 +838,8 @@ function handleSelectedAccountChanged(
}
}
vscode.commands.executeCommand<azurecore.GetSubscriptionsResult>('azure.accounts.getSubscriptions', selectedAccount, true /*ignoreErrors*/).then(response => {
try {
const response = await vscode.commands.executeCommand<azurecore.GetSubscriptionsResult>('azure.accounts.getSubscriptions', selectedAccount, true /*ignoreErrors*/);
if (!response) {
return;
}
@@ -732,8 +856,10 @@ function handleSelectedAccountChanged(
return displayName;
}).sort((a: string, b: string) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()));
const selectedSubscription = subscriptionDropdown.values.length > 0 ? subscriptionValueToSubscriptionMap.get(subscriptionDropdown.values[0]) : undefined;
handleSelectedSubscriptionChanged(context, selectedAccount, selectedSubscription, resourceGroupDropdown);
}, err => { vscode.window.showErrorMessage(localize('azure.accounts.unexpectedSubscriptionsError', "Unexpected error fetching subscriptions for account {0} ({1}): {2}", selectedAccount?.displayInfo.displayName, selectedAccount?.key.accountId, err.message)); });
await handleSelectedSubscriptionChanged(context, selectedAccount, selectedSubscription, resourceGroupDropdown);
} catch (error) {
vscode.window.showErrorMessage(localize('azure.accounts.unexpectedSubscriptionsError', "Unexpected error fetching subscriptions for account {0} ({1}): {2}", selectedAccount?.displayInfo.displayName, selectedAccount?.key.accountId, getErrorMessage(error)));
}
}
function createAzureResourceGroupsDropdown(
@@ -746,35 +872,42 @@ function createAzureResourceGroupsDropdown(
text: loc.resourceGroup,
required: context.fieldInfo.required,
width: context.fieldInfo.labelWidth,
fontWeight: context.fieldInfo.labelFontWeight
cssStyles: context.fieldInfo.labelCSSStyles
});
const resourceGroupDropdown = createDropdown(context.view, {
defaultValue: (context.fieldInfo.required) ? undefined : '',
width: context.fieldInfo.inputWidth,
editable: false,
required: context.fieldInfo.required,
label: loc.resourceGroup
});
resourceGroupDropdown.fireOnTextChange = true;
context.fieldInfo.subFields!.push({
label: label.value!,
variableName: context.fieldInfo.resourceGroupVariableName
});
context.onNewInputComponentCreated(context.fieldInfo.resourceGroupVariableName!, resourceGroupDropdown);
addLabelInputPairToContainer(context.view, context.components, label, resourceGroupDropdown, context.fieldInfo.labelPosition);
subscriptionDropdown.onValueChanged(selectedItem => {
const rgValueChangedEmitter = new vscode.EventEmitter<void>();
resourceGroupDropdown.onValueChanged(() => rgValueChangedEmitter.fire());
context.onNewInputComponentCreated(context.fieldInfo.resourceGroupVariableName!, { component: resourceGroupDropdown });
addLabelInputPairToContainer(context.view, context.components, label, resourceGroupDropdown, context.fieldInfo);
subscriptionDropdown.onValueChanged(async selectedItem => {
const selectedAccount = !accountDropdown || !accountDropdown.value ? undefined : accountValueToAccountMap.get(accountDropdown.value.toString());
const selectedSubscription = subscriptionValueToSubscriptionMap.get(selectedItem.selected);
handleSelectedSubscriptionChanged(context, selectedAccount, selectedSubscription, resourceGroupDropdown);
await handleSelectedSubscriptionChanged(context, selectedAccount, selectedSubscription, resourceGroupDropdown);
rgValueChangedEmitter.fire();
});
return resourceGroupDropdown;
}
function handleSelectedSubscriptionChanged(context: AzureAccountFieldContext, selectedAccount: azdata.Account | undefined, selectedSubscription: azureResource.AzureResourceSubscription | undefined, resourceGroupDropdown: azdata.DropDownComponent): void {
async function handleSelectedSubscriptionChanged(context: AzureAccountFieldContext, selectedAccount: azdata.Account | undefined, selectedSubscription: azureResource.AzureResourceSubscription | undefined, resourceGroupDropdown: azdata.DropDownComponent): Promise<void> {
resourceGroupDropdown.values = [''];
if (!selectedAccount || !selectedSubscription) {
// Don't need to execute command if we don't have both an account and subscription selected
return;
}
vscode.commands.executeCommand<azurecore.GetResourceGroupsResult>('azure.accounts.getResourceGroups', selectedAccount, selectedSubscription, true /*ignoreErrors*/).then(response => {
try {
const response = await vscode.commands.executeCommand<azurecore.GetResourceGroupsResult>('azure.accounts.getResourceGroups', selectedAccount, selectedSubscription, true /*ignoreErrors*/);
//.then(response => {
if (!response) {
return;
}
@@ -788,7 +921,9 @@ function handleSelectedSubscriptionChanged(context: AzureAccountFieldContext, se
resourceGroupDropdown.values = (response.resourceGroups.length !== 0)
? response.resourceGroups.map(resourceGroup => resourceGroup.name).sort((a: string, b: string) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()))
: [''];
}, err => { vscode.window.showErrorMessage(localize('azure.accounts.unexpectedResourceGroupsError', "Unexpected error fetching resource groups for subscription {0} ({1}): {2}", selectedSubscription?.name, selectedSubscription?.id, err.message)); });
} catch (error) {
vscode.window.showErrorMessage(localize('azure.accounts.unexpectedResourceGroupsError', "Unexpected error fetching resource groups for subscription {0} ({1}): {2}", selectedSubscription?.name, selectedSubscription?.id, getErrorMessage(error)));
}
}
/**
@@ -809,7 +944,7 @@ function processAzureLocationsField(context: AzureLocationsFieldContext): azdata
text: context.fieldInfo.label || loc.location,
required: context.fieldInfo.required,
width: context.fieldInfo.labelWidth,
fontWeight: context.fieldInfo.labelFontWeight
cssStyles: context.fieldInfo.labelCSSStyles
});
const locationDropdown = createDropdown(context.view, {
width: context.fieldInfo.inputWidth,
@@ -818,14 +953,18 @@ function processAzureLocationsField(context: AzureLocationsFieldContext): azdata
label: loc.location,
values: context.fieldInfo.locations
});
locationDropdown.fireOnTextChange = true;
context.fieldInfo.subFields = context.fieldInfo.subFields || [];
if (context.fieldInfo.locationVariableName) {
context.fieldInfo.subFields!.push({
label: label.value!,
variableName: context.fieldInfo.locationVariableName
});
context.onNewInputComponentCreated(context.fieldInfo.locationVariableName, locationDropdown, (inputValue: string) => {
return knownAzureLocationNameMappings.get(inputValue) || inputValue;
context.onNewInputComponentCreated(context.fieldInfo.locationVariableName, {
component: locationDropdown,
inputValueTransformer: (inputValue: string) => {
return knownAzureLocationNameMappings.get(inputValue) || inputValue;
}
});
}
if (context.fieldInfo.displayLocationVariableName) {
@@ -833,12 +972,15 @@ function processAzureLocationsField(context: AzureLocationsFieldContext): azdata
label: label.value!,
variableName: context.fieldInfo.displayLocationVariableName
});
context.onNewInputComponentCreated(context.fieldInfo.displayLocationVariableName, locationDropdown);
context.onNewInputComponentCreated(context.fieldInfo.displayLocationVariableName, { component: locationDropdown });
}
context.onNewInputComponentCreated(context.fieldInfo.variableName!, locationDropdown, (inputValue: string) => {
return knownAzureLocationNameMappings.get(inputValue) || inputValue;
context.onNewInputComponentCreated(context.fieldInfo.variableName!, {
component: locationDropdown,
inputValueTransformer: (inputValue: string) => {
return knownAzureLocationNameMappings.get(inputValue) || inputValue;
}
});
addLabelInputPairToContainer(context.view, context.components, label, locationDropdown, context.fieldInfo.labelPosition);
addLabelInputPairToContainer(context.view, context.components, label, locationDropdown, context.fieldInfo);
return locationDropdown;
}
@@ -871,31 +1013,38 @@ export function getPasswordMismatchMessage(fieldName: string): string {
export function setModelValues(inputComponents: InputComponents, model: Model): void {
Object.keys(inputComponents).forEach(key => {
let value;
const input = inputComponents[key].component;
if ('name' in input && 'checked' in input) { //RadioButtonComponent
value = input.name;
} else if ('checked' in input) { // CheckBoxComponent
value = input.checked ? 'true' : 'false';
} else if ('value' in input) { // InputBoxComponent or DropDownComponent
const inputValue = input.value;
if (typeof inputValue === 'string' || typeof inputValue === 'undefined') {
value = inputValue;
} else {
value = inputValue.name;
}
} else {
throw new Error(`Unknown input type with ID ${input.id}`);
}
const inputValueTransformer = inputComponents[key].inputValueTransformer;
if (inputValueTransformer) {
value = inputValueTransformer(value || '');
}
const value = getInputComponentValue(inputComponents, key);
model.setPropertyValue(key, value);
});
}
function getInputComponentValue(inputComponents: InputComponents, key: string): string | undefined {
const input = inputComponents[key].component;
if (input === undefined) {
return undefined;
}
let value;
if (input instanceof RadioGroupLoadingComponentBuilder) {
value = input.value;
} else if ('checked' in input) { // CheckBoxComponent
value = input.checked ? 'true' : 'false';
} else if ('value' in input) { // InputBoxComponent or DropDownComponent
const inputValue = input.value;
if (typeof inputValue === 'string' || typeof inputValue === 'undefined') {
value = inputValue;
} else {
value = inputValue.name;
}
} else {
throw new Error(`Unknown input type with ID ${input.id}`);
}
const inputValueTransformer = inputComponents[key].inputValueTransformer;
if (inputValueTransformer) {
value = inputValueTransformer(value || '');
}
return value;
}
export function isInputBoxEmpty(input: azdata.InputBoxComponent): boolean {
return input.value === undefined || input.value === '';
}