diff --git a/src/sql/media/icons/common-icons.css b/src/sql/media/icons/common-icons.css index ab3fe8663a..5f18584766 100644 --- a/src/sql/media/icons/common-icons.css +++ b/src/sql/media/icons/common-icons.css @@ -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"); diff --git a/src/sql/media/icons/settings.svg b/src/sql/media/icons/settings.svg index cdaf3ceffb..93a9327cad 100644 --- a/src/sql/media/icons/settings.svg +++ b/src/sql/media/icons/settings.svg @@ -1,3 +1,3 @@ - + diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts index 543c8a2f14..1f92599d4f 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts @@ -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 { + protected async awaitStdIn(): Promise { try { let value = await this.inputDeferred.promise; this.cellModel.future.sendInputReply({ value: value }); diff --git a/src/sql/workbench/contrib/notebook/browser/media/light/notebook_views_card_handle.svg b/src/sql/workbench/contrib/notebook/browser/media/light/notebook_views_card_handle.svg index b48d5c1313..8218a34b42 100644 --- a/src/sql/workbench/contrib/notebook/browser/media/light/notebook_views_card_handle.svg +++ b/src/sql/workbench/contrib/notebook/browser/media/light/notebook_views_card_handle.svg @@ -1 +1 @@ - + diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts index 86359b82fc..2e72ce68af 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -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 diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.module.ts b/src/sql/workbench/contrib/notebook/browser/notebook.module.ts index 37fa32300b..044ff2e6d1 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.module.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.module.ts @@ -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(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: [ diff --git a/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.html b/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.html index fd0603203b..ba6e077cac 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.html +++ b/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.html @@ -5,5 +5,6 @@ *--------------------------------------------------------------------------------------------*/ --> - - + + + diff --git a/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.ts index fda0e319bb..abfc9b1df2 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookEditor.component.ts @@ -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(); + 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 { @@ -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 { + 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; + } } diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/cellToolbar.css b/src/sql/workbench/contrib/notebook/browser/notebookViews/cellToolbar.css index d7a20a413c..1388f5b1bd 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/cellToolbar.css +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/cellToolbar.css @@ -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; diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViews.component.html b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViews.component.html index 14c14d7dfb..760ccbb896 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViews.component.html +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViews.component.html @@ -7,9 +7,12 @@
- - -
-
+ + + + + + +
diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViews.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViews.component.ts index a57fc499b6..430ebee4ea 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViews.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViews.component.ts @@ -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(); 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); + } } diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.html b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.html index 47c7fad7d3..d93cdc2393 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.html +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.html @@ -6,13 +6,13 @@ -->
- + diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.ts index c0286bf981..fe2353ad43 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCard.component.ts @@ -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 = 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};}`); + } +}); diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.html b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.html index dbc2fd480a..941680f3a0 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.html +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.html @@ -6,11 +6,11 @@ -->
- + -
+
{{emptyCellText}}
- +
diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.ts index 04015c1b80..d86f481a68 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsCodeCell.component.ts @@ -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'] !== ''); + 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 { + return super.outputs + .filter((output: nb.IDisplayResult) => output.data === undefined || output?.data['text/plain'] !== '') + .map((output: nb.ICellOutput) => ({ ...output })) + .map((output: nb.ICellOutput) => { output.metadata = { ...output.metadata }; return output; }); } } diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.html b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.html index 38798f10f4..428292555e 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.html +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.html @@ -6,10 +6,15 @@ -->
- +
+
{{emptyText}}
+ +
+ +
diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.ts index 47af24fbd3..2e66fb84d8 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.component.ts @@ -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(); diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.css b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.css index 97d5e61398..d70346755a 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.css +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsGrid.css @@ -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; +} diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsModal.component.ts b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsModal.component.ts new file mode 100644 index 0000000000..ccbde60eff --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsModal.component.ts @@ -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: ` +
+
+
{{title}}
+ +
+
+ ` +}) +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}; }`); + } +}); diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsModal.css b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsModal.css new file mode 100644 index 0000000000..bcf9b23b2b --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsModal.css @@ -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; +} diff --git a/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsOptionsModal.ts b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsOptionsModal.ts new file mode 100644 index 0000000000..bd6cf7960e --- /dev/null +++ b/src/sql/workbench/contrib/notebook/browser/notebookViews/notebookViewsOptionsModal.ts @@ -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.$('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(); + } +} diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts index 956f66038e..a8d62630c7 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookViewModel.test.ts @@ -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); }); diff --git a/src/sql/workbench/contrib/notebook/test/stubs.ts b/src/sql/workbench/contrib/notebook/test/stubs.ts index b597ec947f..2e42d30735 100644 --- a/src/sql/workbench/contrib/notebook/test/stubs.ts +++ b/src/sql/workbench/contrib/notebook/test/stubs.ts @@ -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; + metadata: INotebookViewMetadata; notebook: INotebookModel; onViewDeleted: vsEvent.Event; createNewView(name?: string): INotebookView { diff --git a/src/sql/workbench/services/notebook/browser/models/cell.ts b/src/sql/workbench/services/notebook/browser/models/cell.ts index 7445d83df6..7bf4b6be7b 100644 --- a/src/sql/workbench/services/notebook/browser/models/cell.ts +++ b/src/sql/workbench/services/notebook/browser/models/cell.ts @@ -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 = await future.done; @@ -920,6 +921,9 @@ export class CellModel extends Disposable implements ICellModel { } } }; + + this.sendChangeToNotebook(NotebookChangeType.CellAwaitingInput); + return handler(); } diff --git a/src/sql/workbench/services/notebook/browser/models/notebookExtension.ts b/src/sql/workbench/services/notebook/browser/models/notebookExtension.ts index 2eff0741b2..5c30ae475a 100644 --- a/src/sql/workbench/services/notebook/browser/models/notebookExtension.ts +++ b/src/sql/workbench/services/notebook/browser/models/notebookExtension.ts @@ -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 { readonly version = 1; @@ -32,6 +33,7 @@ export class NotebookExtension { 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); } } diff --git a/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewModel.ts b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewModel.ts index 97c5bf3e1b..56961133fc 100644 --- a/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewModel.ts +++ b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewModel.ts @@ -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 { - return this.cells.filter(cell => this.getCellMetadata(cell)?.hidden); + return this.cells.filter(cell => this.getCellMetadata(cell)?.hidden !== false); } public get cells(): Readonly { @@ -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 { diff --git a/src/sql/workbench/services/notebook/browser/notebookViews/notebookViews.d.ts b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViews.d.ts index 449884f92e..d115891a54 100644 --- a/src/sql/workbench/services/notebook/browser/notebookViews/notebookViews.d.ts +++ b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViews.d.ts @@ -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; + 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; - isNew: boolean; + cells: Readonly; hiddenCells: Readonly; displayedCells: Readonly; name: string; - initialize(): void; + initialize(isNew?: boolean): void; nameAvailable(name: string): boolean; getCellMetadata(cell: ICellModel): INotebookViewCell; hideCell(cell: ICellModel): void; diff --git a/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension.ts b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension.ts index 853d345b0d..53aed35b8b 100644 --- a/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension.ts +++ b/src/sql/workbench/services/notebook/browser/notebookViews/notebookViewsExtension.ts @@ -20,6 +20,7 @@ export class NotebookViewsExtension extends NotebookExtension(); + private _onActiveViewChanged = new Emitter(); constructor(protected _notebook: INotebookModel) { super(); @@ -33,6 +34,8 @@ export class NotebookViewsExtension extends NotebookExtension NotebookViewModel.load(view.guid, this)); } } @@ -63,9 +66,12 @@ export class NotebookViewsExtension extends NotebookExtension { 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 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 this.getCellMetadata(cell)); } @@ -134,9 +146,11 @@ export class NotebookViewsExtension extends NotebookExtension { return this._onViewDeleted.event; } + + public get onActiveViewChanged(): Event { + return this._onActiveViewChanged.event; + } } diff --git a/src/sql/workbench/services/notebook/common/contracts.ts b/src/sql/workbench/services/notebook/common/contracts.ts index a2519b9a98..b7966223d3 100644 --- a/src/sql/workbench/services/notebook/common/contracts.ts +++ b/src/sql/workbench/services/notebook/common/contracts.ts @@ -45,8 +45,10 @@ export enum NotebookChangeType { MetadataChanged, TrustChanged, Saved, + CellExecutionStarted, CellExecuted, CellInputVisibilityChanged, + CellAwaitingInput, CellOutputCleared, CellMetadataUpdated }