diff --git a/.eslintrc.json b/.eslintrc.json index fc9c8f771c..ad5f15a890 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -740,6 +740,8 @@ "angular2-grid", "html-query-plan", "turndown", + "gridstack", + "gridstack/**", "mark.js", "vscode-textmate", "vscode-oniguruma", diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 4c83c9d3de..deb81cda1a 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -29,6 +29,7 @@ expressly granted herein, whether by implication, estoppel or otherwise. gc-signals: https://github.com/Microsoft/node-gc-signals getmac: https://github.com/bevry/getmac graceful-fs: https://github.com/isaacs/node-graceful-fs + gridstack: https://github.com/gridstack/gridstack.js 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 @@ -493,6 +494,32 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ========================================= END OF graceful-fs NOTICES AND INFORMATION +%% gridstack NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014-2020 Alain Dumesny, Dylan Weiss, Pavel Reznikov + +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 gridstack NOTICES AND INFORMATION + %% html-query-plan NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) diff --git a/package.json b/package.json index 49844153c4..817fb23aa3 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "chart.js": "^2.9.4", "chokidar": "3.5.1", "graceful-fs": "4.2.3", + "gridstack": "^3.1.3", "html-query-plan": "git://github.com/kburtram/html-query-plan.git#2.6", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.3", diff --git a/src/sql/workbench/contrib/notebook/browser/media/light/notebook_views_card_handle.svg b/src/sql/workbench/contrib/notebook/browser/media/light/notebook_views_card_handle.svg new file mode 100644 index 0000000000..b48d5c1313 --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/media/light/notebook_views_card_handle.svg @@ -0,0 +1 @@ + diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.module.ts b/src/sql/workbench/contrib/notebook/browser/notebook.module.ts index d7d69a382c..37fa32300b 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.module.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.module.ts @@ -33,6 +33,10 @@ import { CollapseComponent } from 'sql/workbench/contrib/notebook/browser/cellVi import { MarkdownToolbarComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/markdownToolbar.component'; import { CellToolbarComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/cellToolbar.component'; import { NotebookEditorComponent } from 'sql/workbench/contrib/notebook/browser/notebookEditor.component'; +import { NotebookViewComponent } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViews.component'; +import { NotebookViewsCodeCellComponent } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component'; +import { NotebookViewsCardComponent } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component'; +import { NotebookViewsGridComponent } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component'; const outputComponentRegistry = Registry.as(OutputComponentExtensions.CellComponentContributions); @@ -54,6 +58,10 @@ export const NotebookModule = (params, selector: string, instantiationService: I PlaceholderCellComponent, NotebookComponent, NotebookEditorComponent, + NotebookViewComponent, + NotebookViewsCardComponent, + NotebookViewsGridComponent, + NotebookViewsCodeCellComponent, ComponentHostDirective, OutputAreaComponent, OutputComponent, diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/cellToolbar.css b/src/sql/workbench/contrib/notebook/browser/notebookViews/cellToolbar.css new file mode 100644 index 0000000000..d7a20a413c --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/cellToolbar.css @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.nb-grid-stack .grid-stack-item.notebook-cell .actionbar { + border-width: 1px; + border-style: solid; + position: absolute; + left: 20px; + top: -20px; +} + +.nb-grid-stack .grid-stack-item.notebook-cell .actionbar .carbon-taskbar .monaco-action-bar.animated { + padding: 0; +} + +.nb-grid-stack .grid-stack-item.notebook-cell .actionbar .carbon-taskbar.monaco-toolbar .monaco-action-bar.animated ul.actions-container { + display: flex; + list-style: none; + margin: 0; + padding: 0; +} + +.nb-grid-stack .grid-stack-item.notebook-cell .actionbar ul.actions-container li.action-item { + display: inline-flex; + margin-right: 0; + text-align: center; +} + +.nb-grid-stack .grid-stack-item.notebook-cell .actionbar ul.actions-container li:last-child { + margin-right: 0; +} +.nb-grid-stack .grid-stack-item.notebook-cell .actionbar .carbon-taskbar .action-label { + padding: 0; +} +.nb-grid-stack .grid-stack-item.notebook-cell .actionbar .monaco-action-bar .action-label { + margin-right: 0; +} +.nb-grid-stack .grid-stack-item.notebook-cell .actionbar ul.actions-container li a.masked-icon { + display: flex; +} +.nb-grid-stack .grid-stack-item.notebook-cell .actionbar ul.actions-container li a.masked-icon, +.nb-grid-stack .grid-stack-item.notebook-cell .actionbar ul.actions-container li a.masked-icon:before { + height: 24px; + width: 29px; +} +.nb-grid-stack .grid-stack-item.notebook-cell .actionbar .codicon.masked-icon, +.vs .nb-grid-stack .grid-stack-item.notebook-cell .actionbar .codicon.masked-icon, +.vs-dark .nb-grid-stack .grid-stack-item.notebook-cell .actionbar .codicon.masked-icon, +.hc-black .nb-grid-stack .grid-stack-item.notebook-cell .actionbar .codicon.masked-icon { + background-image: none; +} + +.nb-grid-stack .grid-stack-item.notebook-cell .actionbar .notebook-button.toolbarIconStop { + margin: 0 2px; + background-size: 20px 25px; + height: 24px; + width: 29px; + background-repeat: no-repeat; +} diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViews.component.html b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViews.component.html new file mode 100644 index 0000000000..14c14d7dfb --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViews.component.html @@ -0,0 +1,15 @@ + +
+
+
+ + +
+
+
+
diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViews.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViews.component.ts new file mode 100644 index 0000000000..84354f0e9f --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViews.component.ts @@ -0,0 +1,278 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { 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'; +import { ICellEditorProvider, INotebookParams, INotebookService, INotebookEditor, NotebookRange, INotebookSection, DEFAULT_NOTEBOOK_PROVIDER, SQL_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/browser/notebookService'; +import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; +import * as notebookUtils from 'sql/workbench/services/notebook/browser/models/notebookUtils'; +import { IBootstrapParams } from 'sql/workbench/services/bootstrap/common/bootstrapParams'; +import { Action, IActionViewItem } from 'vs/base/common/actions'; +import { LabeledMenuItemActionItem } from 'sql/platform/actions/browser/menuEntryActionViewItem'; +import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar'; +import { MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { localize } from 'vs/nls'; +import { Deferred } from 'sql/base/common/promise'; +import { AngularDisposable } from 'sql/base/browser/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { CellType, CellTypes } from 'sql/workbench/services/notebook/common/contracts'; +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 { NotebookViewsGridComponent } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component'; + +export const NOTEBOOKVIEWS_SELECTOR: string = 'notebook-view-component'; + +@Component({ + selector: NOTEBOOKVIEWS_SELECTOR, + templateUrl: decodeURI(require.toUrl('./notebookViews.component.html')) +}) + +export class NotebookViewComponent extends AngularDisposable implements INotebookEditor { + @Input() model: NotebookModel; + @Input() activeView: INotebookView; + @Input() views: NotebookViewsExtension; + + @ViewChild('container', { read: ElementRef }) private _container: ElementRef; + @ViewChild('viewsToolbar', { read: ElementRef }) private _viewsToolbar: ElementRef; + @ViewChild(NotebookViewsGridComponent) private _gridstack: NotebookViewsGridComponent; + @ViewChildren(CodeCellComponent) private _codeCells: QueryList; + @ViewChildren(TextCellComponent) private _textCells: QueryList; + + protected _actionBar: Taskbar; + public previewFeaturesEnabled: boolean = false; + private _modelReadyDeferred = new Deferred(); + + private _scrollTop: number; + + constructor( + @Inject(IBootstrapParams) private _notebookParams: INotebookParams, + @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, + @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 + ) { + super(); + this._register(this._configurationService.onDidChangeConfiguration(e => { + this.previewFeaturesEnabled = this._configurationService.getValue('workbench.enablePreviewFeatures'); + })); + } + + public get notebookParams(): INotebookParams { + return this._notebookParams; + } + + public get id(): string { + return this.notebookParams.notebookUri.toString(); + } + + isDirty(): boolean { + return this.notebookParams.input.isDirty(); + } + isActive(): boolean { + return this._editorService.activeEditor ? this._editorService.activeEditor.matches(this.notebookParams.input) : false; + } + isVisible(): boolean { + let notebookEditor = this.notebookParams.input; + return this._editorService.visibleEditors.some(e => e.matches(notebookEditor)); + } + executeEdits(edits: ISingleNotebookEditOperation[]): boolean { + throw new Error('Method not implemented.'); + } + async runCell(cell: ICellModel): Promise { + await this.modelReady; + let uriString = cell.cellUri.toString(); + if (this.model.cells.findIndex(c => c.cellUri.toString() === uriString) > -1) { + this.selectCell(cell); + return cell.runCell(this._notificationService, this._connectionManagementService); + } else { + throw new Error(localize('cellNotFound', "cell with URI {0} was not found in this model", uriString)); + } + } + + public async runAllCells(startCell?: ICellModel, endCell?: ICellModel): Promise { + await this.modelReady; + let codeCells = this.model.cells.filter(cell => cell.cellType === CellTypes.Code); + if (codeCells && codeCells.length) { + // For the run all cells scenario where neither startId not endId are provided, set defaults + let startIndex = 0; + let endIndex = codeCells.length; + if (!isUndefinedOrNull(startCell)) { + startIndex = codeCells.findIndex(c => c.id === startCell.id); + } + if (!isUndefinedOrNull(endCell)) { + endIndex = codeCells.findIndex(c => c.id === endCell.id); + } + for (let i = startIndex; i < endIndex; i++) { + let cellStatus = await this.runCell(codeCells[i]); + if (!cellStatus) { + throw new Error(localize('cellRunFailed', "Run Cells failed - See error in output of the currently selected cell for more information.")); + } + } + } + return true; + } + clearOutput(cell: ICellModel): Promise { + throw new Error('Method not implemented.'); + } + clearAllOutputs(): Promise { + throw new Error('Method not implemented.'); + } + getSections(): INotebookSection[] { + throw new Error('Method not implemented.'); + } + navigateToSection(sectionId: string): void { + throw new Error('Method not implemented.'); + } + deltaDecorations(newDecorationRange: NotebookRange, oldDecorationRange: NotebookRange): void { + throw new Error('Method not implemented.'); + } + addCell(cellType: CellType, index?: number, event?: UIEvent) { + throw new Error('Method not implemented.'); + } + insertCell(cell: ICellModel) { + this._gridstack.onCellChanged({ cell: cell, event: 'insert' }); + } + + ngOnInit() { + this.initViewsToolbar(); + this._notebookService.addNotebookEditor(this); + this._modelReadyDeferred.resolve(this.model); + this.setScrollPosition(); + + this.doLoad().catch(e => onUnexpectedError(e)); + } + + ngOnDestroy() { + this.dispose(); + } + + ngOnChanges() { + this.initViewsToolbar(); + } + + private async doLoad(): Promise { + await this.awaitNonDefaultProvider(); + await this.model.requestModelLoad(); + await this.model.onClientSessionReady; + this.detectChanges(); + } + + private async awaitNonDefaultProvider(): Promise { + // Wait on registration for now. Long-term would be good to cache and refresh + await this._notebookService.registrationComplete; + this.model.standardKernels = this._notebookParams.input.standardKernels; + // Refresh the provider if we had been using default + let providerInfo = await this._notebookParams.providerInfo; + + if (DEFAULT_NOTEBOOK_PROVIDER === providerInfo.providerId) { + let providers = notebookUtils.getProvidersForFileName(this._notebookParams.notebookUri.fsPath, this._notebookService); + let tsqlProvider = providers.find(provider => provider === SQL_NOTEBOOK_PROVIDER); + providerInfo.providerId = tsqlProvider ? SQL_NOTEBOOK_PROVIDER : providers[0]; + } + } + + public get cells(): ICellModel[] { + return this.model ? this.model.cells : []; + } + + public selectCell(cell: ICellModel, event?: Event) { + if (event) { + event.stopPropagation(); + } + if (!this.model.activeCell || this.model.activeCell.id !== cell.id) { + this.model.updateActiveCell(cell); + this.detectChanges(); + } + } + + private setScrollPosition(): void { + if (this._notebookParams && this._notebookParams.input) { + this._register(this._notebookParams.input.layoutChanged(() => { + let containerElement = this._container.nativeElement; + containerElement.scrollTop = this._scrollTop; + })); + } + } + + /** + * Saves scrollTop value on scroll change + */ + public scrollHandler(event: Event) { + this._scrollTop = (event.srcElement).scrollTop; + } + + public unselectActiveCell() { + this.model.updateActiveCell(undefined); + this.detectChanges(); + } + + protected initViewsToolbar() { + let taskbar = this._viewsToolbar.nativeElement; + + if (!this._actionBar) { + this._actionBar = new Taskbar(taskbar, { actionViewItemProvider: action => this.actionItemProvider(action as Action) }); + this._actionBar.context = this._notebookParams.notebookUri;//this.model; + taskbar.classList.add('in-preview'); + } + + let titleElement = document.createElement('li'); + let titleText = document.createElement('span'); + titleText.innerHTML = this.activeView?.name; + titleElement.appendChild(titleText); + titleElement.style.marginRight = '25px'; + titleElement.style.minHeight = '25px'; + + this._actionBar.setContent([ + { element: titleElement }, + { element: Taskbar.createTaskbarSeparator() }, + ]); + } + + private actionItemProvider(action: Action): IActionViewItem { + // Check extensions to create ActionItem; otherwise, return undefined + // This is similar behavior that exists in MenuItemActionItem + if (action instanceof MenuItemAction) { + + if (action.item.id.includes('jupyter.cmd') && this.previewFeaturesEnabled) { + action.tooltip = action.label; + action.label = ''; + } + return new LabeledMenuItemActionItem(action, this._keybindingService, this._notificationService, 'notebook-button fixed-width'); + } + return undefined; + } + + private detectChanges(): void { + if (!(this._changeRef['destroyed'])) { + this._changeRef.detectChanges(); + } + } + + public get modelReady(): Promise { + return this._modelReadyDeferred.promise; + } + + public get cellEditors(): ICellEditorProvider[] { + let editors: ICellEditorProvider[] = []; + if (this._codeCells) { + this._codeCells.toArray().forEach(cell => editors.push(...cell.cellEditors)); + } + if (this._textCells) { + this._textCells.toArray().forEach(cell => editors.push(...cell.cellEditors)); + editors.push(...this._textCells.toArray()); + } + return editors; + } +} diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.html b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.html new file mode 100644 index 0000000000..47c7fad7d3 --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.html @@ -0,0 +1,34 @@ + + +
+
+
+
+
+ + + + +
+
+
+
+
diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.ts new file mode 100644 index 0000000000..cbe5e2d86c --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./cellToolbar'; +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'; + +@Component({ + selector: 'view-card-component', + templateUrl: decodeURI(require.toUrl('./notebookViewsCard.component.html')) +}) +export class NotebookViewsCardComponent implements OnInit { + private _metadata: INotebookViewCellMetadata; + private _activeView: INotebookView; + + @Input() cell: ICellModel; + @Input() model: NotebookModel; + @Input() views: NotebookViewsExtension; + @Input() ready: boolean; + @Output() onChange: EventEmitter = new EventEmitter(); + + @ViewChild('templateRef') templateRef: TemplateRef; + @ViewChild('item', { read: ElementRef }) private _item: ElementRef; + + constructor( + @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, + ) { } + + ngOnInit() { } + + ngOnChanges() { + if (this.views) { + this._activeView = this.views.getActiveView(); + this._metadata = this.views.getCellMetadata(this.cell); + } + } + + ngAfterContentInit() { + if (this.views) { + this._activeView = this.views.getActiveView(); + this._metadata = this.views.getCellMetadata(this.cell); + } + } + + ngAfterViewInit() { + this.detectChanges(); + } + + get elementRef(): ElementRef { + return this._item; + } + + changed(event: CellChangeEventType) { + this.onChange.emit({ cell: this.cell, event: event }); + } + + detectChanges() { + this._changeRef.detectChanges(); + } + + public selectCell(cell: ICellModel, event?: Event) { + event?.stopPropagation(); + + if (!this.model.activeCell || this.model.activeCell.id !== cell.id) { + this.model.updateActiveCell(cell); + this.changed('active'); + } + } + + public hide(): void { + this.changed('hide'); + } + + public get data(): any { + return this._metadata?.views?.find(v => v.guid === this._activeView.guid); + } + + public get width(): number { + return this.data?.width ? this.data.width : DEFAULT_VIEW_CARD_WIDTH; + } + + public get height(): number { + return this.data.height ? this.data.height : DEFAULT_VIEW_CARD_HEIGHT; + } + + public get x(): number { + return this.data?.x; + } + + public get y(): number { + return this.data?.y; + } + + public get display(): boolean { + if (!this._metadata || !this._activeView) { + return true; + } + + return !this.data?.hidden; + } + + public get showActionBar(): boolean { + return this.cell.active; + } +} diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.html b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.html new file mode 100644 index 0000000000..dbc2fd480a --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.html @@ -0,0 +1,16 @@ + +
+
+ + +
+ {{emptyCellText}} +
+ +
+
diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.ts new file mode 100644 index 0000000000..04015c1b80 --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ChangeDetectorRef, Component, forwardRef, Inject } from '@angular/core'; +import { CodeCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/codeCell.component'; +import { localize } from 'vs/nls'; + + +export const CODE_SELECTOR: string = 'views-code-cell-component'; + +@Component({ + selector: CODE_SELECTOR, + templateUrl: decodeURI(require.toUrl('./notebookViewsCodeCell.component.html')) +}) + +export class NotebookViewsCodeCellComponent extends CodeCellComponent { + public readonly emptyCellText: string = localize('viewsCodeCell.emptyCellText', "Please run this cell to view outputs."); + + constructor(@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef) { + super(changeRef); + } + + get outputs(): nb.ICellOutput[] { + return this.cellModel.outputs.filter((output: nb.IDisplayResult) => output.data && output.data['text/plain'] !== ''); + } +} diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.html b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.html new file mode 100644 index 0000000000..38798f10f4 --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.html @@ -0,0 +1,15 @@ + +
+ + + + +
+
+ {{emptyText}} +
diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.ts new file mode 100644 index 0000000000..bc8f089203 --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./notebookViewsGrid'; +import { Component, OnInit, ViewChildren, QueryList, Input, Inject, forwardRef, ChangeDetectorRef } from '@angular/core'; +import { NotebookViewsCardComponent } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component'; +import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; +import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; +import { GridStack, GridStackEvent, GridStackNode } from 'gridstack'; +import 'gridstack/dist/h5/gridstack-dd-native'; +import { localize } from 'vs/nls'; +import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension'; +import { CellChangeEvent, INotebookViewCell } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews'; + +@Component({ + selector: 'notebook-views-grid-component', + templateUrl: decodeURI(require.toUrl('./notebookViewsGrid.component.html')) +}) +export class NotebookViewsGridComponent implements OnInit { + @Input() cells: ICellModel[]; + @Input() model: NotebookModel; + @Input() views: NotebookViewsExtension; + + @ViewChildren(NotebookViewsCardComponent) private _items: QueryList; + + protected _grid: GridStack; + public loaded: boolean; + + constructor( + @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, + ) { + this.loaded = false; + } + + public get empty(): boolean { + return !this._items || !this._items.find(item => item.display); + } + + public get hiddenItems(): NotebookViewsCardComponent[] { + return this._items.filter(item => !item.display); + } + + public get emptyText(): String { + return localize('emptyText', "This view is empty. Add a cell to this view by clicking the Insert Cells button."); + } + + ngOnInit() { } + + ngAfterViewInit() { + const self = this; + + self._grid = GridStack.init({ + alwaysShowResizeHandle: false, + styleInHead: true + }); + + this.loaded = true; + this.detectChanges(); + + self._grid.on('added', function (e: Event, items: GridStackNode[]) { self.persist('added', items, self._grid, self._items); }); + self._grid.on('removed', function (e: Event, items: GridStackNode[]) { self.persist('removed', items, self._grid, self._items); }); + self._grid.on('change', function (e: Event, items: GridStackNode[]) { self.persist('change', items, self._grid, self._items); }); + } + + private detectChanges(): void { + if (!(this._changeRef['destroyed'])) { + this._changeRef.detectChanges(); + } + } + + async onCellChanged(e: CellChangeEvent): Promise { + const currentView = this.views.getActiveView(); + if (this._grid && currentView) { + const cellElem: HTMLElement = this._grid.el.querySelector(`[data-cell-id='${e.cell.cellGuid}']`); + if (cellElem && e.event === 'hide') { + this._grid.removeWidget(cellElem); + currentView.hideCell(e.cell); + } + + if (e.cell && e.event === 'insert') { + const component = this._items.find(x => x.cell.cellGuid === e.cell.cellGuid); + // Prevent an awkward movement on the grid by moving this out of view first + currentView.moveCell(e.cell, 9999, 0); + currentView.insertCell(e.cell); + + const el = this._grid.getGridItems().find(x => x.getAttribute('data-cell-id') === e.cell.cellGuid); + this._grid.makeWidget(el); + this._grid.update(el, { x: 0, y: 0 }); + this._grid.resizable(el, true); + this._grid.movable(el, true); + + component.detectChanges(); + } + this.detectChanges(); + } + } + + /** + * Update the document model with the gridstack data as metadata + */ + persist(action: GridStackEvent, changedItems: GridStackNode[], grid: GridStack, items: QueryList): void { + changedItems.forEach((changedItem) => { + const cellId = changedItem.el.getAttribute('data-cell-id'); + const item = items.toArray().find(item => item.cell.cellGuid === cellId); + + const activeView = this.views.getActiveView(); + if (item && activeView) { + const update: INotebookViewCell = { + guid: activeView.guid, + x: changedItem.x, + y: changedItem.y, + width: changedItem.w, + height: changedItem.h + }; + + if (action === 'added') { + update.hidden = false; + } else if (action === 'removed') { + update.hidden = true; + } + + this.views.updateCell(item.cell, activeView, update); + } + }); + + } +} diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.css b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.css new file mode 100644 index 0000000000..97d5e61398 --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.css @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.nb-grid-stack { + margin: 0 8px; +} + +.nb-grid-stack + .empty-message{ + text-align: center; +} + +.nb-grid-stack > .grid-stack-item { + margin: 0; +} +.nb-grid-stack > .grid-stack-item .grid-stack-item-content { + display: flex; + cursor: move; + box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.13); + border-radius: 2px; + overflow: visible; +} +.nb-grid-stack > .grid-stack-item .grid-stack-item-content > .grid-stack-content-wrapper { + width: 100%; + display: flex; + flex-direction: column; + flex: 1; + margin-bottom: 20px; +} +.nb-grid-stack > .grid-stack-item .grid-stack-item-content .grid-stack-content-wrapper-inner { + overflow-y: auto; + height: 100%; + cursor: auto; +} +.nb-grid-stack > .grid-stack-item .grid-stack-item-content ::-webkit-scrollbar { + width: 8px; +} +.nb-grid-stack > .grid-stack-item .grid-stack-item-content .grid-stack-content-header { + flex-direction: row; + justify-content: space-between; + display: flex; + margin-bottom: 3px; + padding: 0 5px; +} +.nb-grid-stack > .grid-stack-item .grid-stack-item-content .grid-stack-content-header .action-label.codicon{ + background-size: 16px; + padding: 11px; +} +.nb-grid-stack > .grid-stack-item .grid-stack-item-content .grid-stack-item-title { + font-size: 13px; + line-height: 25px; + font-weight: 400; +} +.nb-grid-stack > .grid-stack-item .grid-stack-item-content .actionbar { + top: -14px; + z-index: 999; +} +.nb-grid-stack > .grid-stack-item.notebook-cell.active .actionbar { + z-index: 1; + transform: translate(0px, 7px); +} +.nb-grid-stack > .grid-stack-item.ui-draggable-dragging > .grid-stack-item-content,.nb-grid-stack > .grid-stack-item.ui-resizable-resizing > .grid-stack-item-content{ + box-shadow:1px 4px 6px rgba(0,0,0,.2); + opacity:.8 +} +.nb-grid-stack > .grid-stack-item > .ui-resizable-se,.nb-grid-stack > .grid-stack-item > .ui-resizable-sw{ + background-image:url("../media/light/notebook_views_card_handle.svg"); + background-repeat:no-repeat; + background-position:center; + transform: rotate(0deg); +} +.nb-grid-stack > .grid-stack-item > .ui-resizable-nw{ + cursor:nw-resize; + width:20px; + height:20px; + left:10px; + top:0 +} +.nb-grid-stack > .grid-stack-item > .ui-resizable-n{ + cursor:n-resize; + height:10px; + top:0; + left:25px; + right:25px +} +.nb-grid-stack > .grid-stack-item > .ui-resizable-ne{ + cursor:ne-resize; + width:20px; + height:20px; + right:10px; + top:0 +} +.nb-grid-stack > .grid-stack-item > .ui-resizable-e{ + cursor:e-resize; + width:10px; + right:10px; + top:15px; + bottom:15px +} +.nb-grid-stack > .grid-stack-item > .ui-resizable-se{ + cursor:se-resize; + width:20px; + height:20px; + right:5px; + bottom:0 +} +.nb-grid-stack > .grid-stack-item > .ui-resizable-s{ + cursor:s-resize; + height:10px; + left:25px; + bottom:0; + right:25px +} +.nb-grid-stack > .grid-stack-item > .ui-resizable-sw{ + cursor:sw-resize; + width:20px; + height:20px; + left:10px; + bottom:0 +} +.nb-grid-stack > .grid-stack-item > .ui-resizable-w{ + cursor:w-resize; + width:10px; + left:10px; + top:15px; + bottom:15px +} diff --git a/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewModel.ts b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewModel.ts index 265e28e270..3ae116bbfd 100644 --- a/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewModel.ts +++ b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewModel.ts @@ -104,4 +104,8 @@ export class NotebookViewModel implements INotebookView { this._notebookViews.removeView(this.guid); this._onDeleted.fire(this); } + + public toJSON() { + return { guid: this.guid, name: this._name } as NotebookViewModel; + } } diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index 909392d94e..413b6a1684 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -103,6 +103,7 @@ function initLoader(opts) { '@angular/platform-browser-dynamic', '@angular/router', 'angular2-grid', + 'gridstack/dist/h5/gridstack-dd-native', 'ng2-charts', 'rxjs/add/observable/of', 'rxjs/add/observable/fromPromise', diff --git a/yarn.lock b/yarn.lock index a331c0f63e..36b37f3045 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4802,6 +4802,11 @@ graceful-fs@^4.1.15, graceful-fs@^4.2.3: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +gridstack@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-3.1.3.tgz#982572b5d7b3608ab0463821a9798cab3766acaf" + integrity sha512-FNmuz5d1qRFXxK/tWj8PsAECyiFOX6Pnj4aaW+zd9BcTI9yY1hT7gq8y5CTBZ3vIy7VE+99jDwvb9WWchs0xlw== + growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"