Notebook Views Actions (#16207)

This adds the actions currently needed by the views
This commit is contained in:
Daniel Grajeda
2021-08-03 23:52:27 -06:00
committed by GitHub
parent 6985d95300
commit 0567141bc4
20 changed files with 1027 additions and 9 deletions

View File

@@ -70,7 +70,7 @@ interface IActionStateData {
commandId?: string;
}
class IMultiStateData<T> {
export class IMultiStateData<T> {
private _stateMap = new Map<T, IActionStateData>();
constructor(mappings: { key: T, value: IActionStateData }[], private _state: T, private _baseClass?: string) {
if (mappings) {
@@ -120,7 +120,7 @@ class IMultiStateData<T> {
}
}
abstract class MultiStateAction<T> extends Action {
export abstract class MultiStateAction<T> extends Action {
constructor(
id: string,
protected states: IMultiStateData<T>,

View File

@@ -166,6 +166,10 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
return this._activeCellId;
}
get outputRef(): ElementRef {
return this.output;
}
private setLoading(isLoading: boolean): void {
this.cellModel.loaded = !isLoading;
this._changeRef.detectChanges();

View File

@@ -0,0 +1,12 @@
#insert-dialog-cell-grid .loading-spinner-container {
flex: 1;
align-self: center;
}
#insert-dialog-cell-grid .loading-spinner {
margin: auto;
}
#insert-dialog-cell-grid input[type="checkbox"] {
display: flex;
-webkit-appearance: none;
outline: none !important;
}

View File

@@ -0,0 +1,298 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./insertCellsModal';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { Button } from 'sql/base/browser/ui/button/button';
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import { Modal } from 'sql/workbench/browser/modal/modal';
import { attachModalDialogStyler } from 'sql/workbench/common/styler';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { ILogService } from 'vs/platform/log/common/log';
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import * as DOM from 'vs/base/browser/dom';
import { attachCheckboxStyler } from 'sql/platform/theme/common/styler';
import { ServiceOptionType } from 'sql/platform/connection/common/interfaces';
import { ServiceOption } from 'azdata';
import * as DialogHelper from 'sql/workbench/browser/modal/dialogHelper';
import { TextCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/textCell.component';
import { ComponentFactoryResolver, ViewContainerRef } from '@angular/core';
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel';
import { inputBorder, inputValidationInfoBorder } from 'vs/platform/theme/common/colorRegistry';
import { localize } from 'vs/nls';
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { toJpeg } from 'html-to-image';
import { truncate } from 'vs/base/common/strings';
type CellOption = {
optionMetadata: ServiceOption,
defaultValue: string,
currentValue: boolean
};
export class CellOptionsModel {
private _optionsMap: { [name: string]: CellOption } = {};
constructor(
optionsMetadata: ServiceOption[],
private onInsert: (cell: ICellModel) => void,
private _context: NotebookViewsExtension,
) {
optionsMetadata.forEach(optionMetadata => {
let defaultValue = this.getDisplayValue(optionMetadata, optionMetadata.defaultValue);
this._optionsMap[optionMetadata.name] = {
optionMetadata: optionMetadata,
defaultValue: optionMetadata.defaultValue,
currentValue: defaultValue
};
});
}
private getDisplayValue(optionMetadata: ServiceOption, optionValue: string): boolean {
let displayValue: boolean = false;
switch (optionMetadata.valueType) {
case ServiceOptionType.boolean:
displayValue = DialogHelper.getBooleanValueFromStringOrBoolean(optionValue);
break;
}
return displayValue;
}
restoreCells(): void {
for (let key in this._optionsMap) {
let optionElement = this._optionsMap[key];
if (optionElement.currentValue === true) {
const activeView = this._context.getActiveView();
const cellToInsert = activeView.getCell(optionElement.optionMetadata.name);
if (cellToInsert) {
this.onInsert(cellToInsert);
}
}
}
}
public setOptionValue(optionName: string, value: boolean): void {
if (this._optionsMap[optionName] !== undefined) {
this._optionsMap[optionName].currentValue = value;
}
}
public getOptionValue(optionName: string): boolean | undefined {
return this._optionsMap[optionName]?.currentValue;
}
}
export class InsertCellsModal extends Modal {
public viewModel: CellOptionsModel;
private _submitButton: Button;
private _cancelButton: Button;
private _optionsMap: { [name: string]: Checkbox } = {};
private _maxTitleLength: number = 20;
constructor(
private onInsert: (cell: ICellModel) => void,
private _context: NotebookViewsExtension,
private _containerRef: ViewContainerRef,
private _componentFactoryResolver: ComponentFactoryResolver,
@ILogService logService: ILogService,
@IThemeService themeService: IThemeService,
@ILayoutService layoutService: ILayoutService,
@IClipboardService clipboardService: IClipboardService,
@IContextKeyService contextKeyService: IContextKeyService,
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService,
) {
super(
localize("insertCellsModal.title", "Insert cells"),
'InsertCellsModal',
telemetryService,
layoutService,
clipboardService,
themeService,
logService,
textResourcePropertiesService,
contextKeyService,
{ hasErrors: true, hasSpinner: true }
);
const options = this.getOptions();
this.viewModel = new CellOptionsModel(options, this.onInsert, this._context);
}
protected renderBody(container: HTMLElement): void {
const grid = DOM.$<HTMLDivElement>('div#insert-dialog-cell-grid');
grid.style.display = 'grid';
grid.style.gridTemplateColumns = '1fr 1fr';
grid.style.gap = '10px';
grid.style.padding = '10px';
grid.style.overflowY = 'auto';
grid.style.maxHeight = 'calc(100% - 40px)';
const gridTitle = DOM.$<HTMLHeadElement>('h2.grid-title');
gridTitle.title = localize("insertCellsModal.selectCells", "Select cell sources");
DOM.append(container, grid);
this.createOptions(grid)
.catch((e) => { this.setError(localize("insertCellsModal.thumbnailError", "Error: Unable to generate thumbnails.")); });
}
protected layout(height: number): void {
// No-op for now. No need to relayout.
}
private async createOptions(container: HTMLElement): Promise<void> {
const activeView = this._context.getActiveView();
const cellsAvailableToInsert = activeView.hiddenCells;
cellsAvailableToInsert.forEach(async (cell) => {
const optionWidget = this.createCheckBoxHelper(
container,
'<div class="loading-spinner-container"><div class="loading-spinner codicon in-progress"></div></div>',
false,
() => this.onOptionChecked(cell.cellGuid)
);
const img = await this.generateScreenshot(cell);
const wrapper = DOM.$<HTMLDivElement>('div.thumnail-wrapper');
const thumbnail = DOM.$<HTMLImageElement>('img.thumbnail');
thumbnail.src = img;
thumbnail.style.maxWidth = '100%';
DOM.append(wrapper, thumbnail);
optionWidget.label = wrapper.outerHTML;
this._optionsMap[cell.cellGuid] = optionWidget;
});
}
private createCheckBoxHelper(container: HTMLElement, label: string, isChecked: boolean, onCheck: (viaKeyboard: boolean) => void): Checkbox {
const checkbox = new Checkbox(DOM.append(container, DOM.$('.dialog-input-section')), {
label: label,
checked: isChecked,
onChange: onCheck,
ariaLabel: label
});
this._register(attachCheckboxStyler(checkbox, this._themeService));
return checkbox;
}
public onOptionChecked(optionName: string) {
this.viewModel.setOptionValue(optionName, (<Checkbox>this._optionsMap[optionName]).checked);
this.validate();
}
private getOptions(): ServiceOption[] {
const activeView = this._context.getActiveView();
const cellsAvailableToInsert = activeView.hiddenCells;
return cellsAvailableToInsert.map((cell) => ({
name: cell.cellGuid,
displayName: truncate(cell.renderedOutputTextContent[0] ?? '', this._maxTitleLength) || localize("insertCellsModal.untitled", "Untitled Cell : {0}", cell.cellGuid),
description: '',
groupName: undefined,
valueType: ServiceOptionType.boolean,
defaultValue: '',
objectType: undefined,
categoryValues: [],
isRequired: false,
isArray: undefined,
}));
}
public override render() {
super.render();
this._submitButton = this.addFooterButton(localize('insertCellsModal.Insert', "Insert"), () => this.onSubmitHandler());
this._cancelButton = this.addFooterButton(localize('insertCellsModal.Cancel', "Cancel"), () => this.onCancelHandler(), 'right', true);
this._register(attachButtonStyler(this._submitButton, this._themeService));
this._register(attachButtonStyler(this._cancelButton, this._themeService));
attachModalDialogStyler(this, this._themeService);
this.validate();
}
private validate() {
if (Object.keys(this._optionsMap).length) {
this._submitButton.enabled = true;
} else {
this._submitButton.enabled = false;
}
}
private onSubmitHandler() {
this.viewModel.restoreCells();
this.close();
}
private onCancelHandler() {
this.close();
}
public close(): void {
return this.hide();
}
public async open(): Promise<void> {
this.show();
}
public override dispose(): void {
super.dispose();
for (let key in this._optionsMap) {
let widget = this._optionsMap[key];
widget.dispose();
delete this._optionsMap[key];
}
}
public async generateScreenshot(cell: ICellModel, screenshotWidth: number = 300, screenshowHeight: number = 300, backgroundColor: string = '#ffffff'): Promise<string> {
let componentFactory = this._componentFactoryResolver.resolveComponentFactory(TextCellComponent);
let component = this._containerRef.createComponent(componentFactory);
component.instance.model = this._context.notebook as NotebookModel;
component.instance.cellModel = cell;
component.instance.handleContentChanged();
const element: HTMLElement = component.instance.outputRef.nativeElement;
const scale = element.clientWidth / screenshotWidth;
const canvasWidth = element.clientWidth / scale;
const canvasHeight = element.clientHeight / scale;
return toJpeg(component.instance.outputRef.nativeElement, { quality: .6, canvasWidth, canvasHeight, backgroundColor });
}
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const inputBorderColor = theme.getColor(inputBorder);
if (inputBorderColor) {
collector.addRule(`
#insert-dialog-cell-grid input[type="checkbox"] + label {
border: 2px solid;
border-color: ${inputBorderColor.toString()};
display: flex;
height: 125px;
overflow: hidden;
}
`);
}
const inputActiveOptionBorderColor = theme.getColor(inputValidationInfoBorder);
if (inputActiveOptionBorderColor) {
collector.addRule(`
#insert-dialog-cell-grid input[type="checkbox"]:checked + label {
border-color: ${inputActiveOptionBorderColor.toString()};
}
`);
}
});

View File

@@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Component, Input, ViewChildren, QueryList, ChangeDetectorRef, forwardRef, Inject, ViewChild, ElementRef } from '@angular/core';
import { Component, Input, ViewChildren, QueryList, ChangeDetectorRef, forwardRef, Inject, ViewChild, ElementRef, ViewContainerRef, ComponentFactoryResolver } from '@angular/core';
import { ICellModel, INotebookModel, ISingleNotebookEditOperation } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { CodeCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/codeCell.component';
import { TextCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/textCell.component';
@@ -26,8 +26,11 @@ import { CellType, CellTypes } from 'sql/workbench/services/notebook/common/cont
import { isUndefinedOrNull } from 'vs/base/common/types';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
import { INotebookView } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
import { INotebookView, INotebookViewMetadata } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
import { NotebookViewsGridComponent } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { DeleteViewAction, InsertCellAction, ViewSettingsAction } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsActions';
import { RunAllCellsAction } from 'sql/workbench/contrib/notebook/browser/notebookActions';
import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
export const NOTEBOOKVIEWS_SELECTOR: string = 'notebook-view-component';
@@ -41,6 +44,7 @@ export class NotebookViewComponent extends AngularDisposable implements INoteboo
@Input() model: NotebookModel;
@Input() activeView: INotebookView;
@Input() views: NotebookViewsExtension;
@Input() notebookMeta: INotebookViewMetadata;
@ViewChild('container', { read: ElementRef }) private _container: ElementRef;
@ViewChild('viewsToolbar', { read: ElementRef }) private _viewsToolbar: ElementRef;
@@ -51,18 +55,21 @@ export class NotebookViewComponent extends AngularDisposable implements INoteboo
protected _actionBar: Taskbar;
public previewFeaturesEnabled: boolean = false;
private _modelReadyDeferred = new Deferred<NotebookModel>();
private _runAllCellsAction: RunAllCellsAction;
private _scrollTop: number;
constructor(
@Inject(IBootstrapParams) private _notebookParams: INotebookParams,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
@Inject(IKeybindingService) private _keybindingService: IKeybindingService,
@Inject(INotificationService) private _notificationService: INotificationService,
@Inject(INotebookService) private _notebookService: INotebookService,
@Inject(IConnectionManagementService) private _connectionManagementService: IConnectionManagementService,
@Inject(IConfigurationService) private _configurationService: IConfigurationService,
@Inject(IEditorService) private _editorService: IEditorService
@Inject(IEditorService) private _editorService: IEditorService,
@Inject(ViewContainerRef) private _containerRef: ViewContainerRef,
@Inject(ComponentFactoryResolver) private _componentFactoryResolver: ComponentFactoryResolver,
) {
super();
this._register(this._configurationService.onDidChangeConfiguration(e => {
@@ -235,9 +242,25 @@ export class NotebookViewComponent extends AngularDisposable implements INoteboo
titleElement.style.marginRight = '25px';
titleElement.style.minHeight = '25px';
let insertCellsAction = this._instantiationService.createInstance(InsertCellAction, this.insertCell.bind(this), this.views, this._containerRef, this._componentFactoryResolver);
this._runAllCellsAction = this._instantiationService.createInstance(RunAllCellsAction, 'notebook.runAllCells', localize('runAllPreview', "Run all"), 'notebook-button masked-pseudo start-outline');
let spacerElement = document.createElement('li');
spacerElement.style.marginLeft = 'auto';
let viewOptions = this._instantiationService.createInstance(ViewSettingsAction, this.views);
let deleteView = this._instantiationService.createInstance(DeleteViewAction, this.views);
this._actionBar.setContent([
{ element: titleElement },
{ element: Taskbar.createTaskbarSeparator() },
{ action: insertCellsAction },
{ action: this._runAllCellsAction },
{ element: spacerElement },
{ action: viewOptions },
{ action: deleteView }
]);
}

View File

@@ -0,0 +1,242 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Action } from 'vs/base/common/actions';
import { ViewOptionsModal } from 'sql/workbench/contrib/notebook/browser/notebookViews/viewOptionsModal';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { localize } from 'vs/nls';
import { InsertCellsModal } from 'sql/workbench/contrib/notebook/browser/notebookViews/insertCellsModal';
import { ComponentFactoryResolver, ViewContainerRef } from '@angular/core';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IDisposable } from 'vs/base/common/lifecycle';
import { CellExecutionState, ICellModel, ViewMode } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { CellActionBase, CellContext, IMultiStateData, MultiStateAction } from 'sql/workbench/contrib/notebook/browser/cellViews/codeActions';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ILogService } from 'vs/platform/log/common/log';
import { getErrorMessage } from 'vs/base/common/errors';
import * as types from 'vs/base/common/types';
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { Separator } from 'sql/base/browser/ui/separator/separator';
import { ToggleMoreActions } from 'sql/workbench/contrib/notebook/browser/cellToolbarActions';
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
import { INotebookView } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
export class ViewSettingsAction extends Action {
private static readonly ID = 'notebookView.viewSettings';
private static readonly LABEL = undefined;
private static readonly ICON = 'notebook-button settings masked-icon';
constructor(
private _context: NotebookViewsExtension,
@IInstantiationService private _instantiationService: IInstantiationService,
) {
super(ViewSettingsAction.ID, ViewSettingsAction.LABEL, ViewSettingsAction.ICON);
}
override async run(): Promise<void> {
const optionsModal = this._instantiationService.createInstance(ViewOptionsModal, this._context.getActiveView());
optionsModal.render();
optionsModal.open();
}
}
export class DeleteViewAction extends Action {
private static readonly ID = 'notebookView.deleteView';
private static readonly LABEL = undefined;
private static readonly ICON = 'notebook-button delete masked-icon';
constructor(
private _notebookViews: NotebookViewsExtension,
@IDialogService private readonly dialogService: IDialogService,
@INotificationService private readonly notificationService: INotificationService
) {
super(DeleteViewAction.ID, DeleteViewAction.LABEL, DeleteViewAction.ICON);
}
override async run(): Promise<void> {
const activeView = this._notebookViews.getActiveView();
if (activeView) {
const confirmDelete = await this.confirmDelete(activeView);
if (confirmDelete) {
this._notebookViews.removeView(activeView.guid);
this._notebookViews.notebook.viewMode = ViewMode.Notebook;
}
} else {
this.notificationService.error(localize('viewsUnableToRemove', "Unable to remove view"));
}
}
private async confirmDelete(view: INotebookView): Promise<boolean> {
const result = await this.dialogService.confirm({
message: localize('confirmDelete', "Are you sure you want to delete view \"{0}\"?", view.name),
primaryButton: localize('delete', "&&Delete"),
type: 'question'
});
if (result.confirmed) {
return true;
}
return false;
}
}
export class InsertCellAction extends Action {
private static readonly ID = 'notebookView.insertCell';
private static readonly LABEL = localize('insertCells', "Insert Cells");
private static readonly ICON = 'notebook-button masked-pseudo add-new';
constructor(
private onInsert: (cell: ICellModel) => void,
private _context: NotebookViewsExtension,
private _containerRef: ViewContainerRef,
private _componentFactoryResolver: ComponentFactoryResolver,
@IInstantiationService private _instantiationService: IInstantiationService,
) {
super(InsertCellAction.ID, InsertCellAction.LABEL, InsertCellAction.ICON);
}
override async run(): Promise<void> {
const optionsModal = this._instantiationService.createInstance(InsertCellsModal, this.onInsert, this._context, this._containerRef, this._componentFactoryResolver);
optionsModal.render();
optionsModal.open();
}
}
export class RunCellAction extends MultiStateAction<CellExecutionState> {
public static ID = 'notebookView.runCell';
public static LABEL = localize('runCell', "Run cell");
private _executionChangedDisposable: IDisposable;
private _context: CellContext;
constructor(context: CellContext, @INotificationService private notificationService: INotificationService,
@IConnectionManagementService private connectionManagementService: IConnectionManagementService,
@IKeybindingService keybindingService: IKeybindingService,
@ILogService logService: ILogService
) {
super(RunCellAction.ID, new IMultiStateData<CellExecutionState>([
{ key: CellExecutionState.Hidden, value: { label: '', className: '', tooltip: '', hideIcon: true } },
{ key: CellExecutionState.Stopped, value: { label: '', className: 'action-label notebook-button masked-pseudo start-outline masked-icon', tooltip: localize('runCell', "Run cell"), commandId: 'notebook.command.runactivecell' } },
{ key: CellExecutionState.Running, value: { label: '', className: 'action-label codicon notebook-button toolbarIconStop', tooltip: localize('stopCell', "Cancel execution") } },
{ key: CellExecutionState.Error, value: { label: '', className: 'toolbarIconRunError', tooltip: localize('errorRunCell', "Error on last run. Click to run again") } },
], CellExecutionState.Hidden), keybindingService, logService);
this.ensureContextIsUpdated(context);
}
public override run(): Promise<void> {
return this.doRun();
}
public async doRun(): Promise<void> {
if (!this._context) {
// TODO should we error?
return;
}
try {
await this._context.cell.runCell(this.notificationService, this.connectionManagementService);
} catch (error) {
let message = getErrorMessage(error);
this.notificationService.error(message);
}
}
private ensureContextIsUpdated(context: CellContext) {
if (context && context !== this._context) {
if (this._executionChangedDisposable) {
this._executionChangedDisposable.dispose();
}
this._context = context;
this.updateStateAndExecutionCount(context.cell.executionState);
this._executionChangedDisposable = this._context.cell.onExecutionStateChange((state) => {
this.updateStateAndExecutionCount(state);
});
}
}
private updateStateAndExecutionCount(state: CellExecutionState) {
let label = '';
let className = '';
if (!types.isUndefinedOrNull(this._context.cell.executionCount)) {
label = `[${this._context.cell.executionCount}]`;
// Heuristic to try and align correctly independent of execution count length. Moving left margin
// back by a few px seems to make things "work" OK, but isn't a super clean solution
if (label.length === 4) {
className = 'execCountTen';
} else if (label.length > 4) {
className = 'execCountHundred';
}
}
this.states.updateStateData(CellExecutionState.Hidden, (data) => {
data.label = label;
data.className = className;
});
this.updateState(state);
}
}
export class HideCellAction extends Action {
private static readonly ID = 'notebookView.hideCell';
private static readonly LABEL = undefined;
private static readonly ICON = 'notebook-button delete masked-icon';
constructor(
private hideFn: () => void,
private context: any
) {
super(HideCellAction.ID, HideCellAction.LABEL, HideCellAction.ICON);
}
override async run(): Promise<void> {
this.hideFn.apply(this.context);
}
}
export class ViewCellInNotebook extends CellActionBase {
constructor(id: string, label: string,
@INotificationService notificationService: INotificationService
) {
super(id, label, undefined, notificationService);
}
doRun(context: CellContext): Promise<void> {
try {
context?.model?.updateActiveCell(context.cell);
context.model.viewMode = ViewMode.Notebook;
} catch (error) {
let message = localize('unableToNavigateToCell', "Unable to navigate to notebook cell.");
this.notificationService.notify({
severity: Severity.Error,
message: message
});
}
return Promise.resolve();
}
}
export class ViewCellToggleMoreActions {
private _actions: (Action | CellActionBase)[] = [];
private _moreActions: ActionBar;
private _moreActionsElement: HTMLElement;
constructor(
@IInstantiationService private instantiationService: IInstantiationService
) {
this._actions.push(
instantiationService.createInstance(ViewCellInNotebook, 'viewCellInNotebook', localize('viewCellInNotebook', "View Cell In Notebook")),
);
}
public onInit(elementRef: HTMLElement, context: CellContext) {
this._moreActionsElement = elementRef;
this._moreActionsElement.setAttribute('aria-haspopup', 'menu');
if (this._moreActionsElement.childNodes.length > 0) {
this._moreActionsElement.removeChild(this._moreActionsElement.childNodes[0]);
}
this._moreActions = new ActionBar(this._moreActionsElement, { orientation: ActionsOrientation.VERTICAL, ariaLabel: localize('moreActionsLabel', "More") });
this._moreActions.context = { target: this._moreActionsElement };
let validActions = this._actions.filter(a => a instanceof Separator || a instanceof CellActionBase && a.canRun(context));
this._moreActions.push(this.instantiationService.createInstance(ToggleMoreActions, validActions, context), { icon: true, label: false });
}
}

View File

@@ -3,18 +3,27 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./cellToolbar';
import * as DOM from 'vs/base/browser/dom';
import { Component, OnInit, Input, ViewChild, TemplateRef, ElementRef, Inject, Output, EventEmitter, ChangeDetectorRef, forwardRef } from '@angular/core';
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel';
import { DEFAULT_VIEW_CARD_HEIGHT, DEFAULT_VIEW_CARD_WIDTH } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewModel';
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
import { CellChangeEventType, INotebookView, INotebookViewCellMetadata } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
import { ITaskbarContent, Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { CellContext } from 'sql/workbench/contrib/notebook/browser/cellViews/codeActions';
import { RunCellAction, HideCellAction, ViewCellToggleMoreActions } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsActions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { CellTypes } from 'sql/workbench/services/notebook/common/contracts';
@Component({
selector: 'view-card-component',
templateUrl: decodeURI(require.toUrl('./notebookViewsCard.component.html'))
})
export class NotebookViewsCardComponent implements OnInit {
public _cellToggleMoreActions: ViewCellToggleMoreActions;
private _actionbar: Taskbar;
private _metadata: INotebookViewCellMetadata;
private _activeView: INotebookView;
@@ -26,12 +35,16 @@ export class NotebookViewsCardComponent implements OnInit {
@ViewChild('templateRef') templateRef: TemplateRef<any>;
@ViewChild('item', { read: ElementRef }) private _item: ElementRef;
@ViewChild('actionbar', { read: ElementRef }) private _actionbarRef: ElementRef;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(IInstantiationService) private _instantiationService: IInstantiationService
) { }
ngOnInit() { }
ngOnInit() {
this.initActionBar();
}
ngOnChanges() {
if (this.views) {
@@ -51,6 +64,31 @@ export class NotebookViewsCardComponent implements OnInit {
this.detectChanges();
}
initActionBar() {
if (this._actionbarRef) {
let taskbarContent: ITaskbarContent[] = [];
let context = new CellContext(this.model, this.cell);
this._actionbar = new Taskbar(this._actionbarRef.nativeElement);
this._actionbar.context = { target: this._actionbarRef.nativeElement };
if (this.cell.cellType === CellTypes.Code) {
let runCellAction = this._instantiationService.createInstance(RunCellAction, context);
taskbarContent.push({ action: runCellAction });
}
let hideButton = new HideCellAction(this.hide, this);
taskbarContent.push({ action: hideButton });
let moreActionsContainer = DOM.$('li.action-item');
this._cellToggleMoreActions = this._instantiationService.createInstance(ViewCellToggleMoreActions);
this._cellToggleMoreActions.onInit(moreActionsContainer, context);
taskbarContent.push({ element: moreActionsContainer });
this._actionbar.setContent(taskbarContent);
}
}
get elementRef(): ElementRef {
return this._item;
}

View File

@@ -0,0 +1,149 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox';
import { Button } from 'sql/base/browser/ui/button/button';
import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import { Modal } from 'sql/workbench/browser/modal/modal';
import { attachModalDialogStyler } from 'sql/workbench/common/styler';
import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { ILogService } from 'vs/platform/log/common/log';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import * as DOM from 'vs/base/browser/dom';
import { attachInputBoxStyler } from 'sql/platform/theme/common/styler';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
import { localize } from 'vs/nls';
import { IInputOptions, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { INotebookView } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
export class ViewOptionsModal extends Modal {
private _submitButton: Button;
private _cancelButton: Button;
private _optionsMap: { [name: string]: InputBox | Checkbox } = {};
private _viewNameInput: InputBox;
constructor(
private _view: INotebookView,
@ILogService logService: ILogService,
@IThemeService themeService: IThemeService,
@ILayoutService layoutService: ILayoutService,
@IClipboardService clipboardService: IClipboardService,
@IContextKeyService contextKeyService: IContextKeyService,
@IAdsTelemetryService telemetryService: IAdsTelemetryService,
@IContextViewService private _contextViewService: IContextViewService,
@ITextResourcePropertiesService textResourcePropertiesService: ITextResourcePropertiesService,
) {
super(
localize("viewOptionsModal.title", "Configure View"),
'ViewOptionsModal',
telemetryService,
layoutService,
clipboardService,
themeService,
logService,
textResourcePropertiesService,
contextKeyService,
{ hasErrors: true, hasSpinner: true }
);
}
protected renderBody(container: HTMLElement): void {
const formWrapper = DOM.$<HTMLDivElement>('div#view-options-form');
formWrapper.style.padding = '10px';
DOM.append(container, formWrapper);
this._viewNameInput = this.createNameInput(formWrapper);
}
protected layout(height: number): void {
}
protected createNameInput(container: HTMLElement): InputBox {
return this.createInputBoxHelper(container, localize('viewOptionsModal.name', "View Name"), this._view.name, {
validationOptions: {
validation: (value: string) => {
if (!value) {
return ({ type: MessageType.ERROR, content: localize('viewOptionsModal.missingRequireField', "This field is required.") });
}
if (this._view.name !== value && !this._view.nameAvailable(value)) {
return ({ type: MessageType.ERROR, content: localize('viewOptionsModal.nameTaken', "This view name has already been taken.") });
}
return undefined;
}
},
ariaLabel: localize('viewOptionsModal.name', "View Name")
});
}
private createInputBoxHelper(container: HTMLElement, label: string, defaultValue: string = '', options?: IInputOptions): InputBox {
const inputContainer = DOM.append(container, DOM.$('.dialog-input-section'));
DOM.append(inputContainer, DOM.$('.dialog-label')).innerText = label;
const input = new InputBox(DOM.append(inputContainer, DOM.$('.dialog-input')), this._contextViewService, options);
input.value = defaultValue;
return input;
}
override render() {
super.render();
this._submitButton = this.addFooterButton(localize('save', "Save"), () => this.onSubmitHandler());
this._cancelButton = this.addFooterButton(localize('cancel', "Cancel"), () => this.onCancelHandler(), 'right', true);
this._register(attachInputBoxStyler(this._viewNameInput!, this._themeService));
this._register(attachButtonStyler(this._submitButton, this._themeService));
this._register(attachButtonStyler(this._cancelButton, this._themeService));
this._register(this._viewNameInput.onDidChange(v => this.validate()));
attachModalDialogStyler(this, this._themeService);
this.validate();
}
private validate() {
let valid = true;
if (this._viewNameInput.validate()) {
valid = false;
}
this._submitButton.enabled = valid;
}
private onSubmitHandler() {
this._view.name = this._viewNameInput.value;
this._view.save();
this.close();
}
private onCancelHandler() {
this.close();
}
public close(): void {
return this.hide();
}
public open(): void {
this.show();
}
public override dispose(): void {
super.dispose();
for (let key in this._optionsMap) {
let widget = this._optionsMap[key];
widget.dispose();
delete this._optionsMap[key];
}
}
}

View File

@@ -0,0 +1,203 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { nb } from 'azdata';
import * as assert from 'assert';
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService';
import { TestConnectionManagementService } from 'sql/platform/connection/test/common/testConnectionManagementService';
import { NullAdsTelemetryService } from 'sql/platform/telemetry/common/adsTelemetryService';
import { NotebookEditorContentManager } from 'sql/workbench/contrib/notebook/browser/models/notebookInput';
import { DeleteViewAction, InsertCellAction } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsActions';
import { SessionManager } from 'sql/workbench/contrib/notebook/test/emptySessionClasses';
import { NotebookManagerStub } from 'sql/workbench/contrib/notebook/test/stubs';
import { ModelFactory } from 'sql/workbench/services/notebook/browser/models/modelFactory';
import { ICellModel, INotebookModelOptions, ViewMode } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel';
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
import { CellTypes } from 'sql/workbench/services/notebook/common/contracts';
import TypeMoq = require('typemoq');
import { URI } from 'vs/base/common/uri';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { NullLogService } from 'vs/platform/log/common/log';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { Memento } from 'vs/workbench/common/memento';
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
import sinon = require('sinon');
import { InsertCellsModal } from 'sql/workbench/contrib/notebook/browser/notebookViews/insertCellsModal';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
let initialNotebookContent: nb.INotebookContents = {
cells: [{
cell_type: CellTypes.Code,
source: ['insert into t1 values (c1, c2)'],
metadata: { language: 'python' },
execution_count: 1
}, {
cell_type: CellTypes.Markdown,
source: ['I am *markdown*'],
metadata: { language: 'python' },
execution_count: 1
}],
metadata: {
kernelspec: {
name: 'mssql',
language: 'sql'
},
},
nbformat: 4,
nbformat_minor: 5
};
suite('Notebook Views Actions', function (): void {
let defaultViewName = 'Default New View';
let notebookManagers = [new NotebookManagerStub()];
let mockSessionManager: TypeMoq.Mock<nb.SessionManager>;
let memento: TypeMoq.Mock<Memento>;
let queryConnectionService: TypeMoq.Mock<TestConnectionManagementService>;
let defaultModelOptions: INotebookModelOptions;
const logService = new NullLogService();
let defaultUri = URI.file('/some/path.ipynb');
let notificationService: TypeMoq.Mock<INotificationService>;
let capabilitiesService: TypeMoq.Mock<ICapabilitiesService>;
let instantiationService: IInstantiationService;
let configurationService: IConfigurationService;
let sandbox: sinon.SinonSandbox;
setup(() => {
sandbox = sinon.sandbox.create();
setupServices();
});
teardown(() => {
sandbox.restore();
});
test('delete view action accept', async function (): Promise<void> {
const dialogService = new TestDialogService();
const notificationService = new TestNotificationService();
const notebookViews = await initializeNotebookViewsExtension(initialNotebookContent);
const newView = notebookViews.createNewView(defaultViewName);
assert.strictEqual(notebookViews.getViews().length, 1, 'View not created');
notebookViews.setActiveView(newView);
assert.deepStrictEqual(notebookViews.getActiveView(), newView, 'Active view not set properly');
const deleteAction = new DeleteViewAction(notebookViews, dialogService, notificationService);
sandbox.stub(deleteAction, 'confirmDelete').withArgs(newView).returns(Promise.resolve(true));
await deleteAction.run();
assert.strictEqual(notebookViews.getViews().length, 0, 'View not deleted');
assert.strictEqual(notebookViews.notebook.viewMode, ViewMode.Notebook, 'View mode was note set to notebook');
});
test('delete view action decline', async function (): Promise<void> {
const dialogService = new TestDialogService();
const notificationService = new TestNotificationService();
const notebookViews = await initializeNotebookViewsExtension(initialNotebookContent);
const newView = notebookViews.createNewView(defaultViewName);
assert.strictEqual(notebookViews.getViews().length, 1, 'View not created');
notebookViews.setActiveView(newView);
assert.strictEqual(notebookViews.getActiveView(), newView, 'Active view not set properly');
const deleteAction = new DeleteViewAction(notebookViews, dialogService, notificationService);
sandbox.stub(deleteAction, 'confirmDelete').withArgs(newView).returns(Promise.resolve(false));
await deleteAction.run();
assert.strictEqual(notebookViews.getViews().length, 1, 'View should not have deleted');
});
test('show insertcellmodal', async function (): Promise<void> {
let opened = false;
let rendered = false;
const notebookViews = await initializeNotebookViewsExtension(initialNotebookContent);
const newView = notebookViews.createNewView(defaultViewName);
notebookViews.setActiveView(newView);
let insertCellsModal = TypeMoq.Mock.ofType(InsertCellsModal, TypeMoq.MockBehavior.Strict,
(cell: ICellModel) => { }, // onInsert
notebookViews, // _context
undefined, // _containerRef
undefined, // _componentFactoryResolver
undefined, // logService
undefined, // themeService
undefined, // layoutService
undefined, // clipboardService
new MockContextKeyService(), // contextkeyservice
undefined, // telemetryService
undefined, // textResourcePropertiesService
);
insertCellsModal.setup(x => x.render()).callback(() => {
rendered = true;
});
insertCellsModal.setup(x => x.open()).callback(() => {
opened = true;
});
const instantiationService = new InstantiationService();
sinon.stub(instantiationService, 'createInstance').withArgs(InsertCellsModal, sinon.match.any, sinon.match.any, sinon.match.any, sinon.match.any).returns(insertCellsModal.object);
const insertCellAction = new InsertCellAction((cell: ICellModel) => { }, notebookViews, undefined, undefined, instantiationService);
await insertCellAction.run();
assert.ok(rendered);
assert.ok(opened);
});
function setupServices() {
mockSessionManager = TypeMoq.Mock.ofType(SessionManager);
notebookManagers[0].sessionManager = mockSessionManager.object;
notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose);
capabilitiesService = TypeMoq.Mock.ofType(TestCapabilitiesService);
memento = TypeMoq.Mock.ofType(Memento, TypeMoq.MockBehavior.Loose, '');
memento.setup(x => x.getMemento(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => void 0);
queryConnectionService = TypeMoq.Mock.ofType(TestConnectionManagementService, TypeMoq.MockBehavior.Loose, memento.object, undefined, new TestStorageService());
queryConnectionService.callBase = true;
let serviceCollection = new ServiceCollection();
instantiationService = new InstantiationService(serviceCollection, true);
configurationService = new TestConfigurationService();
defaultModelOptions = {
notebookUri: defaultUri,
factory: new ModelFactory(instantiationService),
notebookManagers,
contentManager: undefined,
notificationService: notificationService.object,
connectionService: queryConnectionService.object,
providerId: 'SQL',
cellMagicMapper: undefined,
defaultKernel: undefined,
layoutChanged: undefined,
capabilitiesService: capabilitiesService.object
};
}
async function initializeNotebookViewsExtension(contents: nb.INotebookContents): Promise<NotebookViewsExtension> {
let mockContentManager = TypeMoq.Mock.ofType(NotebookEditorContentManager);
mockContentManager.setup(c => c.loadContent()).returns(() => Promise.resolve(contents));
defaultModelOptions.contentManager = mockContentManager.object;
let model = new NotebookModel(defaultModelOptions, undefined, logService, undefined, new NullAdsTelemetryService(), queryConnectionService.object, configurationService);
await model.loadContents();
await model.requestModelLoad();
return new NotebookViewsExtension(model);
}
});