From 678bbe314283f3e3a9125c8b5a2bddb211954e57 Mon Sep 17 00:00:00 2001 From: Arvind Ranasaria Date: Mon, 1 Jun 2020 11:14:59 -0700 Subject: [PATCH] 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 --- .../resource-deployment/src/interfaces.ts | 74 ++- .../src/services/notebookService.ts | 78 ++- .../src/services/resourceTypeService.ts | 8 +- .../src/services/toolsService.ts | 10 + .../src/test/notebookService.test.ts | 4 +- .../deployClusterWizard.ts | 71 ++- .../deployClusterWizardModel.ts | 19 +- .../pages/azureSettingsPage.ts | 34 +- .../pages/clusterSettingsPage.ts | 26 +- .../pages/serviceSettingsPage.ts | 18 +- .../deployClusterWizard/pages/summaryPage.ts | 86 +-- .../src/ui/deploymentInputDialog.ts | 15 +- .../resource-deployment/src/ui/model.ts | 67 ++- .../src/ui/modelViewUtils.ts | 569 +++++++++++------- .../src/ui/notebookWizard/notebookWizard.ts | 83 ++- ...ge.ts => notebookWizardAutoSummaryPage.ts} | 37 +- .../ui/notebookWizard/notebookWizardPage.ts | 55 +- .../ui/radioGroupLoadingComponentBuilder.ts | 82 +++ .../src/ui/resourceTypePickerDialog.ts | 4 +- .../resource-deployment/src/ui/wizardBase.ts | 12 +- extensions/resource-deployment/src/utils.ts | 14 +- .../sample-resource-deployment/package.json | 263 +++++++- .../package.nls.json | 34 +- 23 files changed, 1190 insertions(+), 473 deletions(-) rename extensions/resource-deployment/src/ui/notebookWizard/{notebookWizardSummaryPage.ts => notebookWizardAutoSummaryPage.ts} (60%) create mode 100644 extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index c8e712f4d4..55ab646151 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -42,7 +42,7 @@ export interface DialogDeploymentProvider extends DeploymentProviderBase { } export interface BdcWizardDeploymentProvider extends DeploymentProviderBase { - bdcWizard: WizardInfo; + bdcWizard: BdcWizardInfo; } export interface NotebookWizardDeploymentProvider extends DeploymentProviderBase { @@ -50,7 +50,7 @@ export interface NotebookWizardDeploymentProvider extends DeploymentProviderBase } export interface NotebookDeploymentProvider extends DeploymentProviderBase { - notebook: string | NotebookInfo; + notebook: string | NotebookPathInfo; } export interface WebPageDeploymentProvider extends DeploymentProviderBase { @@ -100,31 +100,32 @@ export interface DeploymentProviderBase { export type DeploymentProvider = DialogDeploymentProvider | BdcWizardDeploymentProvider | NotebookWizardDeploymentProvider | NotebookDeploymentProvider | WebPageDeploymentProvider | DownloadDeploymentProvider | CommandDeploymentProvider; -export interface WizardInfo { - notebook: string | NotebookInfo; +export interface BdcWizardInfo { + notebook: string | NotebookPathInfo; type: BdcDeploymentType; } export interface NotebookWizardInfo extends WizardInfoBase { - notebook: string | NotebookInfo; + notebook: string | NotebookPathInfo; + runNotebook?: boolean; + codeCellInsertionPosition?: number; + pages: NotebookWizardPageInfo[] } -export interface WizardInfoBase extends SharedFieldAttributes { +export interface WizardInfoBase extends FieldInfoBase { taskName?: string; type?: DeploymentType; - runNotebook?: boolean; actionText?: string; title: string; - pages: NotebookWizardPageInfo[]; - summaryPage: NotebookWizardPageInfo; - generateSummaryPage: boolean; + pages: PageInfoBase[]; + isSummaryPageAutoGenerated?: boolean } export interface NotebookWizardPageInfo extends PageInfoBase { description?: string; } export interface NotebookBasedDialogInfo extends DialogInfoBase { - notebook: string | NotebookInfo; + notebook: string | NotebookPathInfo; runNotebook?: boolean; taskName?: string; } @@ -153,18 +154,39 @@ export interface DialogInfoBase { export interface DialogTabInfo extends PageInfoBase { } -export interface PageInfoBase extends SharedFieldAttributes { +export interface PageInfoBase extends FieldInfoBase { title: string; isSummaryPage?: boolean; sections: SectionInfo[]; } -export interface SharedFieldAttributes { +export interface TextCSSStyles { + fontStyle?: FontStyle | undefined; + fontWeight?: FontWeight | undefined; + color?: string; + [key: string]: string | undefined; +} + +export type ComponentCSSStyles = { + [key: string]: string; +}; + + +export interface OptionsInfo { + values: string[] | azdata.CategoryValue[], + defaultValue: string, + optionsType?: OptionsType +} + +export interface FieldInfoBase { labelWidth?: string; inputWidth?: string; labelPosition?: LabelPosition; // Default value is top + fieldWidth?: string; + fieldHeight?: string; + fieldAlignItems?: azdata.AlignItemsType; } -export interface SectionInfo extends SharedFieldAttributes { +export interface SectionInfo extends FieldInfoBase { title?: string; fields?: FieldInfo[]; // Use this if the dialog is not wide. All fields will be displayed in one column, label will be placed on top of the input component. rows?: RowInfo[]; // Use this for wide dialog or wizard. label will be placed to the left of the input component. @@ -174,7 +196,8 @@ export interface SectionInfo extends SharedFieldAttributes { } export interface RowInfo { - fields: FieldInfo[]; + cssStyles?: ComponentCSSStyles; + items: FieldInfo[] | RowInfo[]; } export interface SubFieldInfo { @@ -182,7 +205,7 @@ export interface SubFieldInfo { variableName?: string; } -export interface FieldInfo extends SubFieldInfo, SharedFieldAttributes { +export interface FieldInfo extends SubFieldInfo, FieldInfoBase { subFields?: SubFieldInfo[]; type: FieldType; defaultValue?: string; @@ -194,22 +217,23 @@ export interface FieldInfo extends SubFieldInfo, SharedFieldAttributes { min?: number; max?: number; required?: boolean; - options?: string[] | azdata.CategoryValue[]; + options?: string[] | azdata.CategoryValue[] | OptionsInfo; placeHolder?: string; userName?: string; // needed for sql server's password complexity requirement check, password can not include the login name. description?: string; - fontStyle?: FontStyle; - labelFontWeight?: FontWeight; - textFontWeight?: FontWeight; + labelCSSStyles?: TextCSSStyles; + fontWeight?: FontWeight; links?: azdata.LinkArea[]; editable?: boolean; // for editable drop-down, enabled?: boolean; + isEvaluated?: boolean; } export interface KubeClusterContextFieldInfo extends FieldInfo { configFileVariableName?: string; } export interface AzureAccountFieldInfo extends AzureLocationsFieldInfo { + displaySubscriptionVariableName?: string; subscriptionVariableName?: string; resourceGroupVariableName?: string; } @@ -242,16 +266,20 @@ export enum FieldType { SQLPassword = 'sql_password', Password = 'password', Options = 'options', - RadioOptions = 'radio_options', ReadonlyText = 'readonly_text', Checkbox = 'checkbox', AzureAccount = 'azure_account', AzureLocations = 'azure_locations', FilePicker = 'file_picker', - KubeClusterContextPicker = 'kube_cluster_context_picker' + KubeClusterContextPicker = 'kube_cluster_context_picker', } -export interface NotebookInfo { +export enum OptionsType { + Dropdown = 'dropdown', + Radio = 'radio' +} + +export interface NotebookPathInfo { win32: string; darwin: string; linux: string; diff --git a/extensions/resource-deployment/src/services/notebookService.ts b/extensions/resource-deployment/src/services/notebookService.ts index 9935e5df28..41b1d95422 100644 --- a/extensions/resource-deployment/src/services/notebookService.ts +++ b/extensions/resource-deployment/src/services/notebookService.ts @@ -9,7 +9,7 @@ import * as path from 'path'; import { isString } from 'util'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { NotebookInfo } from '../interfaces'; +import { NotebookPathInfo } from '../interfaces'; import { getDateTimeString, getErrorMessage } from '../utils'; import { IPlatformService } from './platformService'; const localize = nls.loadMessageBundle(); @@ -33,11 +33,13 @@ export interface NotebookExecutionResult { } export interface INotebookService { - launchNotebook(notebook: string | NotebookInfo): Thenable; - launchNotebookWithContent(title: string, content: string): Thenable; - getNotebook(notebook: string | NotebookInfo): Promise; + launchNotebook(notebook: string | NotebookPathInfo): Promise; + launchNotebookWithEdits(notebook: string | NotebookPathInfo, cellStatements: string[], insertionPosition?: number): Promise; + launchNotebookWithContent(title: string, content: string): Promise; + getNotebook(notebook: string | NotebookPathInfo): Promise; + getNotebookPath(notebook: string | NotebookPathInfo): string; executeNotebook(notebook: any, env?: NodeJS.ProcessEnv): Promise; - backgroundExecuteNotebook(taskName: string | undefined, notebookInfo: string | NotebookInfo, tempNoteBookPrefix: string, platformService: IPlatformService): void; + backgroundExecuteNotebook(taskName: string | undefined, notebookInfo: string | NotebookPathInfo | Notebook, tempNotebookPrefix: string, platformService: IPlatformService, env?: NodeJS.ProcessEnv): void; } export class NotebookService implements INotebookService { @@ -48,9 +50,26 @@ export class NotebookService implements INotebookService { * Launch notebook with file path * @param notebook the path of the notebook */ - launchNotebook(notebook: string | NotebookInfo): Thenable { - return this.getNotebookFullPath(notebook).then(notebookPath => { - return this.showNotebookAsUntitled(notebookPath); + async launchNotebook(notebook: string | NotebookPathInfo): Promise { + const notebookPath = await this.getNotebookFullPath(notebook); + return await this.showNotebookAsUntitled(notebookPath); + } + + /** + * Inserts cell code given by {@param cellStatements} in an existing notebook given by {@param notebook} file path at the location + * {@param insertionPosition} and then launches the edited notebook. + * + * @param notebook - the path to notebook that needs to be launched + * @param cellStatements - array of statements to be inserted in a cell + * @param insertionPosition - the position at which cells are inserted. Default is a new cell at the beginning of the notebook. + */ + async launchNotebookWithEdits(notebook: string, cellStatements: string[], insertionPosition: number = 0): Promise { + const openedNotebook = await this.launchNotebook(notebook); + await openedNotebook.edit((editBuilder: azdata.nb.NotebookEditorEdit) => { + editBuilder.insertCell({ + cell_type: 'code', + source: cellStatements + }, insertionPosition); }); } @@ -59,9 +78,9 @@ export class NotebookService implements INotebookService { * @param title the title of the notebook * @param content the notebook content */ - launchNotebookWithContent(title: string, content: string): Thenable { + async launchNotebookWithContent(title: string, content: string): Promise { const uri: vscode.Uri = vscode.Uri.parse(`untitled:${title}`); - return azdata.nb.showNotebookDocument(uri, { + return await azdata.nb.showNotebookDocument(uri, { connectionProfile: undefined, preview: false, initialContent: content, @@ -70,7 +89,7 @@ export class NotebookService implements INotebookService { } - async getNotebook(notebook: string | NotebookInfo): Promise { + async getNotebook(notebook: string | NotebookPathInfo): Promise { const notebookPath = await this.getNotebookFullPath(notebook); return JSON.parse(await this.platformService.readTextFile(notebookPath)); } @@ -83,7 +102,7 @@ export class NotebookService implements INotebookService { const outputFullPath = path.join(workingDirectory, `output-${fileName}`); const additionalEnvironmentVariables: NodeJS.ProcessEnv = env || {}; // Set the azdata eula - // Scenarios using the executeNotebook feature already have the EULA acceptted by the user before executing this. + // Scenarios using the executeNotebook feature already have the EULA accepted by the user before executing this. additionalEnvironmentVariables['ACCEPT_EULA'] = 'yes'; try { await this.platformService.saveTextFile(content, notebookFullPath); @@ -109,15 +128,17 @@ export class NotebookService implements INotebookService { } } - public backgroundExecuteNotebook(taskName: string | undefined, notebookInfo: string | NotebookInfo, tempNotebookPrefix: string, platformService: IPlatformService): void { + backgroundExecuteNotebook(taskName: string = 'Executing notebook', notebookInfo: string | NotebookPathInfo | Notebook, tempNotebookPrefix: string, platformService: IPlatformService, env?: NodeJS.ProcessEnv): void { azdata.tasks.startBackgroundOperation({ - displayName: taskName!, - description: taskName!, + displayName: taskName, + description: taskName, isCancelable: false, operation: async op => { op.updateStatus(azdata.TaskStatus.InProgress); - const notebook = await this.getNotebook(notebookInfo); - const result = await this.executeNotebook(notebook); + const notebook = (typeof notebookInfo === 'object' && 'cells' in notebookInfo) + ? notebookInfo + : await this.getNotebook(notebookInfo); + const result = await this.executeNotebook(notebook, env); if (result.succeeded) { op.updateStatus(azdata.TaskStatus.Succeeded); } else { @@ -129,7 +150,7 @@ export class NotebookService implements INotebookService { platformService.logToOutputChannel(taskFailedMessage); if (selectedOption === viewErrorDetail) { try { - this.launchNotebookWithContent(`${tempNotebookPrefix}-${getDateTimeString()}`, result.outputNotebook); + await this.launchNotebookWithContent(`${tempNotebookPrefix}-${getDateTimeString()}`, result.outputNotebook); } catch (error) { const launchNotebookError = localize('resourceDeployment.FailedToOpenNotebook', "An error occurred launching the output notebook. {1}{2}.", EOL, getErrorMessage(error)); platformService.logToOutputChannel(launchNotebookError); @@ -146,7 +167,7 @@ export class NotebookService implements INotebookService { }); } - async getNotebookFullPath(notebook: string | NotebookInfo): Promise { + async getNotebookFullPath(notebook: string | NotebookPathInfo): Promise { const notebookPath = this.getNotebookPath(notebook); let notebookExists = await this.platformService.fileExists(notebookPath); if (notebookExists) { @@ -168,7 +189,7 @@ export class NotebookService implements INotebookService { * get the notebook path for current platform * @param notebook the notebook path */ - getNotebookPath(notebook: string | NotebookInfo): string { + getNotebookPath(notebook: string | NotebookPathInfo): string { let notebookPath; if (notebook && !isString(notebook)) { const platform = this.platformService.platform(); @@ -199,17 +220,16 @@ export class NotebookService implements INotebookService { return title; } - showNotebookAsUntitled(notebookPath: string): Thenable { + async showNotebookAsUntitled(notebookPath: string): Promise { let targetFileName: string = this.findNextUntitledEditorName(notebookPath); const untitledFileName: vscode.Uri = vscode.Uri.parse(`untitled:${targetFileName}`); - return vscode.workspace.openTextDocument(notebookPath).then((document) => { - let initialContent = document.getText(); - return azdata.nb.showNotebookDocument(untitledFileName, { - connectionProfile: undefined, - preview: false, - initialContent: initialContent, - initialDirtyState: false - }); + const document = await vscode.workspace.openTextDocument(notebookPath); + let initialContent = document.getText(); + return await azdata.nb.showNotebookDocument(untitledFileName, { + connectionProfile: undefined, + preview: false, + initialContent: initialContent, + initialDirtyState: false }); } } diff --git a/extensions/resource-deployment/src/services/resourceTypeService.ts b/extensions/resource-deployment/src/services/resourceTypeService.ts index 7b8ec0615e..385450e8d9 100644 --- a/extensions/resource-deployment/src/services/resourceTypeService.ts +++ b/extensions/resource-deployment/src/services/resourceTypeService.ts @@ -13,7 +13,7 @@ import * as nls from 'vscode-nls'; import { INotebookService } from './notebookService'; import { IPlatformService } from './platformService'; import { IToolsService } from './toolsService'; -import { ResourceType, ResourceTypeOption, NotebookInfo, DeploymentProvider, instanceOfWizardDeploymentProvider, instanceOfDialogDeploymentProvider, instanceOfNotebookDeploymentProvider, instanceOfDownloadDeploymentProvider, instanceOfWebPageDeploymentProvider, instanceOfCommandDeploymentProvider, instanceOfNotebookBasedDialogInfo, instanceOfNotebookWizardDeploymentProvider } from '../interfaces'; +import { ResourceType, ResourceTypeOption, NotebookPathInfo, DeploymentProvider, instanceOfWizardDeploymentProvider, instanceOfDialogDeploymentProvider, instanceOfNotebookDeploymentProvider, instanceOfDownloadDeploymentProvider, instanceOfWebPageDeploymentProvider, instanceOfCommandDeploymentProvider, instanceOfNotebookBasedDialogInfo, instanceOfNotebookWizardDeploymentProvider } from '../interfaces'; import { DeployClusterWizard } from '../ui/deployClusterWizard/deployClusterWizard'; import { DeploymentInputDialog } from '../ui/deploymentInputDialog'; @@ -77,7 +77,7 @@ export class ResourceTypeService implements IResourceTypeService { }); } - private updateNotebookPath(objWithNotebookProperty: { notebook: string | NotebookInfo } | undefined, extensionPath: string): void { + private updateNotebookPath(objWithNotebookProperty: { notebook: string | NotebookPathInfo } | undefined, extensionPath: string): void { if (objWithNotebookProperty && objWithNotebookProperty.notebook) { if (typeof objWithNotebookProperty.notebook === 'string') { objWithNotebookProperty.notebook = path.join(extensionPath, objWithNotebookProperty.notebook); @@ -239,10 +239,10 @@ export class ResourceTypeService implements IResourceTypeService { public startDeployment(provider: DeploymentProvider): void { const self = this; if (instanceOfWizardDeploymentProvider(provider)) { - const wizard = new DeployClusterWizard(provider.bdcWizard, new KubeService(), new AzdataService(this.platformService), this.notebookService); + const wizard = new DeployClusterWizard(provider.bdcWizard, new KubeService(), new AzdataService(this.platformService), this.notebookService, this.toolsService); wizard.open(); } else if (instanceOfNotebookWizardDeploymentProvider(provider)) { - const wizard = new NotebookWizard(provider.notebookWizard, this.notebookService, this.platformService); + const wizard = new NotebookWizard(provider.notebookWizard, this.notebookService, this.platformService, this.toolsService); wizard.open(); } else if (instanceOfDialogDeploymentProvider(provider)) { const dialog = new DeploymentInputDialog(this.notebookService, this.platformService, provider.dialog); diff --git a/extensions/resource-deployment/src/services/toolsService.ts b/extensions/resource-deployment/src/services/toolsService.ts index 6627ce1984..1922498f30 100644 --- a/extensions/resource-deployment/src/services/toolsService.ts +++ b/extensions/resource-deployment/src/services/toolsService.ts @@ -11,10 +11,12 @@ import { IPlatformService } from './platformService'; export interface IToolsService { getToolByName(toolName: string): ITool | undefined; + toolsForCurrentProvider: ITool[]; } export class ToolsService implements IToolsService { private supportedTools: Map; + private currentTools: ITool[] = []; constructor(private _platformService: IPlatformService) { this.supportedTools = new Map( @@ -30,4 +32,12 @@ export class ToolsService implements IToolsService { getToolByName(toolName: string): ITool | undefined { return this.supportedTools.get(toolName); } + + get toolsForCurrentProvider(): ITool[] { + return this.currentTools; + } + + set toolsForCurrentProvider(tools: ITool[]) { + this.currentTools = tools; + } } diff --git a/extensions/resource-deployment/src/test/notebookService.test.ts b/extensions/resource-deployment/src/test/notebookService.test.ts index 4aa3fed11a..f7e619c15b 100644 --- a/extensions/resource-deployment/src/test/notebookService.test.ts +++ b/extensions/resource-deployment/src/test/notebookService.test.ts @@ -7,7 +7,7 @@ import * as TypeMoq from 'typemoq'; import 'mocha'; import { NotebookService } from '../services/notebookService'; import assert = require('assert'); -import { NotebookInfo } from '../interfaces'; +import { NotebookPathInfo } from '../interfaces'; import { IPlatformService } from '../services/platformService'; suite('Notebook Service Tests', function (): void { @@ -35,7 +35,7 @@ suite('Notebook Service Tests', function (): void { const notebookDarwin = 'test-notebook-darwin.ipynb'; const notebookLinux = 'test-notebook-linux.ipynb'; - const notebookInput: NotebookInfo = { + const notebookInput: NotebookPathInfo = { darwin: notebookDarwin, win32: notebookWin32, linux: notebookLinux diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizard.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizard.ts index e970d68f3e..2f047a83ca 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizard.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizard.ts @@ -4,28 +4,33 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; -import * as vscode from 'vscode'; -import { SummaryPage } from './pages/summaryPage'; -import { WizardBase } from '../wizardBase'; -import * as nls from 'vscode-nls'; -import { WizardInfo, BdcDeploymentType } from '../../interfaces'; -import { WizardPageBase } from '../wizardPageBase'; -import { AzureSettingsPage } from './pages/azureSettingsPage'; -import { ClusterSettingsPage } from './pages/clusterSettingsPage'; -import { ServiceSettingsPage } from './pages/serviceSettingsPage'; -import { TargetClusterContextPage } from './pages/targetClusterPage'; -import { IKubeService } from '../../services/kubeService'; -import { IAzdataService } from '../../services/azdataService'; -import { DeploymentProfilePage } from './pages/deploymentProfilePage'; -import { INotebookService } from '../../services/notebookService'; -import { DeployClusterWizardModel, AuthenticationMode } from './deployClusterWizardModel'; -import * as VariableNames from './constants'; +import * as fs from 'fs'; import * as os from 'os'; import { join } from 'path'; -import * as fs from 'fs'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import { BdcDeploymentType, BdcWizardInfo } from '../../interfaces'; +import { IAzdataService } from '../../services/azdataService'; +import { IKubeService } from '../../services/kubeService'; +import { INotebookService } from '../../services/notebookService'; +import { IToolsService } from '../../services/toolsService'; +import { getErrorMessage } from '../../utils'; +import { InputComponents } from '../modelViewUtils'; +import { WizardBase } from '../wizardBase'; +import { WizardPageBase } from '../wizardPageBase'; +import * as VariableNames from './constants'; +import { AuthenticationMode, DeployClusterWizardModel } from './deployClusterWizardModel'; +import { AzureSettingsPage } from './pages/azureSettingsPage'; +import { ClusterSettingsPage } from './pages/clusterSettingsPage'; +import { DeploymentProfilePage } from './pages/deploymentProfilePage'; +import { ServiceSettingsPage } from './pages/serviceSettingsPage'; +import { SummaryPage } from './pages/summaryPage'; +import { TargetClusterContextPage } from './pages/targetClusterPage'; const localize = nls.loadMessageBundle(); -export class DeployClusterWizard extends WizardBase { +export class DeployClusterWizard extends WizardBase, DeployClusterWizardModel> { + private _inputComponents: InputComponents = {}; + private _saveConfigButton: azdata.window.Button; public get kubeService(): IKubeService { @@ -40,6 +45,10 @@ export class DeployClusterWizard extends WizardBase { + await this.scriptToNotebook(); } private getPages(): WizardPageBase[] { @@ -135,19 +144,15 @@ export class DeployClusterWizard extends WizardBase { this.setEnvironmentVariables(process.env); - this.notebookService.launchNotebook(this.wizardInfo.notebook).then((notebook: azdata.nb.NotebookEditor) => { - notebook.edit((editBuilder: azdata.nb.NotebookEditorEdit) => { - // 5 is the position after the 'Set variables' cell in the deployment notebooks - editBuilder.insertCell({ - cell_type: 'code', - source: this.model.getCodeCellContentForNotebook() - }, 5); - }); - }, (error) => { - vscode.window.showErrorMessage(error); - }); + const variableValueStatements = this.model.getCodeCellContentForNotebook(this._toolsService.toolsForCurrentProvider); + const insertionPosition = 5; // Cell number 5 is the position where the python variable setting statements need to be inserted in this.wizardInfo.notebook. + try { + await this.notebookService.launchNotebookWithEdits(this.wizardInfo.notebook, variableValueStatements, insertionPosition); + } catch (error) { + vscode.window.showErrorMessage(getErrorMessage(error)); + } } private setEnvironmentVariables(env: NodeJS.ProcessEnv): void { diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizardModel.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizardModel.ts index 00cf8625e9..47d7e04941 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizardModel.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizardModel.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { EOL } from 'os'; import { delimiter } from 'path'; -import { BdcDeploymentType } from '../../interfaces'; +import { BdcDeploymentType, ITool } from '../../interfaces'; import { BigDataClusterDeploymentProfile, DataResource, HdfsResource, SqlServerMasterResource } from '../../services/bigDataClusterDeploymentProfile'; import { KubeCtlToolName } from '../../services/tools/kubeCtlTool'; -import { getRuntimeBinaryPathEnvironmentVariableName } from '../../utils'; +import { getRuntimeBinaryPathEnvironmentVariableName, setEnvironmentVariablesForInstallPaths } from '../../utils'; import { Model } from '../model'; -import * as VariableNames from './constants'; import { ToolsInstallPath } from './../../constants'; +import * as VariableNames from './constants'; export class DeployClusterWizardModel extends Model { constructor(public deploymentTarget: BdcDeploymentType) { @@ -138,7 +138,7 @@ export class DeployClusterWizardModel extends Model { return targetDeploymentProfile; } - public getCodeCellContentForNotebook(): string[] { + public getCodeCellContentForNotebook(tools: ITool[]): string[] { const profile = this.createTargetProfile(); const statements: string[] = []; if (this.deploymentTarget === BdcDeploymentType.NewAKS) { @@ -166,16 +166,13 @@ export class DeployClusterWizardModel extends Model { statements.push(`os.environ["DOCKER_PASSWORD"] = os.environ["${VariableNames.DockerPassword_VariableName}"]`); } 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[ToolsInstallPath]!)}"`); + const env: NodeJS.ProcessEnv = {}; + setEnvironmentVariablesForInstallPaths(tools, env); + statements.push(`os.environ["${kubeCtlEnvVarName}"] = "${this.escapeForNotebookCodeCell(env[kubeCtlEnvVarName]!)}"`); + statements.push(`os.environ["PATH"] = os.environ["PATH"] + "${delimiter}" + "${this.escapeForNotebookCodeCell(env[ToolsInstallPath]!)}"`); statements.push(`print('Variables have been set successfully.')`); return statements.map(line => line + EOL); } - - private escapeForNotebookCodeCell(original: string): string { - // Escape the \ character for the code cell string value - return original && original.replace(/\\/g, '\\\\'); - } } export enum AuthenticationMode { diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts index c912134e26..a8d009c86b 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/azureSettingsPage.ts @@ -7,7 +7,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { FieldType, LabelPosition, SectionInfo } from '../../../interfaces'; -import { createSection, getDropdownComponent, InputComponents, InputComponent, setModelValues, Validator } from '../../modelViewUtils'; +import { createSection, getDropdownComponent, InputComponentInfo, InputComponents, setModelValues, Validator } from '../../modelViewUtils'; import { WizardPageBase } from '../../wizardPageBase'; import { AksName_VariableName, Location_VariableName, ResourceGroup_VariableName, SubscriptionId_VariableName, VMCount_VariableName, VMSize_VariableName } from '../constants'; import { DeployClusterWizard } from '../deployClusterWizard'; @@ -29,7 +29,7 @@ export class AzureSettingsPage extends WizardPageBase { labelPosition: LabelPosition.Left, spaceBetweenFields: '5px', rows: [{ - fields: [{ + items: [{ type: FieldType.Text, label: localize('deployCluster.SubscriptionField', "Subscription id"), required: false, @@ -38,9 +38,7 @@ export class AzureSettingsPage extends WizardPageBase { description: localize('deployCluster.SubscriptionDescription', "The default subscription will be used if you leave this field blank.") }, { type: FieldType.ReadonlyText, - label: '', - labelWidth: '0px', - defaultValue: localize('deployCluster.SubscriptionHelpText', "{0}"), + label: '{0}', links: [ { text: localize('deployCluster.SubscriptionHelpLink', "View available Azure subscriptions"), @@ -49,7 +47,7 @@ export class AzureSettingsPage extends WizardPageBase { ] }] }, { - fields: [{ + items: [{ type: FieldType.DateTimeText, label: localize('deployCluster.ResourceGroupName', "New resource group name"), required: true, @@ -57,7 +55,7 @@ export class AzureSettingsPage extends WizardPageBase { defaultValue: 'mssql-' }] }, { - fields: [{ + items: [{ type: FieldType.Options, label: localize('deployCluster.Location', "Location"), required: true, @@ -79,9 +77,7 @@ export class AzureSettingsPage extends WizardPageBase { ] }, { type: FieldType.ReadonlyText, - label: '', - labelWidth: '0px', - defaultValue: localize('deployCluster.LocationHelpText', "{0}"), + label: '{0}', links: [ { text: localize('deployCluster.AzureLocationHelpLink', "View available Azure locations"), @@ -90,7 +86,7 @@ export class AzureSettingsPage extends WizardPageBase { ] }] }, { - fields: [{ + items: [{ type: FieldType.DateTimeText, label: localize('deployCluster.AksName', "AKS cluster name"), required: true, @@ -98,7 +94,7 @@ export class AzureSettingsPage extends WizardPageBase { defaultValue: 'mssql-', }] }, { - fields: [ + items: [ { type: FieldType.Number, label: localize('deployCluster.VMCount', "VM count"), @@ -110,7 +106,7 @@ export class AzureSettingsPage extends WizardPageBase { } ] }, { - fields: [{ + items: [{ type: FieldType.Text, label: localize('deployCluster.VMSize', "VM size"), required: true, @@ -118,9 +114,7 @@ export class AzureSettingsPage extends WizardPageBase { defaultValue: 'Standard_E8s_v3' }, { type: FieldType.ReadonlyText, - label: '', - labelWidth: '0px', - defaultValue: localize('deployCluster.VMSizeHelpText', "{0}"), + label: '{0}', links: [ { text: localize('deployCluster.VMSizeHelpLink', "View available VM sizes"), @@ -137,13 +131,14 @@ export class AzureSettingsPage extends WizardPageBase { onNewDisposableCreated: (disposable: vscode.Disposable): void => { self.wizard.registerDisposable(disposable); }, - onNewInputComponentCreated: (name: string, component: InputComponent): void => { - self.inputComponents[name] = { component: component }; + onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { + this.inputComponents[name] = { component: inputComponentInfo.component }; }, onNewValidatorCreated: (validator: Validator): void => { self.validators.push(validator); }, - container: this.wizard.wizardObject + container: this.wizard.wizardObject, + inputComponents: this.wizard.inputComponents }); const formBuilder = view.modelBuilder.formContainer().withFormItems( [{ @@ -184,5 +179,6 @@ export class AzureSettingsPage extends WizardPageBase { return true; }); setModelValues(this.inputComponents, this.wizard.model); + Object.assign(this.wizard.inputComponents, this.inputComponents); } } diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts index 7c404095b9..ab247351cf 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/clusterSettingsPage.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import { EOL } from 'os'; 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, InputComponent } from '../../modelViewUtils'; +import { FieldType, LabelPosition, SectionInfo } from '../../../interfaces'; +import { createSection, getInputBoxComponent, getInvalidSQLPasswordMessage, getPasswordMismatchMessage, InputComponentInfo, InputComponents, isValidSQLPassword, setModelValues, Validator } from '../../modelViewUtils'; import { WizardPageBase } from '../../wizardPageBase'; import * as VariableNames from '../constants'; -import { EOL } from 'os'; +import { DeployClusterWizard } from '../deployClusterWizard'; import { AuthenticationMode } from '../deployClusterWizardModel'; const localize = nls.loadMessageBundle(); @@ -174,7 +174,7 @@ export class ClusterSettingsPage extends WizardPageBase { variableName: VariableNames.DomainServiceAccountPassword_VariableName }, { type: FieldType.Text, - label: localize('deployCluster.AppOwers', "App owners"), + label: localize('deployCluster.AppOwners', "App owners"), required: false, variableName: VariableNames.AppOwners_VariableName, placeHolder: localize('deployCluster.AppOwnersPlaceHolder', "Use comma to separate the values."), @@ -193,12 +193,13 @@ export class ClusterSettingsPage extends WizardPageBase { const basicSettingsGroup = createSection({ view: view, container: self.wizard.wizardObject, + inputComponents: this.wizard.inputComponents, sectionInfo: basicSection, onNewDisposableCreated: (disposable: vscode.Disposable): void => { self.wizard.registerDisposable(disposable); }, - onNewInputComponentCreated: (name: string, component: InputComponent): void => { - self.inputComponents[name] = { component: component }; + onNewInputComponentCreated: (name: string, inputComponent: InputComponentInfo): void => { + self.inputComponents[name] = { component: inputComponent.component }; }, onNewValidatorCreated: (validator: Validator): void => { self.validators.push(validator); @@ -207,12 +208,13 @@ export class ClusterSettingsPage extends WizardPageBase { const activeDirectorySettingsGroup = createSection({ view: view, container: self.wizard.wizardObject, + inputComponents: this.wizard.inputComponents, sectionInfo: activeDirectorySection, onNewDisposableCreated: (disposable: vscode.Disposable): void => { self.wizard.registerDisposable(disposable); }, - onNewInputComponentCreated: (name: string, component: InputComponent): void => { - self.inputComponents[name] = { component: component }; + onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { + this.inputComponents[name] = { component: inputComponentInfo.component }; }, onNewValidatorCreated: (validator: Validator): void => { self.validators.push(validator); @@ -221,12 +223,13 @@ export class ClusterSettingsPage extends WizardPageBase { const dockerSettingsGroup = createSection({ view: view, container: self.wizard.wizardObject, + inputComponents: this.wizard.inputComponents, sectionInfo: dockerSection, onNewDisposableCreated: (disposable: vscode.Disposable): void => { self.wizard.registerDisposable(disposable); }, - onNewInputComponentCreated: (name: string, component: InputComponent): void => { - self.inputComponents[name] = { component: component }; + onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { + this.inputComponents[name] = { component: inputComponentInfo.component }; }, onNewValidatorCreated: (validator: Validator): void => { self.validators.push(validator); @@ -266,6 +269,7 @@ export class ClusterSettingsPage extends WizardPageBase { public onLeave() { setModelValues(this.inputComponents, this.wizard.model); + Object.assign(this.wizard.inputComponents, this.inputComponents); if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) { const variableDNSPrefixMapping: { [s: string]: string } = {}; variableDNSPrefixMapping[VariableNames.AppServiceProxyDNSName_VariableName] = 'bdc-appproxy'; diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts index 476f74b31b..8fca7865d5 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/serviceSettingsPage.ts @@ -5,11 +5,11 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { DeployClusterWizard } from '../deployClusterWizard'; -import { SectionInfo, FieldType } from '../../../interfaces'; -import { Validator, InputComponents, InputComponent, createSection, createGroupContainer, createLabel, createFlexContainer, createTextInput, createNumberInput, setModelValues, getInputBoxComponent, getCheckboxComponent, getDropdownComponent } from '../../modelViewUtils'; +import { FieldType, SectionInfo } from '../../../interfaces'; +import { createFlexContainer, createGroupContainer, createLabel, createNumberInput, createSection, createTextInput, getCheckboxComponent, getDropdownComponent, getInputBoxComponent, InputComponentInfo, InputComponents, setModelValues, Validator } from '../../modelViewUtils'; import { WizardPageBase } from '../../wizardPageBase'; import * as VariableNames from '../constants'; +import { DeployClusterWizard } from '../deployClusterWizard'; import { AuthenticationMode } from '../deployClusterWizardModel'; const localize = nls.loadMessageBundle(); @@ -59,7 +59,7 @@ export class ServiceSettingsPage extends WizardPageBase { inputWidth: NumberInputWidth, spaceBetweenFields: '40px', rows: [{ - fields: [{ + items: [{ type: FieldType.Options, label: localize('deployCluster.MasterSqlServerInstances', "SQL Server master instances"), options: ['1', '3', '4', '5', '6', '7', '8', '9'], @@ -75,7 +75,7 @@ export class ServiceSettingsPage extends WizardPageBase { variableName: VariableNames.ComputePoolScale_VariableName, }] }, { - fields: [{ + items: [{ type: FieldType.Number, label: localize('deployCluster.DataPoolInstances', "Data pool instances"), min: 1, @@ -93,7 +93,7 @@ export class ServiceSettingsPage extends WizardPageBase { variableName: VariableNames.SparkPoolScale_VariableName }] }, { - fields: [ + items: [ { type: FieldType.Number, label: localize('deployCluster.StoragePoolInstances', "Storage pool (HDFS) instances"), @@ -119,12 +119,13 @@ export class ServiceSettingsPage extends WizardPageBase { return createSection({ view: view, container: this.wizard.wizardObject, + inputComponents: this.inputComponents, sectionInfo: sectionInfo, onNewDisposableCreated: (disposable: vscode.Disposable): void => { this.wizard.registerDisposable(disposable); }, - onNewInputComponentCreated: (name: string, component: InputComponent): void => { - this.inputComponents[name] = { component: component }; + onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { + this.inputComponents[name] = { component: inputComponentInfo.component }; }, onNewValidatorCreated: (validator: Validator): void => { } @@ -400,6 +401,7 @@ export class ServiceSettingsPage extends WizardPageBase { public onLeave(): void { setModelValues(this.inputComponents, this.wizard.model); + Object.assign(this.wizard.inputComponents, this.inputComponents); this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { return true; }); diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/summaryPage.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/summaryPage.ts index 1e4a92b5f5..b2699ff145 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/pages/summaryPage.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/pages/summaryPage.ts @@ -43,18 +43,17 @@ export class SummaryPage extends WizardPageBase { title: localize('deployCluster.DeploymentTarget', "Deployment target"), rows: [ { - fields: [ + items: [ { type: FieldType.ReadonlyText, label: localize('deployCluster.Kubeconfig', "Kube config"), defaultValue: this.wizard.model.getStringValue(VariableNames.KubeConfigPath_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }, { type: FieldType.ReadonlyText, label: localize('deployCluster.ClusterContext', "Cluster context"), - defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterContext_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }] } ] @@ -68,33 +67,33 @@ export class SummaryPage extends WizardPageBase { rows: [ { - fields: [ + items: [ { type: FieldType.ReadonlyText, label: localize('deployCluster.DeploymentProfile', "Deployment profile"), defaultValue: this.wizard.model.getStringValue(VariableNames.DeploymentProfile_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }, { type: FieldType.ReadonlyText, label: localize('deployCluster.ClusterName', "Cluster name"), defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterName_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }] }, { - fields: [ + items: [ { type: FieldType.ReadonlyText, label: localize('deployCluster.ControllerUsername', "Controller username"), defaultValue: this.wizard.model.getStringValue(VariableNames.AdminUserName_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }, { type: FieldType.ReadonlyText, label: localize('deployCluster.AuthenticationMode', "Authentication mode"), defaultValue: this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory ? localize('deployCluster.AuthenticationMode.ActiveDirectory', "Active Directory") : localize('deployCluster.AuthenticationMode.Basic', "Basic"), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } } ] } @@ -103,72 +102,72 @@ export class SummaryPage extends WizardPageBase { if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) { clusterSectionInfo.rows!.push({ - fields: [ + items: [ { type: FieldType.ReadonlyText, label: localize('deployCluster.OuDistinguishedName', "Organizational unit"), defaultValue: this.wizard.model.getStringValue(VariableNames.OrganizationalUnitDistinguishedName_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }, { type: FieldType.ReadonlyText, label: localize('deployCluster.DomainControllerFQDNs', "Domain controller FQDNs"), defaultValue: this.wizard.model.getStringValue(VariableNames.DomainControllerFQDNs_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }] }); clusterSectionInfo.rows!.push({ - fields: [ + items: [ { type: FieldType.ReadonlyText, label: localize('deployCluster.DomainDNSIPAddresses', "Domain DNS IP addresses"), defaultValue: this.wizard.model.getStringValue(VariableNames.DomainDNSIPAddresses_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }, { type: FieldType.ReadonlyText, label: localize('deployCluster.DomainDNSName', "Domain DNS name"), defaultValue: this.wizard.model.getStringValue(VariableNames.DomainDNSName_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }] }); clusterSectionInfo.rows!.push({ - fields: [ + items: [ { type: FieldType.ReadonlyText, label: localize('deployCluster.ClusterAdmins', "Cluster admin group"), defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterAdmins_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }, { type: FieldType.ReadonlyText, label: localize('deployCluster.ClusterUsers', "Cluster users"), defaultValue: this.wizard.model.getStringValue(VariableNames.ClusterUsers_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }] }); clusterSectionInfo.rows!.push({ - fields: [ + items: [ { type: FieldType.ReadonlyText, - label: localize('deployCluster.AppOwers', "App owners"), + label: localize('deployCluster.AppOwners', "App owners"), defaultValue: this.wizard.model.getStringValue(VariableNames.AppOwners_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }, { type: FieldType.ReadonlyText, label: localize('deployCluster.AppReaders', "App readers"), defaultValue: this.wizard.model.getStringValue(VariableNames.AppReaders_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }] }); clusterSectionInfo.rows!.push({ - fields: [ + items: [ { type: FieldType.ReadonlyText, label: localize('deployCluster.DomainServiceAccountUserName', "Service account username"), defaultValue: this.wizard.model.getStringValue(VariableNames.DomainServiceAccountUserName_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }] }); } @@ -179,45 +178,45 @@ export class SummaryPage extends WizardPageBase { inputWidth: '200px', title: localize('deployCluster.AzureSettings', "Azure settings"), rows: [{ - fields: [ + items: [ { type: FieldType.ReadonlyText, label: localize('deployCluster.SubscriptionId', "Subscription id"), defaultValue: this.wizard.model.getStringValue(VariableNames.SubscriptionId_VariableName) || localize('deployCluster.DefaultSubscription', "Default Azure Subscription"), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }, { type: FieldType.ReadonlyText, label: localize('deployCluster.ResourceGroup', "Resource group"), defaultValue: this.wizard.model.getStringValue(VariableNames.ResourceGroup_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } } ] }, { - fields: [ + items: [ { type: FieldType.ReadonlyText, label: localize('deployCluster.Location', "Location"), defaultValue: this.wizard.model.getStringValue(VariableNames.Location_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }, { type: FieldType.ReadonlyText, label: localize('deployCluster.AksClusterName', "AKS cluster name"), defaultValue: this.wizard.model.getStringValue(VariableNames.AksName_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } } ] }, { - fields: [ + items: [ { type: FieldType.ReadonlyText, label: localize('deployCluster.VMSize', "VM size"), defaultValue: this.wizard.model.getStringValue(VariableNames.VMSize_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }, { type: FieldType.ReadonlyText, label: localize('deployCluster.VMCount', "VM count"), defaultValue: this.wizard.model.getStringValue(VariableNames.VMCount_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } } ] } @@ -231,35 +230,35 @@ export class SummaryPage extends WizardPageBase { title: localize('deployCluster.ScaleSettings', "Scale settings"), rows: [ { - fields: [{ + items: [{ type: FieldType.ReadonlyText, label: localize('deployCluster.MasterSqlServerInstances', "SQL Server master instances"), defaultValue: this.wizard.model.getStringValue(VariableNames.SQLServerScale_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }, { type: FieldType.ReadonlyText, label: localize('deployCluster.ComputePoolInstances', "Compute pool instances"), defaultValue: this.wizard.model.getStringValue(VariableNames.ComputePoolScale_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }] }, { - fields: [{ + items: [{ type: FieldType.ReadonlyText, label: localize('deployCluster.DataPoolInstances', "Data pool instances"), defaultValue: this.wizard.model.getStringValue(VariableNames.DataPoolScale_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }, { type: FieldType.ReadonlyText, label: localize('deployCluster.SparkPoolInstances', "Spark pool instances"), defaultValue: this.wizard.model.getStringValue(VariableNames.SparkPoolScale_VariableName), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }] }, { - fields: [{ + items: [{ type: FieldType.ReadonlyText, label: localize('deployCluster.StoragePoolInstances', "Storage pool (HDFS) instances"), defaultValue: `${this.wizard.model.getStringValue(VariableNames.HDFSPoolScale_VariableName)} ${this.wizard.model.getBooleanValue(VariableNames.IncludeSpark_VariableName) ? localize('deployCluster.WithSpark', "(Spark included)") : ''}`, - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }] } ] @@ -270,6 +269,7 @@ export class SummaryPage extends WizardPageBase { title: '', component: createSection({ container: this.wizard.wizardObject, + inputComponents: this.wizard.inputComponents, sectionInfo: sectionInfo, view: this.view, onNewDisposableCreated: () => { }, @@ -398,7 +398,7 @@ export class SummaryPage extends WizardPageBase { private createEndpointRow(name: string, dnsVariableName: string, portVariableName: string): azdata.FlexContainer { const items = []; - items.push(createLabel(this.view, { text: name, width: '150px', fontWeight: FontWeight.Bold })); + items.push(createLabel(this.view, { text: name, width: '150px', cssStyles: { fontWeight: FontWeight.Bold } })); if (this.wizard.model.authenticationMode === AuthenticationMode.ActiveDirectory) { items.push(createLabel(this.view, { text: this.wizard.model.getStringValue(dnsVariableName)!, width: '200px' diff --git a/extensions/resource-deployment/src/ui/deploymentInputDialog.ts b/extensions/resource-deployment/src/ui/deploymentInputDialog.ts index ba32444c89..30f024e4ff 100644 --- a/extensions/resource-deployment/src/ui/deploymentInputDialog.ts +++ b/extensions/resource-deployment/src/ui/deploymentInputDialog.ts @@ -4,15 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import { EOL } from 'os'; import * as vscode from 'vscode'; 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, InputComponent } from './modelViewUtils'; -import { Model } from './model'; -import { EOL } from 'os'; +import { INotebookService } from '../services/notebookService'; import { IPlatformService } from '../services/platformService'; +import { DialogBase } from './dialogBase'; +import { Model } from './model'; +import { initializeDialog, InputComponentInfo, InputComponents, setModelValues, Validator } from './modelViewUtils'; const localize = nls.loadMessageBundle(); @@ -42,11 +42,12 @@ export class DeploymentInputDialog extends DialogBase { initializeDialog({ dialogInfo: this.dialogInfo, container: this._dialogObject, + inputComponents: this.inputComponents, onNewDisposableCreated: (disposable: vscode.Disposable): void => { this._toDispose.push(disposable); }, - onNewInputComponentCreated: (name: string, component: InputComponent, inputValueTransformer?: InputValueTransformer): void => { - this.inputComponents[name] = { component: component, inputValueTransformer: inputValueTransformer }; + onNewInputComponentCreated: (name: string, inputComponentInfo: InputComponentInfo): void => { + this.inputComponents[name] = inputComponentInfo; }, onNewValidatorCreated: (validator: Validator): void => { validators.push(validator); diff --git a/extensions/resource-deployment/src/ui/model.ts b/extensions/resource-deployment/src/ui/model.ts index 0cf853fd38..536e389919 100644 --- a/extensions/resource-deployment/src/ui/model.ts +++ b/extensions/resource-deployment/src/ui/model.ts @@ -2,7 +2,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { NoteBookEnvironmentVariablePrefix } from '../interfaces'; +import { EOL } from 'os'; +import { ITool, NoteBookEnvironmentVariablePrefix } from '../interfaces'; +import { setEnvironmentVariablesForInstallPaths, getRuntimeBinaryPathEnvironmentVariableName } from '../utils'; +import { ToolsInstallPath } from '../constants'; +import { delimiter } from 'path'; + + +const NotebookEnvironmentVariablePrefixRegex = new RegExp(`^${NoteBookEnvironmentVariablePrefix}`); export class Model { private propValueObject: { [s: string]: string | undefined } = {}; @@ -32,13 +39,57 @@ export class Model { return value === undefined ? defaultValue : value === 'true'; } - public setEnvironmentVariables(): void { - Object.keys(this.propValueObject).filter(propertyName => propertyName.startsWith(NoteBookEnvironmentVariablePrefix)).forEach(propertyName => { - const value = this.getStringValue(propertyName); - if (value !== undefined && value !== '') { - process.env[propertyName] = value; - } - process.env[propertyName] = value === undefined ? '' : value; + /** + * Returns python code statements for setting variables starting with {@see NoteBookEnvironmentVariablePrefix} as python variables. + * The prefix {@see NoteBookEnvironmentVariablePrefix} is removed and variable name changed to all lowercase to arrive at python variable name. + * The statements returned are escaped for use in cell of a python notebook. + * + * @param tools - optional set of tools for which variable value setting statements need to be generated; + * @param inputFilter - optional parameter to filter out setting of specific variable names. Every variable for which this function returns false is not included + * in the emitted code. + */ + public getCodeCellContentForNotebook(tools: ITool[] = [], inputFilter: (varName: string) => boolean = () => true): string[] { + const statements: string[] = Object.keys(this.propValueObject) + .filter(propertyName => propertyName.startsWith(NoteBookEnvironmentVariablePrefix) && inputFilter(propertyName)) + .map(propertyName => { + const value = this.escapeForNotebookCodeCell(this.getStringValue(propertyName, '')); + const varName = propertyName.replace(NotebookEnvironmentVariablePrefixRegex, '').toLocaleLowerCase(); + return `${varName} = '${value}'${EOL}`; + }); + statements.push(`print('Variables have been set successfully.')${EOL}`); + const env: NodeJS.ProcessEnv = {}; + setEnvironmentVariablesForInstallPaths(tools, env); + tools.forEach(tool => { + const envVarName: string = getRuntimeBinaryPathEnvironmentVariableName(tool.name); + statements.push(`os.environ["${envVarName}"] = "${this.escapeForNotebookCodeCell(env[envVarName]!)}"${EOL}`); }); + if (env[ToolsInstallPath]) { + statements.push(`os.environ["PATH"] = os.environ["PATH"] + "${delimiter}" + "${this.escapeForNotebookCodeCell(env[ToolsInstallPath])}"${EOL}`); + } + statements.push(`print('Environment Variables for tools have been set successfully.')${EOL}`); + return statements; + } + + protected escapeForNotebookCodeCell(original?: string): string | undefined { + // Escape the \ character for the code cell string value + return original && original.replace(/\\/g, '\\\\'); + } + + /** + * Sets the environment variable for each model variable that starts with {@see NoteBookEnvironmentVariablePrefix} in the + * current process. + * + * @param env - env variable object in which the environment variables are populated. Default: process.env + * @param inputFilter - an optional filter to further restrict the variables that are set into the env object. + * Every variable for which this function returns false is not included does not get the env variable set. + * Default all variable meeting prefix requirements are set. + */ + public setEnvironmentVariables(env: NodeJS.ProcessEnv = process.env, inputFilter: (varName: string) => boolean = () => true): void { + Object.keys(this.propValueObject) + .filter(propertyName => propertyName.startsWith(NoteBookEnvironmentVariablePrefix) && inputFilter(propertyName)) + .forEach(propertyName => { + const value = this.getStringValue(propertyName); + env[propertyName] = value === undefined ? '' : value; + }); } } diff --git a/extensions/resource-deployment/src/ui/modelViewUtils.ts b/extensions/resource-deployment/src/ui/modelViewUtils.ts index b0d0ebbd99..50c354dda1 100644 --- a/extensions/resource-deployment/src/ui/modelViewUtils.ts +++ b/extensions/resource-deployment/src/ui/modelViewUtils.ts @@ -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 inputComponents[name].component; @@ -39,40 +48,42 @@ export function getTextComponent(name: string, inputComponents: InputComponents) return 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({ 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({ 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 = { + 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({ 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({ 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({ 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) ?? ''; + 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 { - 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) { + 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 { +/** + * 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 { + 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 { return await createRadioOptions(context); } -async function createRadioOptions(context: FieldContext, getRadioButtonInfo?: (() => Promise<{ values: string[] | azdata.CategoryValue[], defaultValue: string }>)) - : Promise { - 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({ 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)) + : Promise { + 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 { - 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({ - 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 { context.fieldInfo.subFields = []; const accountValueToAccountMap = new Map(); const subscriptionValueToSubscriptionMap = new Map(); @@ -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, resourceGroupDropdown: azdata.DropDownComponent, locationDropdown?: azdata.DropDownComponent -): void { +): Promise { 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('azure.accounts.getSubscriptions', selectedAccount, true /*ignoreErrors*/).then(response => { + try { + const response = await vscode.commands.executeCommand('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(); + 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 { 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('azure.accounts.getResourceGroups', selectedAccount, selectedSubscription, true /*ignoreErrors*/).then(response => { + try { + const response = await vscode.commands.executeCommand('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 === ''; } diff --git a/extensions/resource-deployment/src/ui/notebookWizard/notebookWizard.ts b/extensions/resource-deployment/src/ui/notebookWizard/notebookWizard.ts index 2bfa0a2ef1..64f1b73d5b 100644 --- a/extensions/resource-deployment/src/ui/notebookWizard/notebookWizard.ts +++ b/extensions/resource-deployment/src/ui/notebookWizard/notebookWizard.ts @@ -2,20 +2,23 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as path from 'path'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { INotebookService } from '../../services/notebookService'; +import { INotebookService, Notebook } from '../../services/notebookService'; +import { IToolsService } from '../../services/toolsService'; import { Model } from '../model'; +import { InputComponents, setModelValues } from '../modelViewUtils'; import { WizardBase } from '../wizardBase'; -import { WizardPageBase } from '../wizardPageBase'; import { DeploymentType, NotebookWizardInfo } from './../../interfaces'; import { IPlatformService } from './../../services/platformService'; +import { NotebookWizardAutoSummaryPage } from './notebookWizardAutoSummaryPage'; import { NotebookWizardPage } from './notebookWizardPage'; -import { NotebookWizardSummaryPage } from './notebookWizardSummaryPage'; const localize = nls.loadMessageBundle(); -export class NotebookWizard extends WizardBase { +export class NotebookWizard extends WizardBase { + private _inputComponents: InputComponents = {}; public get notebookService(): INotebookService { return this._notebookService; @@ -29,8 +32,15 @@ export class NotebookWizard extends WizardBase { return this._wizardInfo; } - constructor(private _wizardInfo: NotebookWizardInfo, private _notebookService: INotebookService, private _platformService: IPlatformService) { + public get inputComponents(): InputComponents { + return this._inputComponents; + } + + constructor(private _wizardInfo: NotebookWizardInfo, private _notebookService: INotebookService, private _platformService: IPlatformService, private _toolsService: IToolsService) { super(_wizardInfo.title, new Model()); + if (this._wizardInfo.codeCellInsertionPosition === undefined) { + this._wizardInfo.codeCellInsertionPosition = 0; + } this.wizardObject.doneButton.label = _wizardInfo.actionText || this.wizardObject.doneButton.label; } @@ -41,31 +51,64 @@ export class NotebookWizard extends WizardBase { 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.wizardInfo.actionText = this.wizardInfo.actionText || localize('notebookWizard.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); - }); + protected async onOk(): Promise { + setModelValues(this.inputComponents, this.model); + const env: NodeJS.ProcessEnv = {}; + this.model.setEnvironmentVariables(env, (varName) => { + const isPassword = !!this.inputComponents[varName]?.isPassword; + return isPassword; + }); + console.log(`TCL:: env`, env); + const notebook: Notebook = await this.notebookService.getNotebook(this.wizardInfo.notebook); + // generate python code statements for all variables captured by the wizard + const statements = this.model.getCodeCellContentForNotebook( + this._toolsService.toolsForCurrentProvider, + (varName) => { + const isPassword = !!this.inputComponents[varName]?.isPassword; + return !isPassword; + } + ); + // insert generated code statements into the notebook. + notebook.cells.splice( + this.wizardInfo.codeCellInsertionPosition ?? 0, + 0, + { + cell_type: 'code', + source: statements, + metadata: {}, + outputs: [], + execution_count: 0 + } + ); + try { + if (this.wizardInfo.runNotebook) { + this.notebookService.backgroundExecuteNotebook(this.wizardInfo.taskName, notebook, 'deploy', this.platformService, env); + } else { + Object.assign(process.env, env); + const title = path.basename(this.notebookService.getNotebookPath(this.wizardInfo.notebook)); + await this.notebookService.launchNotebookWithContent(title, JSON.stringify(notebook, undefined, 4)); + } + } catch (error) { + vscode.window.showErrorMessage(error); } } - private getPages(): WizardPageBase[] { - const pages: WizardPageBase[] = []; + private getPages(): NotebookWizardPage[] { + const pages: NotebookWizardPage[] = []; 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)); + if (this.wizardInfo.pages[pageIndex].isSummaryPage && this.wizardInfo.isSummaryPageAutoGenerated) { + // If we are auto-generating the summary page + pages.push(new NotebookWizardAutoSummaryPage(this, pageIndex)); + } else { + pages.push(new NotebookWizardPage(this, pageIndex)); + } } return pages; } diff --git a/extensions/resource-deployment/src/ui/notebookWizard/notebookWizardSummaryPage.ts b/extensions/resource-deployment/src/ui/notebookWizard/notebookWizardAutoSummaryPage.ts similarity index 60% rename from extensions/resource-deployment/src/ui/notebookWizard/notebookWizardSummaryPage.ts rename to extensions/resource-deployment/src/ui/notebookWizard/notebookWizardAutoSummaryPage.ts index b32f6b0ae1..9f9866bbad 100644 --- a/extensions/resource-deployment/src/ui/notebookWizard/notebookWizardSummaryPage.ts +++ b/extensions/resource-deployment/src/ui/notebookWizard/notebookWizardAutoSummaryPage.ts @@ -6,19 +6,23 @@ 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 { createSection, DefaultInputWidth, DefaultLabelWidth, DefaultFieldAlignItems, DefaultFieldWidth, DefaultFieldHeight } from '../modelViewUtils'; import { NotebookWizard } from './notebookWizard'; +import { NotebookWizardPage } from './notebookWizardPage'; const localize = nls.loadMessageBundle(); -export class NotebookWizardSummaryPage extends WizardPageBase { +export class NotebookWizardAutoSummaryPage extends NotebookWizardPage { private formItems: azdata.FormComponent[] = []; private form!: azdata.FormBuilder; private view!: azdata.ModelView; - constructor(wizard: NotebookWizard) { - super(localize('notebookWizard.summaryPageTitle', "Review your configuration"), '', wizard); + constructor(wizard: NotebookWizard, _pageIndex: number) { + super(wizard, + _pageIndex, + wizard.wizardInfo.pages[_pageIndex].title || localize('notebookWizard.autoSummaryPageTitle', "Review your configuration"), + wizard.wizardInfo.pages[_pageIndex].description || '' + ); } public initialize(): void { @@ -29,25 +33,31 @@ export class NotebookWizardSummaryPage extends WizardPageBase { }); } - public onLeave() { + public onLeave(): void { this.wizard.wizardObject.message = { text: '' }; } - public onEnter() { + public onEnter(): void { 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; + const fieldWidth = this.pageInfo.fieldWidth || this.wizard.wizardInfo.fieldWidth || DefaultFieldWidth; + const fieldHeight = this.pageInfo.fieldHeight || this.wizard.wizardInfo.fieldHeight || DefaultFieldHeight; + const fieldAlignItems = this.pageInfo.fieldAlignItems || this.wizard.wizardInfo.fieldAlignItems || DefaultFieldAlignItems; + const labelWidth = this.pageInfo.labelWidth || this.wizard.wizardInfo.labelWidth || DefaultLabelWidth; + const labelPosition = this.pageInfo.labelPosition || this.wizard.wizardInfo.labelPosition || LabelPosition.Left; + const inputWidth = this.pageInfo.inputWidth || this.wizard.wizardInfo.inputWidth || DefaultInputWidth; - this.wizard.wizardInfo.pages.forEach(pageInfo => { + this.wizard.wizardInfo.pages.filter((undefined, index) => index < this._pageIndex).forEach(pageInfo => { const summarySectionInfo: SectionInfo = { labelPosition: labelPosition, labelWidth: labelWidth, inputWidth: inputWidth, + fieldWidth: fieldWidth, + fieldHeight: fieldHeight, + fieldAlignItems: fieldAlignItems, title: '', rows: [] }; @@ -68,6 +78,7 @@ export class NotebookWizardSummaryPage extends WizardPageBase { title: pageInfo.title, component: createSection({ container: this.wizard.wizardObject, + inputComponents: this.wizard.inputComponents, sectionInfo: summarySectionInfo, view: this.view, onNewDisposableCreated: () => { }, @@ -84,11 +95,11 @@ export class NotebookWizardSummaryPage extends WizardPageBase { private addSummaryForVariable(summarySectionInfo: SectionInfo, fieldInfo: SubFieldInfo) { summarySectionInfo!.rows!.push({ - fields: [{ + items: [{ type: FieldType.ReadonlyText, label: fieldInfo.label, defaultValue: this.wizard.model.getStringValue(fieldInfo.variableName!), - labelFontWeight: FontWeight.Bold + labelCSSStyles: { fontWeight: FontWeight.Bold } }] }); } diff --git a/extensions/resource-deployment/src/ui/notebookWizard/notebookWizardPage.ts b/extensions/resource-deployment/src/ui/notebookWizard/notebookWizardPage.ts index b2ae551714..5d25cd94f4 100644 --- a/extensions/resource-deployment/src/ui/notebookWizard/notebookWizardPage.ts +++ b/extensions/resource-deployment/src/ui/notebookWizard/notebookWizardPage.ts @@ -7,58 +7,73 @@ 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 { initializeWizardPage, InputComponentInfo, setModelValues, Validator } from '../modelViewUtils'; import { WizardPageBase } from '../wizardPageBase'; import { NotebookWizard } from './notebookWizard'; const localize = nls.loadMessageBundle(); export class NotebookWizardPage extends WizardPageBase { - 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); + constructor( + wizard: NotebookWizard, + protected _pageIndex: number, + title?: string, + description?: string + ) { + super( + wizard.wizardInfo.pages[_pageIndex].title || title || '', + wizard.wizardInfo.pages[_pageIndex].description || description || '', + wizard + ); } public initialize(): void { - const self = this; initializeWizardPage({ container: this.wizard.wizardObject, + inputComponents: this.wizard.inputComponents, wizardInfo: this.wizard.wizardInfo, pageInfo: this.pageInfo, page: this.pageObject, onNewDisposableCreated: (disposable: vscode.Disposable): void => { - self.wizard.registerDisposable(disposable); + this.wizard.registerDisposable(disposable); }, - onNewInputComponentCreated: (name: string, component: InputComponent): void => { - self.inputComponents[name] = { component: component }; + onNewInputComponentCreated: ( + name: string, + inputComponentInfo: InputComponentInfo + ): void => { + if (name) { + this.wizard.inputComponents[name] = inputComponentInfo; + } }, onNewValidatorCreated: (validator: Validator): void => { - self.validators.push(validator); - } + this.validators.push(validator); + }, }); } - - public onLeave() { - setModelValues(this.inputComponents, this.wizard.model); + public onLeave(): void { // The following callback registration clears previous navigation validators. this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { return true; }); } - public onEnter() { + public onEnter(): void { + if (this.pageInfo.isSummaryPage) { + setModelValues(this.wizard.inputComponents, this.wizard.model); + } + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { this.wizard.wizardObject.message = { text: '' }; if (pcInfo.newPage > pcInfo.lastPage) { const messages: string[] = []; - this.validators.forEach(validator => { + this.validators.forEach((validator) => { const result = validator(); if (!result.valid) { messages.push(result.message); @@ -67,9 +82,15 @@ export class NotebookWizardPage extends WizardPageBase { 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."), + 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 + level: azdata.window.MessageLevel.Error, }; } return messages.length === 0; diff --git a/extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts b/extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts new file mode 100644 index 0000000000..2d1a3df750 --- /dev/null +++ b/extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; +import { OptionsInfo } from '../interfaces'; +import { getErrorMessage } from '../utils'; + +export class RadioGroupLoadingComponentBuilder implements azdata.ComponentBuilder { + private _optionsDivContainer!: azdata.DivContainer; + private _optionsLoadingBuilder: azdata.LoadingComponentBuilder; + private _onValueChangedEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private _currentRadioOption!: azdata.RadioButtonComponent; + constructor(private _view: azdata.ModelView, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void) { + this._optionsDivContainer = this._view!.modelBuilder.divContainer().withProperties({ clickable: false }).component(); + this._optionsLoadingBuilder = this._view!.modelBuilder.loadingComponent().withItem(this._optionsDivContainer); + } + + component(): azdata.LoadingComponent { + return this._optionsLoadingBuilder.component(); + } + + withProperties(properties: U): azdata.ComponentBuilder { + return this._optionsLoadingBuilder.withProperties(properties); + } + + withValidation(validation: (component: azdata.LoadingComponent) => boolean): azdata.ComponentBuilder { + return this._optionsLoadingBuilder.withValidation(validation); + } + + async loadOptions(optionsInfo: OptionsInfo | (() => Promise)): Promise { + if (typeof optionsInfo !== 'object') { + optionsInfo = await optionsInfo(); + } + this.component().loading = true; + this._optionsDivContainer.clearItems(); + let options: (string[] | azdata.CategoryValue[]) = optionsInfo.values!; + let defaultValue: string = optionsInfo.defaultValue!; + try { + options.forEach((op: string | azdata.CategoryValue) => { + const option: azdata.CategoryValue = (typeof op === 'string') + ? { name: op, displayName: op } + : op as azdata.CategoryValue; + const radioOption = this._view!.modelBuilder.radioButton().withProperties({ + label: option.displayName, + checked: option.displayName === defaultValue, + name: option.name, + }).component(); + if (radioOption.checked) { + this._currentRadioOption = radioOption; + this._onValueChangedEmitter.fire(); + } + this._onNewDisposableCreated(radioOption.onDidClick(() => { + this._optionsDivContainer.items + .filter(otherOption => otherOption !== radioOption) + .forEach(otherOption => (otherOption as azdata.RadioButtonComponent).checked = false); + this._currentRadioOption = radioOption; + this._onValueChangedEmitter.fire(); + })); + this._optionsDivContainer.addItem(radioOption); + }); + } + catch (e) { + const errorLoadingRadioOptionsLabel = this._view!.modelBuilder.text().withProperties({ value: getErrorMessage(e), CSSStyles: { 'color': 'Red' } }).component(); + this._optionsDivContainer.addItem(errorLoadingRadioOptionsLabel); + } + this.component().loading = false; + } + + get value(): string | undefined { + return this._currentRadioOption?.label; + } + + get checked(): azdata.RadioButtonComponent { + return this._currentRadioOption; + } + + get onValueChanged(): vscode.Event { + return this._onValueChangedEmitter.event; + } +} diff --git a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts index 6efaabaa83..4bef800891 100644 --- a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts +++ b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts @@ -8,7 +8,7 @@ import * as nls from 'vscode-nls'; import { AgreementInfo, DeploymentProvider, ITool, ResourceType, ToolStatus } from '../interfaces'; import { IResourceTypeService } from '../services/resourceTypeService'; import { IToolsService } from '../services/toolsService'; -import { getErrorMessage, setEnvironmentVariablesForInstallPaths } from '../utils'; +import { getErrorMessage } from '../utils'; import { DialogBase } from './dialogBase'; import { createFlexContainer } from './modelViewUtils'; @@ -337,7 +337,7 @@ export class ResourceTypePickerDialog extends DialogBase { } protected onComplete(): void { - setEnvironmentVariablesForInstallPaths(this._tools); + this.toolsService.toolsForCurrentProvider = this._tools; this.resourceTypeService.startDeployment(this.getCurrentProvider()); } diff --git a/extensions/resource-deployment/src/ui/wizardBase.ts b/extensions/resource-deployment/src/ui/wizardBase.ts index a963e5d34b..7282f9787e 100644 --- a/extensions/resource-deployment/src/ui/wizardBase.ts +++ b/extensions/resource-deployment/src/ui/wizardBase.ts @@ -10,9 +10,9 @@ import { WizardPageBase } from './wizardPageBase'; import { Model } from './model'; const localize = nls.loadMessageBundle(); -export abstract class WizardBase { +export abstract class WizardBase, M extends Model> { private customButtons: azdata.window.Button[] = []; - private pages: WizardPageBase[] = []; + public pages: P[] = []; public wizardObject: azdata.window.Wizard; public toDispose: vscode.Disposable[] = []; @@ -34,8 +34,8 @@ export abstract class WizardBase { newPage.onEnter(); })); - this.toDispose.push(this.wizardObject.doneButton.onClick(() => { - this.onOk(); + this.toDispose.push(this.wizardObject.doneButton.onClick(async () => { + await this.onOk(); this.dispose(); })); this.toDispose.push(this.wizardObject.cancelButton.onClick(() => { @@ -52,14 +52,14 @@ export abstract class WizardBase { } protected abstract initialize(): void; - protected abstract onOk(): void; + protected abstract async onOk(): Promise; protected abstract onCancel(): void; public addButton(button: azdata.window.Button) { this.customButtons.push(button); } - protected setPages(pages: WizardPageBase[]) { + protected setPages(pages: P[]) { this.wizardObject!.pages = pages.map(p => p.pageObject); this.pages = pages; this.pages.forEach((page) => { diff --git a/extensions/resource-deployment/src/utils.ts b/extensions/resource-deployment/src/utils.ts index 216e23165e..61e1e31e24 100644 --- a/extensions/resource-deployment/src/utils.ts +++ b/extensions/resource-deployment/src/utils.ts @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITool, NoteBookEnvironmentVariablePrefix } from './interfaces'; import * as path from 'path'; import { ToolsInstallPath } from './constants'; +import { ITool, NoteBookEnvironmentVariablePrefix } from './interfaces'; export function getErrorMessage(error: any): string { return (error instanceof Error) @@ -21,7 +21,7 @@ export function getRuntimeBinaryPathEnvironmentVariableName(toolName: string): s return `${NoteBookEnvironmentVariablePrefix}${toolName.toUpperCase().replace(/ |-/g, '_')}`; } -export function setEnvironmentVariablesForInstallPaths(tools: ITool[]): void { +export function setEnvironmentVariablesForInstallPaths(tools: ITool[], env: NodeJS.ProcessEnv = process.env): void { // Use Set class to make sure the collection only contains unique values. let installationPaths: Set = new Set(); tools.forEach(t => { @@ -30,12 +30,18 @@ export function setEnvironmentVariablesForInstallPaths(tools: ITool[]): void { // construct an env variable name with NoteBookEnvironmentVariablePrefix prefix // and tool.name as suffix, making sure of using all uppercase characters and only _ as separator const envVarName = getRuntimeBinaryPathEnvironmentVariableName(t.name); - process.env[envVarName] = t.installationPathOrAdditionalInformation; + env[envVarName] = t.installationPathOrAdditionalInformation; installationPaths.add(path.dirname(t.installationPathOrAdditionalInformation)); } }); if (installationPaths.size > 0) { const envVarToolsInstallationPath: string = [...installationPaths.values()].join(path.delimiter); - process.env[ToolsInstallPath] = envVarToolsInstallationPath; + env[ToolsInstallPath] = envVarToolsInstallationPath; + } +} + +export function assert(condition: boolean, message?: string): asserts condition { + if (!condition) { + throw new Error(message); } } diff --git a/samples/sample-resource-deployment/package.json b/samples/sample-resource-deployment/package.json index 91e5d2c3b2..4a5097d742 100644 --- a/samples/sample-resource-deployment/package.json +++ b/samples/sample-resource-deployment/package.json @@ -10,7 +10,7 @@ "aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e", "engines": { "vscode": "*", - "azdata": ">1.10.0" + "azdata": ">=1.19.0" }, "repository": { "type": "git", @@ -23,6 +23,257 @@ ], "contributes": { "resourceDeploymentTypes": [ + { + "name": "test-wizard", + "displayName": "%resource.type.wizard.display.name%", + "description": "%resource.type.wizard.description%", + "platforms": "*", + "icon": { + "light": "./images/book.svg", + "dark": "./images/book_inverse.svg" + }, + "providers": [ + { + "notebookWizard": { + "notebook": "%deployment-notebook-1%", + "type": "new-arc-control-plane", + "runNotebook": false, + "codeCellInsertionPosition": 1, + "actionText": "%deploy.wizard.action%", + "title": "%wizard.new.wizard.title%", + "name": "wizard.new.wizard", + "labelPosition": "left", + "generateSummaryPage": false, + "pages": [ + { + "title": "%wizard.select.cluster.title%", + "sections": [ + { + "fields": [ + { + "type": "kube_cluster_context_picker", + "label": "%wizard.kube.cluster.context%", + "required": true, + "inputWidth": "350px", + "variableName": "AZDATA_NB_VAR_CLUSTER_CONTEXT", + "configFileVariableName": "AZDATA_NB_VAR_CONFIG_FILE" + } + ] + } + ] + }, + { + "title": "%wizard.cluster.config.profile.title%", + "sections": [ + { + "fields": [ + { + "type": "readonly_text", + "label": "%wizard.project.details.description%", + "labelWidth": "600px" + }, + { + "type": "options", + "label": "%wizard.cluster.config.profile%", + "required": true, + "variableName": "AZDATA_NB_VAR_PROFILE", + "editable": false, + "options": { + "values":[ + "aks-dev-test", + "aks-dev-test-ha", + "aks-dev-test", + "aks-private-preview", + "kubeadm-dev-test", + "kubeadm-private-preview" + ], + "defaultValue": "aks-dev-test", + "optionsType": "radio" + } + }, + { + "label": "%wizard.dropdown.options.field%", + "variableName": "AZDATA_NB_VAR_DROPDOWN_OPTIONS", + "type": "options", + "options": { + "values": ["1","2","3"], + "defaultValue": "2", + "optionsType": "dropdown" + } + } + ] + } + ] + }, + { + "title": "%wizard.data.controller.create.summary.title%", + "isSummaryPage": true, + "fieldHeight": "16px", + "sections": [ + { + "title": "", + "collapsible": false, + "fieldWidth": "200px", + "fieldHeight": "12px", + "spaceBetweenFields": 0, + "rows": [ + { + "items": [ + { + "items": [ + { + "label": "%wizard.summary.data.controller%", + "type": "readonly_text", + "enabled": true, + "labelWidth": "185px" + } + ] + }, + { + "items": [ + { + "label": "%wizard.summary.estimated.cost.per.month%", + "type": "readonly_text", + "enabled": true, + "labelWidth": "190px", + "labelCSSStyles": { + "fontWeight": "Bold" + } + + } + ] + } + ] + }, + { + "items": [ + { + "items": [ + { + "label": "%wizard.summary.by.contoso%", + "type": "readonly_text", + "labelWidth": "185px" + } + ] + }, + { + "items": [ + { + "label": "%wizard.summary.free%", + "type": "readonly_text", + "enabled": true, + "defaultValue": "", + "labelWidth": "100px" + } + ] + } + ] + }, + { + "items": [ + { + "items": [ + { + "label": "{0}", + "type": "readonly_text", + "enabled": true, + "labelCSSStyles": { "color": "#0078D4" }, + "labelWidth": "67px", + "links": [ + { + "text": "%wizard.summary.terms.of.use%", + "url": "https://aka.ms/eula-azdata-en" + } + ] + }, + { + "label": "|", + "type": "readonly_text", + "enabled": true, + "defaultValue": "", + "labelWidth": "4px", + "fieldWidth": "6px" + }, + { + "label": "{0}", + "type": "readonly_text", + "enabled": true, + "labelCSSStyles": { "color": "#0078D4" }, + "labelWidth": "102px", + "links": [ + { + "text": "%wizard.summary.terms.privacy.policy%", + "url": "https://go.contoso.com/fwlink/?LinkId=853010" + } + ] + } + ] + } + ] + } + ] + }, + { + "title": "%wizard.summary.terms%", + "fieldHeight": "88px", + "fields":[ + { + "label": "%wizard.summary.terms.description%", + "type": "readonly_text", + "enabled": true, + "labelWidth": "750px" + } + ] + }, + { + "title": "%wizard.summary.kubernetes%", + "fields":[ + { + "label": "%wizard.summary.kube.config.file.path%", + "type": "readonly_text", + "isEvaluated": true, + "defaultValue": "$(AZDATA_NB_VAR_CONFIG_FILE)" + }, + { + "label": "%wizard.summary.cluster.context%", + "type": "readonly_text", + "isEvaluated": true, + "defaultValue": "$(AZDATA_NB_VAR_CLUSTER_CONTEXT)" + }, + { + "label": "%wizard.summary.profile%", + "type": "readonly_text", + "isEvaluated": true, + "defaultValue": "$(AZDATA_NB_VAR_PROFILE)" + } + ] + } + ] + } + ] + }, + "requiredTools": [ + { + "name": "kubectl" + } + ], + "when": true + } + ], + "agreement": { + "template": "%wizard.data.controller.agreement%", + "links": [ + { + "text": "%contoso.agreement.privacy.statement%", + "url": "https://go.contoso.com/fwlink/?LinkId=853010" + }, + { + "text": "%wizard.agreement.contosoCmd.eula%", + "url": "https://aka.ms/eula-contosoCmd-en" + } + ] + } + }, { "name": "x-data-service", "displayName": "%resource-type-display-name%", @@ -81,6 +332,14 @@ "defaultValue": "", "required": true }, + { + "type": "kube_cluster_context_picker", + "label": "%kube.cluster.context%", + "required": true, + "inputWidth": "350px", + "variableName": "AZDATA_NB_VAR_CLUSTER_CONTEXT", + "configFileVariableName": "AZDATA_NB_VAR_CONFIG_FILE" + }, { "label": "%number-field%", "variableName": "AZDATA_NB_VAR_NUMBER", @@ -159,7 +418,7 @@ "links": [ { "text": "%agreement-1-name%", - "url": "https://www.microsoft.com" + "url": "https://www.contoso.com" }, { "text": "%agreement-2-name%", diff --git a/samples/sample-resource-deployment/package.nls.json b/samples/sample-resource-deployment/package.nls.json index fce5465b73..126d4ab84e 100644 --- a/samples/sample-resource-deployment/package.nls.json +++ b/samples/sample-resource-deployment/package.nls.json @@ -12,9 +12,41 @@ "deployment-notebook-2": "./notebooks/deploy-x-data-service-2.ipynb", "text-field": "text field", "password-field": "password field", + "kube.cluster.context": "Kube cluster context", "number-field": "numeric field", "confirm-password": "confirm password", "agreement": "I accept {0} and {1}.", "agreement-1-name": "Agreement 1", - "agreement-2-name": "Agreement 2" + "agreement-2-name": "Agreement 2", + + "resource.type.wizard.display.name": "Test controller", + "resource.type.wizard.description": "Creates a Test controller", + + "wizard.new.wizard.title": "Create Test controller", + "wizard.cluster.environment.title": "What is your target existing Kubernetes cluster environment?", + "wizard.select.cluster.title": "Select from installed existing Kubernetes clusters", + "wizard.kube.cluster.context": "Cluster context", + "wizard.cluster.config.profile.title": "Choose the config profile", + "wizard.cluster.config.profile": "Config profile", + "wizard.dropdown.options.field": "dropdown field", + "wizard.project.details.title": "Project details", + "wizard.project.details.description": "Project details for Contoso corporation", + "wizard.data.controller.create.summary.title": "Review your configuration", + "wizard.summary.data.controller": "Test controller", + "wizard.summary.estimated.cost.per.month": "Estimated cost per month", + "wizard.summary.by.contoso" : "by Contoso", + "wizard.summary.free" : "Free", + "wizard.summary.terms.of.use" : "Terms of use", + "wizard.summary.terms.privacy.policy" : "Privacy policy", + "wizard.summary.terms" : "Terms", + "wizard.summary.terms.description": "By clicking 'Script to notebook', I (a) agree to the legal terms and privacy statement(s) associated with the doing business with Contoso.", + "wizard.summary.kubernetes": "Kubernetes", + "wizard.summary.kube.config.file.path": "Kube config file path", + "wizard.summary.cluster.context": "Cluster context", + "wizard.summary.profile": "Config profile", + "wizard.data.controller.agreement": "I accept {0} and {1}.", + "contoso.agreement.privacy.statement":"contoso Privacy Statement", + "wizard.agreement.contosoCmd.eula":"contoso cmd license terms", + "deploy.wizard.action":"Script to notebook" + }