Features to resource deployment to add arc control create in ARC extension (#10088)

* 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
This commit is contained in:
Arvind Ranasaria
2020-04-29 12:11:48 -07:00
committed by GitHub
parent a9bfdf0fc9
commit 353115668c
21 changed files with 729 additions and 175 deletions

View File

@@ -69,4 +69,3 @@ export const DockerRegistry_VariableName = 'AZDATA_NB_VAR_BDC_REGISTRY';
export const DockerImageTag_VariableName = 'AZDATA_NB_VAR_BDC_DOCKER_IMAGE_TAG';
export const DockerUsername_VariableName = 'AZDATA_NB_VAR_BDC_DOCKER_USERNAME';
export const DockerPassword_VariableName = 'AZDATA_NB_VAR_BDC_DOCKER_PASSWORD';
export const ToolsInstallPath = 'AZDATA_NB_VAR_TOOLS_INSTALLATION_PATH';

View File

@@ -10,7 +10,7 @@ import { KubeCtlToolName } from '../../services/tools/kubeCtlTool';
import { getRuntimeBinaryPathEnvironmentVariableName } from '../../utils';
import { Model } from '../model';
import * as VariableNames from './constants';
import { ToolsInstallPath } from './../../constants';
export class DeployClusterWizardModel extends Model {
constructor(public deploymentTarget: BdcDeploymentType) {
@@ -167,7 +167,7 @@ export class DeployClusterWizardModel extends Model {
}
const kubeCtlEnvVarName: string = getRuntimeBinaryPathEnvironmentVariableName(KubeCtlToolName);
statements.push(`os.environ["${kubeCtlEnvVarName}"] = "${this.escapeForNotebookCodeCell(process.env[kubeCtlEnvVarName]!)}"`);
statements.push(`os.environ["PATH"] = os.environ["PATH"] + "${delimiter}" + "${this.escapeForNotebookCodeCell(process.env[VariableNames.ToolsInstallPath]!)}"`);
statements.push(`os.environ["PATH"] = os.environ["PATH"] + "${delimiter}" + "${this.escapeForNotebookCodeCell(process.env[ToolsInstallPath]!)}"`);
statements.push(`print('Variables have been set successfully.')`);
return statements.map(line => line + EOL);
}

View File

@@ -6,12 +6,13 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { DeployClusterWizard } from '../deployClusterWizard';
import { SectionInfo, FieldType, LabelPosition } from '../../../interfaces';
import { FieldType, LabelPosition, SectionInfo } from '../../../interfaces';
import { createSection, getDropdownComponent, InputComponents, InputComponent, setModelValues, Validator } from '../../modelViewUtils';
import { WizardPageBase } from '../../wizardPageBase';
import { createSection, InputComponents, setModelValues, Validator, getDropdownComponent, MissingRequiredInformationErrorMessage } from '../../modelViewUtils';
import { SubscriptionId_VariableName, ResourceGroup_VariableName, Location_VariableName, AksName_VariableName, VMCount_VariableName, VMSize_VariableName } from '../constants';
import { AksName_VariableName, Location_VariableName, ResourceGroup_VariableName, SubscriptionId_VariableName, VMCount_VariableName, VMSize_VariableName } from '../constants';
import { DeployClusterWizard } from '../deployClusterWizard';
const localize = nls.loadMessageBundle();
const MissingRequiredInformationErrorMessage = localize('deployCluster.MissingRequiredInfoError', "Please fill out the required fields marked with red asterisks.");
export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
private inputComponents: InputComponents = {};
@@ -136,7 +137,7 @@ export class AzureSettingsPage extends WizardPageBase<DeployClusterWizard> {
onNewDisposableCreated: (disposable: vscode.Disposable): void => {
self.wizard.registerDisposable(disposable);
},
onNewInputComponentCreated: (name: string, component: azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent): void => {
onNewInputComponentCreated: (name: string, component: InputComponent): void => {
self.inputComponents[name] = { component: component };
},
onNewValidatorCreated: (validator: Validator): void => {

View File

@@ -8,7 +8,7 @@ import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { DeployClusterWizard } from '../deployClusterWizard';
import { SectionInfo, FieldType, LabelPosition } from '../../../interfaces';
import { createSection, InputComponents, setModelValues, Validator, getInputBoxComponent, isValidSQLPassword, getInvalidSQLPasswordMessage, getPasswordMismatchMessage } from '../../modelViewUtils';
import { createSection, InputComponents, setModelValues, Validator, getInputBoxComponent, isValidSQLPassword, getInvalidSQLPasswordMessage, getPasswordMismatchMessage, InputComponent } from '../../modelViewUtils';
import { WizardPageBase } from '../../wizardPageBase';
import * as VariableNames from '../constants';
import { EOL } from 'os';
@@ -197,7 +197,7 @@ export class ClusterSettingsPage extends WizardPageBase<DeployClusterWizard> {
onNewDisposableCreated: (disposable: vscode.Disposable): void => {
self.wizard.registerDisposable(disposable);
},
onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => {
onNewInputComponentCreated: (name: string, component: InputComponent): void => {
self.inputComponents[name] = { component: component };
},
onNewValidatorCreated: (validator: Validator): void => {
@@ -211,7 +211,7 @@ export class ClusterSettingsPage extends WizardPageBase<DeployClusterWizard> {
onNewDisposableCreated: (disposable: vscode.Disposable): void => {
self.wizard.registerDisposable(disposable);
},
onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => {
onNewInputComponentCreated: (name: string, component: InputComponent): void => {
self.inputComponents[name] = { component: component };
},
onNewValidatorCreated: (validator: Validator): void => {
@@ -225,7 +225,7 @@ export class ClusterSettingsPage extends WizardPageBase<DeployClusterWizard> {
onNewDisposableCreated: (disposable: vscode.Disposable): void => {
self.wizard.registerDisposable(disposable);
},
onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => {
onNewInputComponentCreated: (name: string, component: InputComponent): void => {
self.inputComponents[name] = { component: component };
},
onNewValidatorCreated: (validator: Validator): void => {

View File

@@ -7,7 +7,7 @@ import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { DeployClusterWizard } from '../deployClusterWizard';
import { SectionInfo, FieldType } from '../../../interfaces';
import { Validator, InputComponents, createSection, createGroupContainer, createLabel, createFlexContainer, createTextInput, createNumberInput, setModelValues, getInputBoxComponent, getCheckboxComponent, getDropdownComponent } from '../../modelViewUtils';
import { Validator, InputComponents, InputComponent, createSection, createGroupContainer, createLabel, createFlexContainer, createTextInput, createNumberInput, setModelValues, getInputBoxComponent, getCheckboxComponent, getDropdownComponent } from '../../modelViewUtils';
import { WizardPageBase } from '../../wizardPageBase';
import * as VariableNames from '../constants';
import { AuthenticationMode } from '../deployClusterWizardModel';
@@ -123,7 +123,7 @@ export class ServiceSettingsPage extends WizardPageBase<DeployClusterWizard> {
onNewDisposableCreated: (disposable: vscode.Disposable): void => {
this.wizard.registerDisposable(disposable);
},
onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent): void => {
onNewInputComponentCreated: (name: string, component: InputComponent): void => {
this.inputComponents[name] = { component: component };
},
onNewValidatorCreated: (validator: Validator): void => {

View File

@@ -53,7 +53,7 @@ export class TargetClusterContextPage extends WizardPageBase<DeployClusterWizard
public onEnter() {
if (this.loadDefaultKubeConfigFile) {
let defaultKubeConfigPath = this.wizard.kubeService.getDefautConfigPath();
let defaultKubeConfigPath = this.wizard.kubeService.getDefaultConfigPath();
this.loadClusterContexts(defaultKubeConfigPath);
this.loadDefaultKubeConfigFile = false;
}

View File

@@ -9,10 +9,9 @@ import * as nls from 'vscode-nls';
import { DialogBase } from './dialogBase';
import { INotebookService } from '../services/notebookService';
import { DialogInfo, instanceOfNotebookBasedDialogInfo, NotebookBasedDialogInfo } from '../interfaces';
import { Validator, initializeDialog, InputComponents, setModelValues, InputValueTransformer } from './modelViewUtils';
import { Validator, initializeDialog, InputComponents, setModelValues, InputValueTransformer, InputComponent } from './modelViewUtils';
import { Model } from './model';
import { EOL } from 'os';
import { getDateTimeString, getErrorMessage } from '../utils';
import { IPlatformService } from '../services/platformService';
const localize = nls.loadMessageBundle();
@@ -46,7 +45,7 @@ export class DeploymentInputDialog extends DialogBase {
onNewDisposableCreated: (disposable: vscode.Disposable): void => {
this._toDispose.push(disposable);
},
onNewInputComponentCreated: (name: string, component: azdata.DropDownComponent | azdata.InputBoxComponent | azdata.CheckBoxComponent, inputValueTransformer?: InputValueTransformer): void => {
onNewInputComponentCreated: (name: string, component: InputComponent, inputValueTransformer?: InputValueTransformer): void => {
this.inputComponents[name] = { component: component, inputValueTransformer: inputValueTransformer };
},
onNewValidatorCreated: (validator: Validator): void => {
@@ -88,39 +87,6 @@ export class DeploymentInputDialog extends DialogBase {
}
private executeNotebook(notebookDialogInfo: NotebookBasedDialogInfo): void {
azdata.tasks.startBackgroundOperation({
displayName: notebookDialogInfo.taskName!,
description: notebookDialogInfo.taskName!,
isCancelable: false,
operation: async op => {
op.updateStatus(azdata.TaskStatus.InProgress);
const notebook = await this.notebookService.getNotebook(notebookDialogInfo.notebook);
const result = await this.notebookService.executeNotebook(notebook);
if (result.succeeded) {
op.updateStatus(azdata.TaskStatus.Succeeded);
} else {
op.updateStatus(azdata.TaskStatus.Failed, result.errorMessage);
if (result.outputNotebook) {
const viewErrorDetail = localize('resourceDeployment.ViewErrorDetail', "View error detail");
const taskFailedMessage = localize('resourceDeployment.DeployFailed', "The task \"{0}\" has failed.", notebookDialogInfo.taskName);
const selectedOption = await vscode.window.showErrorMessage(taskFailedMessage, viewErrorDetail);
this.platformService.logToOutputChannel(taskFailedMessage);
if (selectedOption === viewErrorDetail) {
try {
this.notebookService.launchNotebookWithContent(`deploy-${getDateTimeString()}`, result.outputNotebook);
} catch (error) {
const launchNotebookError = localize('resourceDeployment.FailedToOpenNotebook', "An error occurred launching the output notebook. {1}{2}.", EOL, getErrorMessage(error));
this.platformService.logToOutputChannel(launchNotebookError);
vscode.window.showErrorMessage(launchNotebookError);
}
}
} else {
const errorMessage = localize('resourceDeployment.TaskFailedWithNoOutputNotebook', "The task \"{0}\" failed and no output Notebook was generated.", notebookDialogInfo.taskName);
this.platformService.logToOutputChannel(errorMessage);
vscode.window.showErrorMessage(errorMessage);
}
}
}
});
this.notebookService.backgroundExecuteNotebook(notebookDialogInfo.taskName, notebookDialogInfo.notebook, 'deploy', this.platformService);
}
}

View File

@@ -2,23 +2,26 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
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 { DialogInfoBase, FieldType, FieldInfo, SectionInfo, LabelPosition, FontWeight, FontStyle, AzureAccountFieldInfo } from '../interfaces';
import { Model } from './model';
import { getDateTimeString } from '../utils';
import { azureResource } from '../../../azurecore/src/azureResource/azure-resource';
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 * as loc from '../localizedConstants';
import { EOL } from 'os';
import { getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../services/kubeService';
import { getDateTimeString, getErrorMessage } from '../utils';
import { WizardInfoBase } from './../interfaces';
import { Model } from './model';
const localize = nls.loadMessageBundle();
export type Validator = () => { valid: boolean, message: string };
export type InputValueTransformer = (inputValue: string) => string;
export type InputComponents = { [s: string]: { component: azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent; inputValueTransformer?: InputValueTransformer } };
export type InputComponent = azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent | azdata.RadioButtonComponent;
export type InputComponents = { [s: string]: { component: InputComponent; inputValueTransformer?: InputValueTransformer } };
export function getInputBoxComponent(name: string, inputComponents: InputComponents): azdata.InputBoxComponent {
return <azdata.InputBoxComponent>inputComponents[name].component;
@@ -32,6 +35,10 @@ export function getCheckboxComponent(name: string, inputComponents: InputCompone
return <azdata.CheckBoxComponent>inputComponents[name].component;
}
export function getTextComponent(name: string, inputComponents: InputComponents): azdata.TextComponent {
return <azdata.TextComponent>inputComponents[name].component;
}
export const DefaultInputComponentWidth = '400px';
export const DefaultLabelComponentWidth = '200px';
@@ -41,7 +48,8 @@ export interface DialogContext extends CreateContext {
}
export interface WizardPageContext extends CreateContext {
sections: SectionInfo[];
wizardInfo: WizardInfoBase;
pageInfo: PageInfoBase;
page: azdata.window.WizardPage;
container: azdata.window.Wizard;
}
@@ -57,6 +65,24 @@ interface FieldContext extends CreateContext {
view: azdata.ModelView;
}
interface FilePickerInputs {
input: azdata.InputBoxComponent;
browseButton: azdata.ButtonComponent;
}
interface RadioOptionsInputs {
optionsList: azdata.DivContainer;
loader: azdata.LoadingComponent;
}
interface KubeClusterContextFieldContext extends FieldContext {
fieldInfo: KubeClusterContextFieldInfo;
}
interface AzureLocationsFieldContext extends FieldContext {
fieldInfo: AzureLocationsFieldInfo;
}
interface AzureAccountFieldContext extends FieldContext {
fieldInfo: AzureAccountFieldInfo;
}
@@ -65,7 +91,7 @@ interface CreateContext {
container: azdata.window.Dialog | azdata.window.Wizard;
onNewValidatorCreated: (validator: Validator) => void;
onNewDisposableCreated: (disposable: vscode.Disposable) => void;
onNewInputComponentCreated: (name: string, component: azdata.InputBoxComponent | azdata.DropDownComponent | azdata.CheckBoxComponent, inputValueTransformer?: InputValueTransformer) => void;
onNewInputComponentCreated: (name: string, component: InputComponent, inputValueTransformer?: InputValueTransformer) => void;
}
export function createTextInput(view: azdata.ModelView, inputInfo: { defaultValue?: string, ariaLabel: string, required?: boolean, placeHolder?: string, width?: string, enabled?: boolean }): azdata.InputBoxComponent {
@@ -133,6 +159,7 @@ export function initializeDialog(dialogContext: DialogContext): void {
const sections = tabInfo.sections.map(sectionInfo => {
sectionInfo.inputWidth = sectionInfo.inputWidth || tabInfo.inputWidth || DefaultInputComponentWidth;
sectionInfo.labelWidth = sectionInfo.labelWidth || tabInfo.labelWidth || DefaultLabelComponentWidth;
sectionInfo.labelPosition = sectionInfo.labelPosition || tabInfo.labelPosition;
return createSection({
sectionInfo: sectionInfo,
view: view,
@@ -161,9 +188,10 @@ export function initializeDialog(dialogContext: DialogContext): void {
export function initializeWizardPage(context: WizardPageContext): void {
context.page.registerContent((view: azdata.ModelView) => {
const sections = context.sections.map(sectionInfo => {
sectionInfo.inputWidth = sectionInfo.inputWidth || DefaultInputComponentWidth;
sectionInfo.labelWidth = sectionInfo.labelWidth || DefaultLabelComponentWidth;
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.labelPosition = sectionInfo.labelPosition || context.pageInfo.labelPosition || context.wizardInfo.labelPosition;
return createSection({
view: view,
container: context.container,
@@ -180,7 +208,7 @@ export function initializeWizardPage(context: WizardPageContext): void {
componentWidth: '100%'
}
);
const form = formBuilder.withLayout({ width: '100%' }).component();
const form: azdata.FormContainer = formBuilder.withLayout({ width: '100%' }).component();
return view.initializeModel(form);
});
}
@@ -239,12 +267,16 @@ export function createGroupContainer(view: azdata.ModelView, items: azdata.Compo
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) {
function addLabelInputPairToContainer(view: azdata.ModelView, components: azdata.Component[], label: azdata.Component, input: azdata.Component, labelPosition?: LabelPosition, additionalComponents?: azdata.Component[]) {
const inputs = [label, input];
if (additionalComponents && additionalComponents.length > 0) {
inputs.push(...additionalComponents);
}
if (labelPosition && labelPosition === LabelPosition.Left) {
const row = createFlexContainer(view, [label, input]);
const row = createFlexContainer(view, inputs);
components.push(row);
} else {
components.push(label, input);
components.push(...inputs);
}
}
@@ -253,6 +285,9 @@ function processField(context: FieldContext): void {
case FieldType.Options:
processOptionsTypeField(context);
break;
case FieldType.RadioOptions:
processRadioOptionsTypeField(context);
break;
case FieldType.DateTimeText:
processDateTimeTextField(context);
break;
@@ -275,6 +310,15 @@ function processField(context: FieldContext): void {
case FieldType.AzureAccount:
processAzureAccountField(context);
break;
case FieldType.AzureLocations:
processAzureLocationsField(context);
break;
case FieldType.FilePicker:
processFilePickerField(context);
break;
case FieldType.KubeClusterContextPicker:
processKubeConfigClusterPickerField(context);
break;
default:
throw new Error(localize('UnknownFieldTypeError', "Unknown field type: \"{0}\"", context.fieldInfo.type));
}
@@ -416,8 +460,9 @@ 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: context.fieldInfo.defaultValue!, description: '', required: false, width: context.fieldInfo.inputWidth, fontStyle: context.fieldInfo.fontStyle, links: context.fieldInfo.links });
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);
}
@@ -427,23 +472,168 @@ function processCheckboxField(context: FieldContext): void {
context.onNewInputComponentCreated(context.fieldInfo.variableName!, 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 });
const input = createTextInput(context.view, {
defaultValue: defaultFilePath || 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);
input.enabled = false;
const browseFileButton = context.view!.modelBuilder.button().withProperties({ label: loc.browse }).component();
context.onNewDisposableCreated(browseFileButton.onDidClick(async () => {
let fileUris = await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,
defaultUri: vscode.Uri.file(path.dirname(input.value || os_homedir())),
openLabel: loc.select,
filters: {
'Config Files': ['*'],
}
});
if (!fileUris || fileUris.length === 0) {
return;
}
let fileUri = fileUris[0];
input.value = fileUri.fsPath;
}));
addLabelInputPairToContainer(context.view, context.components, label, input, LabelPosition.Left, [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
*/
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 () => {
try {
let currentClusterContext = '';
const clusterContexts: string[] = (await getKubeConfigClusterContexts(filePicker.input.value!)).map(kubeClusterContext => {
if (kubeClusterContext.isCurrentContext) {
currentClusterContext = kubeClusterContext.name;
}
return kubeClusterContext.name;
});
if (clusterContexts.length === 0) {
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)));
}
};
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> {
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 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 {
context.fieldInfo.subFields = [];
const accountValueToAccountMap = new Map<string, azdata.Account>();
const subscriptionValueToSubscriptionMap = new Map<string, azureResource.AzureResourceSubscription>();
const accountDropdown = createAzureAccountDropdown(context);
const subscriptionDropdown = createAzureSubscriptionDropdown(context, subscriptionValueToSubscriptionMap);
const resourceGroupDropdown = createAzureResourceGroupsDropdown(context, accountDropdown, accountValueToAccountMap, subscriptionDropdown, subscriptionValueToSubscriptionMap);
const locationDropdown = createAzureLocationDropdown(context);
const locationDropdown = context.fieldInfo.locations && processAzureLocationsField(context);
accountDropdown.onValueChanged(selectedItem => {
const selectedAccount = accountValueToAccountMap.get(selectedItem.selected)!;
handleSelectedAccountChanged(context, selectedAccount, subscriptionDropdown, subscriptionValueToSubscriptionMap, resourceGroupDropdown, locationDropdown);
});
azdata.accounts.getAllAccounts().then((accounts: azdata.Account[]) => {
// Append a blank value for the "default" option if the field isn't required, this will clear all the dropdowns when selected
// 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 => {
const displayName = `${account.displayInfo.displayName} (${account.displayInfo.userId})`;
@@ -489,6 +679,10 @@ function createAzureSubscriptionDropdown(
required: context.fieldInfo.required,
label: loc.subscription
});
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;
});
@@ -502,18 +696,20 @@ function handleSelectedAccountChanged(
subscriptionDropdown: azdata.DropDownComponent,
subscriptionValueToSubscriptionMap: Map<string, azureResource.AzureResourceSubscription>,
resourceGroupDropdown: azdata.DropDownComponent,
locationDropdown: azdata.DropDownComponent
locationDropdown?: azdata.DropDownComponent
): void {
subscriptionValueToSubscriptionMap.clear();
subscriptionDropdown.values = [];
handleSelectedSubscriptionChanged(context, selectedAccount, undefined, resourceGroupDropdown);
if (selectedAccount) {
if (locationDropdown.values && locationDropdown.values.length === 0) {
locationDropdown.values = context.fieldInfo.locations;
if (locationDropdown) {
if (selectedAccount) {
if (locationDropdown.values && locationDropdown.values.length === 0) {
locationDropdown.values = context.fieldInfo.locations;
}
} else {
locationDropdown.values = [];
return;
}
} else {
locationDropdown.values = [];
return;
}
vscode.commands.executeCommand<azurecore.GetSubscriptionsResult>('azure.accounts.getSubscriptions', selectedAccount, true /*ignoreErrors*/).then(response => {
if (!response) {
@@ -554,6 +750,10 @@ function createAzureResourceGroupsDropdown(
required: context.fieldInfo.required,
label: loc.resourceGroup
});
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 => {
@@ -594,9 +794,13 @@ const knownAzureLocationNameMappings = new Map<string, string>([
['Central US', 'centralus']
]);
function createAzureLocationDropdown(context: AzureAccountFieldContext): azdata.DropDownComponent {
/**
* An Azure Locations field consists of a dropdown field for azure locations
* @param context The context to use to create the field
*/
function processAzureLocationsField(context: AzureLocationsFieldContext): azdata.DropDownComponent {
const label = createLabel(context.view, {
text: loc.location,
text: context.fieldInfo.label || loc.location,
required: context.fieldInfo.required,
width: context.fieldInfo.labelWidth,
fontWeight: context.fieldInfo.labelFontWeight
@@ -608,7 +812,24 @@ function createAzureLocationDropdown(context: AzureAccountFieldContext): azdata.
label: loc.location,
values: context.fieldInfo.locations
});
context.onNewInputComponentCreated(context.fieldInfo.locationVariableName!, locationDropdown, (inputValue: string) => {
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;
});
}
if (context.fieldInfo.displayLocationVariableName) {
context.fieldInfo.subFields!.push({
label: label.value!,
variableName: context.fieldInfo.displayLocationVariableName
});
context.onNewInputComponentCreated(context.fieldInfo.displayLocationVariableName, locationDropdown);
}
context.onNewInputComponentCreated(context.fieldInfo.variableName!, locationDropdown, (inputValue: string) => {
return knownAzureLocationNameMappings.get(inputValue) || inputValue;
});
addLabelInputPairToContainer(context.view, context.components, label, locationDropdown, context.fieldInfo.labelPosition);
@@ -618,12 +839,12 @@ function createAzureLocationDropdown(context: AzureAccountFieldContext): azdata.
export function isValidSQLPassword(password: string, userName: string = 'sa'): boolean {
// Validate SQL Server password
const containsUserName = password && userName !== undefined && password.toUpperCase().includes(userName.toUpperCase());
// Instead of using one RegEx, I am seperating it to make it more readable.
// Instead of using one RegEx, I am separating it to make it more readable.
const hasUpperCase = /[A-Z]/.test(password) ? 1 : 0;
const hasLowerCase = /[a-z]/.test(password) ? 1 : 0;
const hasNumbers = /\d/.test(password) ? 1 : 0;
const hasNonalphas = /\W/.test(password) ? 1 : 0;
return !containsUserName && password.length >= 8 && password.length <= 128 && (hasUpperCase + hasLowerCase + hasNumbers + hasNonalphas >= 3);
const hasNonAlphas = /\W/.test(password) ? 1 : 0;
return !containsUserName && password.length >= 8 && password.length <= 128 && (hasUpperCase + hasLowerCase + hasNumbers + hasNonAlphas >= 3);
}
export function removeValidationMessage(container: azdata.window.Dialog | azdata.window.Wizard, message: string): void {
@@ -646,7 +867,9 @@ export function setModelValues(inputComponents: InputComponents, model: Model):
Object.keys(inputComponents).forEach(key => {
let value;
const input = inputComponents[key].component;
if ('checked' in input) { // CheckBoxComponent
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;
@@ -671,4 +894,3 @@ export function isInputBoxEmpty(input: azdata.InputBoxComponent): boolean {
return input.value === undefined || input.value === '';
}
export const MissingRequiredInformationErrorMessage = localize('deployCluster.MissingRequiredInfoError', "Please fill out the required fields marked with red asterisks.");

View File

@@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { INotebookService } from '../../services/notebookService';
import { Model } from '../model';
import { WizardBase } from '../wizardBase';
import { WizardPageBase } from '../wizardPageBase';
import { DeploymentType, NotebookWizardInfo } from './../../interfaces';
import { IPlatformService } from './../../services/platformService';
import { NotebookWizardPage } from './notebookWizardPage';
import { NotebookWizardSummaryPage } from './notebookWizardSummaryPage';
const localize = nls.loadMessageBundle();
export class NotebookWizard extends WizardBase<NotebookWizard, Model> {
public get notebookService(): INotebookService {
return this._notebookService;
}
public get platformService(): IPlatformService {
return this._platformService;
}
public get wizardInfo(): NotebookWizardInfo {
return this._wizardInfo;
}
constructor(private _wizardInfo: NotebookWizardInfo, private _notebookService: INotebookService, private _platformService: IPlatformService) {
super(_wizardInfo.title, new Model());
this.wizardObject.doneButton.label = _wizardInfo.actionText || this.wizardObject.doneButton.label;
}
public get deploymentType(): DeploymentType | undefined {
return this._wizardInfo.type;
}
protected initialize(): void {
this.setPages(this.getPages());
this.wizardObject.generateScriptButton.hidden = true;
this.wizardInfo.actionText = this.wizardInfo.actionText || localize('deployCluster.ScriptToNotebook', "Script to Notebook");
this.wizardObject.doneButton.label = this.wizardInfo.actionText;
}
protected onCancel(): void {
}
protected onOk(): void {
this.model.setEnvironmentVariables();
if (this.wizardInfo.runNotebook) {
this.notebookService.backgroundExecuteNotebook(this.wizardInfo.taskName, this.wizardInfo.notebook, 'deploy', this.platformService);
} else {
this.notebookService.launchNotebook(this.wizardInfo.notebook).then(() => { }, (error) => {
vscode.window.showErrorMessage(error);
});
}
}
private getPages(): WizardPageBase<NotebookWizard>[] {
const pages: WizardPageBase<NotebookWizard>[] = [];
for (let pageIndex: number = 0; pageIndex < this.wizardInfo.pages.length; pageIndex++) {
pages.push(new NotebookWizardPage(this, pageIndex));
}
if (this.wizardInfo.generateSummaryPage) {
pages.push(new NotebookWizardSummaryPage(this));
}
return pages;
}
}

View File

@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { EOL } from 'os';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { NotebookWizardPageInfo } from '../../interfaces';
import { initializeWizardPage, InputComponents, InputComponent, setModelValues, Validator } from '../modelViewUtils';
import { WizardPageBase } from '../wizardPageBase';
import { NotebookWizard } from './notebookWizard';
const localize = nls.loadMessageBundle();
export class NotebookWizardPage extends WizardPageBase<NotebookWizard> {
private inputComponents: InputComponents = {};
protected get pageInfo(): NotebookWizardPageInfo {
return this.wizard.wizardInfo.pages[this._pageIndex];
}
constructor(wizard: NotebookWizard, private _pageIndex: number) {
super(wizard.wizardInfo.pages[_pageIndex].title, wizard.wizardInfo.pages[_pageIndex].description || '', wizard);
}
public initialize(): void {
const self = this;
initializeWizardPage({
container: this.wizard.wizardObject,
wizardInfo: this.wizard.wizardInfo,
pageInfo: this.pageInfo,
page: this.pageObject,
onNewDisposableCreated: (disposable: vscode.Disposable): void => {
self.wizard.registerDisposable(disposable);
},
onNewInputComponentCreated: (name: string, component: InputComponent): void => {
self.inputComponents[name] = { component: component };
},
onNewValidatorCreated: (validator: Validator): void => {
self.validators.push(validator);
}
});
}
public onLeave() {
setModelValues(this.inputComponents, this.wizard.model);
// The following callback registration clears previous navigation validators.
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
return true;
});
}
public onEnter() {
this.wizard.wizardObject.registerNavigationValidator((pcInfo) => {
this.wizard.wizardObject.message = { text: '' };
if (pcInfo.newPage > pcInfo.lastPage) {
const messages: string[] = [];
this.validators.forEach(validator => {
const result = validator();
if (!result.valid) {
messages.push(result.message);
}
});
if (messages.length > 0) {
this.wizard.wizardObject.message = {
text: messages.length === 1 ? messages[0] : localize('wizardPage.ValidationError', "There are some errors on this page, click 'Show Details' to view the errors."),
description: messages.length === 1 ? undefined : messages.join(EOL),
level: azdata.window.MessageLevel.Error
};
}
return messages.length === 0;
}
return true;
});
}
}

View File

@@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import { SubFieldInfo, FieldType, FontWeight, LabelPosition, SectionInfo } from '../../interfaces';
import { createSection, DefaultInputComponentWidth, DefaultLabelComponentWidth } from '../modelViewUtils';
import { WizardPageBase } from '../wizardPageBase';
import { NotebookWizard } from './notebookWizard';
const localize = nls.loadMessageBundle();
export class NotebookWizardSummaryPage extends WizardPageBase<NotebookWizard> {
private formItems: azdata.FormComponent[] = [];
private form!: azdata.FormBuilder;
private view!: azdata.ModelView;
constructor(wizard: NotebookWizard) {
super(localize('notebookWizard.summaryPageTitle', "Review your configuration"), '', wizard);
}
public initialize(): void {
this.pageObject.registerContent((view: azdata.ModelView) => {
this.view = view;
this.form = view.modelBuilder.formContainer();
return view.initializeModel(this.form!.withLayout({ width: '100%' }).component());
});
}
public onLeave() {
this.wizard.wizardObject.message = { text: '' };
}
public onEnter() {
this.formItems.forEach(item => {
this.form!.removeFormItem(item);
});
this.formItems = [];
const inputWidth = this.wizard.wizardInfo.inputWidth || (this.wizard.wizardInfo.summaryPage && this.wizard.wizardInfo.summaryPage.inputWidth) || DefaultInputComponentWidth;
const labelWidth = this.wizard.wizardInfo.labelWidth || (this.wizard.wizardInfo.summaryPage && this.wizard.wizardInfo.summaryPage.labelWidth) || DefaultLabelComponentWidth;
const labelPosition = this.wizard.wizardInfo.labelPosition || (this.wizard.wizardInfo.summaryPage && this.wizard.wizardInfo.summaryPage.labelPosition) || LabelPosition.Left;
this.wizard.wizardInfo.pages.forEach(pageInfo => {
const summarySectionInfo: SectionInfo = {
labelPosition: labelPosition,
labelWidth: labelWidth,
inputWidth: inputWidth,
title: '',
rows: []
};
pageInfo.sections.forEach(sectionInfo => {
sectionInfo.fields!.forEach(fieldInfo => {
if (fieldInfo.variableName) {
this.addSummaryForVariable(summarySectionInfo, fieldInfo);
}
if (fieldInfo.subFields) {
fieldInfo.subFields.forEach(subFieldInfo => {
this.addSummaryForVariable(summarySectionInfo, subFieldInfo);
});
}
});
});
if (summarySectionInfo!.rows!.length > 0) {
const formComponent: azdata.FormComponent = {
title: pageInfo.title,
component: createSection({
container: this.wizard.wizardObject,
sectionInfo: summarySectionInfo,
view: this.view,
onNewDisposableCreated: () => { },
onNewInputComponentCreated: () => { },
onNewValidatorCreated: () => { }
})
};
this.formItems.push(formComponent);
}
});
this.form.addFormItems(this.formItems);
}
private addSummaryForVariable(summarySectionInfo: SectionInfo, fieldInfo: SubFieldInfo) {
summarySectionInfo!.rows!.push({
fields: [{
type: FieldType.ReadonlyText,
label: fieldInfo.label,
defaultValue: this.wizard.model.getStringValue(fieldInfo.variableName!),
labelFontWeight: FontWeight.Bold
}]
});
}
}

View File

@@ -173,24 +173,26 @@ export class ResourceTypePickerDialog extends DialogBase {
this._optionsContainer.clearItems();
this._optionDropDownMap.clear();
resourceType.options.forEach(option => {
const optionLabel = this._view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: option.displayName
}).component();
optionLabel.width = '150px';
if (resourceType.options) {
resourceType.options.forEach(option => {
const optionLabel = this._view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: option.displayName
}).component();
optionLabel.width = '150px';
const optionSelectBox = this._view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({
values: option.values,
value: option.values[0],
width: '300px',
ariaLabel: option.displayName
}).component();
const optionSelectBox = this._view.modelBuilder.dropDown().withProperties<azdata.DropDownProperties>({
values: option.values,
value: option.values[0],
width: '300px',
ariaLabel: option.displayName
}).component();
this._toDispose.push(optionSelectBox.onValueChanged(() => { this.updateToolsDisplayTable(); }));
this._optionDropDownMap.set(option.name, optionSelectBox);
const row = this._view.modelBuilder.flexContainer().withItems([optionLabel, optionSelectBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '20px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
this._optionsContainer.addItem(row);
});
this._toDispose.push(optionSelectBox.onValueChanged(() => { this.updateToolsDisplayTable(); }));
this._optionDropDownMap.set(option.name, optionSelectBox);
const row = this._view.modelBuilder.flexContainer().withItems([optionLabel, optionSelectBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '20px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
this._optionsContainer.addItem(row);
});
}
this.updateToolsDisplayTable();
}