Notebook views UI (#13914)

* Add notebook editor

Introduce notebook editor component to allow for separate notebook displays in order to accomodate notebook views

* Localize notebook views configuration title

* Refactor view mode and remove the views configuration while it is unused

* Only fire view mode changed event when the value has been changed

* Remove notebook views contribution

* Add metadata capabilities

* Notebook views definitions

* Add notebook views models

* Views test

* Rename type arguments

* Additional tests

* Fix unused import

* Notebook views changes

* Add notebookviews.css

* Update notebookViewModel.ts

* Add cell toolbar styles

* Upgrade gridstack

* Add gridstack styles

* Remove ununsed references

These are part of the next PR and not available yet

* Remove gridstack static file

* Add gridstack as a module in the electron unit tests

* Spacing fixes

* Add copyright notice

* Remove commented code

* Spacing fixes in notebook styles

* Move handle svg to image

* Add typing for gridstack

* Move notebook styles to file

* Rename selector constant

* Rename grid css file

* Add nb-grid-stack class to views grid

* Cell toolbar style adjustments

* Remove unused imports

* Update .eslintrc.json

* Fix outdated instantiation of LabeledMenuItemActionItem

* Address feedback

* Fix from update to main
This commit is contained in:
Daniel Grajeda
2021-05-17 10:16:43 -07:00
committed by GitHub
parent 7e8dccec82
commit 2781279644
18 changed files with 864 additions and 0 deletions

View File

@@ -740,6 +740,8 @@
"angular2-grid",
"html-query-plan",
"turndown",
"gridstack",
"gridstack/**",
"mark.js",
"vscode-textmate",
"vscode-oniguruma",

View File

@@ -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)

View File

@@ -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",

View File

@@ -0,0 +1 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" width='19' height='19' viewBox='0 0 19 19' fill='none'><line x1='0.646447' y1='14.6464' x2='14.6464' y2='0.646447' stroke='#230078D4'/><line x1='8.07125' y1='15' x2='15.0713' y2='7.99996' stroke='#230078D4'/></svg>

After

Width:  |  Height:  |  Size: 282 B

View File

@@ -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<ICellComponentRegistry>(OutputComponentExtensions.CellComponentContributions);
@@ -54,6 +58,10 @@ export const NotebookModule = (params, selector: string, instantiationService: I
PlaceholderCellComponent,
NotebookComponent,
NotebookEditorComponent,
NotebookViewComponent,
NotebookViewsCardComponent,
NotebookViewsGridComponent,
NotebookViewsCodeCellComponent,
ComponentHostDirective,
OutputAreaComponent,
OutputComponent,

View File

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

View File

@@ -0,0 +1,15 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
<div #viewsToolbar class="editor-toolbar actionbar-container" style="flex: 0 0 auto; display: flex; flex-flow: row; width: 100%; align-items: center;"></div>
<div #container class="scrollable" style="flex: 1 1 auto; position: relative; outline: none" (click)="unselectActiveCell()" (scroll)="scrollHandler($event)">
<loading-spinner [loading]="isLoading"></loading-spinner>
<notebook-views-grid-component #gridstack [views]="views" [cells]="cells" [model]="model"></notebook-views-grid-component>
<div class="book-nav" #bookNav [style.visibility]="navigationVisibility">
</div>
</div>
</div>

View File

@@ -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<CodeCellComponent>;
@ViewChildren(TextCellComponent) private _textCells: QueryList<TextCellComponent>;
protected _actionBar: Taskbar;
public previewFeaturesEnabled: boolean = false;
private _modelReadyDeferred = new Deferred<NotebookModel>();
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<boolean> {
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<boolean> {
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<boolean> {
throw new Error('Method not implemented.');
}
clearAllOutputs(): Promise<boolean> {
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<void> {
await this.awaitNonDefaultProvider();
await this.model.requestModelLoad();
await this.model.onClientSessionReady;
this.detectChanges();
}
private async awaitNonDefaultProvider(): Promise<void> {
// 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 = <HTMLElement>this._container.nativeElement;
containerElement.scrollTop = this._scrollTop;
}));
}
}
/**
* Saves scrollTop value on scroll change
*/
public scrollHandler(event: Event) {
this._scrollTop = (<HTMLElement>event.srcElement).scrollTop;
}
public unselectActiveCell() {
this.model.updateActiveCell(undefined);
this.detectChanges();
}
protected initViewsToolbar() {
let taskbar = <HTMLElement>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<INotebookModel> {
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;
}
}

View File

@@ -0,0 +1,34 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<ng-template #templateRef>
<div
*ngIf="display"
class="grid-stack-item"
attr.data-cell-id="{{cell.cellGuid}}"
attr.data-gs-width="{{width}}"
attr.data-gs-height="{{height}}"
attr.data-gs-x="{{x}}"
attr.data-gs-y="{{y}}"
(click)="selectCell(cell, $event)"
>
<div
#item
class="grid-stack-item-content notebook-cell"
[class.active]="cell.active"
>
<div #actionbar [style.display]="showActionBar ? 'block' : 'none'" class="actionbar"></div>
<div class="grid-stack-content-wrapper">
<div class="grid-stack-content-wrapper-inner">
<views-code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
</views-code-cell-component>
<text-cell-component *ngIf="cell.cellType === 'markdown'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
</text-cell-component>
</div>
</div>
</div>
</div>
</ng-template>

View File

@@ -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<any> = new EventEmitter();
@ViewChild('templateRef') templateRef: TemplateRef<any>;
@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;
}
}

View File

@@ -0,0 +1,16 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="width: 100%; height: 100%; display: flex; flex-flow: column">
<div style="flex: 0 0 auto; width: 100%; height: 100%; display: block">
<output-area-component *ngIf="cellModel.outputs && cellModel.outputs.length > 0" [cellModel]="cellModel" [activeCellId]="activeCellId">
</output-area-component>
<div *ngIf="!cellModel.outputs || !cellModel.outputs.length">
{{emptyCellText}}
</div>
<stdin-component *ngIf="isStdInVisible" [onSendInput]="inputDeferred" [stdIn]="stdIn" [cellModel]="cellModel"></stdin-component>
</div>
</div>

View File

@@ -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'] !== '<IPython.core.display.HTML object>');
}
}

View File

@@ -0,0 +1,15 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="nb-grid-stack grid-stack">
<ng-container *ngFor="let cell of cells">
<view-card-component #wrapper [ready]="loaded" [views]="views" [cell]="cell" [model]="model" (onChange)="onCellChanged($event)"></view-card-component>
<ng-content *ngTemplateOutlet='wrapper.templateRef'></ng-content>
</ng-container>
</div>
<div class="empty-message" *ngIf="empty && loaded">
{{emptyText}}
</div>

View File

@@ -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<NotebookViewsCardComponent>;
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<void> {
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<NotebookViewsCardComponent>): 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);
}
});
}
}

View File

@@ -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
}

View File

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

View File

@@ -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',

View File

@@ -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"