mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-13 17:22:15 -05:00
Notebook Views Actions (#16207)
This adds the actions currently needed by the views
This commit is contained in:
@@ -742,6 +742,7 @@
|
||||
"chart.js",
|
||||
"plotly.js",
|
||||
"angular2-grid",
|
||||
"html-to-image",
|
||||
"html-query-plan",
|
||||
"turndown",
|
||||
"gridstack",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()};
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
@@ -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 }
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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`,
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user