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

@@ -742,6 +742,7 @@
"chart.js",
"plotly.js",
"angular2-grid",
"html-to-image",
"html-query-plan",
"turndown",
"gridstack",

View File

@@ -30,6 +30,7 @@ expressly granted herein, whether by implication, estoppel or otherwise.
getmac: https://github.com/bevry/getmac
graceful-fs: https://github.com/isaacs/node-graceful-fs
gridstack: https://github.com/gridstack/gridstack.js
html-to-image: https://github.com/bubkoo/html-to-image
html-query-plan: https://github.com/JustinPealing/html-query-plan
http-proxy-agent: https://github.com/TooTallNate/node-https-proxy-agent
https-proxy-agent: https://github.com/TooTallNate/node-https-proxy-agent
@@ -520,6 +521,32 @@ SOFTWARE.
=========================================
END OF gridstack NOTICES AND INFORMATION
%% html-to-image NOTICES AND INFORMATION BEGIN HERE
=========================================
MIT License
Copyright (c) 2017 W.Y.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
=========================================
END OF html-to-image NOTICES AND INFORMATION
%% html-query-plan NOTICES AND INFORMATION BEGIN HERE
=========================================
The MIT License (MIT)

View File

@@ -78,13 +78,14 @@
"graceful-fs": "4.2.3",
"gridstack": "^3.1.3",
"html-query-plan": "git://github.com/kburtram/html-query-plan.git#2.6",
"html-to-image": "^1.6.2",
"http-proxy-agent": "^2.1.0",
"https-proxy-agent": "^2.2.3",
"iconv-lite-umd": "0.6.8",
"jquery": "3.5.0",
"jschardet": "2.3.0",
"mark.js": "^8.11.1",
"keytar": "7.2.0",
"mark.js": "^8.11.1",
"minimist": "^1.2.5",
"native-is-elevated": "0.4.3",
"native-keymap": "2.2.1",
@@ -223,8 +224,8 @@
"style-loader": "^1.0.0",
"temp-write": "^3.4.0",
"ts-loader": "^6.2.1",
"typemoq": "^0.3.2",
"tsec": "0.1.4",
"typemoq": "^0.3.2",
"typescript": "^4.3.0-dev.20210426",
"typescript-formatter": "7.1.0",
"underscore": "^1.12.1",

View File

@@ -20,6 +20,7 @@
"graceful-fs": "4.2.3",
"gridstack": "^3.1.3",
"html-query-plan": "git://github.com/kburtram/html-query-plan.git#2.6",
"html-to-image": "^1.6.2",
"http-proxy-agent": "^2.1.0",
"https-proxy-agent": "^2.2.3",
"iconv-lite-umd": "0.6.8",

View File

@@ -16,6 +16,7 @@
"chart.js": "^2.9.4",
"gridstack": "^3.1.3",
"html-query-plan": "git://github.com/kburtram/html-query-plan.git#2.6",
"html-to-image": "^1.6.2",
"iconv-lite-umd": "0.6.8",
"jquery": "3.5.0",
"jschardet": "2.3.0",

View File

@@ -180,6 +180,11 @@ has-flag@^3.0.0:
version "2.5.0"
resolved "git://github.com/kburtram/html-query-plan.git#c524feb824e4960897ad875a37af068376a2b4a3"
html-to-image@^1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/html-to-image/-/html-to-image-1.6.2.tgz#9d444424cc638df13db5c7be810ac0d2962f5edd"
integrity sha512-X6X7wJW2KQ+AGqMeBITdntCcQnxBgZY62MdGOi042Y70+0SMe8/iJCzUv8RNaUoXqUjWw5FPyoTDmdGoapTQIQ==
htmlparser2@^3.9.0:
version "3.10.1"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"

View File

@@ -402,6 +402,11 @@ has-flag@^3.0.0:
version "2.5.0"
resolved "git://github.com/kburtram/html-query-plan.git#c524feb824e4960897ad875a37af068376a2b4a3"
html-to-image@^1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/html-to-image/-/html-to-image-1.6.2.tgz#9d444424cc638df13db5c7be810ac0d2962f5edd"
integrity sha512-X6X7wJW2KQ+AGqMeBITdntCcQnxBgZY62MdGOi042Y70+0SMe8/iJCzUv8RNaUoXqUjWw5FPyoTDmdGoapTQIQ==
htmlparser2@^3.9.0:
version "3.10.1"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"

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);
}
});

View File

@@ -60,6 +60,7 @@
'angular2-slickgrid': `${window.location.origin}/static/remote/web/node_modules/angular2-slickgrid/out/bundles/angular2-slickgrid.umd.js`,
'chart.js': `${window.location.origin}/static/remote/web/node_modules/chart.js/dist/Chart.bundle.min.js`,
'html-query-plan': `${window.location.origin}/static/remote/web/node_modules/html-query-plan/dist/index.min.js`,
'html-to-image': `${window.location.origin}/static/web/node_modules/html-to-image/dist/html-to-image.js`,
'ng2-charts': `${window.location.origin}/static/remote/web/node_modules/ng2-charts/bundles/ng2-charts.umd.js`,
'rxjs/Observable': `${window.location.origin}/static/remote/web/node_modules/rxjs/bundles/Rx.min.js?0`,
'rxjs/observable/merge': `${window.location.origin}/static/remote/web/node_modules/rxjs/bundles/Rx.min.js?1`,

View File

@@ -60,6 +60,7 @@
'angular2-slickgrid': `${window.location.origin}/static/node_modules/angular2-slickgrid/out/bundles/angular2-slickgrid.umd.js`,
'chart.js': `${window.location.origin}/static/node_modules/chart.js/dist/Chart.bundle.min.js`,
'html-query-plan': `${window.location.origin}/static/node_modules/html-query-plan/dist/index.min.js`,
'html-to-image': `${window.location.origin}/static/web/node_modules/html-to-image/dist/html-to-image.js`,
'ng2-charts': `${window.location.origin}/static/node_modules/ng2-charts/bundles/ng2-charts.umd.js`,
'rxjs/Observable': `${window.location.origin}/static/node_modules/rxjs/bundles/Rx.min.js?0`,
'rxjs/observable/merge': `${window.location.origin}/static/node_modules/rxjs/bundles/Rx.min.js?1`,

View File

@@ -103,6 +103,7 @@ function initLoader(opts) {
'@angular/platform-browser-dynamic',
'@angular/router',
'angular2-grid',
'html-to-image',
'gridstack/dist/h5/gridstack-dd-native',
'ng2-charts',
'rxjs/add/observable/of',

View File

@@ -5284,6 +5284,11 @@ html-escaper@^2.0.0:
version "2.5.0"
resolved "git://github.com/kburtram/html-query-plan.git#c524feb824e4960897ad875a37af068376a2b4a3"
html-to-image@^1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/html-to-image/-/html-to-image-1.6.2.tgz#9d444424cc638df13db5c7be810ac0d2962f5edd"
integrity sha512-X6X7wJW2KQ+AGqMeBITdntCcQnxBgZY62MdGOi042Y70+0SMe8/iJCzUv8RNaUoXqUjWw5FPyoTDmdGoapTQIQ==
"htmlparser2@>= 3.7.3 < 4.0.0":
version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"