diff --git a/src/sql/media/icons/common-icons.css b/src/sql/media/icons/common-icons.css
index fa92380141..e908539152 100644
--- a/src/sql/media/icons/common-icons.css
+++ b/src/sql/media/icons/common-icons.css
@@ -442,6 +442,15 @@ Includes non-masked style declarations. */
mask-image: url("clear.svg");
}
+.codicon:not(.masked-icon).icon-run-with-parameters {
+ background-image: url("run-with-parameters.svg");
+}
+
+.codicon.masked-icon.icon-run-with-parameters:before {
+ -webkit-mask-image: url("run-with-parameters.svg");
+ mask-image: url("run-with-parameters.svg");
+}
+
.codicon:not(.masked-icon).icon-shield {
background-image: url("shield.svg");
}
diff --git a/src/sql/media/icons/run-with-parameters.svg b/src/sql/media/icons/run-with-parameters.svg
new file mode 100644
index 0000000000..93405cadd9
--- /dev/null
+++ b/src/sql/media/icons/run-with-parameters.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts
index a174d946d8..ec31e8fea8 100644
--- a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts
+++ b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts
@@ -28,7 +28,7 @@ import { INotebookService, INotebookParams, INotebookEditor, INotebookSection, I
import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel';
import { Deferred } from 'sql/base/common/promise';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
-import { AddCellAction, KernelsDropdown, AttachToDropdown, TrustedAction, RunAllCellsAction, ClearAllOutputsAction, CollapseCellsAction } from 'sql/workbench/contrib/notebook/browser/notebookActions';
+import { AddCellAction, KernelsDropdown, AttachToDropdown, TrustedAction, RunAllCellsAction, ClearAllOutputsAction, CollapseCellsAction, RunParametersAction } from 'sql/workbench/contrib/notebook/browser/notebookActions';
import { DropdownMenuActionViewItem } from 'sql/base/browser/ui/buttonMenu/buttonMenu';
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService';
@@ -129,7 +129,6 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
this.notebookService.removeNotebookEditor(this);
}
}
-
public get model(): NotebookModel | null {
return this._model;
}
@@ -387,6 +386,8 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
this._trustedAction = this.instantiationService.createInstance(TrustedAction, 'notebook.Trusted', true);
this._trustedAction.enabled = false;
+ let runParametersAction = this.instantiationService.createInstance(RunParametersAction, 'notebook.runParameters', true, this._notebookParams.notebookUri);
+
let taskbar = this.toolbar.nativeElement;
this._actionBar = new Taskbar(taskbar, { actionViewItemProvider: action => this.actionItemProvider(action as Action) });
this._actionBar.context = this._notebookParams.notebookUri;
@@ -418,6 +419,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
{ action: collapseCellsAction },
{ action: clearResultsButton },
{ action: this._trustedAction },
+ { action: runParametersAction },
]);
} else {
let kernelContainer = document.createElement('div');
@@ -458,10 +460,9 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
{ action: this._trustedAction },
{ action: this._runAllCellsAction },
{ action: clearResultsButton },
- { action: collapseCellsAction }
+ { action: collapseCellsAction },
]);
}
-
}
protected initNavSection(): void {
diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.css b/src/sql/workbench/contrib/notebook/browser/notebook.css
index 6161bd268c..320db99939 100644
--- a/src/sql/workbench/contrib/notebook/browser/notebook.css
+++ b/src/sql/workbench/contrib/notebook/browser/notebook.css
@@ -79,6 +79,16 @@
padding-left: 18px;
width: 16px;
}
+
+.notebookEditor
+ .in-preview
+ .actions-container
+ .action-item
+ .codicon.icon-run-with-parameters:before {
+ padding-left: 20px;
+ width: 18px;
+}
+
.notebookEditor .in-preview .actions-container .action-item:last-child {
margin-right: 14px;
}
diff --git a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts
index a7d0f5f59b..d7011e6079 100644
--- a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts
+++ b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts
@@ -28,6 +28,7 @@ import { INotebookService } from 'sql/workbench/services/notebook/browser/notebo
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { CellContext } from 'sql/workbench/contrib/notebook/browser/cellViews/codeActions';
import { URI } from 'vs/base/common/uri';
+import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
const msgLoading = localize('loading', "Loading kernels...");
export const msgChanging = localize('changing', "Changing kernel...");
@@ -39,6 +40,8 @@ const msgSelectConnection = localize('selectConnection', "Select Connection");
const msgLocalHost = localize('localhost', "localhost");
export const noKernel: string = localize('noKernel', "No Kernel");
+const baseIconClass = 'codicon';
+const maskedIconClass = 'masked-icon';
// Action to add a cell to notebook based on cell type(code/markdown).
export class AddCellAction extends Action {
@@ -101,17 +104,15 @@ export abstract class TooltipFromLabelAction extends Action {
// Action to clear outputs of all code cells.
export class ClearAllOutputsAction extends TooltipFromLabelAction {
private static readonly label = localize('clearResults', "Clear Results");
- private static readonly baseClass = 'codicon';
private static readonly iconClass = 'icon-clear-results';
- private static readonly maskedIconClass = 'masked-icon';
constructor(id: string, toggleTooltip: boolean,
@INotebookService private _notebookService: INotebookService) {
super(id, {
label: ClearAllOutputsAction.label,
- baseClass: ClearAllOutputsAction.baseClass,
+ baseClass: baseIconClass,
iconClass: ClearAllOutputsAction.iconClass,
- maskedIconClass: ClearAllOutputsAction.maskedIconClass,
+ maskedIconClass: maskedIconClass,
shouldToggleTooltip: toggleTooltip
});
}
@@ -170,24 +171,22 @@ export class TrustedAction extends ToggleableAction {
// Constants
private static readonly trustedLabel = localize('trustLabel', "Trusted");
private static readonly notTrustedLabel = localize('untrustLabel', "Not Trusted");
- private static readonly baseClass = 'codicon';
private static readonly previewTrustedCssClass = 'icon-shield';
private static readonly trustedCssClass = 'icon-trusted';
private static readonly previewNotTrustedCssClass = 'icon-shield-x';
private static readonly notTrustedCssClass = 'icon-notTrusted';
- private static readonly maskedIconClass = 'masked-icon';
constructor(
id: string, toggleTooltip: boolean,
@INotebookService private _notebookService: INotebookService
) {
super(id, {
- baseClass: TrustedAction.baseClass,
+ baseClass: baseIconClass,
toggleOnLabel: TrustedAction.trustedLabel,
toggleOnClass: toggleTooltip === true ? TrustedAction.previewTrustedCssClass : TrustedAction.trustedCssClass,
toggleOffLabel: TrustedAction.notTrustedLabel,
toggleOffClass: toggleTooltip === true ? TrustedAction.previewNotTrustedCssClass : TrustedAction.notTrustedCssClass,
- maskedIconClass: TrustedAction.maskedIconClass,
+ maskedIconClass: maskedIconClass,
shouldToggleTooltip: toggleTooltip,
isOn: false
});
@@ -232,22 +231,20 @@ export class RunAllCellsAction extends Action {
export class CollapseCellsAction extends ToggleableAction {
private static readonly collapseCells = localize('collapseAllCells', "Collapse Cells");
private static readonly expandCells = localize('expandAllCells', "Expand Cells");
- private static readonly baseClass = 'codicon';
private static readonly previewCollapseCssClass = 'icon-collapse-cells';
private static readonly collapseCssClass = 'icon-hide-cells';
private static readonly previewExpandCssClass = 'icon-expand-cells';
private static readonly expandCssClass = 'icon-show-cells';
- private static readonly maskedIconClass = 'masked-icon';
constructor(id: string, toggleTooltip: boolean,
@INotebookService private _notebookService: INotebookService) {
super(id, {
- baseClass: CollapseCellsAction.baseClass,
+ baseClass: baseIconClass,
toggleOnLabel: CollapseCellsAction.expandCells,
toggleOnClass: toggleTooltip === true ? CollapseCellsAction.previewExpandCssClass : CollapseCellsAction.expandCssClass,
toggleOffLabel: CollapseCellsAction.collapseCells,
toggleOffClass: toggleTooltip === true ? CollapseCellsAction.previewCollapseCssClass : CollapseCellsAction.collapseCssClass,
- maskedIconClass: CollapseCellsAction.maskedIconClass,
+ maskedIconClass: maskedIconClass,
shouldToggleTooltip: toggleTooltip,
isOn: false
});
@@ -272,6 +269,101 @@ export class CollapseCellsAction extends ToggleableAction {
}
}
+export class RunParametersAction extends TooltipFromLabelAction {
+ private static readonly label = localize('runParameters', "Run with Parameters");
+ private static readonly iconClass = 'icon-run-with-parameters';
+
+ constructor(id: string,
+ toggleTooltip: boolean,
+ context: URI,
+ @IQuickInputService private quickInputService: IQuickInputService,
+ @INotebookService private _notebookService: INotebookService,
+ @INotificationService private notificationService: INotificationService,
+ ) {
+ super(id, {
+ label: RunParametersAction.label,
+ baseClass: baseIconClass,
+ iconClass: RunParametersAction.iconClass,
+ maskedIconClass: maskedIconClass,
+ shouldToggleTooltip: toggleTooltip
+ });
+ }
+
+ /**
+ * Gets Default Parameters in Notebook from Parameter Cell
+ * Uses that as Placeholder values for user to inject new values for
+ * Once user enters all values it will open the new parameterized notebook
+ * with injected parameters value from the QuickInput
+ */
+ public async run(context: URI): Promise {
+ const editor = this._notebookService.findNotebookEditor(context);
+ // Set defaultParameters to the parameter values in parameter cell
+ let defaultParameters = new Map();
+ editor.cells.forEach(cell => {
+ if (cell.isParameter) {
+ for (let parameter of cell.source) {
+ let param = parameter.split('=', 2);
+ defaultParameters.set(param[0].trim(), param[1].trim());
+ }
+ }
+ });
+
+ // Store new parameters values the user inputs
+ let inputParameters = new Map();
+ let uriParams = new URLSearchParams();
+ // Store new parameter values to map based off defaultParameters
+ if (defaultParameters.size === 0) {
+ // If there is no parameter cell indicate to user to create one
+ this.notificationService.notify({
+ severity: Severity.Info,
+ message: localize('noParametersCell', "This notebook cannot run with parameters until a parameter cell is added. [Learn more](https://docs.microsoft.com/sql/azure-data-studio/notebooks/notebooks-parameterization)."),
+ });
+ return;
+ } else {
+ for (let key of defaultParameters.keys()) {
+ let newParameterValue = await this.quickInputService.input({ prompt: key, value: defaultParameters.get(key), ignoreFocusLost: true });
+ // If user cancels or escapes then it stops the action entirely
+ if (newParameterValue === undefined) {
+ return;
+ }
+ inputParameters.set(key, newParameterValue);
+ }
+ // Format the new parameters to be append to the URI
+ for (let key of inputParameters.keys()) {
+ // Will only add new injected parameters when the value is not the same as the defaultParameters values
+ if (inputParameters.get(key) !== defaultParameters.get(key)) {
+ // For empty strings we need to escape the value
+ // so that it is kept when adding uriParams.toString() to filePath
+ if (inputParameters.get(key) === '') {
+ uriParams.append(key, '\'\'');
+ } else {
+ uriParams.append(key, inputParameters.get(key));
+ }
+ }
+ }
+ let stringParams = unescape(uriParams.toString());
+ context = context.with({ query: stringParams });
+ return this.openParameterizedNotebook(context);
+ }
+ }
+
+ /**
+ * This function will be used once the showNotebookDocument can be used
+ * TODO - Call Extensibility API for ShowNotebook
+ * (showNotebookDocument to be utilized in Notebook Service)
+ **/
+ public async openParameterizedNotebook(uri: URI): Promise {
+ // const editor = this._notebookService.findNotebookEditor(uri);
+ // let modelContents = editor.model.toJSON();
+ // let basename = path.basename(uri.fsPath);
+ // let untitledUri = uri.with({ authority: '', scheme: 'untitled', path: basename });
+ // this._notebookService.showNotebookDocument(untitledUri, {
+ // initialContent: modelContents,
+ // preserveFocus: true
+ // });
+ }
+}
+
const showAllKernelsConfigName = 'notebook.showAllKernels';
const workbenchPreviewConfigName = 'workbench.enablePreviewFeatures';
export const noKernelName = localize('noKernel', "No Kernel");
diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts
index 0925db416c..9329c7c631 100644
--- a/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts
+++ b/src/sql/workbench/contrib/notebook/test/browser/notebookActions.test.ts
@@ -7,7 +7,7 @@ import * as assert from 'assert';
import * as azdata from 'azdata';
import * as sinon from 'sinon';
import { TestConfigurationService } from 'sql/platform/connection/test/common/testConfigurationService';
-import { AddCellAction, ClearAllOutputsAction, CollapseCellsAction, KernelsDropdown, msgChanging, NewNotebookAction, noKernelName, RunAllCellsAction, TrustedAction } from 'sql/workbench/contrib/notebook/browser/notebookActions';
+import { AddCellAction, ClearAllOutputsAction, CollapseCellsAction, KernelsDropdown, msgChanging, NewNotebookAction, noKernelName, RunAllCellsAction, RunParametersAction, TrustedAction } from 'sql/workbench/contrib/notebook/browser/notebookActions';
import { ClientSessionStub, ContextViewProviderStub, NotebookComponentStub, NotebookModelStub, NotebookServiceStub } from 'sql/workbench/contrib/notebook/test/stubs';
import { NotebookEditorStub } from 'sql/workbench/contrib/notebook/test/testCommon';
import { ICellModel, INotebookModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
@@ -24,6 +24,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
import { URI } from 'vs/base/common/uri';
+import { MockQuickInputService } from 'sql/workbench/contrib/notebook/test/common/quickInputServiceMock';
class TestClientSession extends ClientSessionStub {
private _errorState: boolean = false;
@@ -257,6 +258,24 @@ suite('Notebook Actions', function (): void {
assert.strictEqual(actualCmdId, NewNotebookAction.INTERNAL_NEW_NOTEBOOK_CMD_ID);
});
+ test.skip('Run with Parameters Action', async function (): Promise {
+ let mockNotification = TypeMoq.Mock.ofType(TestNotificationService);
+ mockNotification.setup(n => n.notify(TypeMoq.It.isAny()));
+ let quickInputService = new MockQuickInputService;
+
+ let action = new RunParametersAction('TestId', true, testUri, quickInputService, mockNotebookService.object, mockNotification.object);
+
+ // Normal use case
+ const testCells = [{
+ isParameter: true,
+ source: ['x=2.0\n', 'y=5.0']
+ }];
+
+ mockNotebookEditor.setup(x => x.cells).returns(() => testCells);
+
+ assert.doesNotThrow(() => action.run(testUri));
+ });
+
suite('Kernels dropdown', async () => {
let kernelsDropdown: KernelsDropdown;
let contextViewProvider: ContextViewProviderStub;
diff --git a/src/sql/workbench/contrib/notebook/test/common/quickInputServiceMock.ts b/src/sql/workbench/contrib/notebook/test/common/quickInputServiceMock.ts
new file mode 100644
index 0000000000..b2193e9e08
--- /dev/null
+++ b/src/sql/workbench/contrib/notebook/test/common/quickInputServiceMock.ts
@@ -0,0 +1,67 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+import * as Types from 'vs/base/common/types';
+
+import { IInputBox, IInputOptions, IPickOptions, IQuickInputButton, IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput';
+import { Event } from 'vs/base/common/event';
+import { CancellationToken } from 'vs/base/common/cancellation';
+
+
+export class MockQuickInputService implements IQuickInputService {
+ declare readonly _serviceBrand: undefined;
+
+ readonly onShow = Event.None;
+ readonly onHide = Event.None;
+
+ readonly quickAccess = undefined!;
+
+ public pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: true }, token?: CancellationToken): Promise;
+ public pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: false }, token?: CancellationToken): Promise;
+ public pick(picks: Promise[]> | QuickPickInput[], options?: Omit, 'canPickMany'>, token?: CancellationToken): Promise {
+ if (Types.isArray(picks)) {
+ return Promise.resolve({ label: 'selectedPick', description: 'pick description', value: 'selectedPick' });
+ } else {
+ return Promise.resolve(undefined);
+ }
+ }
+
+ public input(options?: IInputOptions, token?: CancellationToken): Promise {
+ return Promise.resolve(options ? 'resolved' + options.prompt : 'resolved');
+ }
+
+ backButton!: IQuickInputButton;
+
+ createQuickPick(): IQuickPick {
+ throw new Error('not implemented.');
+ }
+
+ createInputBox(): IInputBox {
+ throw new Error('not implemented.');
+ }
+
+ focus(): void {
+ throw new Error('not implemented.');
+ }
+
+ toggle(): void {
+ throw new Error('not implemented.');
+ }
+
+ navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void {
+ throw new Error('not implemented.');
+ }
+
+ accept(): Promise {
+ throw new Error('not implemented.');
+ }
+
+ back(): Promise {
+ throw new Error('not implemented.');
+ }
+
+ cancel(): Promise {
+ throw new Error('not implemented.');
+ }
+}