diff --git a/extensions/integration-tests/src/notebook.test.ts b/extensions/integration-tests/src/notebook.test.ts index 55d365f232..371407caa5 100644 --- a/extensions/integration-tests/src/notebook.test.ts +++ b/extensions/integration-tests/src/notebook.test.ts @@ -10,7 +10,7 @@ import * as assert from 'assert'; import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { context } from './testContext'; -import { sqlNotebookContent, writeNotebookToFile, sqlKernelMetadata, getFileName, pySparkNotebookContent, pySpark3KernelMetadata, pythonKernelMetadata } from './notebook.util'; +import { sqlNotebookContent, writeNotebookToFile, sqlKernelMetadata, getFileName, pySparkNotebookContent, pySpark3KernelMetadata, pythonKernelMetadata, sqlNotebookMultipleCellsContent } from './notebook.util'; import { getBdcServer } from './testConfig'; import { connectToServer } from './utils'; import * as fs from 'fs'; @@ -50,6 +50,22 @@ if (context.RunTest) { assert(actualOutput2[0] === '1', `Expected result: 1, Actual: '${actualOutput2[0]}'`); }); + test('Sql NB multiple cells test', async function () { + let notebook = await openNotebook(sqlNotebookMultipleCellsContent, sqlKernelMetadata, this.test.title); + const expectedOutput0 = '(1 row affected)'; + for (let i = 0; i < 3; i++) { + let cellOutputs = notebook.document.cells[i].contents.outputs; + console.log('Got cell outputs'); + assert(cellOutputs.length === 3, `Expected length: 3, Actual: '${cellOutputs.length}'`); + let actualOutput0 = (cellOutputs[0]).data['text/html']; + console.log('Got first output'); + assert(actualOutput0 === expectedOutput0, `Expected row count: '${expectedOutput0}', Actual: '${actualOutput0}'`); + let actualOutput2 = (cellOutputs[2]).data['application/vnd.dataresource+json'].data[0]; + assert(actualOutput2[0] === i.toString(), `Expected result: ${i.toString()}, Actual: '${actualOutput2[0]}'`); + console.log('Sql multiple cells NB done'); + } + }); + test('Clear all outputs - SQL notebook ', async function () { let notebook = await openNotebook(sqlNotebookContent, sqlKernelMetadata, this.test.title); await verifyClearAllOutputs(notebook); @@ -81,7 +97,7 @@ if (context.RunTest) { }); } -async function openNotebook(content: azdata.nb.INotebookContents, kernelMetadata: any, testName: string): Promise { +async function openNotebook(content: azdata.nb.INotebookContents, kernelMetadata: any, testName: string, runAllCells?: boolean): Promise { let notebookConfig = vscode.workspace.getConfiguration('notebook'); notebookConfig.update('pythonPath', process.env.PYTHON_TEST_PATH, 1); let server = await getBdcServer(); @@ -91,12 +107,20 @@ async function openNotebook(content: azdata.nb.INotebookContents, kernelMetadata console.log(uri); let notebook = await azdata.nb.showNotebookDocument(uri); console.log('Notebook is opened'); - assert(notebook.document.cells.length === 1, 'Notebook should have 1 cell'); - console.log('Before run notebook cell'); - let ran = await notebook.runCell(notebook.document.cells[0]); - console.log('After run notebook cell'); - assert(ran, 'Notebook runCell should succeed'); - assert(notebook !== undefined && notebook !== null, 'Expected notebook object is defined'); + + if (!runAllCells) { + assert(notebook.document.cells.length === 1, 'Notebook should have 1 cell'); + console.log('Before run notebook cell'); + let ran = await notebook.runCell(notebook.document.cells[0]); + console.log('After run notebook cell'); + assert(ran, 'Notebook runCell should succeed'); + } else { + console.log('Before run all notebook cells'); + let ran = await notebook.runAllCells(); + assert(ran, 'Notebook runCell should succeed'); + assert(notebook !== undefined && notebook !== null, 'Expected notebook object is defined'); + } + return notebook; } async function verifyClearAllOutputs(notebook: azdata.nb.NotebookEditor) { diff --git a/extensions/integration-tests/src/notebook.util.ts b/extensions/integration-tests/src/notebook.util.ts index 024ed0c985..86d81a7254 100644 --- a/extensions/integration-tests/src/notebook.util.ts +++ b/extensions/integration-tests/src/notebook.util.ts @@ -35,6 +35,38 @@ export const pySparkNotebookContent: azdata.nb.INotebookContents = { nbformat_minor: 2 }; +export const pythonNotebookMultipleCellsContent: azdata.nb.INotebookContents = { + cells: [{ + cell_type: CellTypes.Code, + source: '1+1', + metadata: { language: 'python' }, + execution_count: 1 + }, { + cell_type: CellTypes.Code, + source: '1+2', + metadata: { language: 'python' }, + execution_count: 1 + }, { + cell_type: CellTypes.Code, + source: '1+3', + metadata: { language: 'python' }, + execution_count: 1 + }, { + cell_type: CellTypes.Code, + source: '1+4', + metadata: { language: 'python' }, + execution_count: 1 + }], + metadata: { + 'kernelspec': { + 'name': 'python3', + 'display_name': 'Python 3' + } + }, + nbformat: 4, + nbformat_minor: 2 +}; + export const sqlNotebookContent: azdata.nb.INotebookContents = { cells: [{ cell_type: CellTypes.Code, @@ -52,6 +84,33 @@ export const sqlNotebookContent: azdata.nb.INotebookContents = { nbformat_minor: 2 }; +export const sqlNotebookMultipleCellsContent: azdata.nb.INotebookContents = { + cells: [{ + cell_type: CellTypes.Code, + source: 'select 0', + metadata: { language: 'sql' }, + execution_count: 1 + }, { + cell_type: CellTypes.Code, + source: `WAITFOR DELAY '00:00:02'\nselect 1`, + metadata: { language: 'sql' }, + execution_count: 1 + }, { + cell_type: CellTypes.Code, + source: 'select 2', + metadata: { language: 'sql' }, + execution_count: 1 + }], + metadata: { + 'kernelspec': { + 'name': 'SQL', + 'display_name': 'SQL' + } + }, + nbformat: 4, + nbformat_minor: 2 +}; + export const pySpark3KernelMetadata = { 'kernelspec': { 'name': 'pyspark3kernel', diff --git a/scripts/setbackendvariables.sh b/scripts/setbackendvariables.sh old mode 100644 new mode 100755 diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 282f96fab7..6e9a77f848 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -4035,7 +4035,11 @@ declare module 'azdata' { runCell(cell?: NotebookCell): Thenable; /** - * Clears the outputs of all code cells in a Notebook + * Kicks off execution of all code cells. Thenable will resolve only when full execution of all cells is completed. + */ + runAllCells(): Thenable; + + /** Clears the outputs of all code cells in a Notebook * @return A promise that resolves with a value indicating if the outputs are cleared or not. */ clearAllOutputs(): Thenable; diff --git a/src/sql/parts/notebook/media/dark/run_cells_inverse.svg b/src/sql/parts/notebook/media/dark/run_cells_inverse.svg new file mode 100644 index 0000000000..d16dd9f08a --- /dev/null +++ b/src/sql/parts/notebook/media/dark/run_cells_inverse.svg @@ -0,0 +1 @@ +run_cells_inverse \ No newline at end of file diff --git a/src/sql/parts/notebook/media/light/run_cells.svg b/src/sql/parts/notebook/media/light/run_cells.svg new file mode 100644 index 0000000000..9c098066ff --- /dev/null +++ b/src/sql/parts/notebook/media/light/run_cells.svg @@ -0,0 +1 @@ +run_cells \ No newline at end of file diff --git a/src/sql/parts/notebook/notebook.component.ts b/src/sql/parts/notebook/notebook.component.ts index 3793db5e55..67448561ae 100644 --- a/src/sql/parts/notebook/notebook.component.ts +++ b/src/sql/parts/notebook/notebook.component.ts @@ -33,7 +33,7 @@ import * as notebookUtils from 'sql/parts/notebook/notebookUtils'; import { Deferred } from 'sql/base/common/promise'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar'; -import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction, ClearAllOutputsAction } from 'sql/parts/notebook/notebookActions'; +import { KernelsDropdown, AttachToDropdown, AddCellAction, TrustedAction, RunAllCellsAction, ClearAllOutputsAction } from 'sql/parts/notebook/notebookActions'; import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/common/objectExplorerService'; import * as TaskUtilities from 'sql/workbench/common/taskUtilities'; import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes'; @@ -63,6 +63,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe private _modelRegisteredDeferred = new Deferred(); private profile: IConnectionProfile; private _trustedAction: TrustedAction; + private _runAllCellsAction: RunAllCellsAction; private _providerRelatedActions: IAction[] = []; private _scrollTop: number; @@ -372,6 +373,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe let addTextCellButton = new AddCellAction('notebook.AddTextCell', localize('text', 'Text'), 'notebook-button icon-add'); addTextCellButton.cellType = CellTypes.Markdown; + this._runAllCellsAction = new RunAllCellsAction('notebook.runAllCells', localize('runAll', 'Run Cells'), 'notebook-button icon-run-cells'); let clearResultsButton = new ClearAllOutputsAction('notebook.ClearAllOutputs', localize('clearResults', 'Clear Results'), 'notebook-button icon-clear-results'); this._trustedAction = this.instantiationService.createInstance(TrustedAction, 'notebook.Trusted'); @@ -386,6 +388,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe { action: addCodeCellButton }, { action: addTextCellButton }, { action: this._trustedAction }, + { action: this._runAllCellsAction }, { action: clearResultsButton } ]); @@ -483,6 +486,20 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe } } + public async runAllCells(): Promise { + await this.modelReady; + let codeCells = this._model.cells.filter(cell => cell.cellType === CellTypes.Code); + if (codeCells && codeCells.length) { + for (let i = 0; i < codeCells.length; i++) { + let cellStatus = await this.runCell(codeCells[i]); + if (!cellStatus) { + return Promise.reject(new Error(localize('cellRunFailed', 'running cell id {0} failed', codeCells[i].id))); + } + } + } + return true; + } + public async clearAllOutputs(): Promise { try { await this.modelReady; diff --git a/src/sql/parts/notebook/notebook.css b/src/sql/parts/notebook/notebook.css index 18864289b0..2f81051a14 100644 --- a/src/sql/parts/notebook/notebook.css +++ b/src/sql/parts/notebook/notebook.css @@ -50,6 +50,15 @@ background-image: url("./media/dark/add_inverse.svg"); } +.notebookEditor .notebook-button.icon-run-cells{ + background-image: url("./media/light/run_cells.svg"); +} + +.vs-dark .notebookEditor .notebook-button.icon-run-cells, +.hc-black .notebookEditor .notebook-button.icon-run-cells{ + background-image: url("./media/dark/run_cells_inverse.svg"); +} + .notebookEditor .notebook-button.icon-trusted{ background-image: url("./media/light/trusted.svg"); } diff --git a/src/sql/parts/notebook/notebookActions.ts b/src/sql/parts/notebook/notebookActions.ts index 719b6f5f7f..4829369837 100644 --- a/src/sql/parts/notebook/notebookActions.ts +++ b/src/sql/parts/notebook/notebookActions.ts @@ -227,6 +227,25 @@ export class TrustedAction extends ToggleableAction { } } +// Action to run all code cells in a notebook. +export class RunAllCellsAction extends Action { + constructor( + id: string, label: string, cssClass: string + ) { + super(id, label, cssClass); + } + public run(context: NotebookComponent): Promise { + return new Promise((resolve, reject) => { + try { + context.runAllCells(); + resolve(true); + } catch (e) { + reject(e); + } + }); + } +} + export class KernelsDropdown extends SelectBox { private model: NotebookModel; constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, modelReady: Promise) { diff --git a/src/sql/workbench/api/node/extHostNotebookEditor.ts b/src/sql/workbench/api/node/extHostNotebookEditor.ts index f2dd8ae1e3..b60d28d9e6 100644 --- a/src/sql/workbench/api/node/extHostNotebookEditor.ts +++ b/src/sql/workbench/api/node/extHostNotebookEditor.ts @@ -156,6 +156,10 @@ export class ExtHostNotebookEditor implements azdata.nb.NotebookEditor, IDisposa return this._proxy.$runCell(this._id, uri); } + public runAllCells(): Thenable { + return this._proxy.$runAllCells(this._id); + } + public clearAllOutputs(): Thenable { return this._proxy.$clearAllOutputs(this._id); } diff --git a/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts index bd66705936..d32be5f52f 100644 --- a/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts @@ -122,10 +122,16 @@ class MainThreadNotebookEditor extends Disposable { if (!this.editor) { return Promise.resolve(false); } - return this.editor.runCell(cell); } + public runAllCells(): Promise { + if (!this.editor) { + return Promise.resolve(false); + } + return this.editor.runAllCells(); + } + public clearAllOutputs(): Promise { if (!this.editor) { return Promise.resolve(false); @@ -366,6 +372,14 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements return editor.runCell(cell); } + $runAllCells(id: string): Promise { + let editor = this.getEditor(id); + if (!editor) { + return Promise.reject(disposed(`TextEditor(${id})`)); + } + return editor.runAllCells(); + } + $clearAllOutputs(id: string): Promise { let editor = this.getEditor(id); if (!editor) { diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 69556593a9..13ded69af6 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -866,6 +866,7 @@ export interface MainThreadNotebookDocumentsAndEditorsShape extends IDisposable $tryShowNotebookDocument(resource: UriComponents, options: INotebookShowOptions): Promise; $tryApplyEdits(id: string, modelVersionId: number, edits: ISingleNotebookEditOperation[], opts: IUndoStopOptions): Promise; $runCell(id: string, cellUri: UriComponents): Promise; + $runAllCells(id: string): Promise; $clearAllOutputs(id: string): Promise; } diff --git a/src/sql/workbench/services/notebook/common/notebookService.ts b/src/sql/workbench/services/notebook/common/notebookService.ts index fe855600e8..b227023aca 100644 --- a/src/sql/workbench/services/notebook/common/notebookService.ts +++ b/src/sql/workbench/services/notebook/common/notebookService.ts @@ -109,5 +109,6 @@ export interface INotebookEditor { isVisible(): boolean; executeEdits(edits: ISingleNotebookEditOperation[]): boolean; runCell(cell: ICellModel): Promise; + runAllCells(): Promise; clearAllOutputs(): Promise; }