Notebook views UI 2 (#15865)

Provide access to the Notebook Views UI. Feature is hidden behind a feature flag.

Co-authored-by: rkselfhost <rkselfhost@outlook.com>
This commit is contained in:
Daniel Grajeda
2021-08-17 15:15:28 -06:00
committed by GitHub
parent 82576e54a4
commit 640c6f30f4
29 changed files with 989 additions and 140 deletions

View File

@@ -402,6 +402,12 @@ Includes non-masked style declarations. */
mask-image: url("toolbar-image.svg");
}
.codicon.masked-icon.settings::before {
background-image: none;
-webkit-mask-image: url('settings.svg');
mask-image: url('settings.svg');
}
.codicon.masked-icon.split-toggle-on::before {
-webkit-mask-image: url("toolbar-preview-toggle-on.svg");
mask-image: url("toolbar-preview-toggle-on.svg");

View File

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.9297 7.71875V7.8594C13.9297 7.90625 13.9323 7.9531 13.9375 8V8.1406C13.9375 8.1875 13.9349 8.2344 13.9297 8.28125L15.9531 9.53905L14.7109 12.5312L12.3906 12C12.2656 12.1354 12.1354 12.2656 12 12.3906L12.5312 14.7109L9.53905 15.9531L8.28125 13.9297H8.1406C8.09375 13.9297 8.0469 13.9323 8 13.9375H7.8594C7.8125 13.9375 7.7656 13.9349 7.71875 13.9297L6.46095 15.9531L3.46875 14.7109L4 12.3906C3.86458 12.2656 3.73438 12.1354 3.60938 12L1.28906 12.5312L0.046875 9.53905L2.07031 8.28125V8.1406C2.07031 8.09375 2.06771 8.0469 2.0625 8V7.8594C2.0625 7.8125 2.06511 7.7656 2.07031 7.71875L0.046875 6.46095L1.28906 3.46875L3.60938 4C3.73438 3.86458 3.86458 3.73438 4 3.60938L3.46875 1.28906L6.46095 0.046875L7.71875 2.07031H7.8594C7.90625 2.07031 7.9531 2.06771 8 2.0625H8.1406C8.1875 2.0625 8.2344 2.06511 8.28125 2.07031L9.53905 0.046875L12.5312 1.28906L12 3.60938C12.1354 3.73438 12.2656 3.86458 12.3906 4L14.7109 3.46875L15.9531 6.46095L13.9297 7.71875ZM13.0156 8.7344C13.0261 8.6094 13.0364 8.487 13.0469 8.3672C13.0573 8.2474 13.0625 8.1224 13.0625 7.9922C13.0625 7.8724 13.0573 7.75 13.0469 7.625C13.0364 7.5 13.0261 7.3776 13.0156 7.2578L14.8594 6.1094L14.1875 4.48438L12.0703 4.97656C11.9089 4.77865 11.7448 4.59635 11.5781 4.42969C11.4115 4.26302 11.2265 4.09635 11.0234 3.92969L11.5156 1.8125L9.8906 1.14062L8.7344 2.98438C8.6146 2.97396 8.4922 2.96354 8.3672 2.95312C8.2422 2.94271 8.1198 2.9375 8 2.9375C7.875 2.9375 7.7526 2.94271 7.6328 2.95312C7.513 2.96354 7.388 2.97396 7.2578 2.98438L6.1094 1.14062L4.48438 1.8125L4.97656 3.92969C4.77865 4.09114 4.59635 4.25521 4.42969 4.42188C4.26302 4.58854 4.09635 4.77344 3.92969 4.97656L1.8125 4.48438L1.14062 6.1094L2.98438 7.2656C2.97396 7.3906 2.96354 7.513 2.95312 7.6328C2.94271 7.7526 2.9375 7.8776 2.9375 8.0078C2.9375 8.1276 2.94271 8.25 2.95312 8.375C2.96354 8.5 2.97396 8.6224 2.98438 8.7422L1.14062 9.8906L1.8125 11.5156L3.92969 11.0234C4.09114 11.2213 4.25521 11.4037 4.42188 11.5703C4.58854 11.737 4.77344 11.9037 4.97656 12.0703L4.48438 14.1875L6.1094 14.8594L7.2656 13.0156C7.3854 13.0261 7.5078 13.0364 7.6328 13.0469C7.7578 13.0573 7.8802 13.0625 8 13.0625C8.125 13.0625 8.2474 13.0573 8.3672 13.0469C8.487 13.0364 8.612 13.0261 8.7422 13.0156L9.8906 14.8594L11.5156 14.1875L11.0234 12.0703C11.2213 11.9089 11.4037 11.7448 11.5703 11.5781C11.737 11.4115 11.9037 11.2265 12.0703 11.0234L14.1875 11.5156L14.8594 9.8906L13.0156 8.7344ZM8 5.0625C8.40625 5.0625 8.78645 5.138 9.1406 5.28905C9.4948 5.4401 9.8073 5.65105 10.0781 5.9219C10.349 6.1927 10.5573 6.5026 10.7031 6.85155C10.849 7.2005 10.9271 7.58335 10.9375 8C10.9375 8.40625 10.862 8.78645 10.7109 9.1406C10.5599 9.4948 10.349 9.8073 10.0781 10.0781C9.8073 10.349 9.4974 10.5573 9.14845 10.7031C8.7995 10.849 8.41665 10.9271 8 10.9375C7.59375 10.9375 7.21355 10.862 6.8594 10.7109C6.5052 10.5599 6.1927 10.349 5.9219 10.0781C5.65105 9.8073 5.4427 9.4974 5.2969 9.14845C5.15105 8.7995 5.0729 8.41665 5.0625 8C5.0625 7.59375 5.138 7.21355 5.28905 6.8594C5.4401 6.5052 5.65105 6.1927 5.9219 5.9219C6.1927 5.65105 6.5026 5.4427 6.85155 5.2969C7.2005 5.15105 7.58335 5.0729 8 5.0625ZM8 10.0625C8.28645 10.0625 8.5547 10.0104 8.8047 9.90625C9.0547 9.8021 9.27345 9.65365 9.46095 9.46095C9.64845 9.26825 9.79425 9.0495 9.89845 8.8047C10.0026 8.5599 10.0573 8.29165 10.0625 8C10.0625 7.71355 10.0104 7.4453 9.90625 7.1953C9.8021 6.9453 9.65365 6.72655 9.46095 6.53905C9.26825 6.35155 9.0495 6.20575 8.8047 6.10155C8.5599 5.9974 8.29165 5.9427 8 5.9375C7.71355 5.9375 7.4453 5.9896 7.1953 6.09375C6.9453 6.1979 6.72655 6.34635 6.53905 6.53905C6.35155 6.73175 6.20575 6.9505 6.10155 7.1953C5.9974 7.4401 5.9427 7.70835 5.9375 8C5.9375 8.28645 5.9896 8.5547 6.09375 8.8047C6.1979 9.0547 6.34635 9.27345 6.53905 9.46095C6.73175 9.64845 6.9505 9.79425 7.1953 9.89845C7.4401 10.0026 7.70835 10.0573 8 10.0625Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -45,7 +45,7 @@ export class CodeCellComponent extends CellView implements OnInit, OnChanges {
public stdIn: nb.IStdinMessage;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ChangeDetectorRef)) protected _changeRef: ChangeDetectorRef,
) {
super();
}
@@ -110,7 +110,7 @@ export class CodeCellComponent extends CellView implements OnInit, OnChanges {
}
}
private async awaitStdIn(): Promise<void> {
protected async awaitStdIn(): Promise<void> {
try {
let value = await this.inputDeferred.promise;
this.cellModel.future.sendInputReply({ value: value });

View File

@@ -1 +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>
<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='#3aa0f3'/><line x1='8.07125' y1='15' x2='15.0713' y2='7.99996' stroke='#3aa0f3'/></svg>

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 278 B

View File

@@ -349,6 +349,19 @@ configurationRegistry.registerConfiguration({
}
});
configurationRegistry.registerConfiguration({
'id': 'notebookViews',
'title': localize('notebookViews', 'Notebook Views'),
'type': 'object',
'properties': {
'notebookViews.enabled': {
'type': 'boolean',
'default': false,
'description': localize('notebookViews.enabled', "(Preview) Enable Notebook Views")
}
}
});
/* *************** Output components *************** */
// Note: most existing types use the same component to render. In order to
// preserve correct rank order, we register it once for each different rank of

View File

@@ -37,6 +37,8 @@ import { NotebookViewComponent } from 'sql/workbench/contrib/notebook/browser/no
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';
import { TextCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/textCell.component';
import { NotebookViewsModalComponent } from 'sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsModal.component';
const outputComponentRegistry = Registry.as<ICellComponentRegistry>(OutputComponentExtensions.CellComponentContributions);
@@ -53,6 +55,7 @@ export const NotebookModule = (params, selector: string, instantiationService: I
LoadingSpinner,
CodeComponent,
CodeCellComponent,
TextCellComponent,
CellToolbarComponent,
MarkdownToolbarComponent,
PlaceholderCellComponent,
@@ -62,6 +65,7 @@ export const NotebookModule = (params, selector: string, instantiationService: I
NotebookViewsCardComponent,
NotebookViewsGridComponent,
NotebookViewsCodeCellComponent,
NotebookViewsModalComponent,
ComponentHostDirective,
OutputAreaComponent,
OutputComponent,
@@ -71,8 +75,9 @@ export const NotebookModule = (params, selector: string, instantiationService: I
...outputComponents
],
entryComponents: [
NotebookComponent,
NotebookEditorComponent,
TextCellComponent,
NotebookViewsCardComponent,
...outputComponents
],
imports: [

View File

@@ -5,5 +5,6 @@
*--------------------------------------------------------------------------------------------*/
-->
<!-- Notebook View -->
<notebook-component *ngIf="viewMode === 0" [_views]="views" [_model]="model"></notebook-component>
<!-- Dashboard View: To be implemented-->
<notebook-component *ngIf="viewMode === ViewMode.Notebook" [_views]="views" [_model]="model"></notebook-component>
<!-- Dashboard View -->
<notebook-view-component *ngIf="viewMode === ViewMode.Views" [views]="views" [notebookMetadata]="views.metadata" [activeView]="activeView" [model]="model"></notebook-view-component>

View File

@@ -14,7 +14,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry';
import { ILogService } from 'vs/platform/log/common/log';
import { IModelFactory, ViewMode, NotebookContentChange } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { IModelFactory, ViewMode, NotebookContentChange, INotebookModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ModelFactory } from 'sql/workbench/services/notebook/browser/models/modelFactory';
@@ -25,6 +25,9 @@ import { IMenuService, MenuId } from 'vs/platform/actions/common/actions';
import { fillInActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
import { INotebookView } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
import { Deferred } from 'sql/base/common/promise';
import { NotebookChangeType } from 'sql/workbench/services/notebook/common/contracts';
export const NOTEBOOKEDITOR_SELECTOR: string = 'notebookeditor-component';
@@ -33,11 +36,16 @@ export const NOTEBOOKEDITOR_SELECTOR: string = 'notebookeditor-component';
templateUrl: decodeURI(require.toUrl('./notebookEditor.component.html'))
})
export class NotebookEditorComponent extends AngularDisposable {
private readonly defaultViewMode = ViewMode.Notebook;
private profile: IConnectionProfile;
private notebookManagers: INotebookManager[] = [];
private _model: NotebookModel;
private _modelReadyDeferred = new Deferred<NotebookModel>();
public model: NotebookModel;
public views: NotebookViewsExtension;
public activeView: INotebookView;
public viewMode: ViewMode;
public ViewMode = ViewMode;
constructor(
@Inject(ILogService) private readonly logService: ILogService,
@@ -74,6 +82,9 @@ export class NotebookEditorComponent extends AngularDisposable {
await this.createModelAndLoadContents();
await this.setNotebookManager();
await this.loadModel();
this.setActiveView();
this._modelReadyDeferred.resolve(this.model);
}
private async loadModel(): Promise<void> {
@@ -104,16 +115,20 @@ export class NotebookEditorComponent extends AngularDisposable {
}, this.profile, this.logService, this.notificationService, this.adstelemetryService, this.connectionManagementService, this._configurationService, this.capabilitiesService);
let trusted = await this.notebookService.isNotebookTrustCached(this._notebookParams.notebookUri, this.isDirty());
this._model = this._register(model);
this.model = this._register(model);
await this.model.loadContents(trusted);
this.views = new NotebookViewsExtension(this.model);
this.viewMode = this.viewMode ?? this.defaultViewMode;
this._register(model.viewModeChanged((mode) => this.onViewModeChanged()));
this._register(model.contentChanged((change) => this.handleContentChanged(change)));
this._register(model.onCellTypeChanged(() => this.detectChanges()));
this._register(model.layoutChanged(() => this.detectChanges()));
this.views.onViewDeleted(() => this.handleViewDeleted());
this.views.onActiveViewChanged(() => this.handleActiveViewChanged());
this.detectChanges();
}
@@ -166,24 +181,43 @@ export class NotebookEditorComponent extends AngularDisposable {
return this._notebookParams.modelFactory;
}
public get model(): NotebookModel | null {
return this._model;
}
public get viewMode(): ViewMode {
return this.model?.viewMode;
}
private isDirty(): boolean {
return this._notebookParams.input.isDirty();
}
public get modelReady(): Promise<INotebookModel> {
return this._modelReadyDeferred.promise;
}
private handleContentChanged(change: NotebookContentChange) {
// Note: for now we just need to set dirty state and refresh the UI.
if (change.changeType === NotebookChangeType.MetadataChanged) {
this.handleActiveViewChanged();
}
this.detectChanges();
}
private handleViewDeleted() {
this.viewMode = this.model?.viewMode;
this.detectChanges();
}
private handleActiveViewChanged() {
this.setActiveView();
this.detectChanges();
}
public onViewModeChanged(): void {
this.viewMode = this.model?.viewMode;
this.setActiveView();
this.detectChanges();
}
public setActiveView() {
const views = this.views.getViews();
let activeView = this.views.getActiveView() ?? views[0];
this.activeView = activeView;
}
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.nb-grid-stack .grid-stack-item.notebook-cell .actionbar {
.nb-grid-stack .grid-stack-item .notebook-cell .actionbar {
border-width: 1px;
border-style: solid;
position: absolute;
@@ -11,48 +11,48 @@
top: -20px;
}
.nb-grid-stack .grid-stack-item.notebook-cell .actionbar .carbon-taskbar .monaco-action-bar.animated {
.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 {
.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 {
.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 {
.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 {
.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 {
.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 {
.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 {
.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 {
.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 {
.nb-grid-stack .grid-stack-item .notebook-cell .actionbar .notebook-button.toolbarIconStop {
margin: 0 2px;
background-size: 20px 25px;
height: 24px;

View File

@@ -7,9 +7,12 @@
<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>
<notebook-views-grid-component #gridstack [views]="views" [activeView]="activeView" [cells]="cells" [model]="model"></notebook-views-grid-component>
<ng-container *ngFor="let cell of cellsAwaitingInput">
<notebook-views-modal-component class="modal" [title]="cellsAwaitingInputModalTitle">
<views-code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId"></views-code-cell-component>
</notebook-views-modal-component>
</ng-container>
</div>
</div>

View File

@@ -2,8 +2,9 @@
* 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, Input, ViewChildren, QueryList, ChangeDetectorRef, forwardRef, Inject, ViewChild, ElementRef } from '@angular/core';
import { ICellModel, INotebookModel, ISingleNotebookEditOperation } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { ICellModel, INotebookModel, ISingleNotebookEditOperation, NotebookContentChange, } 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';
@@ -11,18 +12,18 @@ import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/no
import * as notebookUtils from 'sql/workbench/services/notebook/browser/models/notebookUtils';
import { IBootstrapParams } from 'sql/workbench/services/bootstrap/common/bootstrapParams';
import { Action } 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 { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
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 { CellType, CellTypes, NotebookChangeType } 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';
@@ -30,7 +31,11 @@ import { INotebookView, INotebookViewMetadata } from 'sql/workbench/services/not
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 { DropdownMenuActionViewItem } from 'sql/base/browser/ui/buttonMenu/buttonMenu';
import { NotebookViewsActionProvider, AddCellAction, RunAllCellsAction } from 'sql/workbench/contrib/notebook/browser/notebookActions';
import * as DOM from 'vs/base/browser/dom';
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { LabeledMenuItemActionItem } from 'sql/platform/actions/browser/menuEntryActionViewItem';
import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
export const NOTEBOOKVIEWS_SELECTOR: string = 'notebook-view-component';
@@ -44,7 +49,7 @@ export class NotebookViewComponent extends AngularDisposable implements INoteboo
@Input() model: NotebookModel;
@Input() activeView: INotebookView;
@Input() views: NotebookViewsExtension;
@Input() notebookMeta: INotebookViewMetadata;
@Input() notebookMetadata: INotebookViewMetadata;
@ViewChild('container', { read: ElementRef }) private _container: ElementRef;
@ViewChild('viewsToolbar', { read: ElementRef }) private _viewsToolbar: ElementRef;
@@ -57,12 +62,16 @@ export class NotebookViewComponent extends AngularDisposable implements INoteboo
private _modelReadyDeferred = new Deferred<NotebookModel>();
private _runAllCellsAction: RunAllCellsAction;
private _scrollTop: number;
public _cellsAwaitingInput: ICellModel[] = [];
public readonly cellsAwaitingInputModalTitle: string = localize('cellAwaitingInputTitle', "Cell Awaiting Input");
public readonly loadingMessage = localize('loading', "Loading");
constructor(
@Inject(IBootstrapParams) private _notebookParams: INotebookParams,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
@Inject(IKeybindingService) private _keybindingService: IKeybindingService,
@Inject(IContextMenuService) private _contextMenuService: IContextMenuService,
@Inject(INotificationService) private _notificationService: INotificationService,
@Inject(INotebookService) private _notebookService: INotebookService,
@Inject(IConnectionManagementService) private _connectionManagementService: IConnectionManagementService,
@@ -120,12 +129,19 @@ export class NotebookViewComponent extends AngularDisposable implements INoteboo
if (!isUndefinedOrNull(endCell)) {
endIndex = codeCells.findIndex(c => c.id === endCell.id);
}
let statusNotification = this._notificationService.notify({ severity: Severity.Info, message: localize('startingExecution', "Starting execution"), progress: { total: endIndex + 1, worked: 0 } });
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."));
statusNotification.updateMessage(localize('runningCell', "Running cell {0} of {1}", (i + 1), (endIndex)));
statusNotification.progress.worked(i);
try {
await this.runCell(codeCells[i]);
} catch (error) {
statusNotification.updateSeverity(Severity.Error);
statusNotification.updateMessage(localize('cellRunFailed', "Run Cells failed - See error in output of the currently selected cell for more information."));
return false;
}
}
statusNotification.close();
}
return true;
}
@@ -153,10 +169,11 @@ export class NotebookViewComponent extends AngularDisposable implements INoteboo
ngOnInit() {
this.initViewsToolbar();
this._notebookService.addNotebookEditor(this);
this._modelReadyDeferred.resolve(this.model);
this._notebookService.addNotebookEditor(this);
this.setScrollPosition();
this._register(this.model.contentChanged((e) => this.handleContentChanged(e)));
this.doLoad().catch(e => onUnexpectedError(e));
}
@@ -249,6 +266,25 @@ export class NotebookViewComponent extends AngularDisposable implements INoteboo
let viewOptions = this._instantiationService.createInstance(ViewSettingsAction, this.views);
let viewsContainer = document.createElement('li');
let viewsActionsProvider = new NotebookViewsActionProvider(viewsContainer, this.views, this.modelReady, this._notebookService, this._notificationService, this._instantiationService);
let viewsButton = this._instantiationService.createInstance(AddCellAction, 'notebook.OpenViews', undefined, 'notebook-button masked-pseudo code');
let viewsDropdownContainer = DOM.$('li.action-item');
viewsDropdownContainer.setAttribute('role', 'presentation');
let viewsDropdownMenuActionViewItem = new DropdownMenuActionViewItem(
viewsButton,
viewsActionsProvider,
this._contextMenuService,
undefined,
this._actionBar.actionRunner,
undefined,
'codicon notebook-button masked-pseudo masked-pseudo-after icon-dashboard-view dropdown-arrow',
this.activeView?.name,
() => AnchorAlignment.RIGHT
);
viewsDropdownMenuActionViewItem.render(viewsDropdownContainer);
viewsDropdownMenuActionViewItem.setActionContext(this._notebookParams.notebookUri);
let deleteView = this._instantiationService.createInstance(DeleteViewAction, this.views);
this._actionBar.setContent([
@@ -257,6 +293,7 @@ export class NotebookViewComponent extends AngularDisposable implements INoteboo
{ action: insertCellsAction },
{ action: this._runAllCellsAction },
{ element: spacerElement },
{ element: viewsDropdownContainer },
{ action: viewOptions },
{ action: deleteView }
]);
@@ -276,6 +313,27 @@ export class NotebookViewComponent extends AngularDisposable implements INoteboo
return undefined;
}
private handleContentChanged(change: NotebookContentChange) {
switch (change.changeType) {
case NotebookChangeType.CellAwaitingInput: {
const cell: ICellModel = change?.cells[0];
this._cellsAwaitingInput.push(cell);
this.detectChanges();
break;
}
case NotebookChangeType.CellExecuted: {
const cell: ICellModel = change?.cells[0];
const indexToRemove = this._cellsAwaitingInput.findIndex(c => c.id === cell.id);
if (indexToRemove >= 0) {
this._cellsAwaitingInput.splice(indexToRemove, 1);
this.model.serializationStateChanged(NotebookChangeType.CellsModified, cell);
this.detectChanges();
}
break;
}
}
}
private detectChanges(): void {
if (!(this._changeRef['destroyed'])) {
this._changeRef.detectChanges();
@@ -297,4 +355,8 @@ export class NotebookViewComponent extends AngularDisposable implements INoteboo
}
return editors;
}
public get cellsAwaitingInput(): ICellModel[] {
return this._gridstack.hiddenItems.filter(i => this._cellsAwaitingInput.includes(i.cell)).map(i => i.cell);
}
}

View File

@@ -6,13 +6,13 @@
-->
<ng-template #templateRef>
<div
*ngIf="display"
*ngIf="visible"
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}}"
attr.gs-w="{{width}}"
attr.gs-h="{{height}}"
attr.gs-x="{{x}}"
attr.gs-y="{{y}}"
(click)="selectCell(cell, $event)"
>
<div
@@ -23,7 +23,7 @@
<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 *ngIf="cell.cellType === 'code'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId" [visible]="visible">
</views-code-cell-component>
<text-cell-component *ngIf="cell.cellType === 'markdown'" [cellModel]="cell" [model]="model" [activeCellId]="activeCellId">
</text-cell-component>

View File

@@ -4,32 +4,36 @@
*--------------------------------------------------------------------------------------------*/
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 { Component, OnInit, Input, ViewChild, TemplateRef, ElementRef, Inject, Output, EventEmitter, ChangeDetectorRef, forwardRef, SimpleChanges } from '@angular/core';
import { CellExecutionState, 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 { CellChangeEventType, INotebookView, INotebookViewCell } 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';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { cellBorder, notebookToolbarSelectBackground } from 'sql/platform/theme/common/colorRegistry';
@Component({
selector: 'view-card-component',
templateUrl: decodeURI(require.toUrl('./notebookViewsCard.component.html'))
})
export class NotebookViewsCardComponent implements OnInit {
public _cellToggleMoreActions: ViewCellToggleMoreActions;
export class NotebookViewsCardComponent extends AngularDisposable implements OnInit {
private _actionbar: Taskbar;
private _metadata: INotebookViewCellMetadata;
private _activeView: INotebookView;
private _metadata: INotebookViewCell;
private _executionState: CellExecutionState;
private _pendingReinitialize: boolean = false;
public _cellToggleMoreActions: ViewCellToggleMoreActions;
@Input() cell: ICellModel;
@Input() model: NotebookModel;
@Input() views: NotebookViewsExtension;
@Input() activeView: INotebookView;
@Input() meta: boolean;
@Input() ready: boolean;
@Output() onChange: EventEmitter<any> = new EventEmitter();
@@ -39,28 +43,50 @@ export class NotebookViewsCardComponent implements OnInit {
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(IInstantiationService) private _instantiationService: IInstantiationService
) { }
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
) {
super();
}
ngOnInit() {
this.initActionBar();
}
ngOnChanges() {
if (this.views) {
this._activeView = this.views.getActiveView();
this._metadata = this.views.getCellMetadata(this.cell);
ngAfterViewInit() {
this.initialize();
}
ngOnChanges(changes: SimpleChanges) {
if (this.activeView && changes['activeView'] && changes['activeView'].currentValue?.guid !== changes['activeView'].previousValue?.guid) {
this._metadata = this.activeView.getCellMetadata(this.cell);
this._pendingReinitialize = true;
}
this.detectChanges();
}
ngAfterContentInit() {
if (this.views) {
this._activeView = this.views.getActiveView();
this._metadata = this.views.getCellMetadata(this.cell);
if (this.activeView) {
this._metadata = this.activeView.getCellMetadata(this.cell);
this._pendingReinitialize = true;
}
this.detectChanges();
}
ngAfterViewChecked() {
if (this._pendingReinitialize) {
this._pendingReinitialize = false;
this.initialize();
}
}
ngAfterViewInit() {
override ngOnDestroy() {
if (this._actionbar) {
this._actionbar.dispose();
}
}
public initialize(): void {
this.initActionBar();
this.detectChanges();
}
@@ -69,6 +95,10 @@ export class NotebookViewsCardComponent implements OnInit {
let taskbarContent: ITaskbarContent[] = [];
let context = new CellContext(this.model, this.cell);
if (this._actionbar) {
this._actionbar.dispose();
}
this._actionbar = new Taskbar(this._actionbarRef.nativeElement);
this._actionbar.context = { target: this._actionbarRef.nativeElement };
@@ -97,6 +127,10 @@ export class NotebookViewsCardComponent implements OnInit {
this.onChange.emit({ cell: this.cell, event: event });
}
get displayInputModal(): boolean {
return this.awaitingInput;
}
detectChanges() {
this._changeRef.detectChanges();
}
@@ -110,39 +144,77 @@ export class NotebookViewsCardComponent implements OnInit {
}
}
public set executionState(state: CellExecutionState) {
if (this._executionState !== state) {
this._executionState = state;
}
}
public get executionState(): CellExecutionState {
return this._executionState;
}
public hide(): void {
this.changed('hide');
}
public get data(): any {
return this._metadata?.views?.find(v => v.guid === this._activeView.guid);
public get metadata(): INotebookViewCell {
return this._metadata;
}
public get width(): number {
return this.data?.width ? this.data.width : DEFAULT_VIEW_CARD_WIDTH;
return this.metadata?.width ? this.metadata.width : DEFAULT_VIEW_CARD_WIDTH;
}
public get height(): number {
return this.data.height ? this.data.height : DEFAULT_VIEW_CARD_HEIGHT;
return this.metadata?.height ? this.metadata.height : DEFAULT_VIEW_CARD_HEIGHT;
}
public get x(): number {
return this.data?.x;
return this.metadata?.x;
}
public get y(): number {
return this.data?.y;
return this.metadata?.y;
}
public get display(): boolean {
if (!this._metadata || !this._activeView) {
/**
* Whether to display the card
*/
public get visible(): boolean {
if (!this.activeView) {
return true;
}
return !this.data?.hidden;
if (!this._metadata) { //Means not initialized
return false;
}
return !!this.activeView.displayedCells.find(c => c.cellGuid === this.cell.cellGuid);
}
/**
* Is the cell expecting input
*/
public get awaitingInput(): boolean {
return this.cell.future && this.cell.future.inProgress;
}
public get showActionBar(): boolean {
return this.cell.active;
}
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const cellBorderColor = theme.getColor(cellBorder);
if (cellBorderColor) {
collector.addRule(`.notebookEditor .nb-grid-stack .notebook-cell.active .actionbar { border-color: ${cellBorderColor};}`);
collector.addRule(`.notebookEditor .nb-grid-stack .notebook-cell.active .actionbar .codicon:before { background-color: ${cellBorderColor};}`);
}
// Cell toolbar background
const notebookToolbarSelectBackgroundColor = theme.getColor(notebookToolbarSelectBackground);
if (notebookToolbarSelectBackgroundColor) {
collector.addRule(`.notebookEditor .nb-grid-stack .notebook-cell.active .actionbar { background-color: ${notebookToolbarSelectBackgroundColor};}`);
}
});

View File

@@ -6,11 +6,11 @@
-->
<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 *ngIf="viewCellModel.outputs && viewCellModel.outputs.length > 0" [cellModel]="viewCellModel" [activeCellId]="activeCellId">
</output-area-component>
<div *ngIf="!cellModel.outputs || !cellModel.outputs.length">
<div *ngIf="(!viewCellModel.outputs || !viewCellModel.outputs.length) && !isStdInVisible">
{{emptyCellText}}
</div>
<stdin-component *ngIf="isStdInVisible" [onSendInput]="inputDeferred" [stdIn]="stdIn" [cellModel]="cellModel"></stdin-component>
<stdin-component *ngIf="isStdInVisible" [onSendInput]="inputDeferred" [stdIn]="stdIn" [cellModel]="viewCellModel"></stdin-component>
</div>
</div>

View File

@@ -4,26 +4,89 @@
*--------------------------------------------------------------------------------------------*/
import { nb } from 'azdata';
import { ChangeDetectorRef, Component, forwardRef, Inject } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Inject, Input, OnChanges, SimpleChange } from '@angular/core';
import { CodeCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/codeCell.component';
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { CellModel } from 'sql/workbench/services/notebook/browser/models/cell';
import { localize } from 'vs/nls';
export const CODE_SELECTOR: string = 'views-code-cell-component';
@Component({
selector: CODE_SELECTOR,
changeDetection: ChangeDetectionStrategy.OnPush,
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.");
export class NotebookViewsCodeCellComponent extends CodeCellComponent implements OnChanges {
@Input() visible: boolean;
@Input() modal: boolean;
@Input() override cellModel: ICellModel;
public override stdIn: nb.IStdinMessage;
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>');
override ngOnInit() {
if (this.cellModel) {
this._register(this.cellModel.onCollapseStateChanged((state) => {
this._changeRef.detectChanges();
}));
this._register(this.cellModel.onParameterStateChanged((state) => {
this._changeRef.detectChanges();
}));
this._register(this.cellModel.onOutputsChanged(() => {
this._changeRef.detectChanges();
}));
// If we have a pre-existing message, listen to that
if (this.cellModel?.future?.msg) {
this.handleStdIn(this.cellModel.future.msg as nb.IStdinMessage);
}
// Register request handler, cleanup on dispose of this component
this.cellModel.setStdInHandler({ handle: (msg) => super.handleStdIn(msg) });
this._register({ dispose: () => this.cellModel.setStdInHandler(undefined) });
}
}
override ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
for (let propName in changes) {
if (propName === 'activeCellId') {
let changedProp = changes[propName];
super.activeCellId = changedProp.currentValue;
break;
}
}
}
get outputs(): readonly nb.ICellOutput[] {
return this.cellModel.outputs;
}
/**
* Override the cell model for Views.
*/
get viewCellModel(): ICellModel {
return new NotebookViewsCellModel(this.cellModel.toJSON(), { notebook: this.cellModel.notebookModel, isTrusted: this.cellModel.trustedMode });
}
get emptyCellText(): string {
return localize('viewsCodeCell.emptyCellText', "Please run this cell to view outputs.");
}
}
export class NotebookViewsCellModel extends CellModel {
/**
* Override the cell output.
* - Hide plain text messages
*/
public override get outputs(): Array<nb.ICellOutput> {
return super.outputs
.filter((output: nb.IDisplayResult) => output.data === undefined || output?.data['text/plain'] !== '<IPython.core.display.HTML object>')
.map((output: nb.ICellOutput) => ({ ...output }))
.map((output: nb.ICellOutput) => { output.metadata = { ...output.metadata }; return output; });
}
}

View File

@@ -6,10 +6,15 @@
-->
<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>
<view-card-component #wrapper [ready]="loaded" [activeView]="activeView" [meta]="cell.metadata" [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>
<div *ngIf="!loaded" style="display: block; position: absolute; height: 100%; width: 100%; z-index: 9999; background: #fff;">
<loading-spinner [loading]="!loaded" [loadingMessage]="loadingMessage"></loading-spinner>
</div>

View File

@@ -36,6 +36,8 @@ export class NotebookViewsGridComponent extends AngularDisposable implements OnI
protected _grid: GridStack;
protected _gridEnabled: boolean;
protected _loaded: boolean;
protected _gridView: INotebookView;
protected _activeCell: ICellModel;
protected _options: INotebookViewsGridOptions = {
cellHeight: 60
@@ -49,11 +51,11 @@ export class NotebookViewsGridComponent extends AngularDisposable implements OnI
}
public get empty(): boolean {
return !this._items || !this._items.find(item => item.display);
return !this._items || !this._items.find(item => item.visible);
}
public get hiddenItems(): NotebookViewsCardComponent[] {
return this._items?.filter(item => !item.display) ?? [];
return this._items?.filter(item => !item.visible) ?? [];
}
public get emptyText(): String {
@@ -77,17 +79,26 @@ export class NotebookViewsGridComponent extends AngularDisposable implements OnI
ngAfterContentChecked() {
//If activeView has changed or not present, we will destroy the grid in order to rebuild it later.
if (!this.activeView || this.activeView.guid !== this.activeView.guid) {
if (!this.activeView || this.activeView.guid !== this._gridView?.guid) {
if (this._grid) {
this.destroyGrid();
this._grid = undefined;
}
}
if (this.activeView && this.activeView.guid !== this._gridView?.guid) {
this.activeView.initialize();
}
if (this.model?.activeCell?.id !== this._activeCell?.id) {
this._activeCell = this.model.activeCell;
this.detectChanges();
}
}
ngAfterViewChecked() {
// If activeView has changed, rebuild the grid
if (!this.activeView || this.activeView.guid !== this.activeView.guid) {
if (this.activeView && this.activeView.guid !== this._gridView?.guid) {
this._gridView = this.activeView;
if (!this._grid) {
this.createGrid();
@@ -103,12 +114,15 @@ export class NotebookViewsGridComponent extends AngularDisposable implements OnI
}
private destroyGrid() {
this._gridEnabled = false;
this._grid.destroy(false);
if (this._grid) {
this._gridEnabled = false;
this._grid.destroy(false);
}
}
private createGrid() {
const isNew = this.activeView.isNew;
if (this._grid) {
this.destroyGrid();
}
@@ -191,6 +205,8 @@ export class NotebookViewsGridComponent extends AngularDisposable implements OnI
}
if (e.cell && e.event === 'insert') {
const component = this._items.find(x => x.cell.cellGuid === e.cell.cellGuid);
this.activeView.insertCell(e.cell);
this.detectChanges();
@@ -200,6 +216,18 @@ export class NotebookViewsGridComponent extends AngularDisposable implements OnI
this._grid.update(el, { x: 0, y: 0 });
this._grid.resizable(el, true);
this._grid.movable(el, true);
component.initialize();
}
if (e.cell && e.event === 'update') {
const el = this._grid.getGridItems().find(x => x.getAttribute('data-cell-id') === e.cell.cellGuid);
const cellData = this.activeView.getCellMetadata(e.cell);
this._grid.update(el, { x: cellData.x, y: cellData.y, w: cellData.width, h: cellData.height });
}
if (e.event === 'active') {
this._activeCell = e.cell;
}
this.detectChanges();

View File

@@ -2,9 +2,305 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
:root .nb-grid-stack .grid-stack-item > .ui-resizable-handle {
filter: none;
}
.nb-grid-stack {
position: relative;
}
.nb-grid-stack.grid-stack-rtl {
direction: ltr;
}
.nb-grid-stack.grid-stack-rtl > .grid-stack-item {
direction: rtl;
}
.nb-grid-stack .grid-stack-placeholder > .placeholder-content {
border: 1px dashed lightgray;
margin: 0;
position: absolute;
width: auto;
z-index: 0 !important;
text-align: center;
}
.nb-grid-stack > .grid-stack-item {
min-width: 8.3333333333%;
position: absolute;
padding: 0;
}
.nb-grid-stack > .grid-stack-item > .grid-stack-item-content {
margin: 0;
position: absolute;
width: auto;
overflow-x: hidden;
overflow-y: auto;
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-handle {
position: absolute;
font-size: 0.1px;
display: block;
-ms-touch-action: none;
touch-action: none;
}
.nb-grid-stack > .grid-stack-item.ui-resizable-disabled > .ui-resizable-handle, .nb-grid-stack > .grid-stack-item.ui-resizable-autohide > .ui-resizable-handle {
display: none;
}
.nb-grid-stack > .grid-stack-item.ui-draggable-dragging, .nb-grid-stack > .grid-stack-item.ui-resizable-resizing {
z-index: 100;
}
.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, 0.2);
opacity: 0.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;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-se {
-webkit-transform: rotate(-45deg);
-moz-transform: rotate(-45deg);
-ms-transform: rotate(-45deg);
-o-transform: rotate(-45deg);
transform: rotate(-45deg);
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-nw {
cursor: nw-resize;
width: 20px;
height: 20px;
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;
top: 0;
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-e {
cursor: e-resize;
width: 10px;
top: 15px;
bottom: 15px;
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-se {
cursor: se-resize;
width: 20px;
height: 20px;
}
.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;
bottom: 0;
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-w {
cursor: w-resize;
width: 10px;
top: 15px;
bottom: 15px;
}
.nb-grid-stack > .grid-stack-item.ui-draggable-dragging > .ui-resizable-handle {
display: none !important;
}
.nb-grid-stack > .grid-stack-item[gs-w='1'] {
width: 8.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-x='1'] {
left: 8.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-min-w='1'] {
min-width: 8.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-max-w='1'] {
max-width: 8.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-w='2'] {
width: 16.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-x='2'] {
left: 16.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-min-w='2'] {
min-width: 16.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-max-w='2'] {
max-width: 16.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-w='3'] {
width: 25%;
}
.nb-grid-stack > .grid-stack-item[gs-x='3'] {
left: 25%;
}
.nb-grid-stack > .grid-stack-item[gs-min-w='3'] {
min-width: 25%;
}
.nb-grid-stack > .grid-stack-item[gs-max-w='3'] {
max-width: 25%;
}
.nb-grid-stack > .grid-stack-item[gs-w='4'] {
width: 33.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-x='4'] {
left: 33.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-min-w='4'] {
min-width: 33.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-max-w='4'] {
max-width: 33.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-w='5'] {
width: 41.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-x='5'] {
left: 41.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-min-w='5'] {
min-width: 41.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-max-w='5'] {
max-width: 41.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-w='6'] {
width: 50%;
}
.nb-grid-stack > .grid-stack-item[gs-x='6'] {
left: 50%;
}
.nb-grid-stack > .grid-stack-item[gs-min-w='6'] {
min-width: 50%;
}
.nb-grid-stack > .grid-stack-item[gs-max-w='6'] {
max-width: 50%;
}
.nb-grid-stack > .grid-stack-item[gs-w='7'] {
width: 58.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-x='7'] {
left: 58.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-min-w='7'] {
min-width: 58.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-max-w='7'] {
max-width: 58.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-w='8'] {
width: 66.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-x='8'] {
left: 66.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-min-w='8'] {
min-width: 66.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-max-w='8'] {
max-width: 66.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-w='9'] {
width: 75%;
}
.nb-grid-stack > .grid-stack-item[gs-x='9'] {
left: 75%;
}
.nb-grid-stack > .grid-stack-item[gs-min-w='9'] {
min-width: 75%;
}
.nb-grid-stack > .grid-stack-item[gs-max-w='9'] {
max-width: 75%;
}
.nb-grid-stack > .grid-stack-item[gs-w='10'] {
width: 83.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-x='10'] {
left: 83.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-min-w='10'] {
min-width: 83.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-max-w='10'] {
max-width: 83.3333333333%;
}
.nb-grid-stack > .grid-stack-item[gs-w='11'] {
width: 91.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-x='11'] {
left: 91.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-min-w='11'] {
min-width: 91.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-max-w='11'] {
max-width: 91.6666666667%;
}
.nb-grid-stack > .grid-stack-item[gs-w='12'] {
width: 100%;
}
.nb-grid-stack > .grid-stack-item[gs-x='12'] {
left: 100%;
}
.nb-grid-stack > .grid-stack-item[gs-min-w='12'] {
min-width: 100%;
}
.nb-grid-stack > .grid-stack-item[gs-max-w='12'] {
max-width: 100%;
}
.nb-grid-stack.grid-stack-1 > .grid-stack-item {
min-width: 100%;
}
.nb-grid-stack.grid-stack-1 > .grid-stack-item[gs-w='1'] {
width: 100%;
}
.nb-grid-stack.grid-stack-1 > .grid-stack-item[gs-x='1'] {
left: 100%;
}
.nb-grid-stack.grid-stack-1 > .grid-stack-item[gs-min-w='1'] {
min-width: 100%;
}
.nb-grid-stack.grid-stack-1 > .grid-stack-item[gs-max-w='1'] {
max-width: 100%;
}
.nb-grid-stack.grid-stack-animate, .nb-grid-stack.grid-stack-animate .grid-stack-item {
-webkit-transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
-moz-transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
-ms-transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
-o-transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
}
.nb-grid-stack.grid-stack-animate .grid-stack-item.ui-draggable-dragging, .nb-grid-stack.grid-stack-animate .grid-stack-item.ui-resizable-resizing, .nb-grid-stack.grid-stack-animate .grid-stack-item.grid-stack-placeholder {
-webkit-transition: left 0s, top 0s, height 0s, width 0s;
-moz-transition: left 0s, top 0s, height 0s, width 0s;
-ms-transition: left 0s, top 0s, height 0s, width 0s;
-o-transition: left 0s, top 0s, height 0s, width 0s;
transition: left 0s, top 0s, height 0s, width 0s;
}
.nb-grid-stack.ui-droppable.ui-droppable-over > *:not(.ui-droppable) {
pointer-events: none;
}
.nb-grid-stack {
margin: 0 8px;
margin: 0 2px;
}
.nb-grid-stack + .empty-message{
@@ -26,7 +322,7 @@
display: flex;
flex-direction: column;
flex: 1;
margin-bottom: 20px;
margin-bottom: 0px;
}
.nb-grid-stack > .grid-stack-item .grid-stack-item-content .grid-stack-content-wrapper-inner {
overflow-y: auto;
@@ -53,18 +349,14 @@
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{
.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;
@@ -102,8 +394,6 @@
cursor:se-resize;
width:20px;
height:20px;
right:5px;
bottom:0
}
.nb-grid-stack > .grid-stack-item > .ui-resizable-s{
cursor:s-resize;
@@ -126,3 +416,11 @@
top:15px;
bottom:15px
}
.nb-grid-stack text-cell-component .notebook-preview {
padding: 0px 0px 0px;
}
.nb-grid-stack output-area-component .notebook-output {
padding: 0px 0px 0px;
}

View File

@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* 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!./notebookViewsModal';
import { Component, Input } from '@angular/core';
import { calloutDialogBodyBackground, calloutDialogForeground, calloutDialogInteriorBorder } from 'sql/platform/theme/common/colorRegistry';
import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
@Component({
selector: 'notebook-views-modal-component',
template: `
<div [class.modal]="displayInputModal">
<div class="content">
<div class="title">{{title}}</div>
<ng-content></ng-content>
</div>
</div>
`
})
export class NotebookViewsModalComponent {
@Input() title: boolean;
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const foreground = theme.getColor(calloutDialogForeground);
if (foreground) {
collector.addRule(`notebook-views-grid-component .modal { position: absolute; background: ${foreground};}`);
}
const internalBorder = theme.getColor(calloutDialogInteriorBorder);
if (internalBorder) {
collector.addRule(`notebook-views-grid-component .modal .content { border-color: ${internalBorder}; }`);
collector.addRule(`notebook-views-grid-component .modal .content .title { border-color: ${internalBorder}; }`);
}
const bodyBackground = theme.getColor(calloutDialogBodyBackground);
if (bodyBackground) {
collector.addRule(`notebook-views-grid-component .modal .content { background: ${bodyBackground}; }`);
}
});

View File

@@ -0,0 +1,18 @@
notebook-view-component .modal{
position: absolute;
opacity: .3;
}
notebook-view-component .modal .content {
height: calc(100% - 60px);
width: calc(100% - 60px);
position: absolute;
top: 30px;
left: 30px;
border: 1px solid;
}
notebook-view-component .modal .content .title {
padding: 10px;
border-bottom: 1px solid;
}

View File

@@ -0,0 +1,141 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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 { 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';
import { attachButtonStyler } from 'vs/platform/theme/common/styler';
export class ViewOptionsModal extends Modal {
private _submitButton: Button;
private _cancelButton: Button;
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#notebookviews-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;
}
public override render() {
super.render();
this._submitButton = this.addFooterButton(localize('save', "Save"), () => this.onSubmitHandler());
this._cancelButton = this.addFooterButton(localize('cancel', "Cancel"), () => this.onCancelHandler());
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();
}
}

View File

@@ -98,7 +98,6 @@ suite('NotebookViewModel', function (): void {
assert.equal(cellsWithNewView.length, 2);
assert.equal(viewModel.cells.length, 2);
assert.equal(viewModel.hiddenCells.length, 0);
assert.equal(viewModel.name, defaultViewName);
});
@@ -111,7 +110,6 @@ suite('NotebookViewModel', function (): void {
assert.equal(cellsWithNewView.length, 2);
assert.equal(viewModel.cells.length, 2);
assert.equal(viewModel.hiddenCells.length, 0);
assert.equal(viewModel.name, defaultViewName);
});

View File

@@ -20,7 +20,7 @@ import { IContextViewProvider, IDelegate } from 'vs/base/browser/ui/contextview/
import { IEditorPane } from 'vs/workbench/common/editor';
import { INotebookShowOptions } from 'sql/workbench/api/common/sqlExtHost.protocol';
import { NotebookViewsExtension } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension';
import { INotebookView, INotebookViewCell, INotebookViews } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
import { INotebookView, INotebookViewCell, INotebookViewMetadata, INotebookViews } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews';
export class NotebookModelStub implements INotebookModel {
constructor(private _languageInfo?: nb.ILanguageInfo, private _cells?: ICellModel[], private _testContents?: nb.INotebookContents) {
@@ -809,6 +809,8 @@ export class NotebookViewStub implements INotebookView {
}
export class NotebookViewsStub implements INotebookViews {
onActiveViewChanged: vsEvent.Event<void>;
metadata: INotebookViewMetadata;
notebook: INotebookModel;
onViewDeleted: vsEvent.Event<void>;
createNewView(name?: string): INotebookView {

View File

@@ -593,6 +593,7 @@ export class CellModel extends Disposable implements ICellModel {
}, false);
this.setFuture(future as FutureInternal);
this.fireExecutionStateChanged();
this.notebookModel.onCellChange(this, NotebookChangeType.CellExecutionStarted);
this._notebookService?.notifyCellExecutionStarted();
// For now, await future completion. Later we should just track and handle cancellation based on model notifications
let result: nb.IExecuteReplyMsg = <nb.IExecuteReplyMsg><any>await future.done;
@@ -920,6 +921,9 @@ export class CellModel extends Disposable implements ICellModel {
}
}
};
this.sendChangeToNotebook(NotebookChangeType.CellAwaitingInput);
return handler();
}

View File

@@ -5,6 +5,7 @@
import { INotebookModel, ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { NotebookChangeType } from 'sql/workbench/services/notebook/common/contracts';
import { deepClone } from 'vs/base/common/objects';
export class NotebookExtension<TNotebookMeta, TCellMeta> {
readonly version = 1;
@@ -32,6 +33,7 @@ export class NotebookExtension<TNotebookMeta, TCellMeta> {
const meta = {};
meta[this.extensionName] = metadata;
cell.metadata[this.extensionNamespace] = meta;
cell.metadata = deepClone(cell.metadata); // creating a new reference for change detection
cell.sendChangeToNotebook(NotebookChangeType.CellsModified);
}
}

View File

@@ -31,11 +31,19 @@ export class NotebookViewModel implements INotebookView {
private _notebookViews: NotebookViewsExtension,
guid?: string
) {
this.guid = generateUuid();
this.guid = guid ?? generateUuid();
}
public initialize(): void {
this._isNew = true;
public static load(guid: string, notebookViews: NotebookViewsExtension): INotebookView {
const view = notebookViews.getViews().find(v => v.guid === guid);
return new NotebookViewModel(view.name, notebookViews, view.guid);
}
public initialize(isNew?: boolean): void {
if (isNew) {
this._isNew = isNew;
}
const cells = this._notebookViews.notebook.cells;
cells.forEach((cell, idx) => { this.initializeCell(cell, idx); });
}
@@ -48,14 +56,21 @@ export class NotebookViewModel implements INotebookView {
meta = this._notebookViews.getCellMetadata(cell);
}
meta.views.push({
guid: this.guid,
hidden: false,
y: idx * DEFAULT_VIEW_CARD_HEIGHT,
x: 0,
width: DEFAULT_VIEW_CARD_WIDTH,
height: DEFAULT_VIEW_CARD_HEIGHT
});
// Ensure that we are not duplicting view entries in cell metadata
if (!meta.views.find(v => v.guid === this.guid)) {
meta.views.push({
guid: this.guid,
hidden: false,
y: idx * DEFAULT_VIEW_CARD_HEIGHT,
x: 0,
width: DEFAULT_VIEW_CARD_WIDTH,
height: DEFAULT_VIEW_CARD_HEIGHT
});
}
}
public cellInitialized(cell: ICellModel): boolean {
return !!this.getCellMetadata(cell);
}
public get name(): string {
@@ -79,7 +94,7 @@ export class NotebookViewModel implements INotebookView {
}
public get hiddenCells(): Readonly<ICellModel[]> {
return this.cells.filter(cell => this.getCellMetadata(cell)?.hidden);
return this.cells.filter(cell => this.getCellMetadata(cell)?.hidden !== false);
}
public get cells(): Readonly<ICellModel[]> {
@@ -94,16 +109,24 @@ export class NotebookViewModel implements INotebookView {
return this._notebookViews.notebook.cells.find(cell => cell.cellGuid === guid);
}
public updateCell(cell: ICellModel, currentView: INotebookView, cellData: INotebookViewCell, override: boolean = false) {
if (!this.cellInitialized(cell)) {
this.initializeCell(cell, 0);
}
this._notebookViews.updateCell(cell, currentView, cellData, override);
}
public insertCell(cell: ICellModel) {
this._notebookViews.updateCell(cell, this, { hidden: false });
this.updateCell(cell, this, { hidden: false });
}
public hideCell(cell: ICellModel) {
this._notebookViews.updateCell(cell, this, { hidden: true });
this.updateCell(cell, this, { hidden: true });
}
public moveCell(cell: ICellModel, x: number, y: number) {
this._notebookViews.updateCell(cell, this, { x, y });
this.updateCell(cell, this, { x, y });
}
public resizeCell(cell: ICellModel, width?: number, height?: number) {
@@ -117,7 +140,7 @@ export class NotebookViewModel implements INotebookView {
data.height = height;
}
this._notebookViews.updateCell(cell, this, data);
this.updateCell(cell, this, data);
}
public getCellSize(cell: ICellModel): any {

View File

@@ -6,23 +6,33 @@
import { ICellModel, INotebookModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
import { Event } from 'vs/base/common/event';
export type CellChangeEventType = 'hide' | 'insert' | 'active';
export type CellChangeEventType = 'hide' | 'insert' | 'active' | 'execution' | 'update';
export type CellChangeEvent = {
cell: ICellModel,
event: CellChangeEventType
};
export interface INotebookViews {
onActiveViewChanged: Event<void>;
createNewView(name?: string): INotebookView;
removeView(guid: string): void;
getActiveView(): INotebookView;
setActiveView(view: INotebookView);
viewNameIsTaken(name: string): boolean;
metadata: INotebookViewMetadata;
}
export interface INotebookView {
readonly guid: string;
readonly onDeleted: Event<INotebookView>;
isNew: boolean;
cells: Readonly<ICellModel[]>;
hiddenCells: Readonly<ICellModel[]>;
displayedCells: Readonly<ICellModel[]>;
name: string;
initialize(): void;
initialize(isNew?: boolean): void;
nameAvailable(name: string): boolean;
getCellMetadata(cell: ICellModel): INotebookViewCell;
hideCell(cell: ICellModel): void;

View File

@@ -20,6 +20,7 @@ export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetad
protected _metadata: INotebookViewMetadata;
private _onViewDeleted = new Emitter<void>();
private _onActiveViewChanged = new Emitter<void>();
constructor(protected _notebook: INotebookModel) {
super();
@@ -33,6 +34,8 @@ export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetad
this.initializeNotebook();
this.initializeCells();
this.commit();
} else {
this._metadata.views = this._metadata.views.map(view => NotebookViewModel.load(view.guid, this));
}
}
@@ -63,9 +66,12 @@ export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetad
const viewName = name || this.generateDefaultViewName();
const view = new NotebookViewModel(viewName, this);
view.initialize();
view.initialize(true);
this._metadata.views.push(view);
//Add view to the views metadata
this._metadata = Object.assign({}, this._metadata, { views: [...this._metadata.views, view] });
this.commit();
return view;
}
@@ -76,21 +82,21 @@ export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetad
let removedView = this._metadata.views.splice(viewToRemove, 1);
// Remove view data for each cell
if (removedView.length) {
if (removedView.length === 1) {
this._notebook?.cells.forEach((cell) => {
let meta = this.getCellMetadata(cell);
meta.views.splice(viewToRemove, 1);
meta.views = meta.views.filter(x => x.guid !== removedView[0].guid);
this.setCellMetadata(cell, meta);
});
}
this.setNotebookMetadata(this.notebook, this._metadata);
}
if (guid === this._metadata.activeView) {
this._metadata.activeView = undefined;
}
this._metadata = Object.assign({}, this._metadata);
this._onViewDeleted.fire();
this.commit();
}
@@ -108,11 +114,13 @@ export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetad
public updateCell(cell: ICellModel, currentView: INotebookView, cellData: INotebookViewCell, override: boolean = false) {
const cellMetadata = this.getCellMetadata(cell);
const viewToUpdate = cellMetadata.views.findIndex(view => view.guid === currentView.guid);
if (cellMetadata) {
const viewToUpdate = cellMetadata.views.findIndex(view => view.guid === currentView.guid);
if (viewToUpdate >= 0) {
cellMetadata.views[viewToUpdate] = override ? cellData : { ...cellMetadata.views[viewToUpdate], ...cellData };
this.setCellMetadata(cell, cellMetadata);
if (viewToUpdate >= 0) {
cellMetadata.views[viewToUpdate] = override ? cellData : { ...cellMetadata.views[viewToUpdate], ...cellData };
this.setCellMetadata(cell, cellMetadata);
}
}
}
@@ -124,6 +132,10 @@ export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetad
return this._metadata.views;
}
public get metadata(): INotebookViewMetadata {
return this._metadata;
}
public getCells(): INotebookViewCellMetadata[] {
return this._notebook.cells.map(cell => this.getCellMetadata(cell));
}
@@ -134,9 +146,11 @@ export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetad
public setActiveView(view: INotebookView) {
this._metadata.activeView = view.guid;
this._onActiveViewChanged.fire();
}
public commit() {
this._metadata = Object.assign({}, this._metadata);
this.setNotebookMetadata(this._notebook, this._metadata);
}
@@ -147,4 +161,8 @@ export class NotebookViewsExtension extends NotebookExtension<INotebookViewMetad
public get onViewDeleted(): Event<void> {
return this._onViewDeleted.event;
}
public get onActiveViewChanged(): Event<void> {
return this._onActiveViewChanged.event;
}
}

View File

@@ -45,8 +45,10 @@ export enum NotebookChangeType {
MetadataChanged,
TrustChanged,
Saved,
CellExecutionStarted,
CellExecuted,
CellInputVisibilityChanged,
CellAwaitingInput,
CellOutputCleared,
CellMetadataUpdated
}