diff --git a/extensions/arc/notebooks/arcDeployment/deploy.arc.data.controller.ipynb b/extensions/arc/notebooks/arcDeployment/deploy.arc.data.controller.ipynb index e61198cd277b..a87aac91238d 100644 --- a/extensions/arc/notebooks/arcDeployment/deploy.arc.data.controller.ipynb +++ b/extensions/arc/notebooks/arcDeployment/deploy.arc.data.controller.ipynb @@ -56,7 +56,7 @@ { "cell_type": "markdown", "source": [ - "### **Setup and Check Prerequisites**" + "### **Setup**" ], "metadata": { "azdata_cell_guid": "e3dd8e75-e15f-44b4-81fc-1f54d6f0b1e2" diff --git a/extensions/arc/package.json b/extensions/arc/package.json index 6404080f931d..82816b83847d 100644 --- a/extensions/arc/package.json +++ b/extensions/arc/package.json @@ -144,9 +144,13 @@ "notebookWizard": { "notebook": "./notebooks/arcDeployment/deploy.arc.data.controller.ipynb", "type": "new-arc-control-plane", - "runNotebook": false, + "doneAction": { + "label": "%deploy.done.action%" + }, + "scriptAction": { + "label": "%deploy.script.action%" + }, "codeCellInsertionPosition": 5, - "actionText": "%deploy.script.to.notebook.action%", "title": "%arc.data.controller.new.wizard.title%", "name": "arc.data.controller.new.wizard", "labelPosition": "left", @@ -521,9 +525,13 @@ { "notebookWizard": { "notebook": "./notebooks/arcDeployment/deploy.sql.existing.arc.ipynb", - "runNotebook": false, + "doneAction": { + "label": "%deploy.done.action%" + }, + "scriptAction": { + "label": "%deploy.script.action%" + }, "codeCellInsertionPosition": 5, - "actionText": "%deploy.script.to.notebook.action%", "title": "%arc.sql.wizard.title%", "name": "arc.sql.wizard", "labelPosition": "left", @@ -682,9 +690,13 @@ { "notebookWizard": { "notebook": "./notebooks/arcDeployment/deploy.postgres.existing.arc.ipynb", - "runNotebook": false, + "doneAction": { + "label": "%deploy.done.action%" + }, + "scriptAction": { + "label": "%deploy.script.action%" + }, "codeCellInsertionPosition": 5, - "actionText": "%deploy.script.to.notebook.action%", "title": "%arc.postgres.wizard.title%", "name": "arc.postgres.wizard", "labelPosition": "left", diff --git a/extensions/arc/package.nls.json b/extensions/arc/package.nls.json index 56c02801664b..7cb1a9b52724 100644 --- a/extensions/arc/package.nls.json +++ b/extensions/arc/package.nls.json @@ -58,8 +58,8 @@ "arc.data.controller.summary.location": "Location", "arc.data.controller.arc.data.controller.agreement": "I accept {0} and {1}.", "microsoft.agreement.privacy.statement":"Microsoft Privacy Statement", - "deploy.script.to.notebook.action":"Script to notebook", - + "deploy.script.action":"Script to notebook", + "deploy.done.action":"Deploy", "resource.type.arc.sql.display.name": "Azure SQL managed instance - Azure Arc (preview)", "resource.type.arc.postgres.display.name": "PostgreSQL Hyperscale server groups - Azure Arc (preview)", diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index 8c1e4501c4cf..56a58acbf879 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -114,21 +114,53 @@ export interface BdcWizardInfo { notebook: string | NotebookPathInfo; type: BdcDeploymentType; } +/** + * An object that configures Script and Done buttons of the wizard. + */ +export interface WizardAction { + label?: string +} +/** + * This object defines the shape, form and behavior of a Notebook Wizard. + */ export interface NotebookWizardInfo extends WizardInfoBase { + /** + * path to the template python notebook that is modified with variables collected in the wizard. A copy of this modified notebook is executed at the end of the wizard either from commonadline of from notebook editor in ADS. + */ notebook: string | NotebookPathInfo; - runNotebook?: boolean; + /** + * 0 based position number where the variables values are inserted into the notebook as python statements. + */ codeCellInsertionPosition?: number; + /** + * This array defines the json for the pages of this wizard. + */ pages: NotebookWizardPageInfo[] } export interface WizardInfoBase extends FieldInfoBase { - taskName?: string; type?: DeploymentType; - actionText?: string; + /** + * done button attributes. + */ + doneAction: WizardAction; + /** + * script button attributes. + */ + scriptAction?: WizardAction; + /** + * title displayed on every page of the wizard + */ title: string; name?: string; + /** + * This array defines the json for the pages of this wizard. + */ pages: PageInfoBase[]; + /** + * if true an auto generated summary page is inserted at the end of the wizard + */ isSummaryPageAutoGenerated?: boolean } @@ -401,11 +433,7 @@ export const enum BdcDeploymentType { ExistingOpenShift = 'existing-openshift' } -export const enum ArcDeploymentType { - NewControlPlane = 'new-control-plane' -} - -export type DeploymentType = ArcDeploymentType | BdcDeploymentType; +export type DeploymentType = BdcDeploymentType; export interface Command { command: string; diff --git a/extensions/resource-deployment/src/localizedConstants.ts b/extensions/resource-deployment/src/localizedConstants.ts index af6d852a0e8f..33bf67a1da54 100644 --- a/extensions/resource-deployment/src/localizedConstants.ts +++ b/extensions/resource-deployment/src/localizedConstants.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vscode-nls'; -import { FieldType, OptionsType } from './interfaces'; import { OptionsSourceType } from './helpers/optionSources'; +import { FieldType, OptionsType } from './interfaces'; const localize = nls.loadMessageBundle(); @@ -37,3 +37,5 @@ export const optionsTypeRadioOrDropdown = localize('optionsTypeRadioOrDropdown', export const azdataEulaNotAccepted = localize('azdataEulaNotAccepted', "Deployment cannot continue. Azure Data CLI license terms have not yet been accepted. Please accept the EULA to enable the features that requires Azure Data CLI."); export const azdataEulaDeclined = localize('azdataEulaDeclined', "Deployment cannot continue. Azure Data CLI license terms were declined.You can either Accept EULA to continue or Cancel this operation"); export const acceptEulaAndSelect = localize('deploymentDialog.RecheckEulaButton', "Accept EULA & Select"); +export const scriptToNotebook = localize('ui.ScriptToNotebookButton', "Script"); +export const deployNotebook = localize('ui.DeployButton', "Run"); diff --git a/extensions/resource-deployment/src/services/notebookService.ts b/extensions/resource-deployment/src/services/notebookService.ts index 9d32ceac2e65..3e26069671f9 100644 --- a/extensions/resource-deployment/src/services/notebookService.ts +++ b/extensions/resource-deployment/src/services/notebookService.ts @@ -33,9 +33,9 @@ export interface NotebookExecutionResult { } export interface INotebookService { - launchNotebook(notebook: string | NotebookPathInfo): Promise; - launchNotebookWithEdits(notebook: string | NotebookPathInfo, cellStatements: string[], insertionPosition?: number): Promise; - launchNotebookWithContent(title: string, content: string): Promise; + openNotebook(notebook: string | NotebookPathInfo): Promise; + openNotebookWithEdits(notebook: string | NotebookPathInfo, cellStatements: string[], insertionPosition?: number): Promise; + openNotebookWithContent(title: string, content: string): Promise; getNotebook(notebook: string | NotebookPathInfo): Promise; getNotebookPath(notebook: string | NotebookPathInfo): string; executeNotebook(notebook: any, env?: NodeJS.ProcessEnv): Promise; @@ -47,38 +47,39 @@ export class NotebookService implements INotebookService { constructor(private platformService: IPlatformService, private extensionPath: string) { } /** - * Launch notebook with file path + * Open notebook with file path * @param notebook the path of the notebook */ - async launchNotebook(notebook: string | NotebookPathInfo): Promise { + async openNotebook(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 insertionPosition} and then opens the edited notebook. * - * @param notebook - the path to notebook that needs to be launched + * @param notebook - the path to notebook that needs to be opened * @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); + async openNotebookWithEdits(notebook: string, cellStatements: string[], insertionPosition: number = 0): Promise { + const openedNotebook = await this.openNotebook(notebook); await openedNotebook.edit((editBuilder: azdata.nb.NotebookEditorEdit) => { editBuilder.insertCell({ cell_type: 'code', source: cellStatements }, insertionPosition); }); + return openedNotebook; } /** - * Launch notebook with file path + * Open notebook with given contents * @param title the title of the notebook * @param content the notebook content */ - async launchNotebookWithContent(title: string, content: string): Promise { + async openNotebookWithContent(title: string, content: string): Promise { const uri: vscode.Uri = vscode.Uri.parse(`untitled:${this.findNextUntitledEditorName(title)}`); return await azdata.nb.showNotebookDocument(uri, { connectionProfile: undefined, @@ -150,11 +151,11 @@ export class NotebookService implements INotebookService { platformService.logToOutputChannel(taskFailedMessage); if (selectedOption === viewErrorDetail) { try { - await this.launchNotebookWithContent(`${tempNotebookPrefix}-${getDateTimeString()}`, result.outputNotebook); + await this.openNotebookWithContent(`${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); - vscode.window.showErrorMessage(launchNotebookError); + const openNotebookError = localize('resourceDeployment.FailedToOpenNotebook', "An error occurred opening the output notebook. {1}{2}.", EOL, getErrorMessage(error)); + platformService.logToOutputChannel(openNotebookError); + vscode.window.showErrorMessage(openNotebookError); } } } else { diff --git a/extensions/resource-deployment/src/services/resourceTypeService.ts b/extensions/resource-deployment/src/services/resourceTypeService.ts index f6be8dca6d36..0230f9a1aeac 100644 --- a/extensions/resource-deployment/src/services/resourceTypeService.ts +++ b/extensions/resource-deployment/src/services/resourceTypeService.ts @@ -10,17 +10,17 @@ import * as os from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import { DeploymentProvider, instanceOfAzureSQLVMDeploymentProvider, instanceOfCommandDeploymentProvider, instanceOfDialogDeploymentProvider, instanceOfDownloadDeploymentProvider, instanceOfNotebookBasedDialogInfo, instanceOfNotebookDeploymentProvider, instanceOfNotebookWizardDeploymentProvider, instanceOfWebPageDeploymentProvider, instanceOfWizardDeploymentProvider, NotebookInfo, NotebookPathInfo, ResourceType, ResourceTypeOption } from '../interfaces'; +import { DeployAzureSQLVMWizard } from '../ui/deployAzureSQLVMWizard/deployAzureSQLVMWizard'; +import { DeployClusterWizard } from '../ui/deployClusterWizard/deployClusterWizard'; +import { DeploymentInputDialog } from '../ui/deploymentInputDialog'; +import { NotebookWizard } from '../ui/notebookWizard/notebookWizard'; +import { AzdataService } from './azdataService'; +import { KubeService } from './kubeService'; import { INotebookService } from './notebookService'; import { IPlatformService } from './platformService'; import { IToolsService } from './toolsService'; -import { ResourceType, ResourceTypeOption, NotebookPathInfo, DeploymentProvider, instanceOfWizardDeploymentProvider, instanceOfDialogDeploymentProvider, instanceOfNotebookDeploymentProvider, instanceOfDownloadDeploymentProvider, instanceOfWebPageDeploymentProvider, instanceOfCommandDeploymentProvider, instanceOfNotebookBasedDialogInfo, instanceOfNotebookWizardDeploymentProvider, NotebookInfo, instanceOfAzureSQLVMDeploymentProvider } from '../interfaces'; -import { DeployClusterWizard } from '../ui/deployClusterWizard/deployClusterWizard'; -import { DeploymentInputDialog } from '../ui/deploymentInputDialog'; -import { KubeService } from './kubeService'; -import { AzdataService } from './azdataService'; -import { NotebookWizard } from '../ui/notebookWizard/notebookWizard'; -import { DeployAzureSQLVMWizard } from '../ui/deployAzureSQLVMWizard/deployAzureSQLVMWizard'; const localize = nls.loadMessageBundle(); export interface IResourceTypeService { @@ -257,7 +257,7 @@ export class ResourceTypeService implements IResourceTypeService { const dialog = new DeploymentInputDialog(this.notebookService, this.platformService, this.toolsService, provider.dialog); dialog.open(); } else if (instanceOfNotebookDeploymentProvider(provider)) { - this.notebookService.launchNotebook(provider.notebook); + this.notebookService.openNotebook(provider.notebook); } else if (instanceOfDownloadDeploymentProvider(provider)) { const taskName = localize('resourceDeployment.DownloadAndLaunchTaskName', "Download and launch installer, URL: {0}", provider.downloadUrl); azdata.tasks.startBackgroundOperation({ diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/deployAzureSQLVMWizard.ts b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/deployAzureSQLVMWizard.ts index ba8db2433620..af25019a5019 100644 --- a/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/deployAzureSQLVMWizard.ts +++ b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/deployAzureSQLVMWizard.ts @@ -73,7 +73,7 @@ export class DeployAzureSQLVMWizard extends WizardBase nb.type === model.getStringValue(NotebookTypeVariableName))?.path : this.dialogInfo.notebook; - this.notebookService.launchNotebook(notebook!).catch(error => { + this.notebookService.openNotebook(notebook!).catch(error => { vscode.window.showErrorMessage(error); }); } diff --git a/extensions/resource-deployment/src/ui/notebookWizard/notebookWizard.ts b/extensions/resource-deployment/src/ui/notebookWizard/notebookWizard.ts index e3b7800ba6ad..5f096dde9bae 100644 --- a/extensions/resource-deployment/src/ui/notebookWizard/notebookWizard.ts +++ b/extensions/resource-deployment/src/ui/notebookWizard/notebookWizard.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as nls from 'vscode-nls'; +import * as loc from '../../localizedConstants'; import { INotebookService, Notebook } from '../../services/notebookService'; import { IToolsService } from '../../services/toolsService'; import { Model } from '../model'; @@ -14,8 +14,6 @@ import { IPlatformService } from './../../services/platformService'; import { NotebookWizardAutoSummaryPage } from './notebookWizardAutoSummaryPage'; import { NotebookWizardPage } from './notebookWizardPage'; -const localize = nls.loadMessageBundle(); - export class NotebookWizard extends WizardBase { private _inputComponents: InputComponents = {}; @@ -40,7 +38,8 @@ export class NotebookWizard extends WizardBase { + try { + const notebook = await this.prepareNotebookAndEnvironment(); + await this.openNotebook(notebook); + } catch (error) { + vscode.window.showErrorMessage(error); + } + } protected async onOk(): Promise { + try { + const notebook = await this.prepareNotebookAndEnvironment(); + const openedNotebook = await this.openNotebook(notebook); + openedNotebook.runAllCells(); + } catch (error) { + vscode.window.showErrorMessage(error); + } + } + + private async openNotebook(notebook: Notebook) { + const notebookPath = this.notebookService.getNotebookPath(this.wizardInfo.notebook); + return await this.notebookService.openNotebookWithContent(notebookPath, JSON.stringify(notebook, undefined, 4)); + } + + private async prepareNotebookAndEnvironment() { await setModelValues(this.inputComponents, this.model); - const env: NodeJS.ProcessEnv = {}; + const env: NodeJS.ProcessEnv = process.env; this.model.setEnvironmentVariables(env, (varName) => { const isPassword = !!this.inputComponents[varName]?.isPassword; return isPassword; @@ -85,17 +104,7 @@ export class NotebookWizard extends WizardBase { ); } + /** + * If the return value is true then done button should be visible to the user + */ + private get isDoneButtonVisible(): boolean { + return !!this.wizard.wizardInfo.doneAction; + } + + /** + * If the return value is true then generateScript button should be visible to the user + */ + private get isGenerateScriptButtonVisible(): boolean { + return !!this.wizard.wizardInfo.scriptAction; + } + public initialize(): void { initializeWizardPage({ container: this.wizard.wizardObject, @@ -64,7 +79,17 @@ export class NotebookWizardPage extends WizardPageBase { }); } - public async onEnter(): Promise { + public async onEnter(pageInfo: WizardPageInfo): Promise { + if (pageInfo.isLastPage) { + // on the last page either one or both of done button and generateScript button are visible depending on configuration of 'runNotebook' in wizard info + this.wizard.wizardObject.doneButton.hidden = !this.isDoneButtonVisible; + this.wizard.wizardObject.generateScriptButton.hidden = !this.isGenerateScriptButtonVisible; + } else { + //on any page but the last page doneButton and generateScriptButton are hidden + this.wizard.wizardObject.doneButton.hidden = true; + this.wizard.wizardObject.generateScriptButton.hidden = true; + } + if (this.pageInfo.isSummaryPage) { await setModelValues(this.wizard.inputComponents, this.wizard.model); } diff --git a/extensions/resource-deployment/src/ui/wizardBase.ts b/extensions/resource-deployment/src/ui/wizardBase.ts index 4c64907a5a96..c4eeb67016aa 100644 --- a/extensions/resource-deployment/src/ui/wizardBase.ts +++ b/extensions/resource-deployment/src/ui/wizardBase.ts @@ -6,9 +6,10 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { WizardPageBase } from './wizardPageBase'; -import { Model } from './model'; import { IToolsService } from '../services/toolsService'; +import { Model } from './model'; +import { WizardPageBase } from './wizardPageBase'; +import { WizardPageInfo } from './wizardPageInfo'; const localize = nls.loadMessageBundle(); export abstract class WizardBase, M extends Model> { @@ -21,37 +22,55 @@ export abstract class WizardBase, M extends Model return this._model; } - constructor(private title: string, name: string, private _model: M, public toolsService: IToolsService) { + protected get useGenerateScriptButton(): boolean { + return this._useGenerateScriptButton; + } + + constructor(private title: string, name: string, private _model: M, public toolsService: IToolsService, private _useGenerateScriptButton: boolean = false) { this.wizardObject = azdata.window.createWizard(title, name); } public async open(): Promise { this.initialize(); + this.wizardObject.generateScriptButton.hidden = true; // by default generateScriptButton stays hidden. this.wizardObject.customButtons = this.customButtons; this.toDispose.push(this.wizardObject.onPageChanged(async (e) => { let previousPage = this.pages[e.lastPage]; let newPage = this.pages[e.newPage]; - await previousPage.onLeave(); - await newPage.onEnter(); + //if we are changing to the first page from no page before, essentially when we load the wizard for the first time, e.lastPage is -1 and previousPage is undefined. + await previousPage?.onLeave(new WizardPageInfo(e.lastPage, this.pages.length)); + if (this.useGenerateScriptButton) { + if (newPage === this.pages.slice(-1)[0]) { + // if newPage is the last page + this.wizardObject.generateScriptButton.hidden = false; //un-hide generateScriptButton on last page + } else { + // if newPage is not the last page + this.wizardObject.generateScriptButton.hidden = true; //hide generateScriptButton if it is not the last page + } + } + await newPage.onEnter(new WizardPageInfo(e.newPage, this.pages.length)); })); this.toDispose.push(this.wizardObject.doneButton.onClick(async () => { await this.onOk(); this.dispose(); })); + this.toDispose.push(this.wizardObject.generateScriptButton.onClick(async () => { + await this.onGenerateScript(); + this.dispose(); + this.wizardObject.close(); // close the wizard. This is already hooked up into doneButton, so it is not needed for that button above. + })); this.toDispose.push(this.wizardObject.cancelButton.onClick(() => { this.onCancel(); this.dispose(); })); await this.wizardObject.open(); - if (this.pages && this.pages.length > 0) { - await this.pages[0].onEnter(); - } } protected abstract initialize(): void; protected abstract async onOk(): Promise; + protected async onGenerateScript(): Promise { } protected abstract onCancel(): void; public addButton(button: azdata.window.Button) { @@ -62,11 +81,15 @@ export abstract class WizardBase, M extends Model this.wizardObject!.pages = pages.map(p => p.pageObject); this.pages = pages; this.pages.forEach((page) => { + page.pageObject.onValidityChanged((isValid: boolean) => { + // generateScriptButton is enabled only when the page is valid. + this.wizardObject.generateScriptButton.enabled = isValid; + }); page.initialize(); }); } - private dispose() { + protected dispose() { let errorOccurred = false; this.toDispose.forEach((disposable: vscode.Disposable) => { try { diff --git a/extensions/resource-deployment/src/ui/wizardPageBase.ts b/extensions/resource-deployment/src/ui/wizardPageBase.ts index 2a317b78dc4a..d959ab2b8405 100644 --- a/extensions/resource-deployment/src/ui/wizardPageBase.ts +++ b/extensions/resource-deployment/src/ui/wizardPageBase.ts @@ -5,8 +5,10 @@ import * as azdata from 'azdata'; import { Validator } from './modelViewUtils'; +import { WizardPageInfo } from './wizardPageInfo'; export abstract class WizardPageBase { + private _page: azdata.window.WizardPage; private _validators: Validator[] = []; @@ -23,9 +25,9 @@ export abstract class WizardPageBase { return this._wizard; } - public async onEnter(): Promise { } + public async onEnter(_pageInfo?: WizardPageInfo): Promise { } - public async onLeave(): Promise { } + public async onLeave(_pageInfo?: WizardPageInfo): Promise { } public abstract initialize(): void; diff --git a/extensions/resource-deployment/src/ui/wizardPageInfo.ts b/extensions/resource-deployment/src/ui/wizardPageInfo.ts new file mode 100644 index 000000000000..f0992091e9e6 --- /dev/null +++ b/extensions/resource-deployment/src/ui/wizardPageInfo.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export class WizardPageInfo { + public get pageCount(): number { + return this._pageCount; + } + + public get currentPageId(): number { + return this._currentPageId; + } + + public get isFirstPage(): boolean { + return this._currentPageId === 0; + } + + public get isLastPage(): boolean { + return this._currentPageId === this._pageCount - 1; + } + + constructor(private _currentPageId: number, private _pageCount: number) { + } + +}