From 6d260c195f79ed32076b29175f64c32545c87b49 Mon Sep 17 00:00:00 2001 From: Abbie Petchtes Date: Thu, 11 Oct 2018 16:14:54 -0700 Subject: [PATCH 01/18] create new notebook editor and add the place holder for toolbar and cell list (#2817) * create new notebook editor and add the place holder for toolbar and cell list * address comments --- .../parts/notebook/notebook.component.html | 16 +++ src/sql/parts/notebook/notebook.component.ts | 43 ++++++ .../parts/notebook/notebook.contribution.ts | 64 +++++++++ src/sql/parts/notebook/notebook.css | 17 +++ src/sql/parts/notebook/notebook.module.ts | 67 ++++++++++ src/sql/parts/notebook/notebookEditor.ts | 95 ++++++++++++++ src/sql/parts/notebook/notebookInput.ts | 124 ++++++++++++++++++ src/vs/workbench/workbench.main.ts | 2 + 8 files changed, 428 insertions(+) create mode 100644 src/sql/parts/notebook/notebook.component.html create mode 100644 src/sql/parts/notebook/notebook.component.ts create mode 100644 src/sql/parts/notebook/notebook.contribution.ts create mode 100644 src/sql/parts/notebook/notebook.css create mode 100644 src/sql/parts/notebook/notebook.module.ts create mode 100644 src/sql/parts/notebook/notebookEditor.ts create mode 100644 src/sql/parts/notebook/notebookInput.ts diff --git a/src/sql/parts/notebook/notebook.component.html b/src/sql/parts/notebook/notebook.component.html new file mode 100644 index 0000000000..cb770cb285 --- /dev/null +++ b/src/sql/parts/notebook/notebook.component.html @@ -0,0 +1,16 @@ + +
+
+
+ PlaceHolder for Toolbar +
+
+
+ Place Holder for cell list +
+
diff --git a/src/sql/parts/notebook/notebook.component.ts b/src/sql/parts/notebook/notebook.component.ts new file mode 100644 index 0000000000..484126f784 --- /dev/null +++ b/src/sql/parts/notebook/notebook.component.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- +* 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!./notebook'; + +import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core'; + +import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; +import { AngularDisposable } from 'sql/base/common/lifecycle'; + +import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import * as themeColors from 'vs/workbench/common/theme'; + +export const NOTEBOOK_SELECTOR: string = 'notebook-component'; + +@Component({ + selector: NOTEBOOK_SELECTOR, + templateUrl: decodeURI(require.toUrl('./notebook.component.html')) +}) +export class NotebookComponent extends AngularDisposable implements OnInit { + @ViewChild('header', { read: ElementRef }) private header: ElementRef; + constructor( + @Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface, + @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, + @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService + ) { + super(); + } + + ngOnInit() { + this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this)); + this.updateTheme(this.themeService.getColorTheme()); + } + + private updateTheme(theme: IColorTheme): void { + let headerEl = this.header.nativeElement; + headerEl.style.borderBottomColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString(); + headerEl.style.borderBottomWidth = '1px'; + headerEl.style.borderBottomStyle = 'solid'; + } +} diff --git a/src/sql/parts/notebook/notebook.contribution.ts b/src/sql/parts/notebook/notebook.contribution.ts new file mode 100644 index 0000000000..69546dd9f3 --- /dev/null +++ b/src/sql/parts/notebook/notebook.contribution.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { Action } from 'vs/base/common/actions'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { TPromise } from 'vs/base/common/winjs.base'; +import * as nls from 'vs/nls'; + +import { NotebookInput, NotebookInputModel } from 'sql/parts/notebook/notebookInput'; +import { NotebookEditor } from 'sql/parts/notebook/notebookEditor'; + +/** + * todo: Will remove this code. + * This is the entry point to open the new Notebook + */ +export class OpenNotebookAction extends Action { + + public static ID = 'OpenNotebookAction'; + public static LABEL = nls.localize('OpenNotebookAction', 'Open Notebook editor'); + + constructor( + id: string, + label: string, + @IEditorService private _editorService: IEditorService + ) { + super(id, label); + } + + public run(): TPromise { + return new TPromise((resolve, reject) => { + let model = new NotebookInputModel('modelViewId', undefined, undefined); + let input = new NotebookInput('modelViewId', model); + this._editorService.openEditor(input, { pinned: true }); + }); + } +} + +// Model View editor registration +const viewModelEditorDescriptor = new EditorDescriptor( + NotebookEditor, + NotebookEditor.ID, + 'Notebook' +); + +Registry.as(EditorExtensions.Editors) + .registerEditor(viewModelEditorDescriptor, [new SyncDescriptor(NotebookInput)]); + +// todo: Will remove this code. +// this is the entry point to open the new Notebook +let actionRegistry = Registry.as(Extensions.WorkbenchActions); +actionRegistry.registerWorkbenchAction( + new SyncActionDescriptor( + OpenNotebookAction, + OpenNotebookAction.ID, + OpenNotebookAction.LABEL + ), + OpenNotebookAction.LABEL +); \ No newline at end of file diff --git a/src/sql/parts/notebook/notebook.css b/src/sql/parts/notebook/notebook.css new file mode 100644 index 0000000000..b5cae2d434 --- /dev/null +++ b/src/sql/parts/notebook/notebook.css @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +/* +.notebookEditor .header .monaco-action-bar .action-label { + padding: 8px; +} + +.notebookEditor .header .monaco-action-bar .action-item { + margin-right: 5px; +} + +.notebookEditor .monaco-action-bar { + overflow: visible; +} +*/ \ No newline at end of file diff --git a/src/sql/parts/notebook/notebook.module.ts b/src/sql/parts/notebook/notebook.module.ts new file mode 100644 index 0000000000..94ba37d12d --- /dev/null +++ b/src/sql/parts/notebook/notebook.module.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the Source EULA. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { forwardRef, NgModule, ComponentFactoryResolver, Inject, ApplicationRef } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { CommonModule, APP_BASE_HREF } from '@angular/common'; +import { BrowserModule } from '@angular/platform-browser'; + + +import { Extensions, IComponentRegistry } from 'sql/platform/dashboard/common/modelComponentRegistry'; +import { ComponentHostDirective } from 'sql/parts/dashboard/common/componentHost.directive'; +import { IBootstrapParams, ISelector, providerIterator } from 'sql/services/bootstrap/bootstrapService'; +import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; +import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox.component'; +import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox.component'; +import { EditableDropDown } from 'sql/base/browser/ui/editableDropdown/editableDropdown.component'; +import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox.component'; +import { NotebookComponent } from 'sql/parts/notebook/notebook.component'; + +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Registry } from 'vs/platform/registry/common/platform'; + +export const NotebookModule = (params, selector: string, instantiationService: IInstantiationService): any => { + @NgModule({ + declarations: [ + Checkbox, + SelectBox, + EditableDropDown, + InputBox, + NotebookComponent, + ComponentHostDirective + ], + entryComponents: [NotebookComponent], + imports: [ + FormsModule, + CommonModule, + BrowserModule + ], + providers: [ + { provide: APP_BASE_HREF, useValue: '/' }, + CommonServiceInterface, + { provide: IBootstrapParams, useValue: params }, + { provide: ISelector, useValue: selector }, + ...providerIterator(instantiationService) + ] + }) + class ModuleClass { + + constructor( + @Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver, + @Inject(ISelector) private selector: string + ) { + } + + ngDoBootstrap(appRef: ApplicationRef) { + const factoryWrapper: any = this._resolver.resolveComponentFactory(NotebookComponent); + factoryWrapper.factory.selector = this.selector; + appRef.bootstrap(factoryWrapper); + } + } + + return ModuleClass; +}; diff --git a/src/sql/parts/notebook/notebookEditor.ts b/src/sql/parts/notebook/notebookEditor.ts new file mode 100644 index 0000000000..eb89f73fcf --- /dev/null +++ b/src/sql/parts/notebook/notebookEditor.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { TPromise } from 'vs/base/common/winjs.base'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorOptions } from 'vs/workbench/common/editor'; +import * as DOM from 'vs/base/browser/dom'; +import { $ } from 'vs/base/browser/builder'; +import { bootstrapAngular } from 'sql/services/bootstrap/bootstrapService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { NotebookInput } from 'sql/parts/notebook/notebookInput'; +import { NotebookModule } from 'sql/parts/notebook/notebook.module'; +import { NOTEBOOK_SELECTOR } from 'sql/parts/notebook/notebook.component'; + +export class NotebookEditor extends BaseEditor { + + public static ID: string = 'workbench.editor.notebookEditor'; + private _notebookContainer: HTMLElement; + protected _input: NotebookInput; + + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @IInstantiationService private instantiationService: IInstantiationService, + ) { + super(NotebookEditor.ID, telemetryService, themeService); + } + + public get input(): NotebookInput { + return this._input; + } + + /** + * Called to create the editor in the parent element. + */ + public createEditor(parent: HTMLElement): void { + } + + /** + * Sets focus on this editor. Specifically, it sets the focus on the hosted text editor. + */ + public focus(): void { + } + + /** + * Updates the internal variable keeping track of the editor's size, and re-calculates the sash position. + * To be called when the container of this editor changes size. + */ + public layout(dimension: DOM.Dimension): void { + } + + public setInput(input: NotebookInput, options: EditorOptions): TPromise { + if (this.input && this.input.matches(input)) { + return TPromise.as(undefined); + } + + const parentElement = this.getContainer(); + + super.setInput(input, options, CancellationToken.None); + + $(parentElement).clearChildren(); + + if (!input.hasBootstrapped) { + let container = DOM.$('.notebookEditor'); + container.style.height = '100%'; + this._notebookContainer = DOM.append(parentElement, container); + this.input.container = this._notebookContainer; + return TPromise.wrap(this.bootstrapAngular(input)); + } else { + this._notebookContainer = DOM.append(parentElement, this.input.container); + return TPromise.wrap(null); + } + } + + /** + * Load the angular components and record for this input that we have done so + */ + private bootstrapAngular(input: NotebookInput): void { + // Get the bootstrap params and perform the bootstrap + input.hasBootstrapped = true; + bootstrapAngular(this.instantiationService, + NotebookModule, + this._notebookContainer, + NOTEBOOK_SELECTOR, + undefined, + undefined + ); + } +} + diff --git a/src/sql/parts/notebook/notebookInput.ts b/src/sql/parts/notebook/notebookInput.ts new file mode 100644 index 0000000000..a2d6b4dc87 --- /dev/null +++ b/src/sql/parts/notebook/notebookInput.ts @@ -0,0 +1,124 @@ +import { TPromise } from 'vs/base/common/winjs.base'; +import { IEditorModel } from 'vs/platform/editor/common/editor'; +import { EditorInput, EditorModel, ConfirmResult } from 'vs/workbench/common/editor'; + +import { Emitter, Event } from 'vs/base/common/event'; + +export type ModeViewSaveHandler = (handle: number) => Thenable; + +export class NotebookInputModel extends EditorModel { + private dirty: boolean; + private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); + get onDidChangeDirty(): Event { return this._onDidChangeDirty.event; } + + constructor(public readonly modelViewId, private readonly handle: number, private saveHandler?: ModeViewSaveHandler) { + super(); + this.dirty = false; + } + + get isDirty(): boolean { + return this.dirty; + } + + public setDirty(dirty: boolean): void { + if (this.dirty === dirty) { + return; + } + + this.dirty = dirty; + this._onDidChangeDirty.fire(); + } + + save(): TPromise { + if (this.saveHandler) { + return TPromise.wrap(this.saveHandler(this.handle)); + } + return TPromise.wrap(true); + } +} +export class NotebookInput extends EditorInput { + + public static ID: string = 'workbench.editorinputs.notebookInput'; + + public hasBootstrapped = false; + // Holds the HTML content for the editor when the editor discards this input and loads another + private _parentContainer: HTMLElement; + + constructor(private _title: string, private _model: NotebookInputModel, + ) { + super(); + this._model.onDidChangeDirty(() => this._onDidChangeDirty.fire()); + + } + + public get title(): string { + return this._title; + } + + public get modelViewId(): string { + return this._model.modelViewId; + } + + public getTypeId(): string { + return NotebookInput.ID; + } + + public resolve(refresh?: boolean): TPromise { + return undefined; + } + + public getName(): string { + return this._title; + } + + public dispose(): void { + this._disposeContainer(); + super.dispose(); + } + + private _disposeContainer() { + if (!this._parentContainer) { + return; + } + + let parentNode = this._parentContainer.parentNode; + if (parentNode) { + parentNode.removeChild(this._parentContainer); + this._parentContainer = null; + } + } + + set container(container: HTMLElement) { + this._disposeContainer(); + this._parentContainer = container; + } + + get container(): HTMLElement { + return this._parentContainer; + } + + /** + * An editor that is dirty will be asked to be saved once it closes. + */ + isDirty(): boolean { + return this._model.isDirty; + } + + /** + * Subclasses should bring up a proper dialog for the user if the editor is dirty and return the result. + */ + confirmSave(): TPromise { + // TODO #2530 support save on close / confirm save. This is significantly more work + // as we need to either integrate with textFileService (seems like this isn't viable) + // or register our own complimentary service that handles the lifecycle operations such + // as close all, auto save etc. + return TPromise.wrap(ConfirmResult.DONT_SAVE); + } + + /** + * Saves the editor if it is dirty. Subclasses return a promise with a boolean indicating the success of the operation. + */ + save(): TPromise { + return this._model.save(); + } +} \ No newline at end of file diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 2faf5ef1a5..8e4307b613 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -189,6 +189,8 @@ import 'sql/parts/dashboard/dashboardConfig.contribution'; import 'sql/parts/modelComponents/components.contribution'; /* View Model Editor */ import 'sql/parts/modelComponents/modelEditor/modelViewEditor.contribution'; +/* Notebook Editor */ +import 'sql/parts/notebook/notebook.contribution'; /* Containers */ import 'sql/parts/dashboard/containers/dashboardWebviewContainer.contribution'; import 'sql/parts/dashboard/containers/dashboardControlHostContainer.contribution'; From 906c4c7f396d3008c8a3593e6b930355a0c27c97 Mon Sep 17 00:00:00 2001 From: Abbie Petchtes Date: Tue, 16 Oct 2018 16:28:15 -0700 Subject: [PATCH 02/18] Add code cell (#2909) * initial work for addig code and code cell type * add cell model and create editor for each cell * formatting * fix resizing issue * small changes * address comment --- .../notebook/cellViews/code.component.html | 13 ++ .../notebook/cellViews/code.component.ts | 130 ++++++++++++++++++ src/sql/parts/notebook/cellViews/code.css | 15 ++ .../cellViews/codeCell.component.html | 14 ++ .../notebook/cellViews/codeCell.component.ts | 40 ++++++ src/sql/parts/notebook/cellViews/codeCell.css | 10 ++ .../parts/notebook/cellViews/interfaces.ts | 31 +++++ .../parts/notebook/notebook.component.html | 5 +- src/sql/parts/notebook/notebook.component.ts | 19 ++- src/sql/parts/notebook/notebook.css | 15 +- src/sql/parts/notebook/notebook.module.ts | 4 + 11 files changed, 277 insertions(+), 19 deletions(-) create mode 100644 src/sql/parts/notebook/cellViews/code.component.html create mode 100644 src/sql/parts/notebook/cellViews/code.component.ts create mode 100644 src/sql/parts/notebook/cellViews/code.css create mode 100644 src/sql/parts/notebook/cellViews/codeCell.component.html create mode 100644 src/sql/parts/notebook/cellViews/codeCell.component.ts create mode 100644 src/sql/parts/notebook/cellViews/codeCell.css create mode 100644 src/sql/parts/notebook/cellViews/interfaces.ts diff --git a/src/sql/parts/notebook/cellViews/code.component.html b/src/sql/parts/notebook/cellViews/code.component.html new file mode 100644 index 0000000000..421f9416e3 --- /dev/null +++ b/src/sql/parts/notebook/cellViews/code.component.html @@ -0,0 +1,13 @@ + +
+
+ Toolbar +
+
+
+
diff --git a/src/sql/parts/notebook/cellViews/code.component.ts b/src/sql/parts/notebook/cellViews/code.component.ts new file mode 100644 index 0000000000..3a8d34fc32 --- /dev/null +++ b/src/sql/parts/notebook/cellViews/code.component.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- +* 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!./code'; + +import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core'; + +import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; +import { AngularDisposable } from 'sql/base/common/lifecycle'; +import { ComponentBase } from 'sql/parts/modelComponents/componentBase'; +import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces'; +import { QueryTextEditor } from 'sql/parts/modelComponents/queryTextEditor'; + +import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import * as themeColors from 'vs/workbench/common/theme'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { SimpleProgressService } from 'vs/editor/standalone/browser/simpleServices'; +import { IProgressService } from 'vs/platform/progress/common/progress'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ITextModel } from 'vs/editor/common/model'; +import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; +import URI from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; +import * as DOM from 'vs/base/browser/dom'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { IModelService } from 'vs/editor/common/services/modelService'; + + +export const CODE_SELECTOR: string = 'code-component'; + +@Component({ + selector: CODE_SELECTOR, + templateUrl: decodeURI(require.toUrl('./code.component.html')) +}) +export class CodeComponent extends AngularDisposable implements OnInit { + @ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef; + @ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef; + @Input() id: string; + @Input() content: string; + @Input() language: string; + + private _editor: QueryTextEditor; + private _editorInput: UntitledEditorInput; + private _editorModel: ITextModel; + private _uri: string; + + constructor( + @Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface, + @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, + @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService, + @Inject(IInstantiationService) private _instantiationService: IInstantiationService, + @Inject(IModelService) private _modelService: IModelService, + @Inject(IModeService) private _modeService: IModeService + ) { + super(); + } + + ngOnInit() { + this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this)); + this.updateTheme(this.themeService.getColorTheme()); + } + + ngOnChanges() { + this.updateLanguageMode(); + this.updateModel(); + } + + ngAfterContentInit(): void { + this.createEditor(); + this._register(DOM.addDisposableListener(window, DOM.EventType.RESIZE, e => { + this.layout(); + })); + } + + private createEditor(): void { + let instantiationService = this._instantiationService.createChild(new ServiceCollection([IProgressService, new SimpleProgressService()])); + this._editor = instantiationService.createInstance(QueryTextEditor); + this._editor.create(this.codeElement.nativeElement); + this._editor.setVisible(true); + let uri = this.createUri(); + this._editorInput = instantiationService.createInstance(UntitledEditorInput, uri, false, this.language, '', ''); + this._editor.setInput(this._editorInput, undefined); + this._editorInput.resolve().then(model => { + this._editorModel = model.textEditorModel; + this._modelService.updateModel(this._editorModel, this.content); + }); + + this._register(this._editor); + this._register(this._editorInput); + this._register(this._editorModel.onDidChangeContent(e => { + this.content = this._editorModel.getValue(); + this._editor.setHeightToScrollHeight(); + })); + this.layout(); + } + + public layout(): void { + this._editor.layout(new DOM.Dimension( + DOM.getContentWidth(this.codeElement.nativeElement), + DOM.getContentHeight(this.codeElement.nativeElement))); + } + + private createUri(): URI { + let uri = URI.from({ scheme: Schemas.untitled, path: `notebook-editor-${this.id}` }); + // Use this to set the internal (immutable) and public (shared with extension) uri properties + this._uri = uri.toString(); + return uri; + } + + /// Editor Functions + private updateModel() { + if (this._editorModel) { + this._modelService.updateModel(this._editorModel, this.content); + } + } + + private updateLanguageMode() { + if (this._editorModel && this._editor) { + this._modeService.getOrCreateMode(this.language).then((modeValue) => { + this._modelService.setMode(this._editorModel, modeValue); + }); + } + } + + private updateTheme(theme: IColorTheme): void { + let toolbarEl = this.toolbarElement.nativeElement; + toolbarEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString(); + } +} diff --git a/src/sql/parts/notebook/cellViews/code.css b/src/sql/parts/notebook/cellViews/code.css new file mode 100644 index 0000000000..e8cadbfbd0 --- /dev/null +++ b/src/sql/parts/notebook/cellViews/code.css @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +code-component { + height: 100%; + width: 100%; + display: block; +} + +code-component .toolbar { + border-right-width: 1px; + border-right-style: solid; +} diff --git a/src/sql/parts/notebook/cellViews/codeCell.component.html b/src/sql/parts/notebook/cellViews/codeCell.component.html new file mode 100644 index 0000000000..b72bbd632d --- /dev/null +++ b/src/sql/parts/notebook/cellViews/codeCell.component.html @@ -0,0 +1,14 @@ + +
+
+ +
+
+ Place Holder for output area +
+
diff --git a/src/sql/parts/notebook/cellViews/codeCell.component.ts b/src/sql/parts/notebook/cellViews/codeCell.component.ts new file mode 100644 index 0000000000..23e7bd21eb --- /dev/null +++ b/src/sql/parts/notebook/cellViews/codeCell.component.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- +* 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!./codeCell'; + +import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core'; + +import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; +import { CellView, ICellModel } from 'sql/parts/notebook/cellViews/interfaces'; + +import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import * as themeColors from 'vs/workbench/common/theme'; + + +export const CODE_SELECTOR: string = 'code-cell-component'; + +@Component({ + selector: CODE_SELECTOR, + templateUrl: decodeURI(require.toUrl('./codeCell.component.html')) +}) +export class CodeCellComponent extends CellView implements OnInit { + @Input() cellModel: ICellModel; + constructor( + @Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface, + @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, + @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService + ) { + super(); + } + + ngOnInit() { + + } + + // Todo: implement layout + public layout() { + + } +} diff --git a/src/sql/parts/notebook/cellViews/codeCell.css b/src/sql/parts/notebook/cellViews/codeCell.css new file mode 100644 index 0000000000..6386355eda --- /dev/null +++ b/src/sql/parts/notebook/cellViews/codeCell.css @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +code-cell-component { + height: 100%; + width: 100%; + display: block; +} diff --git a/src/sql/parts/notebook/cellViews/interfaces.ts b/src/sql/parts/notebook/cellViews/interfaces.ts new file mode 100644 index 0000000000..78fbe50b14 --- /dev/null +++ b/src/sql/parts/notebook/cellViews/interfaces.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { OnDestroy } from '@angular/core'; +import { AngularDisposable } from 'sql/base/common/lifecycle'; + + +export abstract class CellView extends AngularDisposable implements OnDestroy { + constructor() { + super(); + } + + public abstract layout(): void; +} + +export interface ICellModel { + id: string; + language: string; + source: string; + cellType: CellType; +} + +export type CellType = 'code' | 'markdown' | 'raw'; + +export class CellTypes { + public static readonly Code = 'code'; + public static readonly Markdown = 'markdown'; + public static readonly Raw = 'raw'; +} diff --git a/src/sql/parts/notebook/notebook.component.html b/src/sql/parts/notebook/notebook.component.html index cb770cb285..659773a60e 100644 --- a/src/sql/parts/notebook/notebook.component.html +++ b/src/sql/parts/notebook/notebook.component.html @@ -5,12 +5,13 @@ *--------------------------------------------------------------------------------------------*/ -->
-
+
PlaceHolder for Toolbar
- Place Holder for cell list + +
diff --git a/src/sql/parts/notebook/notebook.component.ts b/src/sql/parts/notebook/notebook.component.ts index 484126f784..ed6c78e1f8 100644 --- a/src/sql/parts/notebook/notebook.component.ts +++ b/src/sql/parts/notebook/notebook.component.ts @@ -12,6 +12,7 @@ import { AngularDisposable } from 'sql/base/common/lifecycle'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import * as themeColors from 'vs/workbench/common/theme'; +import { ICellModel, CellTypes } from 'sql/parts/notebook/cellViews/interfaces'; export const NOTEBOOK_SELECTOR: string = 'notebook-component'; @@ -20,13 +21,23 @@ export const NOTEBOOK_SELECTOR: string = 'notebook-component'; templateUrl: decodeURI(require.toUrl('./notebook.component.html')) }) export class NotebookComponent extends AngularDisposable implements OnInit { - @ViewChild('header', { read: ElementRef }) private header: ElementRef; + @ViewChild('toolbar', { read: ElementRef }) private toolbar: ElementRef; + protected cells: Array = []; constructor( @Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface, @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService ) { super(); + + // Todo: This is mock data for cells. Will remove this code when we have a service + let cell1 : ICellModel = { + id: '1', language: 'sql', source: 'select * from sys.tables', cellType: CellTypes.Code + }; + let cell2 : ICellModel = { + id: '2', language: 'sql', source: 'select 1', cellType: CellTypes.Code + }; + this.cells.push(cell1, cell2); } ngOnInit() { @@ -35,9 +46,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit { } private updateTheme(theme: IColorTheme): void { - let headerEl = this.header.nativeElement; - headerEl.style.borderBottomColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString(); - headerEl.style.borderBottomWidth = '1px'; - headerEl.style.borderBottomStyle = 'solid'; + let toolbarEl = this.toolbar.nativeElement; + toolbarEl.style.borderBottomColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString(); } } diff --git a/src/sql/parts/notebook/notebook.css b/src/sql/parts/notebook/notebook.css index b5cae2d434..402aa4e0b5 100644 --- a/src/sql/parts/notebook/notebook.css +++ b/src/sql/parts/notebook/notebook.css @@ -2,16 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* -.notebookEditor .header .monaco-action-bar .action-label { - padding: 8px; +.notebookEditor .editor-toolbar { + border-bottom-width: 1px; + border-bottom-style: solid; } - -.notebookEditor .header .monaco-action-bar .action-item { - margin-right: 5px; -} - -.notebookEditor .monaco-action-bar { - overflow: visible; -} -*/ \ No newline at end of file diff --git a/src/sql/parts/notebook/notebook.module.ts b/src/sql/parts/notebook/notebook.module.ts index 94ba37d12d..ce4237d300 100644 --- a/src/sql/parts/notebook/notebook.module.ts +++ b/src/sql/parts/notebook/notebook.module.ts @@ -23,6 +23,8 @@ import { NotebookComponent } from 'sql/parts/notebook/notebook.component'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; +import { CodeComponent } from 'sql/parts/notebook/cellViews/code.component'; +import { CodeCellComponent } from 'sql/parts/notebook/cellViews/codeCell.component'; export const NotebookModule = (params, selector: string, instantiationService: IInstantiationService): any => { @NgModule({ @@ -31,6 +33,8 @@ export const NotebookModule = (params, selector: string, instantiationService: I SelectBox, EditableDropDown, InputBox, + CodeComponent, + CodeCellComponent, NotebookComponent, ComponentHostDirective ], From 972f857c71a6d89da3ea08be66ca8620b0602e36 Mon Sep 17 00:00:00 2001 From: Abbie Petchtes Date: Tue, 23 Oct 2018 12:22:19 -0700 Subject: [PATCH 03/18] Cell code fit and finish (#2972) * add the look and feel for code cell * formatting * adding the active cell * formatting --- .../notebook/cellViews/code.component.ts | 3 ++ .../cellViews/codeCell.component.html | 2 +- .../notebook/cellViews/codeCell.component.ts | 9 +++- src/sql/parts/notebook/cellViews/codeCell.css | 7 ++- .../parts/notebook/cellViews/interfaces.ts | 1 + .../parts/notebook/notebook.component.html | 6 ++- src/sql/parts/notebook/notebook.component.ts | 24 ++++++--- src/sql/parts/notebook/notebook.css | 6 +++ src/sql/parts/notebook/notebookStyles.ts | 50 +++++++++++++++++++ 9 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 src/sql/parts/notebook/notebookStyles.ts diff --git a/src/sql/parts/notebook/cellViews/code.component.ts b/src/sql/parts/notebook/cellViews/code.component.ts index 3a8d34fc32..f5528dc0c4 100644 --- a/src/sql/parts/notebook/cellViews/code.component.ts +++ b/src/sql/parts/notebook/cellViews/code.component.ts @@ -40,6 +40,7 @@ export class CodeComponent extends AngularDisposable implements OnInit { @Input() content: string; @Input() language: string; + private readonly _minimumHeight = 30; private _editor: QueryTextEditor; private _editorInput: UntitledEditorInput; private _editorModel: ITextModel; @@ -78,6 +79,7 @@ export class CodeComponent extends AngularDisposable implements OnInit { this._editor = instantiationService.createInstance(QueryTextEditor); this._editor.create(this.codeElement.nativeElement); this._editor.setVisible(true); + this._editor.setMinimumHeight(this._minimumHeight); let uri = this.createUri(); this._editorInput = instantiationService.createInstance(UntitledEditorInput, uri, false, this.language, '', ''); this._editor.setInput(this._editorInput, undefined); @@ -99,6 +101,7 @@ export class CodeComponent extends AngularDisposable implements OnInit { this._editor.layout(new DOM.Dimension( DOM.getContentWidth(this.codeElement.nativeElement), DOM.getContentHeight(this.codeElement.nativeElement))); + this._editor.setHeightToScrollHeight(); } private createUri(): URI { diff --git a/src/sql/parts/notebook/cellViews/codeCell.component.html b/src/sql/parts/notebook/cellViews/codeCell.component.html index b72bbd632d..801b9eb3c7 100644 --- a/src/sql/parts/notebook/cellViews/codeCell.component.html +++ b/src/sql/parts/notebook/cellViews/codeCell.component.html @@ -8,7 +8,7 @@
-
+
Place Holder for output area
diff --git a/src/sql/parts/notebook/cellViews/codeCell.component.ts b/src/sql/parts/notebook/cellViews/codeCell.component.ts index 23e7bd21eb..522f71ca4b 100644 --- a/src/sql/parts/notebook/cellViews/codeCell.component.ts +++ b/src/sql/parts/notebook/cellViews/codeCell.component.ts @@ -20,6 +20,7 @@ export const CODE_SELECTOR: string = 'code-cell-component'; templateUrl: decodeURI(require.toUrl('./codeCell.component.html')) }) export class CodeCellComponent extends CellView implements OnInit { + @ViewChild('output', { read: ElementRef }) private output: ElementRef; @Input() cellModel: ICellModel; constructor( @Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface, @@ -30,11 +31,17 @@ export class CodeCellComponent extends CellView implements OnInit { } ngOnInit() { - + this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this)); + this.updateTheme(this.themeService.getColorTheme()); } // Todo: implement layout public layout() { } + + private updateTheme(theme: IColorTheme): void { + let outputElement = this.output.nativeElement; + outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString(); + } } diff --git a/src/sql/parts/notebook/cellViews/codeCell.css b/src/sql/parts/notebook/cellViews/codeCell.css index 6386355eda..8a9a18e0a7 100644 --- a/src/sql/parts/notebook/cellViews/codeCell.css +++ b/src/sql/parts/notebook/cellViews/codeCell.css @@ -4,7 +4,10 @@ *--------------------------------------------------------------------------------------------*/ code-cell-component { - height: 100%; - width: 100%; display: block; } + +code-cell-component .notebook-output { + border-top-width: 1px; + border-top-style: solid; +} diff --git a/src/sql/parts/notebook/cellViews/interfaces.ts b/src/sql/parts/notebook/cellViews/interfaces.ts index 78fbe50b14..0102a9e89b 100644 --- a/src/sql/parts/notebook/cellViews/interfaces.ts +++ b/src/sql/parts/notebook/cellViews/interfaces.ts @@ -20,6 +20,7 @@ export interface ICellModel { language: string; source: string; cellType: CellType; + active: boolean; } export type CellType = 'code' | 'markdown' | 'raw'; diff --git a/src/sql/parts/notebook/notebook.component.html b/src/sql/parts/notebook/notebook.component.html index 659773a60e..e13738fb3b 100644 --- a/src/sql/parts/notebook/notebook.component.html +++ b/src/sql/parts/notebook/notebook.component.html @@ -11,7 +11,9 @@
- - +
+ + +
diff --git a/src/sql/parts/notebook/notebook.component.ts b/src/sql/parts/notebook/notebook.component.ts index ed6c78e1f8..26af70af40 100644 --- a/src/sql/parts/notebook/notebook.component.ts +++ b/src/sql/parts/notebook/notebook.component.ts @@ -3,9 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./notebook'; +import './notebookStyles'; -import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core'; +import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, ViewChildren } from '@angular/core'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; import { AngularDisposable } from 'sql/base/common/lifecycle'; @@ -23,6 +23,7 @@ export const NOTEBOOK_SELECTOR: string = 'notebook-component'; export class NotebookComponent extends AngularDisposable implements OnInit { @ViewChild('toolbar', { read: ElementRef }) private toolbar: ElementRef; protected cells: Array = []; + private _activeCell: ICellModel; constructor( @Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface, @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, @@ -31,11 +32,11 @@ export class NotebookComponent extends AngularDisposable implements OnInit { super(); // Todo: This is mock data for cells. Will remove this code when we have a service - let cell1 : ICellModel = { - id: '1', language: 'sql', source: 'select * from sys.tables', cellType: CellTypes.Code + let cell1: ICellModel = { + id: '1', language: 'sql', source: 'select * from sys.tables', cellType: CellTypes.Code, active: false }; - let cell2 : ICellModel = { - id: '2', language: 'sql', source: 'select 1', cellType: CellTypes.Code + let cell2: ICellModel = { + id: '2', language: 'sql', source: 'select 1', cellType: CellTypes.Code, active: false }; this.cells.push(cell1, cell2); } @@ -49,4 +50,15 @@ export class NotebookComponent extends AngularDisposable implements OnInit { let toolbarEl = this.toolbar.nativeElement; toolbarEl.style.borderBottomColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString(); } + + public selectCell(cell: ICellModel) { + if (cell !== this._activeCell) { + if (this._activeCell) { + this._activeCell.active = false; + } + this._activeCell = cell; + this._activeCell.active = true; + this._changeRef.detectChanges(); + } + } } diff --git a/src/sql/parts/notebook/notebook.css b/src/sql/parts/notebook/notebook.css index 402aa4e0b5..ce0d058eed 100644 --- a/src/sql/parts/notebook/notebook.css +++ b/src/sql/parts/notebook/notebook.css @@ -6,3 +6,9 @@ border-bottom-width: 1px; border-bottom-style: solid; } + +.notebookEditor .notebook-cell { + margin: 10px 20px 10px; + border-width: 1px; + border-style: solid; +} diff --git a/src/sql/parts/notebook/notebookStyles.ts b/src/sql/parts/notebook/notebookStyles.ts new file mode 100644 index 0000000000..82a1aafa07 --- /dev/null +++ b/src/sql/parts/notebook/notebookStyles.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./notebook'; + +import { registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { activeContrastBorder, buttonBackground } from 'vs/platform/theme/common/colorRegistry'; + +registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { + + // Active border + const activeBorder = theme.getColor(buttonBackground); + if (activeBorder) { + collector.addRule(` + .notebookEditor .notebook-cell.active { + border-color: ${activeBorder}; + border-width: 2px; + } + `); + } + + // Inactive border + const inactiveBorder = theme.getColor(SIDE_BAR_BACKGROUND); + if (inactiveBorder) { + collector.addRule(` + .notebookEditor .notebook-cell { + border-color: ${inactiveBorder}; + border-width: 1px; + } + `); + } + + // Styling with Outline color (e.g. high contrast theme) + const outline = theme.getColor(activeContrastBorder); + if (outline) { + collector.addRule(` + .notebookEditor .notebook-cell.active { + outline-color: ${outline}; + outline-width: 1px; + outline-style: solid; + } + + .notebookEditor .notebook-cell:hover:not(.active) { + outline-style: dashed; + } + `); + } +}); From 2859bee4c0bba71b142c06873c9b4c31437e3cae Mon Sep 17 00:00:00 2001 From: Raj <44002319+rajmusuku@users.noreply.github.com> Date: Wed, 24 Oct 2018 13:47:41 -0700 Subject: [PATCH 04/18] 1133: Notebook file registration changes (#2969) * 1133: Notebook file registration changes * File registration stuff --- src/sql/parts/common/customInputConverter.ts | 41 +++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/sql/parts/common/customInputConverter.ts b/src/sql/parts/common/customInputConverter.ts index 90301ea6d2..78cc3f454b 100644 --- a/src/sql/parts/common/customInputConverter.ts +++ b/src/sql/parts/common/customInputConverter.ts @@ -12,6 +12,7 @@ import { QueryInput } from 'sql/parts/query/common/queryInput'; import URI from 'vs/base/common/uri'; import { IQueryEditorOptions } from 'sql/parts/query/common/queryEditorService'; import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput'; +import { NotebookInput, NotebookInputModel } from 'sql/parts/notebook/notebookInput'; const fs = require('fs'); @@ -28,7 +29,7 @@ export const sqlModeId = 'sql'; * to that type. * @param input The input to check for conversion * @param options Editor options for controlling the conversion - * @param instantiationService The instatianation service to use to create the new input types + * @param instantiationService The instantiation service to use to create the new input types */ export function convertEditorInput(input: EditorInput, options: IQueryEditorOptions, instantiationService: IInstantiationService): EditorInput { let denyQueryEditor = options && options.denyQueryEditor; @@ -48,8 +49,20 @@ export function convertEditorInput(input: EditorInput, options: IQueryEditorOpti let queryPlanInput: QueryPlanInput = instantiationService.createInstance(QueryPlanInput, queryPlanXml, 'aaa', undefined); return queryPlanInput; } - } + //Notebook + uri = getNotebookEditorUri(input); + if(uri){ + //TODO: We need to pass in notebook data either through notebook input or notebook service + let notebookData: string = fs.readFileSync(uri.fsPath); + let fileName: string = input? input.getName() : 'untitled'; + let filePath: string = uri.fsPath; + let notebookInputModel = new NotebookInputModel(filePath, undefined, undefined); + //TO DO: Second paramter has to be the content. + let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel); + return notebookInput; + } + } return input; } @@ -80,6 +93,7 @@ export function getSupportedInputResource(input: IEditorInput): URI { // file extensions for the inputs we support (should be all upper case for comparison) const sqlFileTypes = ['SQL']; const sqlPlanFileTypes = ['SQLPLAN']; +const notebookFileType = ['IPYNB']; /** * If input is a supported query editor file, return it's URI. Otherwise return undefined. @@ -129,6 +143,29 @@ function getQueryPlanEditorUri(input: EditorInput): URI { return undefined; } + +/** + * If input is a supported notebook editor file (.ipynb), return it's URI. Otherwise return undefined. + * @param input The EditorInput to get the URI of. + */ +function getNotebookEditorUri(input: EditorInput): URI { + if (!input || !input.getName()) { + return undefined; + } + + // If this editor is not already of type notebook input + if (!(input instanceof NotebookInput)) { + let uri: URI = getSupportedInputResource(input); + if (uri) { + if (hasFileExtension(notebookFileType, input, false)) { + return uri; + } + } + } + + return undefined; +} + /** * Checks whether the given EditorInput is set to either undefined or sql mode * @param input The EditorInput to check the mode of From 533f2734f1047b2386b250da8049e3671789d873 Mon Sep 17 00:00:00 2001 From: Abbie Petchtes Date: Fri, 26 Oct 2018 16:20:06 -0700 Subject: [PATCH 05/18] Add markdown cell to Notebook (#3014) * add markdown cell * add markdown preview for Notebook * formatting * address comment --- .../markdown-language-features/package.json | 12 +++- .../src/commandManager.ts | 6 +- .../src/commands/index.ts | 2 + .../src/commands/showNotebookPreview.ts | 20 ++++++ .../src/extension.ts | 2 + .../src/markdownEngine.ts | 6 ++ .../notebook/cellViews/code.component.ts | 24 +++---- .../cellViews/codeCell.component.html | 2 +- .../parts/notebook/cellViews/interfaces.ts | 3 +- .../cellViews/textCell.component.html | 13 ++++ .../notebook/cellViews/textCell.component.ts | 67 +++++++++++++++++++ src/sql/parts/notebook/cellViews/textCell.css | 13 ++++ .../parts/notebook/notebook.component.html | 4 +- src/sql/parts/notebook/notebook.component.ts | 5 +- src/sql/parts/notebook/notebook.module.ts | 2 + 15 files changed, 162 insertions(+), 19 deletions(-) create mode 100644 extensions/markdown-language-features/src/commands/showNotebookPreview.ts create mode 100644 src/sql/parts/notebook/cellViews/textCell.component.html create mode 100644 src/sql/parts/notebook/cellViews/textCell.component.ts create mode 100644 src/sql/parts/notebook/cellViews/textCell.css diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index b49638d970..4dfaba0cf8 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -23,7 +23,8 @@ "onCommand:markdown.showLockedPreviewToSide", "onCommand:markdown.showSource", "onCommand:markdown.showPreviewSecuritySelector", - "onWebviewPanel:markdown.preview" + "onWebviewPanel:markdown.preview", + "onCommand:notebook.showPreview" ], "contributes": { "commands": [ @@ -77,6 +78,11 @@ "command": "markdown.preview.toggleLock", "title": "%markdown.preview.toggleLock.title%", "category": "Markdown" + }, + { + "command": "notebook.showPreview", + "title": "notebook.showPreview", + "category": "Notebook" } ], "menus": { @@ -154,6 +160,10 @@ { "command": "markdown.preview.toggleLock", "when": "markdownPreviewFocus" + }, + { + "command": "notebook.showPreview", + "when": "false" } ] }, diff --git a/extensions/markdown-language-features/src/commandManager.ts b/extensions/markdown-language-features/src/commandManager.ts index 174f30cd44..966049df92 100644 --- a/extensions/markdown-language-features/src/commandManager.ts +++ b/extensions/markdown-language-features/src/commandManager.ts @@ -8,7 +8,8 @@ import * as vscode from 'vscode'; export interface Command { readonly id: string; - execute(...args: any[]): void; + // {{SQL CARBON EDIT}} + execute(...args: any[]): any; } export class CommandManager { @@ -26,7 +27,8 @@ export class CommandManager { return command; } - private registerCommand(id: string, impl: (...args: any[]) => void, thisArg?: any) { + // {{SQL CARBON EDIT}} + private registerCommand(id: string, impl: (...args: any[]) => any, thisArg?: any) { if (this.commands.has(id)) { return; } diff --git a/extensions/markdown-language-features/src/commands/index.ts b/extensions/markdown-language-features/src/commands/index.ts index 087f93987f..09c37d1bfd 100644 --- a/extensions/markdown-language-features/src/commands/index.ts +++ b/extensions/markdown-language-features/src/commands/index.ts @@ -11,3 +11,5 @@ export { RefreshPreviewCommand } from './refreshPreview'; export { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector'; export { MoveCursorToPositionCommand } from './moveCursorToPosition'; export { ToggleLockCommand } from './toggleLock'; +// {{SQL CARBON EDIT}} +export { ShowNotebookPreview } from './showNotebookPreview'; diff --git a/extensions/markdown-language-features/src/commands/showNotebookPreview.ts b/extensions/markdown-language-features/src/commands/showNotebookPreview.ts new file mode 100644 index 0000000000..ddab003f02 --- /dev/null +++ b/extensions/markdown-language-features/src/commands/showNotebookPreview.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; + +import { Command } from '../commandManager'; +import { MarkdownEngine } from '../markdownEngine'; + +export class ShowNotebookPreview implements Command { + public readonly id = 'notebook.showPreview'; + + public constructor( + private readonly engine: MarkdownEngine + ) { } + + public async execute(document: vscode.Uri, textContent: string): Promise { + return this.engine.renderText(document, textContent); + } +} \ No newline at end of file diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index b73ed0f6c1..99335209bf 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -59,6 +59,8 @@ export function activate(context: vscode.ExtensionContext) { commandManager.register(new commands.OnPreviewStyleLoadErrorCommand()); commandManager.register(new commands.OpenDocumentLinkCommand(engine)); commandManager.register(new commands.ToggleLockCommand(previewManager)); + // {{SQL CARBON EDIT}} + commandManager.register(new commands.ShowNotebookPreview(engine)); context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => { logger.updateConfiguration(); diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 2571c04875..618e7edc0f 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -86,6 +86,12 @@ export class MarkdownEngine { return { text, offset }; } + // {{SQL CARBON EDIT}} + public async renderText(document: vscode.Uri, text: string): Promise { + const engine = await this.getEngine(document); + return engine.render(text); + } + public async render(document: vscode.Uri, stripFrontmatter: boolean, text: string): Promise { let offset = 0; if (stripFrontmatter) { diff --git a/src/sql/parts/notebook/cellViews/code.component.ts b/src/sql/parts/notebook/cellViews/code.component.ts index f5528dc0c4..582b7aabc7 100644 --- a/src/sql/parts/notebook/cellViews/code.component.ts +++ b/src/sql/parts/notebook/cellViews/code.component.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./code'; -import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core'; +import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter } from '@angular/core'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; import { AngularDisposable } from 'sql/base/common/lifecycle'; import { ComponentBase } from 'sql/parts/modelComponents/componentBase'; import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces'; import { QueryTextEditor } from 'sql/parts/modelComponents/queryTextEditor'; +import { ICellModel } from 'sql/parts/notebook/cellViews/interfaces'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import * as themeColors from 'vs/workbench/common/theme'; @@ -26,7 +27,6 @@ import * as DOM from 'vs/base/browser/dom'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; - export const CODE_SELECTOR: string = 'code-component'; @Component({ @@ -36,9 +36,8 @@ export const CODE_SELECTOR: string = 'code-component'; export class CodeComponent extends AngularDisposable implements OnInit { @ViewChild('toolbar', { read: ElementRef }) private toolbarElement: ElementRef; @ViewChild('editor', { read: ElementRef }) private codeElement: ElementRef; - @Input() id: string; - @Input() content: string; - @Input() language: string; + @Input() cellModel: ICellModel; + @Output() public onContentChanged = new EventEmitter(); private readonly _minimumHeight = 30; private _editor: QueryTextEditor; @@ -81,18 +80,19 @@ export class CodeComponent extends AngularDisposable implements OnInit { this._editor.setVisible(true); this._editor.setMinimumHeight(this._minimumHeight); let uri = this.createUri(); - this._editorInput = instantiationService.createInstance(UntitledEditorInput, uri, false, this.language, '', ''); + this._editorInput = instantiationService.createInstance(UntitledEditorInput, uri, false, this.cellModel.language, '', ''); this._editor.setInput(this._editorInput, undefined); this._editorInput.resolve().then(model => { this._editorModel = model.textEditorModel; - this._modelService.updateModel(this._editorModel, this.content); + this._modelService.updateModel(this._editorModel, this.cellModel.source); }); this._register(this._editor); this._register(this._editorInput); this._register(this._editorModel.onDidChangeContent(e => { - this.content = this._editorModel.getValue(); this._editor.setHeightToScrollHeight(); + this.cellModel.source = this._editorModel.getValue(); + this.onContentChanged.emit(); })); this.layout(); } @@ -105,22 +105,22 @@ export class CodeComponent extends AngularDisposable implements OnInit { } private createUri(): URI { - let uri = URI.from({ scheme: Schemas.untitled, path: `notebook-editor-${this.id}` }); + let uri = URI.from({ scheme: Schemas.untitled, path: `notebook-editor-${this.cellModel.id}` }); // Use this to set the internal (immutable) and public (shared with extension) uri properties - this._uri = uri.toString(); + this.cellModel.cellUri = uri; return uri; } /// Editor Functions private updateModel() { if (this._editorModel) { - this._modelService.updateModel(this._editorModel, this.content); + this._modelService.updateModel(this._editorModel, this.cellModel.source); } } private updateLanguageMode() { if (this._editorModel && this._editor) { - this._modeService.getOrCreateMode(this.language).then((modeValue) => { + this._modeService.getOrCreateMode(this.cellModel.language).then((modeValue) => { this._modelService.setMode(this._editorModel, modeValue); }); } diff --git a/src/sql/parts/notebook/cellViews/codeCell.component.html b/src/sql/parts/notebook/cellViews/codeCell.component.html index 801b9eb3c7..b7f97fed40 100644 --- a/src/sql/parts/notebook/cellViews/codeCell.component.html +++ b/src/sql/parts/notebook/cellViews/codeCell.component.html @@ -6,7 +6,7 @@ -->
- +
Place Holder for output area diff --git a/src/sql/parts/notebook/cellViews/interfaces.ts b/src/sql/parts/notebook/cellViews/interfaces.ts index 0102a9e89b..6ffe170a98 100644 --- a/src/sql/parts/notebook/cellViews/interfaces.ts +++ b/src/sql/parts/notebook/cellViews/interfaces.ts @@ -5,7 +5,7 @@ import { OnDestroy } from '@angular/core'; import { AngularDisposable } from 'sql/base/common/lifecycle'; - +import URI from 'vs/base/common/uri'; export abstract class CellView extends AngularDisposable implements OnDestroy { constructor() { @@ -21,6 +21,7 @@ export interface ICellModel { source: string; cellType: CellType; active: boolean; + cellUri?: URI; } export type CellType = 'code' | 'markdown' | 'raw'; diff --git a/src/sql/parts/notebook/cellViews/textCell.component.html b/src/sql/parts/notebook/cellViews/textCell.component.html new file mode 100644 index 0000000000..98265758f8 --- /dev/null +++ b/src/sql/parts/notebook/cellViews/textCell.component.html @@ -0,0 +1,13 @@ + +
+
+ +
+
+
+
diff --git a/src/sql/parts/notebook/cellViews/textCell.component.ts b/src/sql/parts/notebook/cellViews/textCell.component.ts new file mode 100644 index 0000000000..2f5e64be7f --- /dev/null +++ b/src/sql/parts/notebook/cellViews/textCell.component.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- +* 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!./textCell'; + +import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core'; + +import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; +import { CellView, ICellModel } from 'sql/parts/notebook/cellViews/interfaces'; + +import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import * as themeColors from 'vs/workbench/common/theme'; +import { ICommandService } from 'vs/platform/commands/common/commands'; + +export const TEXT_SELECTOR: string = 'text-cell-component'; + +@Component({ + selector: TEXT_SELECTOR, + templateUrl: decodeURI(require.toUrl('./textCell.component.html')) +}) +export class TextCellComponent extends CellView implements OnInit { + @ViewChild('preview', { read: ElementRef }) private output: ElementRef; + @Input() cellModel: ICellModel; + private _content: string; + constructor( + @Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface, + @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, + @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService, + @Inject(ICommandService) private _commandService: ICommandService + ) { + super(); + } + + ngOnChanges() { + this.updatePreview(); + } + + private updatePreview() { + if (this._content !== this.cellModel.source) { + this._content = this.cellModel.source; + // todo: pass in the notebook filename instead of undefined value + this._commandService.executeCommand('notebook.showPreview', undefined, this._content).then((htmlcontent) => { + let outputElement = this.output.nativeElement; + outputElement.innerHTML = htmlcontent; + }); + } + } + + ngOnInit() { + this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this)); + this.updateTheme(this.themeService.getColorTheme()); + } + + // Todo: implement layout + public layout() { + } + + private updateTheme(theme: IColorTheme): void { + let outputElement = this.output.nativeElement; + outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString(); + } + + public handleContentChanged(): void { + this.updatePreview(); + } +} diff --git a/src/sql/parts/notebook/cellViews/textCell.css b/src/sql/parts/notebook/cellViews/textCell.css new file mode 100644 index 0000000000..5f6d5d2d16 --- /dev/null +++ b/src/sql/parts/notebook/cellViews/textCell.css @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +text-cell-component { + display: block; +} + +text-cell-component .notebook-preview { + border-top-width: 1px; + border-top-style: solid; +} diff --git a/src/sql/parts/notebook/notebook.component.html b/src/sql/parts/notebook/notebook.component.html index e13738fb3b..0715fc62b6 100644 --- a/src/sql/parts/notebook/notebook.component.html +++ b/src/sql/parts/notebook/notebook.component.html @@ -12,8 +12,10 @@
- + + +
diff --git a/src/sql/parts/notebook/notebook.component.ts b/src/sql/parts/notebook/notebook.component.ts index 26af70af40..802666d8f2 100644 --- a/src/sql/parts/notebook/notebook.component.ts +++ b/src/sql/parts/notebook/notebook.component.ts @@ -38,7 +38,10 @@ export class NotebookComponent extends AngularDisposable implements OnInit { let cell2: ICellModel = { id: '2', language: 'sql', source: 'select 1', cellType: CellTypes.Code, active: false }; - this.cells.push(cell1, cell2); + let cell3: ICellModel = { + id: '3', language: 'markdown', source: '## This is test!', cellType: CellTypes.Markdown, active: false + }; + this.cells.push(cell1, cell2, cell3); } ngOnInit() { diff --git a/src/sql/parts/notebook/notebook.module.ts b/src/sql/parts/notebook/notebook.module.ts index ce4237d300..0bc516dcb5 100644 --- a/src/sql/parts/notebook/notebook.module.ts +++ b/src/sql/parts/notebook/notebook.module.ts @@ -25,6 +25,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { Registry } from 'vs/platform/registry/common/platform'; import { CodeComponent } from 'sql/parts/notebook/cellViews/code.component'; import { CodeCellComponent } from 'sql/parts/notebook/cellViews/codeCell.component'; +import { TextCellComponent } from 'sql/parts/notebook/cellViews/textCell.component'; export const NotebookModule = (params, selector: string, instantiationService: IInstantiationService): any => { @NgModule({ @@ -35,6 +36,7 @@ export const NotebookModule = (params, selector: string, instantiationService: I InputBox, CodeComponent, CodeCellComponent, + TextCellComponent, NotebookComponent, ComponentHostDirective ], From fc3bf45a7f30e0e6fa7410832a02985f9cce7d94 Mon Sep 17 00:00:00 2001 From: Kevin Cunnane Date: Wed, 31 Oct 2018 22:01:40 -0700 Subject: [PATCH 06/18] Port most notebook model code over to be behind a service (#3068) - Defines a new NotebookService in Azure Data Studio which will be used to interact with notebooks. Since notebooks can require per-file instantiation the provider is just used to create & track managers for a given URI. - Inject this into notebook.component.ts and pass required parameters that'll be used to properly initialize a manger into the method. Actual initialization not done yet. - Port over & recompile notebook model code - Define most required APIs in sqlops.proposed.d.ts. In the future, these will be used by extensions to contribute their own providers. --- src/sql/parts/common/customInputConverter.ts | 4 +- .../notebook/cellViews/code.component.ts | 2 +- .../notebook/cellViews/codeCell.component.ts | 3 +- .../parts/notebook/cellViews/interfaces.ts | 18 - .../notebook/cellViews/textCell.component.ts | 3 +- src/sql/parts/notebook/models/cell.ts | 321 +++++++++ .../parts/notebook/models/clientSession.ts | 360 ++++++++++ src/sql/parts/notebook/models/contracts.ts | 47 ++ src/sql/parts/notebook/models/modelFactory.ts | 23 + .../parts/notebook/models/modelInterfaces.ts | 372 +++++++++++ .../notebook/models/notebookConnection.ts | 94 +++ .../parts/notebook/models/notebookModel.ts | 474 +++++++++++++ .../notebook/models/sparkMagicContexts.ts | 194 ++++++ src/sql/parts/notebook/notebook.component.ts | 55 +- .../parts/notebook/notebook.contribution.ts | 8 +- src/sql/parts/notebook/notebookConstants.ts | 19 + src/sql/parts/notebook/notebookEditor.ts | 9 +- src/sql/parts/notebook/notebookInput.ts | 34 +- src/sql/parts/notebook/notebookUtils.ts | 38 ++ src/sql/parts/notebook/spark/sparkUtils.ts | 16 + src/sql/services/notebook/notebookService.ts | 59 ++ .../services/notebook/notebookServiceImpl.ts | 60 ++ src/sql/sqlops.proposed.d.ts | 620 +++++++++++++++++- src/sql/workbench/api/node/extHostNotebook.ts | 76 +++ .../workbench/api/node/mainThreadNotebook.ts | 77 +++ .../workbench/api/node/sqlExtHost.api.impl.ts | 11 +- .../api/node/sqlExtHost.contribution.ts | 3 +- .../workbench/api/node/sqlExtHost.protocol.ts | 21 +- .../workbench/electron-browser/workbench.ts | 5 + 29 files changed, 2973 insertions(+), 53 deletions(-) create mode 100644 src/sql/parts/notebook/models/cell.ts create mode 100644 src/sql/parts/notebook/models/clientSession.ts create mode 100644 src/sql/parts/notebook/models/contracts.ts create mode 100644 src/sql/parts/notebook/models/modelFactory.ts create mode 100644 src/sql/parts/notebook/models/modelInterfaces.ts create mode 100644 src/sql/parts/notebook/models/notebookConnection.ts create mode 100644 src/sql/parts/notebook/models/notebookModel.ts create mode 100644 src/sql/parts/notebook/models/sparkMagicContexts.ts create mode 100644 src/sql/parts/notebook/notebookConstants.ts create mode 100644 src/sql/parts/notebook/notebookUtils.ts create mode 100644 src/sql/parts/notebook/spark/sparkUtils.ts create mode 100644 src/sql/services/notebook/notebookService.ts create mode 100644 src/sql/services/notebook/notebookServiceImpl.ts create mode 100644 src/sql/workbench/api/node/extHostNotebook.ts create mode 100644 src/sql/workbench/api/node/mainThreadNotebook.ts diff --git a/src/sql/parts/common/customInputConverter.ts b/src/sql/parts/common/customInputConverter.ts index 78cc3f454b..9531be9010 100644 --- a/src/sql/parts/common/customInputConverter.ts +++ b/src/sql/parts/common/customInputConverter.ts @@ -54,10 +54,8 @@ export function convertEditorInput(input: EditorInput, options: IQueryEditorOpti uri = getNotebookEditorUri(input); if(uri){ //TODO: We need to pass in notebook data either through notebook input or notebook service - let notebookData: string = fs.readFileSync(uri.fsPath); let fileName: string = input? input.getName() : 'untitled'; - let filePath: string = uri.fsPath; - let notebookInputModel = new NotebookInputModel(filePath, undefined, undefined); + let notebookInputModel = new NotebookInputModel(uri, undefined, undefined); //TO DO: Second paramter has to be the content. let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel); return notebookInput; diff --git a/src/sql/parts/notebook/cellViews/code.component.ts b/src/sql/parts/notebook/cellViews/code.component.ts index 582b7aabc7..462798818c 100644 --- a/src/sql/parts/notebook/cellViews/code.component.ts +++ b/src/sql/parts/notebook/cellViews/code.component.ts @@ -11,7 +11,6 @@ import { AngularDisposable } from 'sql/base/common/lifecycle'; import { ComponentBase } from 'sql/parts/modelComponents/componentBase'; import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces'; import { QueryTextEditor } from 'sql/parts/modelComponents/queryTextEditor'; -import { ICellModel } from 'sql/parts/notebook/cellViews/interfaces'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import * as themeColors from 'vs/workbench/common/theme'; @@ -26,6 +25,7 @@ import { Schemas } from 'vs/base/common/network'; import * as DOM from 'vs/base/browser/dom'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; +import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; export const CODE_SELECTOR: string = 'code-component'; diff --git a/src/sql/parts/notebook/cellViews/codeCell.component.ts b/src/sql/parts/notebook/cellViews/codeCell.component.ts index 522f71ca4b..5e02d19dc7 100644 --- a/src/sql/parts/notebook/cellViews/codeCell.component.ts +++ b/src/sql/parts/notebook/cellViews/codeCell.component.ts @@ -7,10 +7,11 @@ import 'vs/css!./codeCell'; import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; -import { CellView, ICellModel } from 'sql/parts/notebook/cellViews/interfaces'; +import { CellView } from 'sql/parts/notebook/cellViews/interfaces'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import * as themeColors from 'vs/workbench/common/theme'; +import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; export const CODE_SELECTOR: string = 'code-cell-component'; diff --git a/src/sql/parts/notebook/cellViews/interfaces.ts b/src/sql/parts/notebook/cellViews/interfaces.ts index 6ffe170a98..dcf46d6887 100644 --- a/src/sql/parts/notebook/cellViews/interfaces.ts +++ b/src/sql/parts/notebook/cellViews/interfaces.ts @@ -5,7 +5,6 @@ import { OnDestroy } from '@angular/core'; import { AngularDisposable } from 'sql/base/common/lifecycle'; -import URI from 'vs/base/common/uri'; export abstract class CellView extends AngularDisposable implements OnDestroy { constructor() { @@ -14,20 +13,3 @@ export abstract class CellView extends AngularDisposable implements OnDestroy { public abstract layout(): void; } - -export interface ICellModel { - id: string; - language: string; - source: string; - cellType: CellType; - active: boolean; - cellUri?: URI; -} - -export type CellType = 'code' | 'markdown' | 'raw'; - -export class CellTypes { - public static readonly Code = 'code'; - public static readonly Markdown = 'markdown'; - public static readonly Raw = 'raw'; -} diff --git a/src/sql/parts/notebook/cellViews/textCell.component.ts b/src/sql/parts/notebook/cellViews/textCell.component.ts index 2f5e64be7f..7ff14e4e06 100644 --- a/src/sql/parts/notebook/cellViews/textCell.component.ts +++ b/src/sql/parts/notebook/cellViews/textCell.component.ts @@ -7,11 +7,12 @@ import 'vs/css!./textCell'; import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; -import { CellView, ICellModel } from 'sql/parts/notebook/cellViews/interfaces'; +import { CellView } from 'sql/parts/notebook/cellViews/interfaces'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import * as themeColors from 'vs/workbench/common/theme'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; export const TEXT_SELECTOR: string = 'text-cell-component'; diff --git a/src/sql/parts/notebook/models/cell.ts b/src/sql/parts/notebook/models/cell.ts new file mode 100644 index 0000000000..0d277f9527 --- /dev/null +++ b/src/sql/parts/notebook/models/cell.ts @@ -0,0 +1,321 @@ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { Event, Emitter } from 'vs/base/common/event'; +import URI from 'vs/base/common/uri'; + +import { nb } from 'sqlops'; +import { ICellModelOptions, IModelFactory } from './modelInterfaces'; +import * as notebookUtils from '../notebookUtils'; +import { CellTypes, CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts'; +import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; + +let modelId = 0; + + +export class CellModel implements ICellModel { + private static LanguageMapping: Map; + + private _cellType: nb.CellType; + private _source: string; + private _language: string; + private _future: nb.IFuture; + private _outputs: nb.ICellOutput[] = []; + private _isEditMode: boolean; + private _onOutputsChanged = new Emitter>(); + private _onCellModeChanged = new Emitter(); + public id: string; + private _isTrusted: boolean; + private _active: boolean; + private _cellUri: URI; + + constructor(private factory: IModelFactory, cellData?: nb.ICell, private _options?: ICellModelOptions) { + this.id = `${modelId++}`; + CellModel.CreateLanguageMappings(); + // Do nothing for now + if (cellData) { + this.fromJSON(cellData); + } else { + this._cellType = CellTypes.Code; + this._source = ''; + } + this._isEditMode = this._cellType !== CellTypes.Markdown; + this.setDefaultLanguage(); + if (_options && _options.isTrusted) { + this._isTrusted = true; + } else { + this._isTrusted = false; + } + } + + public equals(other: ICellModel) { + return other && other.id === this.id; + } + + public get onOutputsChanged(): Event> { + return this._onOutputsChanged.event; + } + + public get onCellModeChanged(): Event { + return this._onCellModeChanged.event; + } + + public get isEditMode(): boolean { + return this._isEditMode; + } + + public get future(): nb.IFuture { + return this._future; + } + + public set isEditMode(isEditMode: boolean) { + this._isEditMode = isEditMode; + this._onCellModeChanged.fire(this._isEditMode); + // Note: this does not require a notebook update as it does not change overall state + } + + public get trustedMode(): boolean { + return this._isTrusted; + } + + public set trustedMode(isTrusted: boolean) { + if (this._isTrusted !== isTrusted) { + this._isTrusted = isTrusted; + this._onOutputsChanged.fire(this._outputs); + } + } + + public get active(): boolean { + return this._active; + } + + public set active(value: boolean) { + this._active = value; + } + + public get cellUri(): URI { + return this._cellUri; + } + + public set cellUri(value: URI) { + this._cellUri = value; + } + + public get options(): ICellModelOptions { + return this._options; + } + + public get cellType(): CellType { + return this._cellType; + } + + public get source(): string { + return this._source; + } + + public set source(newSource: string) { + if (this._source !== newSource) { + this._source = newSource; + this.sendChangeToNotebook(NotebookChangeType.CellSourceUpdated); + } + } + + public get language(): string { + return this._language; + } + + public set language(newLanguage: string) { + this._language = newLanguage; + } + + /** + * Sets the future which will be used to update the output + * area for this cell + */ + setFuture(future: nb.IFuture): void { + if (this._future === future) { + // Nothing to do + return; + } + // Setting the future indicates the cell is running which enables trusted mode. + // See https://jupyter-notebook.readthedocs.io/en/stable/security.html + + this._isTrusted = true; + + if (this._future) { + this._future.dispose(); + } + this.clearOutputs(); + this._future = future; + future.setReplyHandler({ handle: (msg) => this.handleReply(msg) }); + future.setIOPubHandler({ handle: (msg) => this.handleIOPub(msg) }); + } + + private clearOutputs(): void { + this._outputs = []; + this.fireOutputsChanged(); + } + + private fireOutputsChanged(): void { + this._onOutputsChanged.fire(this.outputs); + this.sendChangeToNotebook(NotebookChangeType.CellOutputUpdated); + } + + private sendChangeToNotebook(change: NotebookChangeType): void { + if (this._options && this._options.notebook) { + this._options.notebook.onCellChange(this, change); + } + } + + public get outputs(): ReadonlyArray { + return this._outputs; + } + + private handleReply(msg: nb.IShellMessage): void { + // TODO #931 we should process this. There can be a payload attached which should be added to outputs. + // In all other cases, it is a no-op + let output: nb.ICellOutput = msg.content as nb.ICellOutput; + } + + private handleIOPub(msg: nb.IIOPubMessage): void { + let msgType = msg.header.msg_type; + let displayId = this.getDisplayId(msg); + let output: nb.ICellOutput; + switch (msgType) { + case 'execute_result': + case 'display_data': + case 'stream': + case 'error': + output = msg.content as nb.ICellOutput; + output.output_type = msgType; + break; + case 'clear_output': + // TODO wait until next message before clearing + // let wait = (msg as KernelMessage.IClearOutputMsg).content.wait; + this.clearOutputs(); + break; + case 'update_display_data': + output = msg.content as nb.ICellOutput; + output.output_type = 'display_data'; + // TODO #930 handle in-place update of displayed data + // targets = this._displayIdMap.get(displayId); + // if (targets) { + // for (let index of targets) { + // model.set(index, output); + // } + // } + break; + default: + break; + } + // TODO handle in-place update of displayed data + // if (displayId && msgType === 'display_data') { + // targets = this._displayIdMap.get(displayId) || []; + // targets.push(model.length - 1); + // this._displayIdMap.set(displayId, targets); + // } + if (output) { + this._outputs.push(output); + this.fireOutputsChanged(); + } + } + + private getDisplayId(msg: nb.IIOPubMessage): string | undefined { + let transient = (msg.content.transient || {}); + return transient['display_id'] as string; + } + + public toJSON(): nb.ICell { + let cellJson: Partial = { + cell_type: this._cellType, + source: this._source, + metadata: { + } + }; + if (this._cellType === CellTypes.Code) { + cellJson.metadata.language = this._language, + cellJson.outputs = this._outputs; + cellJson.execution_count = 1; // TODO: keep track of actual execution count + + } + return cellJson as nb.ICell; + } + + public fromJSON(cell: nb.ICell): void { + if (!cell) { + return; + } + this._cellType = cell.cell_type; + this._source = Array.isArray(cell.source) ? cell.source.join('') : cell.source; + this._language = (cell.metadata && cell.metadata.language) ? cell.metadata.language : 'python'; + if (cell.outputs) { + for (let output of cell.outputs) { + // For now, we're assuming it's OK to save these as-is with no modification + this.addOutput(output); + } + } + } + + private addOutput(output: nb.ICellOutput) { + this._normalize(output); + this._outputs.push(output); + } + + /** + * Normalize an output. + */ + private _normalize(value: nb.ICellOutput): void { + if (notebookUtils.isStream(value)) { + if (Array.isArray(value.text)) { + value.text = (value.text as string[]).join('\n'); + } + } + } + + private static CreateLanguageMappings(): void { + if (CellModel.LanguageMapping) { + return; + } + CellModel.LanguageMapping = new Map(); + CellModel.LanguageMapping['pyspark'] = 'python'; + CellModel.LanguageMapping['pyspark3'] = 'python'; + CellModel.LanguageMapping['python'] = 'python'; + CellModel.LanguageMapping['scala'] = 'scala'; + } + + private get languageInfo(): nb.ILanguageInfo { + if (this._options && this._options.notebook && this._options.notebook.languageInfo) { + return this._options.notebook.languageInfo; + } + return undefined; + } + + private setDefaultLanguage(): void { + this._language = 'python'; + // In languageInfo, set the language to the "name" property + // If the "name" property isn't defined, check the "mimeType" property + // Otherwise, default to python as the language + let languageInfo = this.languageInfo; + if (languageInfo) { + if (languageInfo.name) { + // check the LanguageMapping to determine if a mapping is necessary (example 'pyspark' -> 'python') + if (CellModel.LanguageMapping[languageInfo.name]) { + this._language = CellModel.LanguageMapping[languageInfo.name]; + } else { + this._language = languageInfo.name; + } + } else if (languageInfo.mimetype) { + this._language = languageInfo.mimetype; + } + } + let mimeTypePrefix = 'x-'; + if (this._language.includes(mimeTypePrefix)) { + this._language = this._language.replace(mimeTypePrefix, ''); + } + } +} diff --git a/src/sql/parts/notebook/models/clientSession.ts b/src/sql/parts/notebook/models/clientSession.ts new file mode 100644 index 0000000000..9869d62e01 --- /dev/null +++ b/src/sql/parts/notebook/models/clientSession.ts @@ -0,0 +1,360 @@ + +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the Source EULA. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +// This code is based on @jupyterlab/packages/apputils/src/clientsession.tsx + +'use strict'; + +import { nb } from 'sqlops'; +import * as nls from 'vs/nls'; +import URI from 'vs/base/common/uri'; +import { Event, Emitter } from 'vs/base/common/event'; + +import { IClientSession, IKernelPreference, IClientSessionOptions } from './modelInterfaces'; +import { Deferred } from 'sql/base/common/promise'; + +import * as notebookUtils from '../notebookUtils'; +import * as sparkUtils from '../spark/sparkUtils'; +import { INotebookManager } from 'sql/services/notebook/notebookService'; +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; +import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection'; + +/** + * Implementation of a client session. This is a model over session operations, + * which may come from the session manager or a specific session. + */ +export class ClientSession implements IClientSession { + //#region private fields with public accessors + private _terminatedEmitter = new Emitter(); + private _kernelChangedEmitter = new Emitter(); + private _statusChangedEmitter = new Emitter(); + private _iopubMessageEmitter = new Emitter(); + private _unhandledMessageEmitter = new Emitter(); + private _propertyChangedEmitter = new Emitter<'path' | 'name' | 'type'>(); + private _path: string; + private _type: string; + private _name: string; + private _isReady: boolean; + private _ready: Deferred; + private _kernelChangeCompleted: Deferred; + private _kernelPreference: IKernelPreference; + private _kernelDisplayName: string; + private _errorMessage: string; + //#endregion + + private _serverLoadFinished: Promise; + private _session: nb.ISession; + private isServerStarted: boolean; + private notebookManager: INotebookManager; + private _connection: NotebookConnection; + private _kernelConfigActions: ((kernelName: string) => Promise)[] = []; + + constructor(private options: IClientSessionOptions) { + this._path = options.path; + this.notebookManager = options.notebookManager; + this._isReady = false; + this._ready = new Deferred(); + this._kernelChangeCompleted = new Deferred(); + } + + public async initialize(connection?: NotebookConnection): Promise { + try { + this._kernelConfigActions.push((kernelName: string) => { return this.runTasksBeforeSessionStart(kernelName); }); + this._connection = connection; + this._serverLoadFinished = this.startServer(); + await this._serverLoadFinished; + await this.initializeSession(); + } catch (err) { + this._errorMessage = notebookUtils.getErrorMessage(err); + } + // Always resolving for now. It's up to callers to check for error case + this._isReady = true; + this._ready.resolve(); + this._kernelChangeCompleted.resolve(); + } + + private async startServer(): Promise { + let serverManager = this.notebookManager.serverManager; + if (serverManager && !serverManager.isStarted) { + await serverManager.startServer(); + if (!serverManager.isStarted) { + throw new Error(nls.localize('ServerNotStarted', 'Server did not start for unknown reason')); + } + this.isServerStarted = serverManager.isStarted; + } else { + this.isServerStarted = true; + } + } + + private async initializeSession(): Promise { + await this._serverLoadFinished; + if (this.isServerStarted) { + if (!this.notebookManager.sessionManager.isReady) { + await this.notebookManager.sessionManager.ready; + } + if (this._kernelPreference && this._kernelPreference.shouldStart) { + await this.startSessionInstance(this._kernelPreference.name); + } + } + } + + private async startSessionInstance(kernelName: string): Promise { + let session: nb.ISession; + try { + session = await this.notebookManager.sessionManager.startNew({ + path: this.path, + kernelName: kernelName + // TODO add kernel name if saved in the document + }); + session.defaultKernelLoaded = true; + } catch (err) { + // TODO move registration + if (err && err.response && err.response.status === 501) { + this.options.notificationService.warn(nls.localize('sparkKernelRequiresConnection', 'Kernel {0} was not found. The default kernel will be used instead.', kernelName)); + session = await this.notebookManager.sessionManager.startNew({ + path: this.path, + kernelName: undefined + }); + } else { + throw err; + } + session.defaultKernelLoaded = false; + } + this._session = session; + await this.runKernelConfigActions(kernelName); + this._statusChangedEmitter.fire(session); + } + + private async runKernelConfigActions(kernelName: string): Promise { + for (let startAction of this._kernelConfigActions) { + await startAction(kernelName); + } + } + + public dispose(): void { + // No-op for now + } + + /** + * Indicates the server has finished loading. It may have failed to load in + * which case the view will be in an error state. + */ + public get serverLoadFinished(): Promise { + return this._serverLoadFinished; + } + + + //#region IClientSession Properties + public get terminated(): Event { + return this._terminatedEmitter.event; + } + public get kernelChanged(): Event { + return this._kernelChangedEmitter.event; + } + public get statusChanged(): Event { + return this._statusChangedEmitter.event; + } + public get iopubMessage(): Event { + return this._iopubMessageEmitter.event; + } + public get unhandledMessage(): Event { + return this._unhandledMessageEmitter.event; + } + public get propertyChanged(): Event<'path' | 'name' | 'type'> { + return this._propertyChangedEmitter.event; + } + public get kernel(): nb.IKernel | null { + return this._session ? this._session.kernel : undefined; + } + public get path(): string { + return this._path; + } + public get name(): string { + return this._name; + } + public get type(): string { + return this._type; + } + public get status(): nb.KernelStatus { + if (!this.isReady) { + return 'starting'; + } + return this._session ? this._session.status : 'dead'; + } + public get isReady(): boolean { + return this._isReady; + } + public get ready(): Promise { + return this._ready.promise; + } + public get kernelChangeCompleted(): Promise { + return this._kernelChangeCompleted.promise; + } + public get kernelPreference(): IKernelPreference { + return this._kernelPreference; + } + public set kernelPreference(value: IKernelPreference) { + this._kernelPreference = value; + } + public get kernelDisplayName(): string { + return this._kernelDisplayName; + } + public get errorMessage(): string { + return this._errorMessage; + } + public get isInErrorState(): boolean { + return !!this._errorMessage; + } + //#endregion + + //#region Not Yet Implemented + /** + * Change the current kernel associated with the document. + */ + async changeKernel(options: nb.IKernelSpec): Promise { + this._kernelChangeCompleted = new Deferred(); + this._isReady = false; + let oldKernel = this.kernel; + let newKernel = this.kernel; + + let kernel = await this.doChangeKernel(options); + try { + await kernel.ready; + } catch (error) { + // Cleanup some state before re-throwing + this._isReady = kernel.isReady; + this._kernelChangeCompleted.resolve(); + throw error; + } + newKernel = this._session ? kernel : this._session.kernel; + this._isReady = kernel.isReady; + // Send resolution events to listeners + this._kernelChangeCompleted.resolve(); + this._kernelChangedEmitter.fire({ + oldValue: oldKernel, + newValue: newKernel + }); + return kernel; + } + + /** + * Helper method to either call ChangeKernel on current session, or start a new session + * @param options + */ + private async doChangeKernel(options: nb.IKernelSpec): Promise { + let kernel: nb.IKernel; + if (this._session) { + kernel = await this._session.changeKernel(options); + await this.runKernelConfigActions(kernel.name); + } else { + kernel = await this.startSessionInstance(options.name).then(() => this.kernel); + } + return kernel; + } + + public async runTasksBeforeSessionStart(kernelName: string): Promise { + // TODO we should move all Spark-related code to SparkMagicContext + if (this._session && this._connection && this.isSparkKernel(kernelName)) { + // TODO may need to reenable a way to get the credential + // await this._connection.getCredential(); + // %_do_not_call_change_endpoint is a SparkMagic command that lets users change endpoint options, + // such as user/profile/host name/auth type + + let server = URI.parse(sparkUtils.getLivyUrl(this._connection.host, this._connection.knoxport)).toString(); + let doNotCallChangeEndpointParams = + `%_do_not_call_change_endpoint --username=${this._connection.user} --password=${this._connection.password} --server=${server} --auth=Basic_Access`; + let future = this._session.kernel.requestExecute({ + code: doNotCallChangeEndpointParams + }, true); + await future.done; + } + } + + public async updateConnection(connection: NotebookConnection): Promise { + if (!this.kernel) { + // TODO is there any case where skipping causes errors? Do far it seems like it gets called twice + return; + } + this._connection = (connection.connectionProfile.id !== '-1') ? connection : this._connection; + // if kernel is not set, don't run kernel config actions + // this should only occur when a cell is cancelled, which interrupts the kernel + if (this.kernel && this.kernel.name) { + await this.runKernelConfigActions(this.kernel.name); + } + } + + isSparkKernel(kernelName: string): any { + return kernelName && kernelName.toLowerCase().indexOf('spark') > -1; + } + + /** + * Kill the kernel and shutdown the session. + * + * @returns A promise that resolves when the session is shut down. + */ + public async shutdown(): Promise { + // Always try to shut down session + if (this._session && this._session.id) { + this.notebookManager.sessionManager.shutdown(this._session.id); + } + let serverManager = this.notebookManager.serverManager; + if (serverManager) { + await serverManager.stopServer(); + } + } + + /** + * Select a kernel for the session. + */ + selectKernel(): Promise { + throw new Error('Not implemented'); + } + + /** + * Restart the session. + * + * @returns A promise that resolves with whether the kernel has restarted. + * + * #### Notes + * If there is a running kernel, present a dialog. + * If there is no kernel, we start a kernel with the last run + * kernel name and resolves with `true`. If no kernel has been started, + * this is a no-op, and resolves with `false`. + */ + restart(): Promise { + throw new Error('Not implemented'); + } + + /** + * Change the session path. + * + * @param path - The new session path. + * + * @returns A promise that resolves when the session has renamed. + * + * #### Notes + * This uses the Jupyter REST API, and the response is validated. + * The promise is fulfilled on a valid response and rejected otherwise. + */ + setPath(path: string): Promise { + throw new Error('Not implemented'); + } + + /** + * Change the session name. + */ + setName(name: string): Promise { + throw new Error('Not implemented'); + } + + /** + * Change the session type. + */ + setType(type: string): Promise { + throw new Error('Not implemented'); + } + //#endregion +} diff --git a/src/sql/parts/notebook/models/contracts.ts b/src/sql/parts/notebook/models/contracts.ts new file mode 100644 index 0000000000..9a918d5730 --- /dev/null +++ b/src/sql/parts/notebook/models/contracts.ts @@ -0,0 +1,47 @@ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +export type CellType = 'code' | 'markdown' | 'raw'; + +export class CellTypes { + public static readonly Code = 'code'; + public static readonly Markdown = 'markdown'; + public static readonly Raw = 'raw'; +} + +// to do: add all mime types +export type MimeType = 'text/plain' | 'text/html'; + +// to do: add all mime types +export class MimeTypes { + public static readonly PlainText = 'text/plain'; + public static readonly HTML = 'text/html'; +} + +export type OutputType = + | 'execute_result' + | 'display_data' + | 'stream' + | 'error' + | 'update_display_data'; + +export class OutputTypes { + public static readonly ExecuteResult = 'execute_result'; + public static readonly DisplayData = 'display_data'; + public static readonly Stream = 'stream'; + public static readonly Error = 'error'; + public static readonly UpdateDisplayData = 'update_display_data'; +} + +export enum NotebookChangeType { + CellsAdded, + CellDeleted, + CellSourceUpdated, + CellOutputUpdated, + DirtyStateChanged +} diff --git a/src/sql/parts/notebook/models/modelFactory.ts b/src/sql/parts/notebook/models/modelFactory.ts new file mode 100644 index 0000000000..37dfca2639 --- /dev/null +++ b/src/sql/parts/notebook/models/modelFactory.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { nb } from 'sqlops'; + +import { CellModel } from './cell'; +import { IClientSession, IClientSessionOptions, ICellModelOptions, ICellModel, IModelFactory } from './modelInterfaces'; +import { ClientSession } from './clientSession'; + +export class ModelFactory implements IModelFactory { + + public createCell(cell: nb.ICell, options: ICellModelOptions): ICellModel { + return new CellModel(this, cell, options); + } + + public createClientSession(options: IClientSessionOptions): IClientSession { + return new ClientSession(options); + } +} diff --git a/src/sql/parts/notebook/models/modelInterfaces.ts b/src/sql/parts/notebook/models/modelInterfaces.ts new file mode 100644 index 0000000000..418413d290 --- /dev/null +++ b/src/sql/parts/notebook/models/modelInterfaces.ts @@ -0,0 +1,372 @@ + +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the Source EULA. See License.txt in the project root for license information. +*--------------------------------------------------------------------------------------------*/ + +// This code is based on @jupyterlab/packages/apputils/src/clientsession.tsx + +'use strict'; + +import { nb } from 'sqlops'; +import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import URI from 'vs/base/common/uri'; +import { INotificationService } from 'vs/platform/notification/common/notification'; + +import { CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts'; +import { INotebookManager } from 'sql/services/notebook/notebookService'; +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; +import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection'; +import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement'; + +export interface IClientSessionOptions { + path: string; + notebookManager: INotebookManager; + notificationService: INotificationService; +} + +/** + * The interface of client session object. + * + * The client session represents the link between + * a path and its kernel for the duration of the lifetime + * of the session object. The session can have no current + * kernel, and can start a new kernel at any time. + */ +export interface IClientSession extends IDisposable { + /** + * A signal emitted when the session is shut down. + */ + readonly terminated: Event; + + /** + * A signal emitted when the kernel changes. + */ + readonly kernelChanged: Event; + + /** + * A signal emitted when the kernel status changes. + */ + readonly statusChanged: Event; + + /** + * A signal emitted for a kernel messages. + */ + readonly iopubMessage: Event; + + /** + * A signal emitted for an unhandled kernel message. + */ + readonly unhandledMessage: Event; + + /** + * A signal emitted when a session property changes. + */ + readonly propertyChanged: Event<'path' | 'name' | 'type'>; + + /** + * The current kernel associated with the document. + */ + readonly kernel: nb.IKernel | null; + + /** + * The current path associated with the client session. + */ + readonly path: string; + + /** + * The current name associated with the client session. + */ + readonly name: string; + + /** + * The type of the client session. + */ + readonly type: string; + + /** + * The current status of the client session. + */ + readonly status: nb.KernelStatus; + + /** + * Whether the session is ready. + */ + readonly isReady: boolean; + + /** + * Whether the session is in an unusable state + */ + readonly isInErrorState: boolean; + /** + * The error information, if this session is in an error state + */ + readonly errorMessage: string; + + /** + * A promise that is fulfilled when the session is ready. + */ + readonly ready: Promise; + + /** + * A promise that is fulfilled when the session completes a kernel change. + */ + readonly kernelChangeCompleted: Promise; + + /** + * The kernel preference. + */ + kernelPreference: IKernelPreference; + + /** + * The display name of the kernel. + */ + readonly kernelDisplayName: string; + + /** + * Initializes the ClientSession, by starting the server and + * connecting to the SessionManager. + * This will optionally start a session if the kernel preferences + * indicate this is desired + */ + initialize(connection?: NotebookConnection): Promise; + + /** + * Change the current kernel associated with the document. + */ + changeKernel( + options: nb.IKernelSpec + ): Promise; + + /** + * Kill the kernel and shutdown the session. + * + * @returns A promise that resolves when the session is shut down. + */ + shutdown(): Promise; + + /** + * Select a kernel for the session. + */ + selectKernel(): Promise; + + /** + * Restart the session. + * + * @returns A promise that resolves with whether the kernel has restarted. + * + * #### Notes + * If there is a running kernel, present a dialog. + * If there is no kernel, we start a kernel with the last run + * kernel name and resolves with `true`. If no kernel has been started, + * this is a no-op, and resolves with `false`. + */ + restart(): Promise; + + /** + * Change the session path. + * + * @param path - The new session path. + * + * @returns A promise that resolves when the session has renamed. + * + * #### Notes + * This uses the Jupyter REST API, and the response is validated. + * The promise is fulfilled on a valid response and rejected otherwise. + */ + setPath(path: string): Promise; + + /** + * Change the session name. + */ + setName(name: string): Promise; + + /** + * Change the session type. + */ + setType(type: string): Promise; + + /** + * Updates the connection + */ + updateConnection(connection: NotebookConnection): void; +} + +export interface IDefaultConnection { + defaultConnection: IConnectionProfile; + otherConnections: IConnectionProfile[]; +} + +/** + * A kernel preference. + */ +export interface IKernelPreference { + /** + * The name of the kernel. + */ + readonly name?: string; + + /** + * The preferred kernel language. + */ + readonly language?: string; + + /** + * The id of an existing kernel. + */ + readonly id?: string; + + /** + * Whether to prefer starting a kernel. + */ + readonly shouldStart?: boolean; + + /** + * Whether a kernel can be started. + */ + readonly canStart?: boolean; + + /** + * Whether to auto-start the default kernel if no matching kernel is found. + */ + readonly autoStartDefault?: boolean; +} + +export interface INotebookModel { + /** + * Client Session in the notebook, used for sending requests to the notebook service + */ + readonly clientSession: IClientSession; + /** + * LanguageInfo saved in the query book + */ + readonly languageInfo: nb.ILanguageInfo; + + /** + * The notebook service used to call backend APIs + */ + readonly notebookManager: INotebookManager; + + /** + * Event fired on first initialization of the kernel and + * on subsequent change events + */ + readonly kernelChanged: Event; + + /** + * Event fired on first initialization of the kernels and + * on subsequent change events + */ + readonly kernelsChanged: Event; + + /** + * Default kernel + */ + defaultKernel?: nb.IKernelSpec; + + /** + * Event fired on first initialization of the contexts and + * on subsequent change events + */ + readonly contextsChanged: Event; + + /** + * The specs for available kernels, or undefined if these have + * not been loaded yet + */ + readonly specs: nb.IAllKernels | undefined; + + /** + * The specs for available contexts, or undefined if these have + * not been loaded yet + */ + readonly contexts: IDefaultConnection | undefined; + + /** + * The trusted mode of the NoteBook + */ + trustedMode: boolean; + + /** + * Change the current kernel from the Kernel dropdown + * @param displayName kernel name (as displayed in Kernel dropdown) + */ + changeKernel(displayName: string): void; + + /** + * Change the current context (if applicable) + */ + changeContext(host: string): void; + + /** + * Adds a cell to the end of the model + */ + addCell(cellType: CellType): void; + + /** + * Deletes a cell + */ + deleteCell(cellModel: ICellModel): void; + + /** + * Save the model to its backing content manager. + * Serializes the model and then calls through to save it + */ + saveModel(): Promise; + + /** + * Notifies the notebook of a change in the cell + */ + onCellChange(cell: ICellModel, change: NotebookChangeType): void; +} + +export interface ICellModelOptions { + notebook: INotebookModel; + isTrusted: boolean; +} + +export interface ICellModel { + cellUri: URI; + id: string; + language: string; + source: string; + cellType: CellType; + trustedMode: boolean; + active: boolean; + equals(cellModel: ICellModel): boolean; + toJSON(): nb.ICell; +} + +export interface IModelFactory { + + createCell(cell: nb.ICell, options: ICellModelOptions): ICellModel; + createClientSession(options: IClientSessionOptions): IClientSession; +} + + +export interface INotebookModelOptions { + /** + * Path to the local or remote notebook + */ + path: string; + + /** + * Factory for creating cells and client sessions + */ + factory: IModelFactory; + + notebookManager: INotebookManager; + + notificationService: INotificationService; + connectionService: IConnectionManagementService; +} + +// TODO would like to move most of these constants to an extension +export namespace notebookConstants { + export const hadoopKnoxProviderName = 'HADOOP_KNOX'; + export const python3 = 'python3'; + export const python3DisplayName = 'Python 3'; + export const defaultSparkKernel = 'pyspark3kernel'; + +} \ No newline at end of file diff --git a/src/sql/parts/notebook/models/notebookConnection.ts b/src/sql/parts/notebook/models/notebookConnection.ts new file mode 100644 index 0000000000..507ee11622 --- /dev/null +++ b/src/sql/parts/notebook/models/notebookConnection.ts @@ -0,0 +1,94 @@ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { localize } from 'vs/nls'; + +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; + +export namespace constants { + export const hostPropName = 'host'; + export const userPropName = 'user'; + export const knoxPortPropName = 'knoxport'; + export const clusterPropName = 'clustername'; + export const passwordPropName = 'password'; + export const defaultKnoxPort = '30443'; +} +/** + * This is a temporary connection definition, with known properties for Knox gateway connections. + * Long term this should be refactored to an extension contribution + * + * @export + * @class NotebookConnection + */ +export class NotebookConnection { + private _host: string; + private _knoxPort: string; + + constructor(private _connectionProfile: IConnectionProfile) { + if (!this._connectionProfile) { + throw new Error(localize('connectionInfoMissing', 'connectionInfo is required')); + } + } + + public get connectionProfile(): IConnectionProfile { + return this.connectionProfile; + } + + + public get host(): string { + if (!this._host) { + this.ensureHostAndPort(); + } + return this._host; + } + + /** + * Sets host and port values, using any ',' or ':' delimited port in the hostname in + * preference to the built in port. + */ + private ensureHostAndPort(): void { + this._host = this.connectionProfile.options[constants.hostPropName]; + this._knoxPort = NotebookConnection.getKnoxPortOrDefault(this.connectionProfile); + // determine whether the host has either a ',' or ':' in it + this.setHostAndPort(','); + this.setHostAndPort(':'); + } + + // set port and host correctly after we've identified that a delimiter exists in the host name + private setHostAndPort(delimeter: string): void { + let originalHost = this._host; + let index = originalHost.indexOf(delimeter); + if (index > -1) { + this._host = originalHost.slice(0, index); + this._knoxPort = originalHost.slice(index + 1); + } + } + + public get user(): string { + return this.connectionProfile.options[constants.userPropName]; + } + + public get password(): string { + return this.connectionProfile.options[constants.passwordPropName]; + } + + public get knoxport(): string { + if (!this._knoxPort) { + this.ensureHostAndPort(); + } + return this._knoxPort; + } + + private static getKnoxPortOrDefault(connectionProfile: IConnectionProfile): string { + let port = connectionProfile.options[constants.knoxPortPropName]; + if (!port) { + port = constants.defaultKnoxPort; + } + return port; + } +} diff --git a/src/sql/parts/notebook/models/notebookModel.ts b/src/sql/parts/notebook/models/notebookModel.ts new file mode 100644 index 0000000000..b718266cf0 --- /dev/null +++ b/src/sql/parts/notebook/models/notebookModel.ts @@ -0,0 +1,474 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { nb } from 'sqlops'; + +import { localize } from 'vs/nls'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; + +import { CellModel } from './cell'; +import { IClientSession, INotebookModel, IDefaultConnection, INotebookModelOptions, ICellModel, notebookConstants } from './modelInterfaces'; +import { NotebookChangeType, CellTypes, CellType } from 'sql/parts/notebook/models/contracts'; +import { nbversion } from '../notebookConstants'; +import * as notebookUtils from '../notebookUtils'; +import { INotebookManager } from 'sql/services/notebook/notebookService'; +import { SparkMagicContexts } from 'sql/parts/notebook/models/sparkMagicContexts'; +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; +import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection'; + +/* +* Used to control whether a message in a dialog/wizard is displayed as an error, +* warning, or informational message. Default is error. +*/ +export enum MessageLevel { + Error = 0, + Warning = 1, + Information = 2 +} + +export class ErrorInfo { + constructor(public readonly message: string, public readonly severity: MessageLevel) { + } +} +export interface NotebookContentChange { + /** + * What was the change that occurred? + */ + changeType: NotebookChangeType; + /** + * Optional cells that were changed + */ + cells?: ICellModel | ICellModel[]; + /** + * Optional index of the change, indicating the cell at which an insert or + * delete occurred + */ + cellIndex?: number; + /** + * Optional value indicating if the notebook is in a dirty or clean state after this change + * + * @type {boolean} + * @memberof NotebookContentChange + */ + isDirty?: boolean; +} + +export class NotebookModel extends Disposable implements INotebookModel { + private _contextsChangedEmitter = new Emitter(); + private _contentChangedEmitter = new Emitter(); + private _kernelsChangedEmitter = new Emitter(); + private _inErrorState: boolean = false; + private _clientSession: IClientSession; + private _sessionLoadFinished: Promise; + private _onClientSessionReady = new Emitter(); + private _activeContexts: IDefaultConnection; + private _trustedMode: boolean; + + private _cells: ICellModel[]; + private _defaultLanguageInfo: nb.ILanguageInfo; + private onErrorEmitter = new Emitter(); + private _savedKernelInfo: nb.IKernelInfo; + private readonly _nbformat: number = nbversion.MAJOR_VERSION; + private readonly _nbformatMinor: number = nbversion.MINOR_VERSION; + private _hadoopConnection: NotebookConnection; + private _defaultKernel: nb.IKernelSpec; + + constructor(private notebookOptions: INotebookModelOptions, startSessionImmediately?: boolean, private connectionProfile?: IConnectionProfile) { + super(); + if (!notebookOptions || !notebookOptions.path || !notebookOptions.notebookManager) { + throw new Error('path or notebook service not defined'); + } + if (startSessionImmediately) { + this.backgroundStartSession(); + } + this._trustedMode = false; + } + + public get notebookManager(): INotebookManager { + return this.notebookOptions.notebookManager; + } + + public get hasServerManager(): boolean { + // If the service has a server manager, then we can show the start button + return !!this.notebookManager.serverManager; + } + + public get contentChanged(): Event { + return this._contentChangedEmitter.event; + } + + public get isSessionReady(): boolean { + return !!this._clientSession; + } + + /** + * ClientSession object which handles management of a session instance, + * plus startup of the session manager which can return key metadata about the + * notebook environment + */ + public get clientSession(): IClientSession { + return this._clientSession; + } + + public get kernelChanged(): Event { + return this.clientSession.kernelChanged; + } + + public get kernelsChanged(): Event { + return this._kernelsChangedEmitter.event; + } + + public get defaultKernel(): nb.IKernelSpec { + return this._defaultKernel; + } + + public get contextsChanged(): Event { + return this._contextsChangedEmitter.event; + } + + public get cells(): ICellModel[] { + return this._cells; + } + + public get contexts(): IDefaultConnection { + return this._activeContexts; + } + + public get specs(): nb.IAllKernels | undefined { + return this.notebookManager.sessionManager.specs; + } + + public get inErrorState(): boolean { + return this._inErrorState; + } + + public get onError(): Event { + return this.onErrorEmitter.event; + } + + public get trustedMode(): boolean { + return this._trustedMode; + } + + public set trustedMode(isTrusted: boolean) { + this._trustedMode = isTrusted; + if (this._cells) { + this._cells.forEach(c => { + c.trustedMode = this._trustedMode; + }); + } + } + + /** + * Indicates the server has finished loading. It may have failed to load in + * which case the view will be in an error state. + */ + public get sessionLoadFinished(): Promise { + return this._sessionLoadFinished; + } + + /** + * Notifies when the client session is ready for use + */ + public get onClientSessionReady(): Event { + return this._onClientSessionReady.event; + } + + public async requestModelLoad(isTrusted: boolean = false): Promise { + try { + this._trustedMode = isTrusted; + let contents = await this.notebookManager.contentManager.getNotebookContents(this.notebookOptions.path); + let factory = this.notebookOptions.factory; + // if cells already exist, create them with language info (if it is saved) + this._cells = undefined; + if (contents) { + this._defaultLanguageInfo = this.getDefaultLanguageInfo(contents); + this._savedKernelInfo = this.getSavedKernelInfo(contents); + if (contents.cells && contents.cells.length > 0) { + this._cells = contents.cells.map(c => factory.createCell(c, { notebook: this, isTrusted: isTrusted })); + } + } + if (!this._cells) { + this._cells = [this.createCell(CellTypes.Code)]; + } + } catch (error) { + this._inErrorState = true; + throw error; + } + } + + addCell(cellType: CellType): void { + if (this.inErrorState || !this._cells) { + return; + } + let cell = this.createCell(cellType); + this._cells.push(cell); + this._contentChangedEmitter.fire({ + changeType: NotebookChangeType.CellsAdded, + cells: [cell] + }); + } + + private createCell(cellType: CellType): ICellModel { + let singleCell: nb.ICell = { + cell_type: cellType, + source: '', + metadata: {}, + execution_count: 1 + }; + return this.notebookOptions.factory.createCell(singleCell, { notebook: this, isTrusted: true }); + } + + deleteCell(cellModel: CellModel): void { + if (this.inErrorState || !this._cells) { + return; + } + let index = this._cells.findIndex((cell) => cell.equals(cellModel)); + if (index > -1) { + this._cells.splice(index, 1); + this._contentChangedEmitter.fire({ + changeType: NotebookChangeType.CellDeleted, + cells: [cellModel], + cellIndex: index + }); + } else { + this.notifyError(localize('deleteCellFailed', 'Failed to delete cell.')); + } + } + + private notifyError(error: string): void { + this.onErrorEmitter.fire(new ErrorInfo(error, MessageLevel.Error)); + } + + public backgroundStartSession(): void { + this._clientSession = this.notebookOptions.factory.createClientSession({ + path: this.notebookOptions.path, + notebookManager: this.notebookManager, + notificationService: this.notebookOptions.notificationService + }); + let id: string = this.connectionProfile ? this.connectionProfile.id : undefined; + + this._hadoopConnection = this.connectionProfile ? new NotebookConnection(this.connectionProfile) : undefined; + this._clientSession.initialize(this._hadoopConnection); + this._sessionLoadFinished = this._clientSession.ready.then(async () => { + if (this._clientSession.isInErrorState) { + this.setErrorState(this._clientSession.errorMessage); + } else { + this._onClientSessionReady.fire(this._clientSession); + // Once session is loaded, can use the session manager to retrieve useful info + this.loadKernelInfo(); + await this.loadActiveContexts(undefined); + } + }); + } + + public get languageInfo(): nb.ILanguageInfo { + return this._defaultLanguageInfo; + } + + private updateLanguageInfo(info: nb.ILanguageInfo) { + if (info) { + this._defaultLanguageInfo = info; + } + } + + public changeKernel(displayName: string): void { + let spec = this.getSpecNameFromDisplayName(displayName); + this.doChangeKernel(spec); + } + + private doChangeKernel(kernelSpec: nb.IKernelSpec): void { + this._clientSession.changeKernel(kernelSpec) + .then((kernel) => { + kernel.ready.then(() => { + if (kernel.info) { + this.updateLanguageInfo(kernel.info.language_info); + } + }, err => undefined); + return this.updateKernelInfo(kernel); + }).catch((err) => { + this.notifyError(localize('changeKernelFailed', 'Failed to change kernel: {0}', notebookUtils.getErrorMessage(err))); + // TODO should revert kernels dropdown + }); + + } + + public changeContext(host: string): void { + try { + let newConnection: IConnectionProfile = this._activeContexts.otherConnections.find((connection) => connection.options['host'] === host); + if (!newConnection && this._activeContexts.defaultConnection.options['host'] === host) { + newConnection = this._activeContexts.defaultConnection; + } + if (newConnection) { + SparkMagicContexts.configureContext(newConnection, this.notebookOptions); + this._hadoopConnection = new NotebookConnection(newConnection); + this._clientSession.updateConnection(this._hadoopConnection); + } + } catch (err) { + let msg = notebookUtils.getErrorMessage(err); + this.notifyError(localize('changeContextFailed', 'Changing context failed: {0}', msg)); + } + } + + private loadKernelInfo(): void { + this.clientSession.kernelChanged(async (e) => { + await this.loadActiveContexts(e); + }); + try { + let sessionManager = this.notebookManager.sessionManager; + if (sessionManager) { + let defaultKernel = SparkMagicContexts.getDefaultKernel(sessionManager.specs, this.connectionProfile, this._savedKernelInfo, this.notebookOptions.notificationService); + this._defaultKernel = defaultKernel; + this._clientSession.statusChanged(async (session) => { + if (session && session.defaultKernelLoaded === true) { + this._kernelsChangedEmitter.fire(defaultKernel); + } else if (session && !session.defaultKernelLoaded) { + this._kernelsChangedEmitter.fire({ name: notebookConstants.python3, display_name: notebookConstants.python3DisplayName }); + } + }); + this.doChangeKernel(defaultKernel); + } + } catch (err) { + let msg = notebookUtils.getErrorMessage(err); + this.notifyError(localize('loadKernelFailed', 'Loading kernel info failed: {0}', msg)); + } + } + + // Get default language if saved in notebook file + // Otherwise, default to python + private getDefaultLanguageInfo(notebook: nb.INotebook): nb.ILanguageInfo { + return notebook!.metadata!.language_info || { + name: 'python', + version: '', + mimetype: 'x-python' + }; + } + + // Get default kernel info if saved in notebook file + private getSavedKernelInfo(notebook: nb.INotebook): nb.IKernelInfo { + return notebook!.metadata!.kernelspec; + } + + private getSpecNameFromDisplayName(displayName: string): nb.IKernelSpec { + displayName = this.sanitizeDisplayName(displayName); + let kernel: nb.IKernelSpec = this.specs.kernels.find(k => k.display_name.toLowerCase() === displayName.toLowerCase()); + if (!kernel) { + return undefined; // undefined is handled gracefully in the session to default to the default kernel + } else if (!kernel.name) { + kernel.name = this.specs.defaultKernel; + } + return kernel; + } + + private setErrorState(errMsg: string): void { + this._inErrorState = true; + let msg = localize('startSessionFailed', 'Could not start session: {0}', errMsg); + this.notifyError(msg); + + } + + public dispose(): void { + super.dispose(); + this.handleClosed(); + } + + public async handleClosed(): Promise { + try { + if (this._clientSession) { + await this._clientSession.shutdown(); + this._clientSession = undefined; + } + } catch (err) { + this.notifyError(localize('shutdownError', 'An error occurred when closing the notebook: {0}', err)); + } + } + + private async loadActiveContexts(kernelChangedArgs: nb.IKernelChangedArgs): Promise { + this._activeContexts = await SparkMagicContexts.getContextsForKernel(this.notebookOptions.connectionService, kernelChangedArgs, this.connectionProfile); + this._contextsChangedEmitter.fire(); + let defaultHadoopConnection = new NotebookConnection(this.contexts.defaultConnection); + this.changeContext(defaultHadoopConnection.host); + } + + /** + * Sanitizes display name to remove IP address in order to fairly compare kernels + * In some notebooks, display name is in the format () + * example: PySpark (25.23.32.4) + * @param displayName Display Name for the kernel + */ + public sanitizeDisplayName(displayName: string): string { + let name = displayName; + if (name) { + let index = name.indexOf('('); + name = (index > -1) ? name.substr(0, index - 1).trim() : name; + } + return name; + } + + public async saveModel(): Promise { + let notebook = this.toJSON(); + if (!notebook) { + return false; + } + await this.notebookManager.contentManager.save(this.notebookOptions.path, notebook); + this._contentChangedEmitter.fire({ + changeType: NotebookChangeType.DirtyStateChanged, + isDirty: false + }); + return true; + } + + private async updateKernelInfo(kernel: nb.IKernel): Promise { + if (kernel) { + try { + let spec = await kernel.getSpec(); + this._savedKernelInfo = { + name: kernel.name, + display_name: spec.display_name, + language: spec.language + }; + } catch (err) { + // Don't worry about this for now. Just use saved values + } + } + } + /** + * Serialize the model to JSON. + */ + toJSON(): nb.INotebook { + let cells: nb.ICell[] = this.cells.map(c => c.toJSON()); + let metadata = Object.create(null) as nb.INotebookMetadata; + // TODO update language and kernel when these change + metadata.kernelspec = this._savedKernelInfo; + metadata.language_info = this.languageInfo; + return { + metadata, + nbformat_minor: this._nbformatMinor, + nbformat: this._nbformat, + cells + }; + } + + onCellChange(cell: CellModel, change: NotebookChangeType): void { + let changeInfo: NotebookContentChange = { + changeType: change, + cells: [cell] + }; + switch (change) { + case NotebookChangeType.CellOutputUpdated: + case NotebookChangeType.CellSourceUpdated: + changeInfo.changeType = NotebookChangeType.DirtyStateChanged; + changeInfo.isDirty = true; + break; + default: + // Do nothing for now + } + this._contentChangedEmitter.fire(changeInfo); + } + +} diff --git a/src/sql/parts/notebook/models/sparkMagicContexts.ts b/src/sql/parts/notebook/models/sparkMagicContexts.ts new file mode 100644 index 0000000000..2a9f55f694 --- /dev/null +++ b/src/sql/parts/notebook/models/sparkMagicContexts.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as path from 'path'; +import { nb } from 'sqlops'; + +import * as json from 'vs/base/common/json'; +import * as pfs from 'vs/base/node/pfs'; +import { localize } from 'vs/nls'; +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; +import { IDefaultConnection, notebookConstants, INotebookModelOptions } from 'sql/parts/notebook/models/modelInterfaces'; +import * as notebookUtils from '../notebookUtils'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement'; + +export class SparkMagicContexts { + + public static get DefaultContext(): IDefaultConnection { + // TODO NOTEBOOK REFACTOR fix default connection handling + let defaultConnection: IConnectionProfile = { + providerName: notebookConstants.hadoopKnoxProviderName, + id: '-1', + options: + { + host: localize('selectConnection', 'Select Connection') + } + }; + + return { + // default context if no other contexts are applicable + defaultConnection: defaultConnection, + otherConnections: [defaultConnection] + }; + } + + /** + * Get all of the applicable contexts for a given kernel + * @param apiWrapper ApiWrapper + * @param kernelChangedArgs kernel changed args (both old and new kernel info) + * @param profile current connection profile + */ + public static async getContextsForKernel(connectionService: IConnectionManagementService, kernelChangedArgs?: nb.IKernelChangedArgs, profile?: IConnectionProfile): Promise { + let connections: IDefaultConnection = this.DefaultContext; + if (!profile) { + if (!kernelChangedArgs || !kernelChangedArgs.newValue || + (kernelChangedArgs.oldValue && kernelChangedArgs.newValue.id === kernelChangedArgs.oldValue.id)) { + // nothing to do, kernels are the same or new kernel is undefined + return connections; + } + } + if (kernelChangedArgs && kernelChangedArgs.newValue && kernelChangedArgs.newValue.name) { + switch (kernelChangedArgs.newValue.name) { + case (notebookConstants.python3): + // python3 case, use this.DefaultContext for the only connection + break; + //TO DO: Handle server connections based on kernel type. Right now, we call the same method for all kernel types. + default: + connections = await this.getActiveContexts(connectionService, profile); + } + } else { + connections = await this.getActiveContexts(connectionService, profile); + } + return connections; + } + + /** + * Get all active contexts and sort them + * @param apiWrapper ApiWrapper + * @param profile current connection profile + */ + public static async getActiveContexts(connectionService: IConnectionManagementService, profile: IConnectionProfile): Promise { + let defaultConnection: IConnectionProfile = SparkMagicContexts.DefaultContext.defaultConnection; + let activeConnections: IConnectionProfile[] = await connectionService.getActiveConnections(); + // If no connections exist, only show 'n/a' + if (activeConnections.length === 0) { + return SparkMagicContexts.DefaultContext; + } + activeConnections = activeConnections.filter(conn => conn.providerName === notebookConstants.hadoopKnoxProviderName); + + // If launched from the right click or server dashboard, connection profile data exists, so use that as default + if (profile && profile.options) { + let profileConnection = activeConnections.filter(conn => conn.options['host'] === profile.options['host']); + if (profileConnection) { + defaultConnection = profileConnection[0]; + } + } else { + if (activeConnections.length > 0) { + defaultConnection = activeConnections[0]; + } else { + // TODO NOTEBOOK REFACTOR change this so it's no longer incompatible with IConnectionProfile + defaultConnection = { + providerName: notebookConstants.hadoopKnoxProviderName, + id: '-1', + options: + { + host: localize('addConnection', 'Add new connection') + } + }; + activeConnections.push(defaultConnection); + } + } + return { + otherConnections: activeConnections, + defaultConnection: defaultConnection + }; + } + + public static async configureContext(connection: IConnectionProfile, options: INotebookModelOptions): Promise { + let sparkmagicConfDir = path.join(notebookUtils.getUserHome(), '.sparkmagic'); + // TODO NOTEBOOK REFACTOR re-enable this or move to extension. Requires config files to be available in order to work + // await notebookUtils.mkDir(sparkmagicConfDir); + + // let hadoopConnection = new Connection({ options: connection.options }, undefined, connection.connectionId); + // await hadoopConnection.getCredential(); + // // Default to localhost in config file. + // let creds: ICredentials = { + // 'url': 'http://localhost:8088' + // }; + + // let configPath = notebookUtils.getTemplatePath(options.extensionContext.extensionPath, path.join('jupyter_config', 'sparkmagic_config.json')); + // let fileBuffer: Buffer = await pfs.readFile(configPath); + // let fileContents: string = fileBuffer.toString(); + // let config: ISparkMagicConfig = json.parse(fileContents); + // SparkMagicContexts.updateConfig(config, creds, sparkmagicConfDir); + + // let configFilePath = path.join(sparkmagicConfDir, 'config.json'); + // await pfs.writeFile(configFilePath, JSON.stringify(config)); + + return {'SPARKMAGIC_CONF_DIR': sparkmagicConfDir}; + } + /** + * + * @param specs kernel specs (comes from session manager) + * @param connectionInfo connection profile + * @param savedKernelInfo kernel info loaded from + */ + public static getDefaultKernel(specs: nb.IAllKernels, connectionInfo: IConnectionProfile, savedKernelInfo: nb.IKernelInfo, notificationService: INotificationService): nb.IKernelSpec { + let defaultKernel = specs.kernels.find((kernel) => kernel.name === specs.defaultKernel); + let profile = connectionInfo as IConnectionProfile; + if (specs && connectionInfo && profile.providerName === notebookConstants.hadoopKnoxProviderName) { + // set default kernel to default spark kernel if profile exists + // otherwise, set default to kernel info loaded from existing file + defaultKernel = !savedKernelInfo ? specs.kernels.find((spec) => spec.name === notebookConstants.defaultSparkKernel) : savedKernelInfo; + } else { + // Handle kernels + if (savedKernelInfo && savedKernelInfo.name.toLowerCase().indexOf('spark') > -1) { + notificationService.warn(localize('sparkKernelRequiresConnection', 'Cannot use kernel {0} as no connection is active. The default kernel of Python3 will be used instead.', savedKernelInfo.display_name)); + } + } + + // If no default kernel specified (should never happen), default to python3 + if (!defaultKernel) { + defaultKernel = { + name: notebookConstants.python3, + display_name: notebookConstants.python3DisplayName + }; + } + return defaultKernel; + } + + private static updateConfig(config: ISparkMagicConfig, creds: ICredentials, homePath: string): void { + config.kernel_python_credentials = creds; + config.kernel_scala_credentials = creds; + config.kernel_r_credentials = creds; + config.logging_config.handlers.magicsHandler.home_path = homePath; + } +} + +interface ICredentials { + 'url': string; +} + +interface ISparkMagicConfig { + kernel_python_credentials: ICredentials; + kernel_scala_credentials: ICredentials; + kernel_r_credentials: ICredentials; + logging_config: { + handlers: { + magicsHandler: { + home_path: string; + } + } + }; + +} + +export interface IKernelJupyterID { + id: string; + jupyterId: string; +} diff --git a/src/sql/parts/notebook/notebook.component.ts b/src/sql/parts/notebook/notebook.component.ts index 802666d8f2..ccaa9eb1ba 100644 --- a/src/sql/parts/notebook/notebook.component.ts +++ b/src/sql/parts/notebook/notebook.component.ts @@ -5,17 +5,44 @@ import './notebookStyles'; -import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, ViewChildren } from '@angular/core'; +import { nb } from 'sqlops'; + +import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild } from '@angular/core'; + +import URI from 'vs/base/common/uri'; +import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import * as themeColors from 'vs/workbench/common/theme'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; import { AngularDisposable } from 'sql/base/common/lifecycle'; -import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import * as themeColors from 'vs/workbench/common/theme'; -import { ICellModel, CellTypes } from 'sql/parts/notebook/cellViews/interfaces'; +import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts'; +import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; +import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement'; +import { INotebookService, INotebookParams } from 'sql/services/notebook/notebookService'; +import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService'; export const NOTEBOOK_SELECTOR: string = 'notebook-component'; +class CellModelStub implements ICellModel { + public cellUri: URI; + constructor(public id: string, + public language: string, + public source: string, + public cellType: CellType, + public trustedMode: boolean = false, + public active: boolean = false + ) { } + + equals(cellModel: ICellModel): boolean { + throw new Error('Method not implemented.'); + } + toJSON(): nb.ICell { + throw new Error('Method not implemented.'); + } +} + @Component({ selector: NOTEBOOK_SELECTOR, templateUrl: decodeURI(require.toUrl('./notebook.component.html')) @@ -27,20 +54,18 @@ export class NotebookComponent extends AngularDisposable implements OnInit { constructor( @Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface, @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, - @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService + @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService, + @Inject(IConnectionManagementService) private connectionManagementService: IConnectionManagementService, + @Inject(INotificationService) private notificationService: INotificationService, + @Inject(INotebookService) private notebookService: INotebookService, + @Inject(IBootstrapParams) private notebookParams: INotebookParams ) { super(); - // Todo: This is mock data for cells. Will remove this code when we have a service - let cell1: ICellModel = { - id: '1', language: 'sql', source: 'select * from sys.tables', cellType: CellTypes.Code, active: false - }; - let cell2: ICellModel = { - id: '2', language: 'sql', source: 'select 1', cellType: CellTypes.Code, active: false - }; - let cell3: ICellModel = { - id: '3', language: 'markdown', source: '## This is test!', cellType: CellTypes.Markdown, active: false - }; + // TODO NOTEBOOK REFACTOR: This is mock data for cells. Will remove this code when we have a service + let cell1 : ICellModel = new CellModelStub ('1', 'sql', 'select * from sys.tables', CellTypes.Code); + let cell2 : ICellModel = new CellModelStub ('2', 'sql', 'select 1', CellTypes.Code); + let cell3 : ICellModel = new CellModelStub ('3', 'markdown', '## This is test!', CellTypes.Markdown); this.cells.push(cell1, cell2, cell3); } diff --git a/src/sql/parts/notebook/notebook.contribution.ts b/src/sql/parts/notebook/notebook.contribution.ts index 69546dd9f3..86fa63417e 100644 --- a/src/sql/parts/notebook/notebook.contribution.ts +++ b/src/sql/parts/notebook/notebook.contribution.ts @@ -11,9 +11,14 @@ import { Action } from 'vs/base/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; +import { Schemas } from 'vs/base/common/network'; import { NotebookInput, NotebookInputModel } from 'sql/parts/notebook/notebookInput'; import { NotebookEditor } from 'sql/parts/notebook/notebookEditor'; +import URI from 'vs/base/common/uri'; + + +let counter = 0; /** * todo: Will remove this code. @@ -34,7 +39,8 @@ export class OpenNotebookAction extends Action { public run(): TPromise { return new TPromise((resolve, reject) => { - let model = new NotebookInputModel('modelViewId', undefined, undefined); + let untitledUri = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter++}`}); + let model = new NotebookInputModel(untitledUri, undefined, undefined); let input = new NotebookInput('modelViewId', model); this._editorService.openEditor(input, { pinned: true }); }); diff --git a/src/sql/parts/notebook/notebookConstants.ts b/src/sql/parts/notebook/notebookConstants.ts new file mode 100644 index 0000000000..54ff5d1527 --- /dev/null +++ b/src/sql/parts/notebook/notebookConstants.ts @@ -0,0 +1,19 @@ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +export namespace nbversion { + /** + * The major version of the notebook format. + */ + export const MAJOR_VERSION: number = 4; + + /** + * The minor version of the notebook format. + */ + export const MINOR_VERSION: number = 2; +} \ No newline at end of file diff --git a/src/sql/parts/notebook/notebookEditor.ts b/src/sql/parts/notebook/notebookEditor.ts index eb89f73fcf..660362d310 100644 --- a/src/sql/parts/notebook/notebookEditor.ts +++ b/src/sql/parts/notebook/notebookEditor.ts @@ -16,6 +16,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { NotebookInput } from 'sql/parts/notebook/notebookInput'; import { NotebookModule } from 'sql/parts/notebook/notebook.module'; import { NOTEBOOK_SELECTOR } from 'sql/parts/notebook/notebook.component'; +import { INotebookParams, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService'; export class NotebookEditor extends BaseEditor { @@ -83,12 +84,16 @@ export class NotebookEditor extends BaseEditor { private bootstrapAngular(input: NotebookInput): void { // Get the bootstrap params and perform the bootstrap input.hasBootstrapped = true; + let params: INotebookParams = { + notebookUri: input.notebookUri, + providerId: input.providerId ? input.providerId : DEFAULT_NOTEBOOK_PROVIDER + }; bootstrapAngular(this.instantiationService, NotebookModule, this._notebookContainer, NOTEBOOK_SELECTOR, - undefined, - undefined + params, + input ); } } diff --git a/src/sql/parts/notebook/notebookInput.ts b/src/sql/parts/notebook/notebookInput.ts index a2d6b4dc87..788bd73eff 100644 --- a/src/sql/parts/notebook/notebookInput.ts +++ b/src/sql/parts/notebook/notebookInput.ts @@ -1,21 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + import { TPromise } from 'vs/base/common/winjs.base'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { EditorInput, EditorModel, ConfirmResult } from 'vs/workbench/common/editor'; - import { Emitter, Event } from 'vs/base/common/event'; +import URI from 'vs/base/common/uri'; export type ModeViewSaveHandler = (handle: number) => Thenable; export class NotebookInputModel extends EditorModel { private dirty: boolean; private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); - get onDidChangeDirty(): Event { return this._onDidChangeDirty.event; } - - constructor(public readonly modelViewId, private readonly handle: number, private saveHandler?: ModeViewSaveHandler) { + private _providerId: string; + constructor(public readonly notebookUri: URI, private readonly handle: number, private saveHandler?: ModeViewSaveHandler) { super(); this.dirty = false; } + public get providerId(): string { + return this._providerId; + } + + public set providerId(value: string) { + this._providerId = value; + } + + get onDidChangeDirty(): Event { + return this._onDidChangeDirty.event; + } + get isDirty(): boolean { return this.dirty; } @@ -55,8 +73,12 @@ export class NotebookInput extends EditorInput { return this._title; } - public get modelViewId(): string { - return this._model.modelViewId; + public get notebookUri(): URI { + return this._model.notebookUri; + } + + public get providerId(): string { + return this._model.providerId; } public getTypeId(): string { diff --git a/src/sql/parts/notebook/notebookUtils.ts b/src/sql/parts/notebook/notebookUtils.ts new file mode 100644 index 0000000000..d250f5c4bb --- /dev/null +++ b/src/sql/parts/notebook/notebookUtils.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { nb } from 'sqlops'; +import * as os from 'os'; +import * as pfs from 'vs/base/node/pfs'; +import { localize } from 'vs/nls'; +import { IOutputChannel } from 'vs/workbench/parts/output/common/output'; + + +/** + * Test whether an output is from a stream. + */ +export function isStream(output: nb.ICellOutput): output is nb.IStreamResult { + return output.output_type === 'stream'; +} + +export function getErrorMessage(error: Error | string): string { + return (error instanceof Error) ? error.message : error; +} + +export function getUserHome(): string { + return process.env.HOME || process.env.USERPROFILE; +} + +export async function mkDir(dirPath: string, outputChannel?: IOutputChannel): Promise { + let exists = await pfs.dirExists(dirPath); + if (!exists) { + if (outputChannel) { + outputChannel.append(localize('mkdirOutputMsg', '... Creating {0}', dirPath) + os.EOL); + } + await pfs.mkdirp(dirPath); + } +} diff --git a/src/sql/parts/notebook/spark/sparkUtils.ts b/src/sql/parts/notebook/spark/sparkUtils.ts new file mode 100644 index 0000000000..b0c4b59a46 --- /dev/null +++ b/src/sql/parts/notebook/spark/sparkUtils.ts @@ -0,0 +1,16 @@ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +// TODO: The content of this file should be refactored to an extension +export function getKnoxUrl(host: string, port: string): string { + return `https://${host}:${port}/gateway`; +} + +export function getLivyUrl(serverName: string, port: string): string { + return getKnoxUrl(serverName, port) + '/default/livy/v1/'; +} \ No newline at end of file diff --git a/src/sql/services/notebook/notebookService.ts b/src/sql/services/notebook/notebookService.ts new file mode 100644 index 0000000000..02a26008db --- /dev/null +++ b/src/sql/services/notebook/notebookService.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as sqlops from 'sqlops'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import URI from 'vs/base/common/uri'; +import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService'; + +export const SERVICE_ID = 'notebookService'; +export const INotebookService = createDecorator(SERVICE_ID); + +export const DEFAULT_NOTEBOOK_PROVIDER = 'builtin'; + +export interface INotebookService { + _serviceBrand: any; + + /** + * Register a metadata provider + */ + registerProvider(providerId: string, provider: INotebookProvider): void; + + /** + * Register a metadata provider + */ + unregisterProvider(providerId: string): void; + + /** + * Initializes and returns a Notebook manager that can handle all important calls to open, display, and + * run cells in a notebook. + * @param providerId ID for the provider to be used to instantiate a backend notebook service + * @param uri URI for a notebook that is to be opened. Based on this an existing manager may be used, or + * a new one may need to be created + */ + getOrCreateNotebookManager(providerId: string, uri: URI): Thenable; + + shutdown(): void; +} + +export interface INotebookProvider { + readonly providerId: string; + getNotebookManager(notebookUri: URI): Thenable; + handleNotebookClosed(notebookUri: URI): void; +} + +export interface INotebookManager { + providerId: string; + readonly contentManager: sqlops.nb.ContentManager; + readonly sessionManager: sqlops.nb.SessionManager; + readonly serverManager: sqlops.nb.ServerManager; +} + +export interface INotebookParams extends IBootstrapParams { + notebookUri: URI; + providerId: string; +} \ No newline at end of file diff --git a/src/sql/services/notebook/notebookServiceImpl.ts b/src/sql/services/notebook/notebookServiceImpl.ts new file mode 100644 index 0000000000..629c96b044 --- /dev/null +++ b/src/sql/services/notebook/notebookServiceImpl.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { nb } from 'sqlops'; +import * as nls from 'vs/nls'; +import { INotebookService, INotebookManager, INotebookProvider } from 'sql/services/notebook/notebookService'; +import URI from 'vs/base/common/uri'; + +export class NotebookService implements INotebookService { + _serviceBrand: any; + + private _providers: Map = new Map(); + private _managers: Map = new Map(); + + registerProvider(providerId: string, provider: INotebookProvider): void { + this._providers.set(providerId, provider); + } + + unregisterProvider(providerId: string): void { + this._providers.delete(providerId); + } + + public shutdown(): void { + this._managers.forEach(manager => { + if (manager.serverManager) { + // TODO should this thenable be awaited? + manager.serverManager.stopServer(); + } + }); + } + + async getOrCreateNotebookManager(providerId: string, uri: URI): Promise { + if (!uri) { + throw new Error(nls.localize('notebookUriNotDefined', 'No URI was passed when creating a notebook manager')); + } + let manager = this._managers.get(uri); + if (!manager) { + manager = await this.doWithProvider(providerId, (provider) => provider.getNotebookManager(uri)); + if (manager) { + this._managers.set(uri, manager); + } + } + return manager; + } + + // PRIVATE HELPERS ///////////////////////////////////////////////////// + private doWithProvider(providerId: string, op: (provider: INotebookProvider) => Thenable): Thenable { + // Make sure the provider exists before attempting to retrieve accounts + let provider = this._providers.get(providerId); + if (!provider) { + return Promise.reject(new Error(nls.localize('notebookServiceNoProvider', 'Notebook provider does not exist'))).then(); + } + + return op(provider); + } +} \ No newline at end of file diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 16214c70dc..cafe5d5422 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -52,8 +52,8 @@ declare module 'sqlops' { } export interface TreeComponentView extends vscode.Disposable { - onNodeCheckedChanged: vscode.Event>; - onDidChangeSelection: vscode.Event>; + onNodeCheckedChanged: vscode.Event>; + onDidChangeSelection: vscode.Event>; } export class TreeComponentItem extends vscode.TreeItem { @@ -1365,4 +1365,620 @@ declare module 'sqlops' { */ export function openConnectionDialog(providers?: string[], initialConnectionProfile?: IConnectionProfile, connectionCompletionOptions?: IConnectionCompletionOptions): Thenable; } + + export namespace nb { + export function registerNotebookProvider(provider: NotebookProvider): vscode.Disposable; + + export interface NotebookProvider { + handle: number; + readonly providerId: string; + getNotebookManager(notebookUri: vscode.Uri): Thenable; + handleNotebookClosed(notebookUri: vscode.Uri): void; + } + + export interface NotebookManager { + /** + * Manages reading and writing contents to/from files. + * Files may be local or remote, with this manager giving them a chance to convert and migrate + * from specific notebook file types to and from a standard type for this UI + */ + readonly contentManager: ContentManager; + /** + * A SessionManager that handles starting, stopping and handling notifications around sessions. + * Each notebook has 1 session associated with it, and the session is responsible + * for kernel management + */ + readonly sessionManager: SessionManager; + /** + * (Optional) ServerManager to handle server lifetime management operations. + * Depending on the implementation this may not be needed. + */ + readonly serverManager?: ServerManager; + } + + /** + * Defines the contracts needed to manage the lifetime of a notebook server. + */ + export interface ServerManager { + /** + * Indicates if the server is started at the current time + */ + readonly isStarted: boolean; + + /** + * Event sent when the server has started. This can be used to query + * the manager for server settings + */ + readonly onServerStarted: vscode.Event; + + /** + * Starts the server. Some server types may not support or require this. + * Should no-op if server is already started + */ + startServer(): Thenable; + + /** + * Stops the server. Some server types may not support or require this + */ + stopServer(): Thenable; + } + + //#region Content APIs + /** + * Handles interacting with file and folder contents + */ + export interface ContentManager { + /* Reads contents from a Uri representing a local or remote notebook and returns a + * JSON object containing the cells and metadata about the notebook + */ + getNotebookContents(path: string): Thenable; + + /** + * Save a file. + * + * @param path - The desired file path. + * + * @param notebook - notebook to be saved. + * + * @returns A thenable which resolves with the file content model when the + * file is saved. + */ + save(path: string, notebook: INotebook): Thenable; + } + + export interface INotebook { + + readonly cells: ICell[]; + readonly metadata: INotebookMetadata; + readonly nbformat: number; + readonly nbformat_minor: number; + } + + export interface INotebookMetadata { + kernelspec: IKernelInfo; + language_info?: ILanguageInfo; + } + + export interface IKernelInfo { + name: string; + language?: string; + display_name?: string; + } + + export interface ILanguageInfo { + name: string; + version: string; + mimetype?: string; + codemirror_mode?: string | ICodeMirrorMode; + } + + export interface ICodeMirrorMode { + name: string; + version: string; + } + + export interface ICell { + cell_type: CellType; + source: string | string[]; + metadata: { + language?: string; + }; + execution_count: number; + outputs?: ICellOutput[]; + } + + export type CellType = 'code' | 'markdown' | 'raw'; + + export interface ICellOutput { + output_type: OutputType; + } + export interface IStreamResult extends ICellOutput { + /** + * Stream output field defining the stream name, for example stdout + */ + name: string; + /** + * Stream output field defining the multiline stream text + */ + text: string | Buffer; + } + export interface IDisplayResult extends ICellOutput { + /** + * Mime bundle expected to contain mime type -> contents mappings. + * This is dynamic and is controlled by kernels, so cannot be more specific + */ + data: {}; + /** + * Optional metadata, also a mime bundle + */ + metadata?: {}; + } + export interface IExecuteResult extends IDisplayResult { + /** + * Number of times the cell was executed + */ + executionCount: number; + } + export interface IErrorResult extends ICellOutput { + /** + * Exception name + */ + ename: string; + /** + * Exception value + */ + evalue: string; + /** + * Stacktrace equivalent + */ + traceback?: string[]; + } + + export type OutputType = + | 'execute_result' + | 'display_data' + | 'stream' + | 'error' + | 'update_display_data'; + + //#endregion + + //#region Session APIs + export interface SessionManager { + /** + * Indicates whether the manager is ready. + */ + readonly isReady: boolean; + + /** + * A Thenable that is fulfilled when the manager is ready. + */ + readonly ready: Thenable; + + readonly specs: IAllKernels | undefined; + + startNew(options: ISessionOptions): Thenable; + + shutdown(id: string): Thenable; + } + + export interface ISession { + /** + * Is change of kernels supported for this session? + */ + canChangeKernels: boolean; + /* + * Unique id of the session. + */ + readonly id: string; + + /** + * The current path associated with the session. + */ + readonly path: string; + + /** + * The current name associated with the session. + */ + readonly name: string; + + /** + * The type of the session. + */ + readonly type: string; + + /** + * The status indicates if the kernel is healthy, dead, starting, etc. + */ + readonly status: KernelStatus; + + /** + * The kernel. + * + * #### Notes + * This is a read-only property, and can be altered by [changeKernel]. + */ + readonly kernel: IKernel; + + /** + * Tracks whether the default kernel failed to load + * This could be for a reason such as the kernel name not being recognized as a valid kernel; + */ + defaultKernelLoaded?: boolean; + + changeKernel(kernelInfo: IKernelSpec): Thenable; + } + + export interface ISessionOptions { + /** + * The path (not including name) to the session. + */ + path: string; + /** + * The name of the session. + */ + name?: string; + /** + * The type of the session. + */ + type?: string; + /** + * The type of kernel (e.g. python3). + */ + kernelName?: string; + /** + * The id of an existing kernel. + */ + kernelId?: string; + } + + export interface IKernel { + readonly id: string; + readonly name: string; + readonly supportsIntellisense: boolean; + /** + * Test whether the kernel is ready. + */ + readonly isReady: boolean; + + /** + * A Thenable that is fulfilled when the kernel is ready. + */ + readonly ready: Thenable; + + /** + * The cached kernel info. + * + * #### Notes + * This value will be null until the kernel is ready. + */ + readonly info: IInfoReply | null; + + /** + * Gets the full specification for this kernel, which can be serialized to + * a noteobok file + */ + getSpec(): Thenable; + + /** + * Send an `execute_request` message. + * + * @param content - The content of the request. + * + * @param disposeOnDone - Whether to dispose of the future when done. + * + * @returns A kernel future. + * + * #### Notes + * See [Messaging in + * Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#execute). + * + * This method returns a kernel future, rather than a Thenable, since execution may + * have many response messages (for example, many iopub display messages). + * + * Future `onReply` is called with the `execute_reply` content when the + * shell reply is received and validated. + * + * **See also:** [[IExecuteReply]] + */ + requestExecute(content: IExecuteRequest, disposeOnDone?: boolean): IFuture; + + + /** + * Send a `complete_request` message. + * + * @param content - The content of the request. + * + * @returns A Thenable that resolves with the response message. + * + * #### Notes + * See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#completion). + * + * Fulfills with the `complete_reply` content when the shell reply is + * received and validated. + */ + requestComplete(content: ICompleteRequest): Thenable; + + } + + export interface IInfoReply { + protocol_version: string; + implementation: string; + implementation_version: string; + language_info: ILanguageInfo; + banner: string; + help_links: { + text: string; + url: string; + }[]; + } + + /** + * The contents of a requestExecute message sent to the server. + */ + export interface IExecuteRequest extends IExecuteOptions { + code: string; + } + + /** + * The options used to configure an execute request. + */ + export interface IExecuteOptions { + /** + * Whether to execute the code as quietly as possible. + * The default is `false`. + */ + silent?: boolean; + + /** + * Whether to store history of the execution. + * The default `true` if silent is False. + * It is forced to `false ` if silent is `true`. + */ + store_history?: boolean; + + /** + * A mapping of names to expressions to be evaluated in the + * kernel's interactive namespace. + */ + user_expressions?: {}; + + /** + * Whether to allow stdin requests. + * The default is `true`. + */ + allow_stdin?: boolean; + + /** + * Whether to the abort execution queue on an error. + * The default is `false`. + */ + stop_on_error?: boolean; + } + + /** + * The content of a `'complete_request'` message. + * + * See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#completion). + * + * **See also:** [[ICompleteReply]], [[IKernel.complete]] + */ + export interface ICompleteRequest { + code: string; + cursor_pos: number; + } + + export interface ICompletionContent { + matches: string[]; + cursor_start: number; + cursor_end: number; + metadata: any; + status: 'ok' | 'error'; + } + /** + * A `'complete_reply'` message on the `'stream'` channel. + * + * See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#completion). + * + * **See also:** [[ICompleteRequest]], [[IKernel.complete]] + */ + export interface ICompleteReplyMsg extends IShellMessage { + content: ICompletionContent; + } + + /** + * The valid Kernel status states. + */ + export type KernelStatus = + | 'unknown' + | 'starting' + | 'reconnecting' + | 'idle' + | 'busy' + | 'restarting' + | 'dead' + | 'connected'; + + /** + * An arguments object for the kernel changed event. + */ + export interface IKernelChangedArgs { + oldValue: IKernel | null; + newValue: IKernel | null; + } + + /// -------- JSON objects, and objects primarily intended not to have methods ----------- + export interface IAllKernels { + defaultKernel: string; + kernels: IKernelSpec[]; + } + export interface IKernelSpec { + name: string; + language?: string; + display_name?: string; + } + + export interface MessageHandler { + handle(message: T): void | Thenable; + } + + + /** + * A Future interface for responses from the kernel. + * + * When a message is sent to a kernel, a Future is created to handle any + * responses that may come from the kernel. + */ + export interface IFuture extends vscode.Disposable { + + /** + * The original outgoing message. + */ + readonly msg: IMessage; + + /** + * A Thenable that resolves when the future is done. + * + * #### Notes + * The future is done when there are no more responses expected from the + * kernel. + * + * The `done` Thenable resolves to the reply message if there is one, + * otherwise it resolves to `undefined`. + */ + readonly done: Thenable; + + /** + * Set the reply handler for the kernel future. + * + * #### Notes + * If the handler returns a Thenable, all kernel message processing pauses + * until the Thenable is resolved. If there is a reply message, the future + * `done` Thenable also resolves to the reply message after this handler has + * been called. + */ + setReplyHandler(handler: MessageHandler): void; + + /** + * Sets the stdin handler for the kernel future. + * + * #### Notes + * If the handler returns a Thenable, all kernel message processing pauses + * until the Thenable is resolved. + */ + setStdInHandler(handler: MessageHandler): void; + + /** + * Sets the iopub handler for the kernel future. + * + * #### Notes + * If the handler returns a Thenable, all kernel message processing pauses + * until the Thenable is resolved. + */ + setIOPubHandler(handler: MessageHandler): void; + + /** + * Register hook for IOPub messages. + * + * @param hook - The callback invoked for an IOPub message. + * + * #### Notes + * The IOPub hook system allows you to preempt the handlers for IOPub + * messages handled by the future. + * + * The most recently registered hook is run first. A hook can return a + * boolean or a Thenable to a boolean, in which case all kernel message + * processing pauses until the Thenable is fulfilled. If a hook return value + * resolves to false, any later hooks will not run and the function will + * return a Thenable resolving to false. If a hook throws an error, the error + * is logged to the console and the next hook is run. If a hook is + * registered during the hook processing, it will not run until the next + * message. If a hook is removed during the hook processing, it will be + * deactivated immediately. + */ + registerMessageHook( + hook: (msg: IIOPubMessage) => boolean | Thenable + ): void; + + /** + * Remove a hook for IOPub messages. + * + * @param hook - The hook to remove. + * + * #### Notes + * If a hook is removed during the hook processing, it will be deactivated immediately. + */ + removeMessageHook( + hook: (msg: IIOPubMessage) => boolean | Thenable + ): void; + + /** + * Send an `input_reply` message. + */ + sendInputReply(content: IInputReply): void; + } + + /** + * The valid channel names. + */ + export type Channel = 'shell' | 'iopub' | 'stdin'; + + /** + * Kernel message header content. + * + * See [Messaging in Jupyter](https://jupyter-client.readthedocs.io/en/latest/messaging.html#general-message-format). + * + * **See also:** [[IMessage]] + */ + export interface IHeader { + username: string; + version: string; + session: string; + msg_id: string; + msg_type: string; + } + + /** + * A kernel message + */ + export interface IMessage { + type: Channel; + header: IHeader; + parent_header: IHeader | {}; + metadata: {}; + content: any; + } + + /** + * A kernel message on the `'shell'` channel. + */ + export interface IShellMessage extends IMessage { + channel: 'shell'; + } + + /** + * A kernel message on the `'iopub'` channel. + */ + export interface IIOPubMessage extends IMessage { + channel: 'iopub'; + } + + /** + * A kernel message on the `'stdin'` channel. + */ + export interface IStdinMessage extends IMessage { + channel: 'stdin'; + } + + /** + * The content of an `'input_reply'` message. + */ + export interface IInputReply { + value: string; + } + + //#endregion + + } + } diff --git a/src/sql/workbench/api/node/extHostNotebook.ts b/src/sql/workbench/api/node/extHostNotebook.ts new file mode 100644 index 0000000000..33560101d4 --- /dev/null +++ b/src/sql/workbench/api/node/extHostNotebook.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as sqlops from 'sqlops'; +import * as vscode from 'vscode'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IMainContext } from 'vs/workbench/api/node/extHost.protocol'; +import { Disposable } from 'vs/workbench/api/node/extHostTypes'; +import { localize } from 'vs/nls'; + + +import { ExtHostNotebookShape, MainThreadNotebookShape, SqlMainContext } from 'sql/workbench/api/node/sqlExtHost.protocol'; + +export class ExtHostNotebook implements ExtHostNotebookShape { + private static _handlePool: number = 0; + + private readonly _proxy: MainThreadNotebookShape; + private _providers = new Map(); + + constructor(private _mainContext: IMainContext) { + this._proxy = _mainContext.getProxy(SqlMainContext.MainThreadNotebook); + } + + //#region APIs called by main thread + getNotebookManager(notebookUri: vscode.Uri): Thenable { + throw new Error('Not implemented'); + } + handleNotebookClosed(notebookUri: vscode.Uri): void { + throw new Error('Not implemented'); + } + + //#endregion + + //#region APIs called by extensions + registerNotebookProvider(provider: sqlops.nb.NotebookProvider): vscode.Disposable { + if (!provider || !provider.providerId) { + throw new Error(localize('providerRequired', 'A NotebookProvider with valid providerId must be passed to this method')); + } + const handle = this._addNewProvider(provider); + this._proxy.$registerNotebookProvider(provider.providerId, handle); + return this._createDisposable(handle); + } + //#endregion + + + //#region private methods + private _createDisposable(handle: number): Disposable { + return new Disposable(() => { + this._providers.delete(handle); + this._proxy.$unregisterNotebookProvider(handle); + }); + } + + private _nextHandle(): number { + return ExtHostNotebook._handlePool++; + } + + private _withProvider(handle: number, ctor: { new(...args: any[]): A }, callback: (adapter: A) => TPromise): TPromise { + let provider = this._providers.get(handle); + if (!(provider instanceof ctor)) { + return TPromise.wrapError(new Error('no adapter found')); + } + return callback(provider); + } + + private _addNewProvider(adapter: sqlops.nb.NotebookProvider): number { + const handle = this._nextHandle(); + this._providers.set(handle, adapter); + return handle; + } + //#endregion +} + diff --git a/src/sql/workbench/api/node/mainThreadNotebook.ts b/src/sql/workbench/api/node/mainThreadNotebook.ts new file mode 100644 index 0000000000..b510f57122 --- /dev/null +++ b/src/sql/workbench/api/node/mainThreadNotebook.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as sqlops from 'sqlops'; +import { SqlExtHostContext, SqlMainContext, ExtHostNotebookShape, MainThreadNotebookShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; +import { INotebookService, INotebookProvider, INotebookManager } from 'sql/services/notebook/notebookService'; +import URI from 'vs/base/common/uri'; + +@extHostNamedCustomer(SqlMainContext.MainThreadNotebook) +export class MainThreadNotebook extends Disposable implements MainThreadNotebookShape { + + private _proxy: ExtHostNotebookShape; + private _registrations: { [handle: number]: NotebookProviderWrapper } = Object.create(null); + + constructor( + extHostContext: IExtHostContext, + @INotebookService private notebookService: INotebookService + ) { + super(); + if (extHostContext) { + this._proxy = extHostContext.getProxy(SqlExtHostContext.ExtHostNotebook); + } + } + + //#region Extension host callable methods + public $registerNotebookProvider(providerId: string, handle: number): void { + let notebookProvider = new NotebookProviderWrapper(providerId, handle); + this._registrations[providerId] = notebookProvider; + this.notebookService.registerProvider(providerId, notebookProvider); + } + + public $unregisterNotebookProvider(handle: number): void { + let registration = this._registrations[handle]; + if (registration) { + this.notebookService.unregisterProvider(registration.providerId); + registration.dispose(); + delete this._registrations[handle]; + } + } + + //#endregion + +} + +class NotebookProviderWrapper extends Disposable implements INotebookProvider { + + constructor(public readonly providerId, public readonly handle: number) { + super(); + } + + getNotebookManager(notebookUri: URI): Thenable { + // TODO must call through to setup in the extension host + return Promise.resolve(new NotebookManagerWrapper(this.providerId)); + } + + handleNotebookClosed(notebookUri: URI): void { + // TODO implement call through to extension host + } + + +} + +class NotebookManagerWrapper implements INotebookManager { + constructor(public readonly providerId) { + + } + sessionManager: sqlops.nb.SessionManager; + contentManager: sqlops.nb.ContentManager; + serverManager: sqlops.nb.ServerManager; + +} diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index 3c99d11f94..4b80847baa 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -37,6 +37,7 @@ import { ExtHostModelViewDialog } from 'sql/workbench/api/node/extHostModelViewD import { ExtHostModelViewTreeViews } from 'sql/workbench/api/node/extHostModelViewTree'; import { ExtHostQueryEditor } from 'sql/workbench/api/node/extHostQueryEditor'; import { ExtHostBackgroundTaskManagement } from './extHostBackgroundTaskManagement'; +import { ExtHostNotebook } from 'sql/workbench/api/node/extHostNotebook'; export interface ISqlExtensionApiFactory { vsCodeFactory(extension: IExtensionDescription): typeof vscode; @@ -73,6 +74,7 @@ export function createApiFactory( const extHostDashboard = rpcProtocol.set(SqlExtHostContext.ExtHostDashboard, new ExtHostDashboard(rpcProtocol)); const extHostModelViewDialog = rpcProtocol.set(SqlExtHostContext.ExtHostModelViewDialog, new ExtHostModelViewDialog(rpcProtocol, extHostModelView, extHostBackgroundTaskManagement)); const extHostQueryEditor = rpcProtocol.set(SqlExtHostContext.ExtHostQueryEditor, new ExtHostQueryEditor(rpcProtocol)); + const extHostNotebook = rpcProtocol.set(SqlExtHostContext.ExtHostNotebook, new ExtHostNotebook(rpcProtocol)); return { @@ -402,6 +404,12 @@ export function createApiFactory( } }; + const nb = { + registerNotebookProvider(provider: sqlops.nb.NotebookProvider): vscode.Disposable { + return extHostNotebook.registerNotebookProvider(provider); + } + }; + return { accounts, connection, @@ -437,7 +445,8 @@ export function createApiFactory( CardType: sqlExtHostTypes.CardType, Orientation: sqlExtHostTypes.Orientation, SqlThemeIcon: sqlExtHostTypes.SqlThemeIcon, - TreeComponentItem: sqlExtHostTypes.TreeComponentItem + TreeComponentItem: sqlExtHostTypes.TreeComponentItem, + nb: nb }; } }; diff --git a/src/sql/workbench/api/node/sqlExtHost.contribution.ts b/src/sql/workbench/api/node/sqlExtHost.contribution.ts index c5bacce74a..f8baed3939 100644 --- a/src/sql/workbench/api/node/sqlExtHost.contribution.ts +++ b/src/sql/workbench/api/node/sqlExtHost.contribution.ts @@ -23,7 +23,8 @@ import 'sql/workbench/api/node/mainThreadDashboardWebview'; import 'sql/workbench/api/node/mainThreadQueryEditor'; import 'sql/workbench/api/node/mainThreadModelView'; import 'sql/workbench/api/node/mainThreadModelViewDialog'; -import './mainThreadAccountManagement'; +import 'sql/workbench/api/node/mainThreadNotebook'; +import 'sql/workbench/api/node/mainThreadAccountManagement'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; export class SqlExtHostContribution implements IWorkbenchContribution { diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 6694a5e502..82819fffee 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -545,6 +545,7 @@ export const SqlMainContext = { MainThreadDashboard: createMainId('MainThreadDashboard'), MainThreadModelViewDialog: createMainId('MainThreadModelViewDialog'), MainThreadQueryEditor: createMainId('MainThreadQueryEditor'), + MainThreadNotebook: createMainId('MainThreadNotebook') }; export const SqlExtHostContext = { @@ -563,7 +564,8 @@ export const SqlExtHostContext = { ExtHostModelViewTreeViews: createExtId('ExtHostModelViewTreeViews'), ExtHostDashboard: createExtId('ExtHostDashboard'), ExtHostModelViewDialog: createExtId('ExtHostModelViewDialog'), - ExtHostQueryEditor: createExtId('ExtHostQueryEditor') + ExtHostQueryEditor: createExtId('ExtHostQueryEditor'), + ExtHostNotebook: createExtId('ExtHostNotebook') }; export interface MainThreadDashboardShape extends IDisposable { @@ -703,4 +705,21 @@ export interface ExtHostQueryEditorShape { export interface MainThreadQueryEditorShape extends IDisposable { $connect(fileUri: string, connectionId: string): Thenable; $runQuery(fileUri: string): void; +} + +export interface ExtHostNotebookShape { + + /** + * Looks up a notebook manager for a given notebook URI + * @param {vscode.Uri} notebookUri + * @returns {Thenable} handle of the manager to be used when sending + */ + getNotebookManager(notebookUri: vscode.Uri): Thenable; + handleNotebookClosed(notebookUri: vscode.Uri): void; + +} + +export interface MainThreadNotebookShape extends IDisposable { + $registerNotebookProvider(providerId: string, handle: number): void; + $unregisterNotebookProvider(handle: number): void; } \ No newline at end of file diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 87e4a81d9c..bf7947d84b 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -172,6 +172,8 @@ import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService' import { WorkbenchThemeService } from 'vs/workbench/services/themes/electron-browser/workbenchThemeService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IUriDisplayService, UriDisplayService } from 'vs/platform/uriDisplay/common/uriDisplay'; +import { NotebookService } from 'sql/services/notebook/notebookServiceImpl'; +import { INotebookService } from 'sql/services/notebook/notebookService'; interface WorkbenchParams { configuration: IWindowConfiguration; @@ -573,11 +575,14 @@ export class Workbench extends Disposable implements IPartService { serviceCollection.set(IInsightsDialogService, this.instantiationService.createInstance(InsightsDialogService)); let accountManagementService = this.instantiationService.createInstance(AccountManagementService, undefined); serviceCollection.set(IAccountManagementService, accountManagementService); + let notebookService = this.instantiationService.createInstance(NotebookService); + serviceCollection.set(INotebookService, notebookService); serviceCollection.set(IAccountPickerService, this.instantiationService.createInstance(AccountPickerService)); serviceCollection.set(IProfilerService, this.instantiationService.createInstance(ProfilerService)); this._register(toDisposable(() => connectionManagementService.shutdown())); this._register(toDisposable(() => accountManagementService.shutdown())); + this._register(toDisposable(() => notebookService.shutdown())); this._register(toDisposable(() => capabilitiesService.shutdown())); } From d434724a54aec5b6b62327c33acc80a09fa38fd7 Mon Sep 17 00:00:00 2001 From: Kevin Cunnane Date: Thu, 1 Nov 2018 17:00:46 -0700 Subject: [PATCH 07/18] Remove handle from API (#3093) - This was missed in previous checkin --- src/sql/sqlops.proposed.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index cafe5d5422..bc3ec20702 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -1370,7 +1370,6 @@ declare module 'sqlops' { export function registerNotebookProvider(provider: NotebookProvider): vscode.Disposable; export interface NotebookProvider { - handle: number; readonly providerId: string; getNotebookManager(notebookUri: vscode.Uri): Thenable; handleNotebookClosed(notebookUri: vscode.Uri): void; From 5da89ac05b6fcdb5e4683b25b8abd97b86ba566d Mon Sep 17 00:00:00 2001 From: Yurong He <43652751+linda07@users.noreply.github.com> Date: Mon, 5 Nov 2018 17:55:13 -0800 Subject: [PATCH 08/18] Add localContentManger and dummy sessionManager (#3130) * - keyboard binding to arrow keys - toggle markdown editor by double click * Added localContentManger and dummpy sessionManager --- .../cellViews/textCell.component.html | 4 +- .../notebook/cellViews/textCell.component.ts | 9 ++- .../parts/notebook/notebook.component.html | 2 +- src/sql/parts/notebook/notebook.component.ts | 24 ++++++++ .../services/notebook/localContentManager.ts | 32 +++++++++++ .../services/notebook/notebookServiceImpl.ts | 56 ++++++++++++++++++- src/sql/services/notebook/sessionManager.ts | 31 ++++++++++ 7 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 src/sql/services/notebook/localContentManager.ts create mode 100644 src/sql/services/notebook/sessionManager.ts diff --git a/src/sql/parts/notebook/cellViews/textCell.component.html b/src/sql/parts/notebook/cellViews/textCell.component.html index 98265758f8..e770ff98aa 100644 --- a/src/sql/parts/notebook/cellViews/textCell.component.html +++ b/src/sql/parts/notebook/cellViews/textCell.component.html @@ -6,8 +6,8 @@ -->
- +
-
+
diff --git a/src/sql/parts/notebook/cellViews/textCell.component.ts b/src/sql/parts/notebook/cellViews/textCell.component.ts index 7ff14e4e06..829ec38fc3 100644 --- a/src/sql/parts/notebook/cellViews/textCell.component.ts +++ b/src/sql/parts/notebook/cellViews/textCell.component.ts @@ -24,6 +24,7 @@ export class TextCellComponent extends CellView implements OnInit { @ViewChild('preview', { read: ElementRef }) private output: ElementRef; @Input() cellModel: ICellModel; private _content: string; + private isEditMode: boolean; constructor( @Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface, @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, @@ -31,6 +32,7 @@ export class TextCellComponent extends CellView implements OnInit { @Inject(ICommandService) private _commandService: ICommandService ) { super(); + this.isEditMode = true; } ngOnChanges() { @@ -41,7 +43,7 @@ export class TextCellComponent extends CellView implements OnInit { if (this._content !== this.cellModel.source) { this._content = this.cellModel.source; // todo: pass in the notebook filename instead of undefined value - this._commandService.executeCommand('notebook.showPreview', undefined, this._content).then((htmlcontent) => { + this._commandService.executeCommand('notebook.showPreview', undefined, this._content).then((htmlcontent) => { let outputElement = this.output.nativeElement; outputElement.innerHTML = htmlcontent; }); @@ -65,4 +67,9 @@ export class TextCellComponent extends CellView implements OnInit { public handleContentChanged(): void { this.updatePreview(); } + + public toggleEditMode(): void { + this.isEditMode = !this.isEditMode; + this._changeRef.detectChanges(); + } } diff --git a/src/sql/parts/notebook/notebook.component.html b/src/sql/parts/notebook/notebook.component.html index 0715fc62b6..16f106af6c 100644 --- a/src/sql/parts/notebook/notebook.component.html +++ b/src/sql/parts/notebook/notebook.component.html @@ -11,7 +11,7 @@
-
+
diff --git a/src/sql/parts/notebook/notebook.component.ts b/src/sql/parts/notebook/notebook.component.ts index ccaa9eb1ba..2b529cd88d 100644 --- a/src/sql/parts/notebook/notebook.component.ts +++ b/src/sql/parts/notebook/notebook.component.ts @@ -89,4 +89,28 @@ export class NotebookComponent extends AngularDisposable implements OnInit { this._changeRef.detectChanges(); } } + + public onKeyDown(event) { + switch (event.key) { + case 'ArrowDown': + case 'ArrowRight': + let nextIndex = (this.findCellIndex(this._activeCell) + 1)%this.cells.length; + this.selectCell(this.cells[nextIndex]); + break; + case 'ArrowUp': + case 'ArrowLeft': + let index = this.findCellIndex(this._activeCell); + if (index === 0) { + index = this.cells.length; + } + this.selectCell(this.cells[--index]); + break; + default: + break; + } + } + + findCellIndex(cellModel: ICellModel): number { + return this.cells.findIndex((cell) => cell.id === cellModel.id); + } } diff --git a/src/sql/services/notebook/localContentManager.ts b/src/sql/services/notebook/localContentManager.ts new file mode 100644 index 0000000000..4d539fda5b --- /dev/null +++ b/src/sql/services/notebook/localContentManager.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; +import { nb } from 'sqlops'; + +import * as json from 'vs/base/common/json'; +import * as pfs from 'vs/base/node/pfs'; + +import ContentManager = nb.ContentManager; +import INotebook = nb.INotebook; + +export class LocalContentManager implements ContentManager { + public async getNotebookContents(path: string): Promise { + if (!path) { + return undefined; + } + // Note: intentionally letting caller handle exceptions + let notebookFileBuffer = await pfs.readFile(path); + return json.parse(notebookFileBuffer.toString()); + } + + public async save(path: string, notebook: INotebook): Promise { + // Convert to JSON with pretty-print functionality + let contents = JSON.stringify(notebook, undefined, ' '); + await pfs.writeFile(path, contents); + return notebook; + } + +} diff --git a/src/sql/services/notebook/notebookServiceImpl.ts b/src/sql/services/notebook/notebookServiceImpl.ts index 629c96b044..af8db2c2f7 100644 --- a/src/sql/services/notebook/notebookServiceImpl.ts +++ b/src/sql/services/notebook/notebookServiceImpl.ts @@ -7,8 +7,11 @@ import { nb } from 'sqlops'; import * as nls from 'vs/nls'; -import { INotebookService, INotebookManager, INotebookProvider } from 'sql/services/notebook/notebookService'; +import { INotebookService, INotebookManager, INotebookProvider, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/services/notebook/notebookService'; import URI from 'vs/base/common/uri'; +import { LocalContentManager } from 'sql/services/notebook/localContentManager'; +import { session } from 'electron'; +import { SessionManager } from 'sql/services/notebook/sessionManager'; export class NotebookService implements INotebookService { _serviceBrand: any; @@ -16,6 +19,11 @@ export class NotebookService implements INotebookService { private _providers: Map = new Map(); private _managers: Map = new Map(); + constructor() { + let defaultProvider = new BuiltinProvider(); + this.registerProvider(defaultProvider.providerId, defaultProvider); + } + registerProvider(providerId: string, provider: INotebookProvider): void { this._providers.set(providerId, provider); } @@ -57,4 +65,48 @@ export class NotebookService implements INotebookService { return op(provider); } -} \ No newline at end of file +} + +export class BuiltinProvider implements INotebookProvider { + private manager: BuiltInNotebookManager; + + constructor() { + this.manager = new BuiltInNotebookManager(); + } + public get providerId(): string { + return DEFAULT_NOTEBOOK_PROVIDER; + } + + getNotebookManager(notebookUri: URI): Thenable { + return Promise.resolve(this.manager); + } + handleNotebookClosed(notebookUri: URI): void { + // No-op + } +} + +export class BuiltInNotebookManager implements INotebookManager { + private _contentManager: nb.ContentManager; + private _sessionManager: nb.SessionManager; + + constructor() { + this._contentManager = new LocalContentManager(); + this._sessionManager = new SessionManager(); + } + public get providerId(): string { + return DEFAULT_NOTEBOOK_PROVIDER; + } + + public get contentManager(): nb.ContentManager { + return this._contentManager; + } + + public get serverManager(): nb.ServerManager { + return undefined; + } + + public get sessionManager(): nb.SessionManager { + return this._sessionManager; + } + +} diff --git a/src/sql/services/notebook/sessionManager.ts b/src/sql/services/notebook/sessionManager.ts new file mode 100644 index 0000000000..5ac4dc1f0a --- /dev/null +++ b/src/sql/services/notebook/sessionManager.ts @@ -0,0 +1,31 @@ +'use strict'; + +import { nb } from 'sqlops'; +import { Session } from 'electron'; + +export class SessionManager implements nb.SessionManager { + private _sessionManager: nb.SessionManager; + + constructor() { + + } + public get isReady(): boolean { + return this._sessionManager.isReady; + } + + public get ready(): Thenable { + return this._sessionManager.ready; + } + public get specs(): nb.IAllKernels { + return this._sessionManager.specs; + } + + startNew(options: nb.ISessionOptions): Thenable { + return this._sessionManager.startNew(options); + } + + shutdown(id: string): Thenable { + return this.shutdown(id); + } + +} From ecd40de7ec2ddc78ec1e481bb2fcb95a0b8c476f Mon Sep 17 00:00:00 2001 From: Kevin Cunnane Date: Tue, 6 Nov 2018 16:31:37 -0800 Subject: [PATCH 09/18] Integrate notebook service with notebook UI (#3143) Port notebookView code over to notebook.component.ts. Integrate loading of notebook contents into the UI --- src/sql/parts/common/customInputConverter.ts | 2 +- .../loadingComponent.component.ts | 5 +- .../loadingSpinner.component.ts | 30 ++++ src/sql/parts/notebook/cellViews/textCell.css | 1 + .../parts/notebook/models/modelInterfaces.ts | 4 + .../parts/notebook/models/notebookModel.ts | 7 +- .../parts/notebook/notebook.component.html | 3 +- src/sql/parts/notebook/notebook.component.ts | 135 ++++++++++++++---- .../parts/notebook/notebook.contribution.ts | 4 +- src/sql/parts/notebook/notebook.module.ts | 2 + src/sql/parts/notebook/notebookEditor.ts | 3 +- src/sql/parts/notebook/notebookInput.ts | 10 +- src/sql/services/notebook/notebookService.ts | 5 + 13 files changed, 168 insertions(+), 43 deletions(-) create mode 100644 src/sql/parts/modelComponents/loadingSpinner.component.ts diff --git a/src/sql/parts/common/customInputConverter.ts b/src/sql/parts/common/customInputConverter.ts index 9531be9010..fb3f161d53 100644 --- a/src/sql/parts/common/customInputConverter.ts +++ b/src/sql/parts/common/customInputConverter.ts @@ -55,7 +55,7 @@ export function convertEditorInput(input: EditorInput, options: IQueryEditorOpti if(uri){ //TODO: We need to pass in notebook data either through notebook input or notebook service let fileName: string = input? input.getName() : 'untitled'; - let notebookInputModel = new NotebookInputModel(uri, undefined, undefined); + let notebookInputModel = new NotebookInputModel(uri, undefined, false, undefined); //TO DO: Second paramter has to be the content. let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel); return notebookInput; diff --git a/src/sql/parts/modelComponents/loadingComponent.component.ts b/src/sql/parts/modelComponents/loadingComponent.component.ts index ab5687dffe..660cf0f7ba 100644 --- a/src/sql/parts/modelComponents/loadingComponent.component.ts +++ b/src/sql/parts/modelComponents/loadingComponent.component.ts @@ -5,7 +5,7 @@ import 'vs/css!./loadingComponent'; import { - Component, Input, Inject, ChangeDetectorRef, forwardRef, OnDestroy, AfterViewInit, ViewChild, ElementRef + Component, Input, Inject, ChangeDetectorRef, forwardRef, OnDestroy, AfterViewInit, ElementRef } from '@angular/core'; import * as sqlops from 'sqlops'; @@ -31,9 +31,6 @@ export default class LoadingComponent extends ComponentBase implements IComponen @Input() descriptor: IComponentDescriptor; @Input() modelStore: IModelStore; - @ViewChild('spinnerElement', { read: ElementRef }) private _spinnerElement: ElementRef; - @ViewChild('childElement', { read: ElementRef }) private _childElement: ElementRef; - constructor( @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef, @Inject(forwardRef(() => ElementRef)) el: ElementRef) { diff --git a/src/sql/parts/modelComponents/loadingSpinner.component.ts b/src/sql/parts/modelComponents/loadingSpinner.component.ts new file mode 100644 index 0000000000..2fcd417a13 --- /dev/null +++ b/src/sql/parts/modelComponents/loadingSpinner.component.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./loadingComponent'; +import { + Component, Input, Inject, ChangeDetectorRef, forwardRef, ElementRef +} from '@angular/core'; + +import * as nls from 'vs/nls'; + +@Component({ + selector: 'loading-spinner', + template: ` +
+
+
+ ` +}) +export default class LoadingSpinner { + private readonly _loadingTitle = nls.localize('loadingMessage', 'Loading'); + + @Input() loading: boolean; + + constructor( + @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef, + @Inject(forwardRef(() => ElementRef)) el: ElementRef) { + } +} diff --git a/src/sql/parts/notebook/cellViews/textCell.css b/src/sql/parts/notebook/cellViews/textCell.css index 5f6d5d2d16..f030bd8b51 100644 --- a/src/sql/parts/notebook/cellViews/textCell.css +++ b/src/sql/parts/notebook/cellViews/textCell.css @@ -10,4 +10,5 @@ text-cell-component { text-cell-component .notebook-preview { border-top-width: 1px; border-top-style: solid; + user-select: initial; } diff --git a/src/sql/parts/notebook/models/modelInterfaces.ts b/src/sql/parts/notebook/models/modelInterfaces.ts index 418413d290..ddd75c4179 100644 --- a/src/sql/parts/notebook/models/modelInterfaces.ts +++ b/src/sql/parts/notebook/models/modelInterfaces.ts @@ -234,6 +234,10 @@ export interface IKernelPreference { } export interface INotebookModel { + /** + * Cell List for this model + */ + readonly cells: ReadonlyArray; /** * Client Session in the notebook, used for sending requests to the notebook service */ diff --git a/src/sql/parts/notebook/models/notebookModel.ts b/src/sql/parts/notebook/models/notebookModel.ts index b718266cf0..88f06bd076 100644 --- a/src/sql/parts/notebook/models/notebookModel.ts +++ b/src/sql/parts/notebook/models/notebookModel.ts @@ -20,6 +20,7 @@ import { INotebookManager } from 'sql/services/notebook/notebookService'; import { SparkMagicContexts } from 'sql/parts/notebook/models/sparkMagicContexts'; import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection'; +import { INotification, Severity } from 'vs/platform/notification/common/notification'; /* * Used to control whether a message in a dialog/wizard is displayed as an error, @@ -71,7 +72,7 @@ export class NotebookModel extends Disposable implements INotebookModel { private _cells: ICellModel[]; private _defaultLanguageInfo: nb.ILanguageInfo; - private onErrorEmitter = new Emitter(); + private onErrorEmitter = new Emitter(); private _savedKernelInfo: nb.IKernelInfo; private readonly _nbformat: number = nbversion.MAJOR_VERSION; private readonly _nbformatMinor: number = nbversion.MINOR_VERSION; @@ -147,7 +148,7 @@ export class NotebookModel extends Disposable implements INotebookModel { return this._inErrorState; } - public get onError(): Event { + public get onError(): Event { return this.onErrorEmitter.event; } @@ -242,7 +243,7 @@ export class NotebookModel extends Disposable implements INotebookModel { } private notifyError(error: string): void { - this.onErrorEmitter.fire(new ErrorInfo(error, MessageLevel.Error)); + this.onErrorEmitter.fire({ message: error, severity: Severity.Error }); } public backgroundStartSession(): void { diff --git a/src/sql/parts/notebook/notebook.component.html b/src/sql/parts/notebook/notebook.component.html index 16f106af6c..c7225fded4 100644 --- a/src/sql/parts/notebook/notebook.component.html +++ b/src/sql/parts/notebook/notebook.component.html @@ -10,7 +10,8 @@ PlaceHolder for Toolbar
-
+
+
diff --git a/src/sql/parts/notebook/notebook.component.ts b/src/sql/parts/notebook/notebook.component.ts index 2b529cd88d..8df7bcabb7 100644 --- a/src/sql/parts/notebook/notebook.component.ts +++ b/src/sql/parts/notebook/notebook.component.ts @@ -12,36 +12,25 @@ import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, V import URI from 'vs/base/common/uri'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import * as themeColors from 'vs/workbench/common/theme'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { INotificationService, INotification } from 'vs/platform/notification/common/notification'; +import { localize } from 'vs/nls'; import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; import { AngularDisposable } from 'sql/base/common/lifecycle'; -import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts'; -import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; +import { CellTypes, CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts'; +import { ICellModel, INotebookModel, IModelFactory, INotebookModelOptions } from 'sql/parts/notebook/models/modelInterfaces'; import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement'; -import { INotebookService, INotebookParams } from 'sql/services/notebook/notebookService'; +import { INotebookService, INotebookParams, INotebookManager } from 'sql/services/notebook/notebookService'; import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService'; +import { NotebookModel, ErrorInfo, MessageLevel, NotebookContentChange } from 'sql/parts/notebook/models/notebookModel'; +import { ModelFactory } from 'sql/parts/notebook/models/modelFactory'; +import * as notebookUtils from './notebookUtils'; +import { Deferred } from 'sql/base/common/promise'; +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; export const NOTEBOOK_SELECTOR: string = 'notebook-component'; -class CellModelStub implements ICellModel { - public cellUri: URI; - constructor(public id: string, - public language: string, - public source: string, - public cellType: CellType, - public trustedMode: boolean = false, - public active: boolean = false - ) { } - - equals(cellModel: ICellModel): boolean { - throw new Error('Method not implemented.'); - } - toJSON(): nb.ICell { - throw new Error('Method not implemented.'); - } -} @Component({ selector: NOTEBOOK_SELECTOR, @@ -49,8 +38,16 @@ class CellModelStub implements ICellModel { }) export class NotebookComponent extends AngularDisposable implements OnInit { @ViewChild('toolbar', { read: ElementRef }) private toolbar: ElementRef; - protected cells: Array = []; + private _model: NotebookModel; + private _isInErrorState: boolean = false; + private _errorMessage: string; private _activeCell: ICellModel; + protected isLoading: boolean; + private notebookManager: INotebookManager; + private _modelReadyDeferred = new Deferred(); + private profile: IConnectionProfile; + + constructor( @Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface, @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, @@ -61,17 +58,18 @@ export class NotebookComponent extends AngularDisposable implements OnInit { @Inject(IBootstrapParams) private notebookParams: INotebookParams ) { super(); - - // TODO NOTEBOOK REFACTOR: This is mock data for cells. Will remove this code when we have a service - let cell1 : ICellModel = new CellModelStub ('1', 'sql', 'select * from sys.tables', CellTypes.Code); - let cell2 : ICellModel = new CellModelStub ('2', 'sql', 'select 1', CellTypes.Code); - let cell3 : ICellModel = new CellModelStub ('3', 'markdown', '## This is test!', CellTypes.Markdown); - this.cells.push(cell1, cell2, cell3); + this.profile = this.notebookParams!.profile; + this.isLoading = true; } ngOnInit() { this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this)); this.updateTheme(this.themeService.getColorTheme()); + this.doLoad(); + } + + protected get cells(): ReadonlyArray { + return this._model ? this._model.cells : []; } private updateTheme(theme: IColorTheme): void { @@ -110,7 +108,84 @@ export class NotebookComponent extends AngularDisposable implements OnInit { } } - findCellIndex(cellModel: ICellModel): number { - return this.cells.findIndex((cell) => cell.id === cellModel.id); + private async doLoad(): Promise { + try { + await this.loadModel(); + this.setLoading(false); + this._modelReadyDeferred.resolve(this._model); + } catch (error) { + this.setViewInErrorState(localize('displayFailed', 'Could not display contents: {0}', error)); + this.setLoading(false); + this._modelReadyDeferred.reject(error); + } } + + private setLoading(isLoading: boolean): void { + this.isLoading = isLoading; + this._changeRef.detectChanges(); + } + + private async loadModel(): Promise { + this.notebookManager = await this.notebookService.getOrCreateNotebookManager(this.notebookParams.providerId, this.notebookParams.notebookUri); + let model = new NotebookModel({ + factory: this.modelFactory, + path: this.notebookParams.notebookUri.fsPath, + connectionService: this.connectionManagementService, + notificationService: this.notificationService, + notebookManager: this.notebookManager + }, false, this.profile); + model.onError((errInfo: INotification) => this.handleModelError(errInfo)); + model.backgroundStartSession(); + await model.requestModelLoad(this.notebookParams.isTrusted); + model.contentChanged((change) => this.handleContentChanged(change)); + this._model = model; + this._register(model); + this._changeRef.detectChanges(); + } + + private get modelFactory(): IModelFactory { + if (!this.notebookParams.modelFactory) { + this.notebookParams.modelFactory = new ModelFactory(); + } + return this.notebookParams.modelFactory; + } + private handleModelError(notification: INotification): void { + this.notificationService.notify(notification); + } + + private handleContentChanged(change: NotebookContentChange) { + // Note: for now we just need to set dirty state and refresh the UI. + this.setDirty(true); + this._changeRef.detectChanges(); + } + + findCellIndex(cellModel: ICellModel): number { + return this._model.cells.findIndex((cell) => cell.id === cellModel.id); + } + + private setViewInErrorState(error: any): any { + this._isInErrorState = true; + this._errorMessage = notebookUtils.getErrorMessage(error); + // For now, send message as error notification #870 covers having dedicated area for this + this.notificationService.error(error); + } + + public async save(): Promise { + try { + let saved = await this._model.saveModel(); + return saved; + } catch (err) { + this.notificationService.error(localize('saveFailed', 'Failed to save notebook: {0}', notebookUtils.getErrorMessage(err))); + return false; + } + } + + private setDirty(isDirty: boolean): void { + // TODO reenable handling of isDirty + // if (this.editor) { + // this.editor.isDirty = isDirty; + // } + } + + } diff --git a/src/sql/parts/notebook/notebook.contribution.ts b/src/sql/parts/notebook/notebook.contribution.ts index 86fa63417e..bcb6df8259 100644 --- a/src/sql/parts/notebook/notebook.contribution.ts +++ b/src/sql/parts/notebook/notebook.contribution.ts @@ -40,8 +40,8 @@ export class OpenNotebookAction extends Action { public run(): TPromise { return new TPromise((resolve, reject) => { let untitledUri = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter++}`}); - let model = new NotebookInputModel(untitledUri, undefined, undefined); - let input = new NotebookInput('modelViewId', model); + let model = new NotebookInputModel(untitledUri, undefined, false, undefined); + let input = new NotebookInput('modelViewId', model,); this._editorService.openEditor(input, { pinned: true }); }); } diff --git a/src/sql/parts/notebook/notebook.module.ts b/src/sql/parts/notebook/notebook.module.ts index 0bc516dcb5..2c5964b99d 100644 --- a/src/sql/parts/notebook/notebook.module.ts +++ b/src/sql/parts/notebook/notebook.module.ts @@ -26,6 +26,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { CodeComponent } from 'sql/parts/notebook/cellViews/code.component'; import { CodeCellComponent } from 'sql/parts/notebook/cellViews/codeCell.component'; import { TextCellComponent } from 'sql/parts/notebook/cellViews/textCell.component'; +import LoadingSpinner from 'sql/parts/modelComponents/loadingSpinner.component'; export const NotebookModule = (params, selector: string, instantiationService: IInstantiationService): any => { @NgModule({ @@ -34,6 +35,7 @@ export const NotebookModule = (params, selector: string, instantiationService: I SelectBox, EditableDropDown, InputBox, + LoadingSpinner, CodeComponent, CodeCellComponent, TextCellComponent, diff --git a/src/sql/parts/notebook/notebookEditor.ts b/src/sql/parts/notebook/notebookEditor.ts index 660362d310..3df4253354 100644 --- a/src/sql/parts/notebook/notebookEditor.ts +++ b/src/sql/parts/notebook/notebookEditor.ts @@ -86,7 +86,8 @@ export class NotebookEditor extends BaseEditor { input.hasBootstrapped = true; let params: INotebookParams = { notebookUri: input.notebookUri, - providerId: input.providerId ? input.providerId : DEFAULT_NOTEBOOK_PROVIDER + providerId: input.providerId ? input.providerId : DEFAULT_NOTEBOOK_PROVIDER, + isTrusted: input.isTrusted }; bootstrapAngular(this.instantiationService, NotebookModule, diff --git a/src/sql/parts/notebook/notebookInput.ts b/src/sql/parts/notebook/notebookInput.ts index 788bd73eff..117c54d244 100644 --- a/src/sql/parts/notebook/notebookInput.ts +++ b/src/sql/parts/notebook/notebookInput.ts @@ -17,7 +17,7 @@ export class NotebookInputModel extends EditorModel { private dirty: boolean; private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); private _providerId: string; - constructor(public readonly notebookUri: URI, private readonly handle: number, private saveHandler?: ModeViewSaveHandler) { + constructor(public readonly notebookUri: URI, private readonly handle: number, private _isTrusted: boolean = false, private saveHandler?: ModeViewSaveHandler) { super(); this.dirty = false; } @@ -30,6 +30,10 @@ export class NotebookInputModel extends EditorModel { this._providerId = value; } + get isTrusted(): boolean { + return this._isTrusted; + } + get onDidChangeDirty(): Event { return this._onDidChangeDirty.event; } @@ -93,6 +97,10 @@ export class NotebookInput extends EditorInput { return this._title; } + public get isTrusted(): boolean { + return this._model.isTrusted; + } + public dispose(): void { this._disposeContainer(); super.dispose(); diff --git a/src/sql/services/notebook/notebookService.ts b/src/sql/services/notebook/notebookService.ts index 02a26008db..2ab77d9dfa 100644 --- a/src/sql/services/notebook/notebookService.ts +++ b/src/sql/services/notebook/notebookService.ts @@ -9,6 +9,8 @@ import * as sqlops from 'sqlops'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import URI from 'vs/base/common/uri'; import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService'; +import { ModelFactory } from 'sql/parts/notebook/models/modelFactory'; +import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; export const SERVICE_ID = 'notebookService'; export const INotebookService = createDecorator(SERVICE_ID); @@ -56,4 +58,7 @@ export interface INotebookManager { export interface INotebookParams extends IBootstrapParams { notebookUri: URI; providerId: string; + isTrusted: boolean; + profile?: IConnectionProfile; + modelFactory?: ModelFactory; } \ No newline at end of file From 71c14a0837861a0b9a441bed954f2fb2d5eccc87 Mon Sep 17 00:00:00 2001 From: Raj <44002319+rajmusuku@users.noreply.github.com> Date: Wed, 7 Nov 2018 14:19:33 -0800 Subject: [PATCH 10/18] Output view changes (#3146) * 1133: Notebook file registration changes * File registration stuff * Yarn files * Outputview Changes * Misc changes * Changes to code component name space * Output view changes * notebook output view changes * Latest changes * Output view changes * Code review changes on output view * CSS file and misc changes --- ThirdPartyNotices.txt | 38 + build/yarn.lock | 1 + extensions/azurecore/yarn.lock | 53 + extensions/import/yarn.lock | 6 + package.json | 4 + .../notebook/cellViews/code.component.ts | 2 - .../cellViews/codeCell.component.html | 7 +- .../notebook/cellViews/codeCell.component.ts | 4 - .../parts/notebook/cellViews/interfaces.ts | 2 + .../notebook/cellViews/output.component.html | 11 + .../notebook/cellViews/output.component.ts | 77 ++ .../cellViews/outputArea.component.html | 12 + .../cellViews/outputArea.component.ts | 30 + .../parts/notebook/models/modelInterfaces.ts | 1 + src/sql/parts/notebook/notebook.module.ts | 6 +- .../parts/notebook/outputs/common/jsonext.ts | 54 + .../notebook/outputs/common/mimemodel.ts | 87 ++ .../parts/notebook/outputs/common/nbformat.ts | 494 ++++++++ .../outputs/common/outputProcessor.ts | 110 ++ .../outputs/common/renderMimeInterfaces.ts | 360 ++++++ src/sql/parts/notebook/outputs/common/url.ts | 184 +++ src/sql/parts/notebook/outputs/factories.ts | 94 ++ src/sql/parts/notebook/outputs/registry.ts | 352 ++++++ src/sql/parts/notebook/outputs/renderers.ts | 629 ++++++++++ src/sql/parts/notebook/outputs/sanitizer.ts | 1053 +++++++++++++++++ .../parts/notebook/outputs/style/index.css | 467 ++++++++ src/sql/parts/notebook/outputs/widgets.ts | 348 ++++++ src/sql/services/notebook/notebookService.ts | 3 + .../services/notebook/notebookServiceImpl.ts | 18 +- src/typings/index.d.ts | 3 + .../modules/@types/htmlparser2/index.d.ts | 106 ++ .../modules/@types/htmlparser2/typings.json | 19 + .../modules/@types/sanitize-html/index.d.ts | 70 ++ .../modules/@types/sanitize-html/typings.json | 28 + src/typings/modules/ansi_up/index.d.ts | 50 + src/typings/modules/ansi_up/typings.json | 8 + .../electron-browser/bootstrap/index.js | 5 +- yarn.lock | 138 ++- 38 files changed, 4913 insertions(+), 21 deletions(-) create mode 100644 src/sql/parts/notebook/cellViews/output.component.html create mode 100644 src/sql/parts/notebook/cellViews/output.component.ts create mode 100644 src/sql/parts/notebook/cellViews/outputArea.component.html create mode 100644 src/sql/parts/notebook/cellViews/outputArea.component.ts create mode 100644 src/sql/parts/notebook/outputs/common/jsonext.ts create mode 100644 src/sql/parts/notebook/outputs/common/mimemodel.ts create mode 100644 src/sql/parts/notebook/outputs/common/nbformat.ts create mode 100644 src/sql/parts/notebook/outputs/common/outputProcessor.ts create mode 100644 src/sql/parts/notebook/outputs/common/renderMimeInterfaces.ts create mode 100644 src/sql/parts/notebook/outputs/common/url.ts create mode 100644 src/sql/parts/notebook/outputs/factories.ts create mode 100644 src/sql/parts/notebook/outputs/registry.ts create mode 100644 src/sql/parts/notebook/outputs/renderers.ts create mode 100644 src/sql/parts/notebook/outputs/sanitizer.ts create mode 100644 src/sql/parts/notebook/outputs/style/index.css create mode 100644 src/sql/parts/notebook/outputs/widgets.ts create mode 100644 src/typings/modules/@types/htmlparser2/index.d.ts create mode 100644 src/typings/modules/@types/htmlparser2/typings.json create mode 100644 src/typings/modules/@types/sanitize-html/index.d.ts create mode 100644 src/typings/modules/@types/sanitize-html/typings.json create mode 100644 src/typings/modules/ansi_up/index.d.ts create mode 100644 src/typings/modules/ansi_up/typings.json diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 1ca7809193..c3b2fa83aa 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -34,6 +34,7 @@ expressly granted herein, whether by implication, estoppel or otherwise. jquery-ui: https://github.com/jquery/jquery-ui jquery.event.drag: https://github.com/devongovett/jquery.event.drag jschardet: https://github.com/aadsm/jschardet + JupyterLab: https://github.com/jupyterlab/jupyterlab make-error: https://github.com/JsCommunity/make-error minimist: https://github.com/substack/minimist moment: https://github.com/moment/moment @@ -1166,6 +1167,43 @@ That's all there is to it! ========================================= END OF jschardet NOTICES AND INFORMATION +%% JupyterLab NOTICES AND INFORMATION BEGIN HERE +Copyright (c) 2015 Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Semver File License +=================== + +The semver.py file is from https://github.com/podhmo/python-semver +which is licensed under the "MIT" license. See the semver.py file for details. + +END OF JupyterLab NOTICES AND INFORMATION + %% make-error NOTICES AND INFORMATION BEGIN HERE ========================================= ISC © Julien Fontanet diff --git a/build/yarn.lock b/build/yarn.lock index 7e5ce01e6b..c92047d496 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -46,6 +46,7 @@ "@types/minimatch@^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== "@types/node@*": version "8.0.51" diff --git a/extensions/azurecore/yarn.lock b/extensions/azurecore/yarn.lock index 2ebfde1b74..4483da3cf8 100644 --- a/extensions/azurecore/yarn.lock +++ b/extensions/azurecore/yarn.lock @@ -10,6 +10,7 @@ "@types/node@^8.0.24": version "8.10.36" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.36.tgz#eac05d576fbcd0b4ea3c912dc58c20475c08d9e4" + integrity sha512-SL6KhfM7PTqiFmbCW3eVNwVBZ+88Mrzbuvn9olPsfv43mbiWaFY+nRcz/TGGku0/lc2FepdMbImdMY1JrQ+zbw== "@types/node@^8.0.47": version "8.10.30" @@ -44,14 +45,17 @@ ajv@^5.3.0: ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= asn1@0.1.11: version "0.1.11" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.1.11.tgz#559be18376d08a4ec4dbe80877d27818639b2df7" + integrity sha1-VZvhg3bQik7E2+gId9J4GGObLfc= asn1@~0.2.3: version "0.2.4" @@ -68,6 +72,7 @@ assert-plus@1.0.0, assert-plus@^1.0.0: assert-plus@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.1.5.tgz#ee74009413002d84cec7219c6ac811812e723160" + integrity sha1-7nQAlBMALYTOxyGcasgRgS5yMWA= async@2.6.0: version "2.6.0" @@ -79,6 +84,7 @@ async@2.6.0: async@>=0.6.0, async@^2.0.1: version "2.6.1" resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" + integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== dependencies: lodash "^4.17.10" @@ -90,6 +96,7 @@ asynckit@^0.4.0: aws-sign2@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.5.0.tgz#c57103f7a17fc037f02d7c2e64b602ea223f7d63" + integrity sha1-xXED96F/wDfwLXwuZLYC6iI/fWM= aws-sign2@~0.7.0: version "0.7.0" @@ -132,16 +139,19 @@ bcrypt-pbkdf@^1.0.0: bl@~1.0.0: version "1.0.3" resolved "http://registry.npmjs.org/bl/-/bl-1.0.3.tgz#fc5421a28fd4226036c3b3891a66a25bc64d226e" + integrity sha1-/FQhoo/UImA2w7OJGmaiW8ZNIm4= dependencies: readable-stream "~2.0.5" bluebird@^2.9.30: version "2.11.0" resolved "http://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" + integrity sha1-U0uQM8AiyVecVro7Plpcqvu2UOE= boom@2.x.x: version "2.10.1" resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + integrity sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8= dependencies: hoek "2.x.x" @@ -166,6 +176,7 @@ buffer-equal-constant-time@1.0.1: caseless@~0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + integrity sha1-cVuW6phBWTzDMGeSP17GDr2k99c= caseless@~0.12.0: version "0.12.0" @@ -175,6 +186,7 @@ caseless@~0.12.0: chalk@^1.0.0: version "1.1.3" resolved "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= dependencies: ansi-styles "^2.2.1" escape-string-regexp "^1.0.2" @@ -202,6 +214,7 @@ combined-stream@1.0.6: combined-stream@^1.0.5, combined-stream@~1.0.1, combined-stream@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" + integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== dependencies: delayed-stream "~1.0.0" @@ -213,6 +226,7 @@ commander@2.15.1: commander@^2.8.1: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== concat-map@0.0.1: version "0.0.1" @@ -222,16 +236,19 @@ concat-map@0.0.1: core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= cryptiles@2.x.x: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + integrity sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g= dependencies: boom "2.x.x" ctype@0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/ctype/-/ctype-0.5.3.tgz#82c18c2461f74114ef16c135224ad0b9144ca12f" + integrity sha1-gsGMJGH3QRTvFsE1IkrQuRRMoS8= dashdash@^1.12.0: version "1.14.1" @@ -285,10 +302,12 @@ ecdsa-sig-formatter@1.0.10: escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= extend@~3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== extsprintf@1.3.0: version "1.3.0" @@ -313,10 +332,12 @@ fast-json-stable-stringify@^2.0.0: forever-agent@~0.6.0, forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= form-data@~1.0.0-rc1: version "1.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.1.tgz#ae315db9a4907fa065502304a66d7733475ee37c" + integrity sha1-rjFduaSQf6BlUCMEpm13M0de43w= dependencies: async "^2.0.1" combined-stream "^1.0.5" @@ -339,12 +360,14 @@ fs.realpath@^1.0.0: generate-function@^2.0.0: version "2.3.1" resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== dependencies: is-property "^1.0.2" generate-object-property@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + integrity sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA= dependencies: is-property "^1.0.0" @@ -380,6 +403,7 @@ har-schema@^2.0.0: har-validator@^1.6.1: version "1.8.0" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-1.8.0.tgz#d83842b0eb4c435960aeb108a067a3aa94c0eeb2" + integrity sha1-2DhCsOtMQ1lgrrEIoGejqpTA7rI= dependencies: bluebird "^2.9.30" chalk "^1.0.0" @@ -397,6 +421,7 @@ har-validator@~5.1.0: has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= dependencies: ansi-regex "^2.0.0" @@ -408,6 +433,7 @@ has-flag@^3.0.0: hawk@~3.1.0: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + integrity sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ= dependencies: boom "2.x.x" cryptiles "2.x.x" @@ -422,10 +448,12 @@ he@1.1.1: hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + integrity sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0= http-signature@~0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-0.11.0.tgz#1796cf67a001ad5cd6849dca0991485f09089fe6" + integrity sha1-F5bPZ6ABrVzWhJ3KCZFIXwkIn+Y= dependencies: asn1 "0.1.11" assert-plus "^0.1.5" @@ -451,6 +479,7 @@ inflight@^1.0.4: inherits@2, inherits@~2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= is-buffer@^1.1.6: version "1.1.6" @@ -460,10 +489,12 @@ is-buffer@^1.1.6: is-my-ip-valid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" + integrity sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ== is-my-json-valid@^2.12.0: version "2.19.0" resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz#8fd6e40363cd06b963fa877d444bfb5eddc62175" + integrity sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q== dependencies: generate-function "^2.0.0" generate-object-property "^1.1.0" @@ -474,6 +505,7 @@ is-my-json-valid@^2.12.0: is-property@^1.0.0, is-property@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= is-stream@^1.1.0: version "1.1.0" @@ -488,10 +520,12 @@ is-typedarray@~1.0.0: isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= isstream@~0.1.1, isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= jsbn@~0.1.0: version "0.1.1" @@ -511,10 +545,12 @@ json-schema@0.2.3: json-stringify-safe@~5.0.0, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" + integrity sha1-T9kss04OnbPInIYi7PUfm5eMbLk= jsprim@^1.2.2: version "1.4.1" @@ -556,6 +592,7 @@ mime-db@~1.36.0: mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.2: version "2.1.20" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" + integrity sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A== dependencies: mime-db "~1.36.0" @@ -634,10 +671,12 @@ ms@2.0.0: node-uuid@~1.4.0: version "1.4.8" resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" + integrity sha1-sEDrCSOWivq/jTL7HxfxFn/auQc= oauth-sign@~0.8.0: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM= oauth-sign@~0.9.0: version "0.9.0" @@ -669,6 +708,7 @@ postinstall-build@^5.0.1: process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + integrity sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M= psl@^1.1.24: version "1.1.29" @@ -683,6 +723,7 @@ punycode@^1.4.1: qs@~5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/qs/-/qs-5.1.0.tgz#4d932e5c7ea411cca76a312d39a606200fd50cd9" + integrity sha1-TZMuXH6kEcynajEtOaYGIA/VDNk= qs@~6.5.2: version "6.5.2" @@ -692,6 +733,7 @@ qs@~6.5.2: readable-stream@~2.0.5: version "2.0.6" resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + integrity sha1-j5A0HmilPMySh4jaz80Rs265t44= dependencies: core-util-is "~1.0.0" inherits "~2.0.1" @@ -703,6 +745,7 @@ readable-stream@~2.0.5: request@2.63.0: version "2.63.0" resolved "http://registry.npmjs.org/request/-/request-2.63.0.tgz#c83e7c3485e5d9bf9b146318429bc48f1253d8be" + integrity sha1-yD58NIXl2b+bFGMYQpvEjxJT2L4= dependencies: aws-sign2 "~0.5.0" bl "~1.0.0" @@ -807,6 +850,7 @@ should@^13.2.1: sntp@1.x.x: version "1.0.9" resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + integrity sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg= dependencies: hoek "2.x.x" @@ -829,14 +873,17 @@ sshpk@^1.7.0: string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= stringstream@~0.0.4: version "0.0.6" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72" + integrity sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA== strip-ansi@^3.0.0: version "3.0.1" resolved "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= dependencies: ansi-regex "^2.0.0" @@ -850,6 +897,7 @@ supports-color@5.4.0: supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= through@^2.3.8: version "2.3.8" @@ -859,6 +907,7 @@ through@^2.3.8: tough-cookie@>=0.12.0, tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== dependencies: psl "^1.1.24" punycode "^1.4.1" @@ -873,6 +922,7 @@ tunnel-agent@^0.6.0: tunnel-agent@~0.4.0: version "0.4.3" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + integrity sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us= tunnel@0.0.5: version "0.0.5" @@ -901,6 +951,7 @@ typemoq@^2.1.0: util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2: version "3.3.2" @@ -934,7 +985,9 @@ wrappy@1: xpath.js@~1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1" + integrity sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ== xtend@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= diff --git a/extensions/import/yarn.lock b/extensions/import/yarn.lock index 3dc0f8a91b..b1e352edce 100644 --- a/extensions/import/yarn.lock +++ b/extensions/import/yarn.lock @@ -12,6 +12,7 @@ agent-base@4, agent-base@^4.1.0: applicationinsights@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.0.1.tgz#53446b830fe8d5d619eee2a278b31d3d25030927" + integrity sha1-U0Rrgw/o1dYZ7uKieLMdPSUDCSc= dependencies: diagnostic-channel "0.2.0" diagnostic-channel-publishers "0.2.1" @@ -143,10 +144,12 @@ decompress@^4.2.0: diagnostic-channel-publishers@0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3" + integrity sha1-ji1geottef6IC1SLxYzGvrKIxPM= diagnostic-channel@0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17" + integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc= dependencies: semver "^5.3.0" @@ -366,6 +369,7 @@ seek-bzip@^1.0.5: semver@^5.3.0: version "5.6.0" resolved "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" + integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== "service-downloader@github:anthonydresser/service-downloader#0.1.5": version "0.1.5" @@ -438,6 +442,7 @@ util-deprecate@~1.0.1: vscode-extension-telemetry@0.0.18: version "0.0.18" resolved "https://registry.npmjs.org/vscode-extension-telemetry/-/vscode-extension-telemetry-0.0.18.tgz#602ba20d8c71453aa34533a291e7638f6e5c0327" + integrity sha512-Vw3Sr+dZwl+c6PlsUwrTtCOJkgrmvS3OUVDQGcmpXWAgq9xGq6as0K4pUx+aGqTjzLAESmWSrs6HlJm6J6Khcg== dependencies: applicationinsights "1.0.1" @@ -492,3 +497,4 @@ yauzl@^2.4.2: zone.js@0.7.6: version "0.7.6" resolved "https://registry.npmjs.org/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009" + integrity sha1-+7w50+AmHQmG8boGMG6zrrDSIAk= diff --git a/package.json b/package.json index 3abb6216c7..1a05cc19af 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,10 @@ "@angular/router": "~4.1.3", "@angular/upgrade": "~4.1.3", "@types/chart.js": "^2.7.31", + "@types/htmlparser2": "^3.7.31", "angular2-grid": "2.0.6", "angular2-slickgrid": "github:Microsoft/angular2-slickgrid#1.4.6", + "ansi_up": "^3.0.0", "applicationinsights": "0.18.0", "chart.js": "^2.6.0", "fast-plist": "0.1.2", @@ -64,6 +66,7 @@ "pretty-data": "^0.40.0", "reflect-metadata": "^0.1.8", "rxjs": "5.4.0", + "sanitize-html": "^1.19.1", "semver": "^5.5.0", "slickgrid": "github:anthonydresser/SlickGrid#2.3.28", "spdlog": "0.7.1", @@ -85,6 +88,7 @@ "@types/keytar": "4.0.1", "@types/minimist": "1.2.0", "@types/mocha": "2.2.39", + "@types/sanitize-html": "^1.18.2", "@types/semver": "5.3.30", "@types/sinon": "1.16.34", "@types/winreg": "^1.2.30", diff --git a/src/sql/parts/notebook/cellViews/code.component.ts b/src/sql/parts/notebook/cellViews/code.component.ts index 462798818c..9b3c270ae7 100644 --- a/src/sql/parts/notebook/cellViews/code.component.ts +++ b/src/sql/parts/notebook/cellViews/code.component.ts @@ -8,8 +8,6 @@ import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetecto import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; import { AngularDisposable } from 'sql/base/common/lifecycle'; -import { ComponentBase } from 'sql/parts/modelComponents/componentBase'; -import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces'; import { QueryTextEditor } from 'sql/parts/modelComponents/queryTextEditor'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; diff --git a/src/sql/parts/notebook/cellViews/codeCell.component.html b/src/sql/parts/notebook/cellViews/codeCell.component.html index b7f97fed40..1351e5ef2f 100644 --- a/src/sql/parts/notebook/cellViews/codeCell.component.html +++ b/src/sql/parts/notebook/cellViews/codeCell.component.html @@ -8,7 +8,8 @@
-
- Place Holder for output area +
+ +
-
+
\ No newline at end of file diff --git a/src/sql/parts/notebook/cellViews/codeCell.component.ts b/src/sql/parts/notebook/cellViews/codeCell.component.ts index 5e02d19dc7..c15f3cc42d 100644 --- a/src/sql/parts/notebook/cellViews/codeCell.component.ts +++ b/src/sql/parts/notebook/cellViews/codeCell.component.ts @@ -21,10 +21,8 @@ export const CODE_SELECTOR: string = 'code-cell-component'; templateUrl: decodeURI(require.toUrl('./codeCell.component.html')) }) export class CodeCellComponent extends CellView implements OnInit { - @ViewChild('output', { read: ElementRef }) private output: ElementRef; @Input() cellModel: ICellModel; constructor( - @Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface, @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService ) { @@ -42,7 +40,5 @@ export class CodeCellComponent extends CellView implements OnInit { } private updateTheme(theme: IColorTheme): void { - let outputElement = this.output.nativeElement; - outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString(); } } diff --git a/src/sql/parts/notebook/cellViews/interfaces.ts b/src/sql/parts/notebook/cellViews/interfaces.ts index dcf46d6887..1f188b6663 100644 --- a/src/sql/parts/notebook/cellViews/interfaces.ts +++ b/src/sql/parts/notebook/cellViews/interfaces.ts @@ -13,3 +13,5 @@ export abstract class CellView extends AngularDisposable implements OnDestroy { public abstract layout(): void; } + + diff --git a/src/sql/parts/notebook/cellViews/output.component.html b/src/sql/parts/notebook/cellViews/output.component.html new file mode 100644 index 0000000000..f05a51ae65 --- /dev/null +++ b/src/sql/parts/notebook/cellViews/output.component.html @@ -0,0 +1,11 @@ + +
+
+
+
+
diff --git a/src/sql/parts/notebook/cellViews/output.component.ts b/src/sql/parts/notebook/cellViews/output.component.ts new file mode 100644 index 0000000000..c3f8e9d4fe --- /dev/null +++ b/src/sql/parts/notebook/cellViews/output.component.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- +* 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!./code'; + +import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter } from '@angular/core'; +import { AngularDisposable } from 'sql/base/common/lifecycle'; +import { nb } from 'sqlops'; +import { INotebookService } from 'sql/services/notebook/notebookService'; +import { MimeModel } from 'sql/parts/notebook/outputs/common/mimemodel'; +import * as outputProcessor from '../outputs/common/outputProcessor'; +import { RenderMimeRegistry } from 'sql/parts/notebook/outputs/registry'; +import 'vs/css!sql/parts/notebook/outputs/style/index'; + +export const OUTPUT_SELECTOR: string = 'output-component'; + +@Component({ + selector: OUTPUT_SELECTOR, + templateUrl: decodeURI(require.toUrl('./output.component.html')) +}) +export class OutputComponent extends AngularDisposable implements OnInit { + @ViewChild('output', { read: ElementRef }) private outputElement: ElementRef; + @Input() cellOutput: nb.ICellOutput; + private readonly _minimumHeight = 30; + registry: RenderMimeRegistry; + trusted: boolean = false; + + + constructor( + @Inject(INotebookService) private _notebookService: INotebookService + ) { + super(); + this.registry = _notebookService.getMimeRegistry(); + } + + ngOnInit() { + let node = this.outputElement.nativeElement; + let output = this.cellOutput; + let options = outputProcessor.getBundleOptions({ value: output, trusted: this.trusted }); + // TODO handle safe/unsafe mapping + this.createRenderedMimetype(options, node); + } + + public layout(): void { + } + + protected createRenderedMimetype(options: MimeModel.IOptions, node: HTMLElement): void { + let mimeType = this.registry.preferredMimeType( + options.data, + options.trusted ? 'any' : 'ensure' + ); + if (mimeType) { + let output = this.registry.createRenderer(mimeType); + output.node = node; + let model = new MimeModel(options); + output.renderModel(model).catch(error => { + // Manually append error message to output + output.node.innerHTML = `
Javascript Error: ${error.message}
`; + // Remove mime-type-specific CSS classes + output.node.className = 'p-Widget jp-RenderedText'; + output.node.setAttribute( + 'data-mime-type', + 'application/vnd.jupyter.stderr' + ); + }); + //this.setState({ node: node }); + } else { + // TODO Localize + node.innerHTML = + `No ${options.trusted ? '' : '(safe) '}renderer could be ` + + 'found for output. It has the following MIME types: ' + + Object.keys(options.data).join(', '); + //this.setState({ node: node }); + } + } +} diff --git a/src/sql/parts/notebook/cellViews/outputArea.component.html b/src/sql/parts/notebook/cellViews/outputArea.component.html new file mode 100644 index 0000000000..15c0fe1f71 --- /dev/null +++ b/src/sql/parts/notebook/cellViews/outputArea.component.html @@ -0,0 +1,12 @@ + +
+
+ + +
+
\ No newline at end of file diff --git a/src/sql/parts/notebook/cellViews/outputArea.component.ts b/src/sql/parts/notebook/cellViews/outputArea.component.ts new file mode 100644 index 0000000000..461de0da21 --- /dev/null +++ b/src/sql/parts/notebook/cellViews/outputArea.component.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- +* 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!./code'; +import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, OnDestroy, ViewChild, Output, EventEmitter } from '@angular/core'; +import { AngularDisposable } from 'sql/base/common/lifecycle'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; + +export const OUTPUT_AREA_SELECTOR: string = 'output-area-component'; + +@Component({ + selector: OUTPUT_AREA_SELECTOR, + templateUrl: decodeURI(require.toUrl('./outputArea.component.html')) +}) +export class OutputAreaComponent extends AngularDisposable implements OnInit { + @Input() cellModel: ICellModel; + + private readonly _minimumHeight = 30; + + constructor( + @Inject(IModeService) private _modeService: IModeService + ) { + super(); + } + ngOnInit(): void { + + } +} diff --git a/src/sql/parts/notebook/models/modelInterfaces.ts b/src/sql/parts/notebook/models/modelInterfaces.ts index ddd75c4179..50caeb3e76 100644 --- a/src/sql/parts/notebook/models/modelInterfaces.ts +++ b/src/sql/parts/notebook/models/modelInterfaces.ts @@ -338,6 +338,7 @@ export interface ICellModel { cellType: CellType; trustedMode: boolean; active: boolean; + readonly outputs: ReadonlyArray; equals(cellModel: ICellModel): boolean; toJSON(): nb.ICell; } diff --git a/src/sql/parts/notebook/notebook.module.ts b/src/sql/parts/notebook/notebook.module.ts index 2c5964b99d..d60a84edbe 100644 --- a/src/sql/parts/notebook/notebook.module.ts +++ b/src/sql/parts/notebook/notebook.module.ts @@ -26,6 +26,8 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { CodeComponent } from 'sql/parts/notebook/cellViews/code.component'; import { CodeCellComponent } from 'sql/parts/notebook/cellViews/codeCell.component'; import { TextCellComponent } from 'sql/parts/notebook/cellViews/textCell.component'; +import { OutputAreaComponent } from 'sql/parts/notebook/cellViews/outputArea.component'; +import { OutputComponent } from 'sql/parts/notebook/cellViews/output.component'; import LoadingSpinner from 'sql/parts/modelComponents/loadingSpinner.component'; export const NotebookModule = (params, selector: string, instantiationService: IInstantiationService): any => { @@ -40,7 +42,9 @@ export const NotebookModule = (params, selector: string, instantiationService: I CodeCellComponent, TextCellComponent, NotebookComponent, - ComponentHostDirective + ComponentHostDirective, + OutputAreaComponent, + OutputComponent ], entryComponents: [NotebookComponent], imports: [ diff --git a/src/sql/parts/notebook/outputs/common/jsonext.ts b/src/sql/parts/notebook/outputs/common/jsonext.ts new file mode 100644 index 0000000000..95de8717f1 --- /dev/null +++ b/src/sql/parts/notebook/outputs/common/jsonext.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +/** + * A type alias for a JSON primitive. + */ +export declare type JSONPrimitive = boolean | number | string | null; +/** + * A type alias for a JSON value. + */ +export declare type JSONValue = JSONPrimitive | JSONObject | JSONArray; +/** + * A type definition for a JSON object. + */ +export interface JSONObject { + [key: string]: JSONValue; +} +/** + * A type definition for a JSON array. + */ +export interface JSONArray extends Array { +} +/** + * A type definition for a readonly JSON object. + */ +export interface ReadonlyJSONObject { + readonly [key: string]: ReadonlyJSONValue; +} +/** + * A type definition for a readonly JSON array. + */ +export interface ReadonlyJSONArray extends ReadonlyArray { +} +/** + * A type alias for a readonly JSON value. + */ +export declare type ReadonlyJSONValue = JSONPrimitive | ReadonlyJSONObject | ReadonlyJSONArray; +/** + * Test whether a JSON value is a primitive. + * + * @param value - The JSON value of interest. + * + * @returns `true` if the value is a primitive,`false` otherwise. + */ +export function isPrimitive(value: any): boolean { + return ( + value === null || + typeof value === 'boolean' || + typeof value === 'number' || + typeof value === 'string' + ); +} diff --git a/src/sql/parts/notebook/outputs/common/mimemodel.ts b/src/sql/parts/notebook/outputs/common/mimemodel.ts new file mode 100644 index 0000000000..3b88c489d7 --- /dev/null +++ b/src/sql/parts/notebook/outputs/common/mimemodel.ts @@ -0,0 +1,87 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ +import { IRenderMime } from './renderMimeInterfaces'; +import { ReadonlyJSONObject } from './jsonext'; + +/** + * The default mime model implementation. + */ +export class MimeModel implements IRenderMime.IMimeModel { + /** + * Construct a new mime model. + */ + constructor(options: MimeModel.IOptions = {}) { + this.trusted = !!options.trusted; + this._data = options.data || {}; + this._metadata = options.metadata || {}; + this._callback = options.callback; + } + + /** + * Whether the model is trusted. + */ + readonly trusted: boolean; + + /** + * The data associated with the model. + */ + get data(): ReadonlyJSONObject { + return this._data; + } + + /** + * The metadata associated with the model. + */ + get metadata(): ReadonlyJSONObject { + return this._metadata; + } + + /** + * Set the data associated with the model. + * + * #### Notes + * Depending on the implementation of the mime model, + * this call may or may not have deferred effects, + */ + setData(options: IRenderMime.ISetDataOptions): void { + this._data = options.data || this._data; + this._metadata = options.metadata || this._metadata; + this._callback(options); + } + + private _callback: (options: IRenderMime.ISetDataOptions) => void; + private _data: ReadonlyJSONObject; + private _metadata: ReadonlyJSONObject; +} + +/** + * The namespace for MimeModel class statics. + */ +export namespace MimeModel { + /** + * The options used to create a mime model. + */ + export interface IOptions { + /** + * Whether the model is trusted. Defaults to `false`. + */ + trusted?: boolean; + + /** + * A callback function for when the data changes. + */ + callback?: (options: IRenderMime.ISetDataOptions) => void; + + /** + * The initial mime data. + */ + data?: ReadonlyJSONObject; + + /** + * The initial mime metadata. + */ + metadata?: ReadonlyJSONObject; + } +} \ No newline at end of file diff --git a/src/sql/parts/notebook/outputs/common/nbformat.ts b/src/sql/parts/notebook/outputs/common/nbformat.ts new file mode 100644 index 0000000000..12ebc816cb --- /dev/null +++ b/src/sql/parts/notebook/outputs/common/nbformat.ts @@ -0,0 +1,494 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +// Notebook format interfaces +// https://nbformat.readthedocs.io/en/latest/format_description.html +// https://github.com/jupyter/nbformat/blob/master/nbformat/v4/nbformat.v4.schema.json + + +import { JSONObject } from './jsonext'; +import { nb } from 'sqlops'; + +/** + * A namespace for nbformat interfaces. + */ +export namespace nbformat { + /** + * The major version of the notebook format. + */ + export const MAJOR_VERSION: number = 4; + + /** + * The minor version of the notebook format. + */ + export const MINOR_VERSION: number = 2; + + /** + * The kernelspec metadata. + */ + export interface IKernelspecMetadata extends JSONObject { + name: string; + display_name: string; + } + + /** + * The language info metatda + */ + export interface ILanguageInfoMetadata extends JSONObject { + name: string; + codemirror_mode?: string | JSONObject; + file_extension?: string; + mimetype?: string; + pygments_lexer?: string; + } + + /** + * The default metadata for the notebook. + */ + export interface INotebookMetadata extends JSONObject { + kernelspec?: IKernelspecMetadata; + language_info?: ILanguageInfoMetadata; + orig_nbformat: number; + } + + /** + * The notebook content. + */ + export interface INotebookContent { + metadata: INotebookMetadata; + nbformat_minor: number; + nbformat: number; + cells: ICell[]; + } + + /** + * A multiline string. + */ + export type MultilineString = string | string[]; + + /** + * A mime-type keyed dictionary of data. + */ + export interface IMimeBundle extends JSONObject { + [key: string]: MultilineString | JSONObject; + } + + /** + * Media attachments (e.g. inline images). + */ + export interface IAttachments { + [key: string]: IMimeBundle; + } + + /** + * The code cell's prompt number. Will be null if the cell has not been run. + */ + export type ExecutionCount = number | null; + + /** + * Cell output metadata. + */ + export type OutputMetadata = JSONObject; + + /** + * Validate a mime type/value pair. + * + * @param type - The mimetype name. + * + * @param value - The value associated with the type. + * + * @returns Whether the type/value pair are valid. + */ + export function validateMimeValue( + type: string, + value: MultilineString | JSONObject + ): boolean { + // Check if "application/json" or "application/foo+json" + const jsonTest = /^application\/(.*?)+\+json$/; + const isJSONType = type === 'application/json' || jsonTest.test(type); + + let isString = (x: any) => { + return Object.prototype.toString.call(x) === '[object String]'; + }; + + // If it is an array, make sure if is not a JSON type and it is an + // array of strings. + if (Array.isArray(value)) { + if (isJSONType) { + return false; + } + let valid = true; + (value as string[]).forEach(v => { + if (!isString(v)) { + valid = false; + } + }); + return valid; + } + + // If it is a string, make sure we are not a JSON type. + if (isString(value)) { + return !isJSONType; + } + + // It is not a string, make sure it is a JSON type. + if (!isJSONType) { + return false; + } + + // It is a JSON type, make sure it is a valid JSON object. + // return JSONExt.isObject(value); + return true; + } + + /** + * Cell-level metadata. + */ + export interface IBaseCellMetadata extends JSONObject { + /** + * Whether the cell is trusted. + * + * #### Notes + * This is not strictly part of the nbformat spec, but it is added by + * the contents manager. + * + * See https://jupyter-notebook.readthedocs.io/en/latest/security.html. + */ + trusted: boolean; + + /** + * The cell's name. If present, must be a non-empty string. + */ + name: string; + + /** + * The cell's tags. Tags must be unique, and must not contain commas. + */ + tags: string[]; + } + + /** + * The base cell interface. + */ + export interface IBaseCell { + /** + * String identifying the type of cell. + */ + cell_type: string; + + /** + * Contents of the cell, represented as an array of lines. + */ + source: MultilineString; + + /** + * Cell-level metadata. + */ + metadata: Partial; + } + + /** + * Metadata for the raw cell. + */ + export interface IRawCellMetadata extends IBaseCellMetadata { + /** + * Raw cell metadata format for nbconvert. + */ + format: string; + } + + /** + * A raw cell. + */ + export interface IRawCell extends IBaseCell { + /** + * String identifying the type of cell. + */ + cell_type: 'raw'; + + /** + * Cell-level metadata. + */ + metadata: Partial; + + /** + * Cell attachments. + */ + attachments?: IAttachments; + } + + /** + * A markdown cell. + */ + export interface IMarkdownCell extends IBaseCell { + /** + * String identifying the type of cell. + */ + cell_type: 'markdown'; + + /** + * Cell attachments. + */ + attachments?: IAttachments; + } + + /** + * Metadata for a code cell. + */ + export interface ICodeCellMetadata extends IBaseCellMetadata { + /** + * Whether the cell is collapsed/expanded. + */ + collapsed: boolean; + + /** + * Whether the cell's output is scrolled, unscrolled, or autoscrolled. + */ + scrolled: boolean | 'auto'; + } + + /** + * A code cell. + */ + export interface ICodeCell extends IBaseCell { + /** + * String identifying the type of cell. + */ + cell_type: 'code'; + + /** + * Cell-level metadata. + */ + metadata: Partial; + + /** + * Execution, display, or stream outputs. + */ + outputs: IOutput[]; + + /** + * The code cell's prompt number. Will be null if the cell has not been run. + */ + execution_count: ExecutionCount; + } + + /** + * An unrecognized cell. + */ + export interface IUnrecognizedCell extends IBaseCell { } + + /** + * A cell union type. + */ + export type ICell = IRawCell | IMarkdownCell | ICodeCell | IUnrecognizedCell; + + /** + * Test whether a cell is a raw cell. + */ + export function isRaw(cell: ICell): cell is IRawCell { + return cell.cell_type === 'raw'; + } + + /** + * Test whether a cell is a markdown cell. + */ + export function isMarkdown(cell: ICell): cell is IMarkdownCell { + return cell.cell_type === 'markdown'; + } + + /** + * Test whether a cell is a code cell. + */ + export function isCode(cell: ICell): cell is ICodeCell { + return cell.cell_type === 'code'; + } + + /** + * A union metadata type. + */ + export type ICellMetadata = + | IBaseCellMetadata + | IRawCellMetadata + | ICodeCellMetadata; + + /** + * The valid output types. + */ + export type OutputType = + | 'execute_result' + | 'display_data' + | 'stream' + | 'error' + | 'update_display_data'; + + + /** + * Result of executing a code cell. + */ + export interface IExecuteResult extends nb.ICellOutput { + /** + * Type of cell output. + */ + output_type: 'execute_result'; + + /** + * A result's prompt number. + */ + execution_count: ExecutionCount; + + /** + * A mime-type keyed dictionary of data. + */ + data: IMimeBundle; + + /** + * Cell output metadata. + */ + metadata: OutputMetadata; + } + + /** + * Data displayed as a result of code cell execution. + */ + export interface IDisplayData extends nb.ICellOutput { + /** + * Type of cell output. + */ + output_type: 'display_data'; + + /** + * A mime-type keyed dictionary of data. + */ + data: IMimeBundle; + + /** + * Cell output metadata. + */ + metadata: OutputMetadata; + } + + /** + * Data displayed as an update to existing display data. + */ + export interface IDisplayUpdate extends nb.ICellOutput { + /** + * Type of cell output. + */ + output_type: 'update_display_data'; + + /** + * A mime-type keyed dictionary of data. + */ + data: IMimeBundle; + + /** + * Cell output metadata. + */ + metadata: OutputMetadata; + } + + /** + * Stream output from a code cell. + */ + export interface IStream extends nb.ICellOutput { + /** + * Type of cell output. + */ + output_type: 'stream'; + + /** + * The name of the stream. + */ + name: StreamType; + + /** + * The stream's text output. + */ + text: MultilineString; + } + + /** + * An alias for a stream type. + */ + export type StreamType = 'stdout' | 'stderr'; + + /** + * Output of an error that occurred during code cell execution. + */ + export interface IError extends nb.ICellOutput { + /** + * Type of cell output. + */ + output_type: 'error'; + + /** + * The name of the error. + */ + ename: string; + + /** + * The value, or message, of the error. + */ + evalue: string; + + /** + * The error's traceback. + */ + traceback: string[]; + } + + /** + * Unrecognized output. + */ + export interface IUnrecognizedOutput extends nb.ICellOutput { } + + /** + * Test whether an output is an execute result. + */ + export function isExecuteResult(output: IOutput): output is IExecuteResult { + return output.output_type === 'execute_result'; + } + + /** + * Test whether an output is from display data. + */ + export function isDisplayData(output: IOutput): output is IDisplayData { + return output.output_type === 'display_data'; + } + + /** + * Test whether an output is from updated display data. + */ + export function isDisplayUpdate(output: IOutput): output is IDisplayUpdate { + return output.output_type === 'update_display_data'; + } + + /** + * Test whether an output is from a stream. + */ + export function isStream(output: IOutput): output is IStream { + return output.output_type === 'stream'; + } + + /** + * Test whether an output is from a stream. + */ + export function isError(output: IOutput): output is IError { + return output.output_type === 'error'; + } + + /** + * An output union type. + */ + export type IOutput = + | IUnrecognizedOutput + | IExecuteResult + | IDisplayData + | IStream + | IError; +} + +export interface ICellOutputWithIdAndTrust extends nb.ICellOutput { + id: number; + trusted: boolean; +} diff --git a/src/sql/parts/notebook/outputs/common/outputProcessor.ts b/src/sql/parts/notebook/outputs/common/outputProcessor.ts new file mode 100644 index 0000000000..11ad262d25 --- /dev/null +++ b/src/sql/parts/notebook/outputs/common/outputProcessor.ts @@ -0,0 +1,110 @@ + +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + +import { JSONObject } from './jsonext'; +import { MimeModel } from './mimemodel'; +import * as JSONExt from './jsonext'; +import { nbformat } from './nbformat'; +import { nb } from 'sqlops'; + +/** + * A multiline string. + */ +export type MultilineString = string | string[]; + +/** + * A mime-type keyed dictionary of data. + */ +export interface IMimeBundle extends JSONObject { + [key: string]: MultilineString | JSONObject; +} + +/** + * Get the data from a notebook output. + */ +export function getData(output: nb.ICellOutput): JSONObject { + let bundle: IMimeBundle = {}; + if ( + nbformat.isExecuteResult(output) || + nbformat.isDisplayData(output) || + nbformat.isDisplayUpdate(output) + ) { + bundle = (output as nbformat.IExecuteResult).data; + } else if (nbformat.isStream(output)) { + if (output.name === 'stderr') { + bundle['application/vnd.jupyter.stderr'] = output.text; + } else { + bundle['application/vnd.jupyter.stdout'] = output.text; + } + } else if (nbformat.isError(output)) { + let traceback = output.traceback.join('\n'); + bundle['application/vnd.jupyter.stderr'] = + traceback || `${output.ename}: ${output.evalue}`; + } + return convertBundle(bundle); +} + +/** + * Get the metadata from an output message. + */ +export function getMetadata(output: nbformat.IOutput): JSONObject { + let value: JSONObject = Object.create(null); + if (nbformat.isExecuteResult(output) || nbformat.isDisplayData(output)) { + for (let key in output.metadata) { + value[key] = extract(output.metadata, key); + } + } + return value; +} + +/** + * Get the bundle options given output model options. + */ +export function getBundleOptions( + options: IOutputModelOptions +): MimeModel.IOptions { + let data = getData(options.value); + let metadata = getMetadata(options.value); + let trusted = !!options.trusted; + return { data, metadata, trusted }; +} + +/** + * Extract a value from a JSONObject. + */ +export function extract(value: JSONObject, key: string): {} { + let item = value[key]; + if (JSONExt.isPrimitive(item)) { + return item; + } + return JSON.parse(JSON.stringify(item)); +} + +/** + * Convert a mime bundle to mime data. + */ +function convertBundle(bundle: nbformat.IMimeBundle): JSONObject { + let map: JSONObject = Object.create(null); + for (let mimeType in bundle) { + map[mimeType] = extract(bundle, mimeType); + } + return map; +} + +/** + * The options used to create a notebook output model. + */ +export interface IOutputModelOptions { + /** + * The raw output value. + */ + value: nbformat.IOutput; + + /** + * Whether the output is trusted. The default is false. + */ + trusted?: boolean; +} \ No newline at end of file diff --git a/src/sql/parts/notebook/outputs/common/renderMimeInterfaces.ts b/src/sql/parts/notebook/outputs/common/renderMimeInterfaces.ts new file mode 100644 index 0000000000..3585a0d112 --- /dev/null +++ b/src/sql/parts/notebook/outputs/common/renderMimeInterfaces.ts @@ -0,0 +1,360 @@ + +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ +import { ReadonlyJSONObject } from './jsonext'; + +/** + * A namespace for rendermime associated interfaces. + */ +export namespace IRenderMime { + /** + * A model for mime data. + */ + export interface IMimeModel { + /** + * Whether the data in the model is trusted. + */ + readonly trusted: boolean; + + /** + * The data associated with the model. + */ + readonly data: ReadonlyJSONObject; + + /** + * The metadata associated with the model. + */ + readonly metadata: ReadonlyJSONObject; + + /** + * Set the data associated with the model. + * + * #### Notes + * Calling this function may trigger an asynchronous operation + * that could cause the renderer to be rendered with a new model + * containing the new data. + */ + setData(options: ISetDataOptions): void; + } + + /** + * The options used to update a mime model. + */ + export interface ISetDataOptions { + /** + * The new data object. + */ + data?: ReadonlyJSONObject; + + /** + * The new metadata object. + */ + metadata?: ReadonlyJSONObject; + } + + + /** + * The options used to initialize a document widget factory. + * + * This interface is intended to be used by mime renderer extensions + * to define a document opener that uses its renderer factory. + */ + export interface IDocumentWidgetFactoryOptions { + /** + * The name of the widget to display in dialogs. + */ + readonly name: string; + + /** + * The name of the document model type. + */ + readonly modelName?: string; + + /** + * The primary file type of the widget. + */ + readonly primaryFileType: string; + + /** + * The file types the widget can view. + */ + readonly fileTypes: ReadonlyArray; + + /** + * The file types for which the factory should be the default. + */ + readonly defaultFor?: ReadonlyArray; + + /** + * The file types for which the factory should be the default for rendering, + * if that is different than the default factory (which may be for editing) + * If undefined, then it will fall back on the default file type. + */ + readonly defaultRendered?: ReadonlyArray; + } + + /** + * A file type to associate with the renderer. + */ + export interface IFileType { + /** + * The name of the file type. + */ + readonly name: string; + + /** + * The mime types associated the file type. + */ + readonly mimeTypes: ReadonlyArray; + + /** + * The extensions of the file type (e.g. `".txt"`). Can be a compound + * extension (e.g. `".table.json`). + */ + readonly extensions: ReadonlyArray; + + /** + * An optional display name for the file type. + */ + readonly displayName?: string; + + /** + * An optional pattern for a file name (e.g. `^Dockerfile$`). + */ + readonly pattern?: string; + + /** + * The icon class name for the file type. + */ + readonly iconClass?: string; + + /** + * The icon label for the file type. + */ + readonly iconLabel?: string; + + /** + * The file format for the file type ('text', 'base64', or 'json'). + */ + readonly fileFormat?: string; + } + + /** + * An interface for using a RenderMime.IRenderer for output and read-only documents. + */ + export interface IExtension { + /** + * The ID of the extension. + * + * #### Notes + * The convention for extension IDs in JupyterLab is the full NPM package + * name followed by a colon and a unique string token, e.g. + * `'@jupyterlab/apputils-extension:settings'` or `'foo-extension:bar'`. + */ + readonly id: string; + + /** + * A renderer factory to be registered to render the MIME type. + */ + readonly rendererFactory: IRendererFactory; + + /** + * The rank passed to `RenderMime.addFactory`. If not given, + * defaults to the `defaultRank` of the factory. + */ + readonly rank?: number; + + /** + * The timeout after user activity to re-render the data. + */ + readonly renderTimeout?: number; + + /** + * Preferred data type from the model. Defaults to `string`. + */ + readonly dataType?: 'string' | 'json'; + + /** + * The options used to open a document with the renderer factory. + */ + readonly documentWidgetFactoryOptions?: + | IDocumentWidgetFactoryOptions + | ReadonlyArray; + + /** + * The optional file type associated with the extension. + */ + readonly fileTypes?: ReadonlyArray; + } + + /** + * The interface for a module that exports an extension or extensions as + * the default value. + */ + export interface IExtensionModule { + /** + * The default export. + */ + readonly default: IExtension | ReadonlyArray; + } + + /** + * A widget which displays the contents of a mime model. + */ + export interface IRenderer { + /** + * Render a mime model. + * + * @param model - The mime model to render. + * + * @returns A promise which resolves when rendering is complete. + * + * #### Notes + * This method may be called multiple times during the lifetime + * of the widget to update it if and when new data is available. + */ + renderModel(model: IRenderMime.IMimeModel): Promise; + + /** + * Node to be updated by the renderer + */ + node: HTMLElement; + } + + /** + * The interface for a renderer factory. + */ + export interface IRendererFactory { + /** + * Whether the factory is a "safe" factory. + * + * #### Notes + * A "safe" factory produces renderer widgets which can render + * untrusted model data in a usable way. *All* renderers must + * handle untrusted data safely, but some may simply failover + * with a "Run cell to view output" message. A "safe" renderer + * is an indication that its sanitized output will be useful. + */ + readonly safe: boolean; + + /** + * The mime types handled by this factory. + */ + readonly mimeTypes: ReadonlyArray; + + /** + * The default rank of the factory. If not given, defaults to 100. + */ + readonly defaultRank?: number; + + /** + * Create a renderer which displays the mime data. + * + * @param options - The options used to render the data. + */ + createRenderer(options: IRendererOptions): IRenderer; + } + + /** + * The options used to create a renderer. + */ + export interface IRendererOptions { + /** + * The preferred mimeType to render. + */ + mimeType: string; + + /** + * The html sanitizer. + */ + sanitizer: ISanitizer; + + /** + * An optional url resolver. + */ + resolver: IResolver | null; + + /** + * An optional link handler. + */ + linkHandler: ILinkHandler | null; + + /** + * The LaTeX typesetter. + */ + latexTypesetter: ILatexTypesetter | null; + } + + /** + * An object that handles html sanitization. + */ + export interface ISanitizer { + /** + * Sanitize an HTML string. + */ + sanitize(dirty: string): string; + } + + /** + * An object that handles links on a node. + */ + export interface ILinkHandler { + /** + * Add the link handler to the node. + * + * @param node: the node for which to handle the link. + * + * @param path: the path to open when the link is clicked. + * + * @param id: an optional element id to scroll to when the path is opened. + */ + handleLink(node: HTMLElement, path: string, id?: string): void; + } + + /** + * An object that resolves relative URLs. + */ + export interface IResolver { + /** + * Resolve a relative url to a correct server path. + */ + resolveUrl(url: string): Promise; + + /** + * Get the download url of a given absolute server path. + */ + getDownloadUrl(path: string): Promise; + + /** + * Whether the URL should be handled by the resolver + * or not. + * + * #### Notes + * This is similar to the `isLocal` check in `URLExt`, + * but can also perform additional checks on whether the + * resolver should handle a given URL. + */ + isLocal?: (url: string) => boolean; + } + + /** + * The interface for a LaTeX typesetter. + */ + export interface ILatexTypesetter { + /** + * Typeset a DOM element. + * + * @param element - the DOM element to typeset. The typesetting may + * happen synchronously or asynchronously. + * + * #### Notes + * The application-wide rendermime object has a settable + * `latexTypesetter` property which is used wherever LaTeX + * typesetting is required. Extensions wishing to provide their + * own typesetter may replace that on the global `lab.rendermime`. + */ + typeset(element: HTMLElement): void; + } +} diff --git a/src/sql/parts/notebook/outputs/common/url.ts b/src/sql/parts/notebook/outputs/common/url.ts new file mode 100644 index 0000000000..5386ee1daa --- /dev/null +++ b/src/sql/parts/notebook/outputs/common/url.ts @@ -0,0 +1,184 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { JSONObject } from './jsonext'; +import URI from 'vs/base/common/uri'; + +/** + * The namespace for URL-related functions. + */ +export namespace URLExt { + /** + * Normalize a url. + */ + export function normalize(url: string): string { + return URI.parse(url).toString(); + } + + /** + * Join a sequence of url components and normalizes as in node `path.join`. + * + * @param parts - The url components. + * + * @returns the joined url. + */ + export function join(...parts: string[]): string { + parts = parts || []; + + // Isolate the top element. + const top = parts[0] || ''; + + // Check whether protocol shorthand is being used. + const shorthand = top.indexOf('//') === 0; + + // Parse the top element into a header collection. + const header = top.match(/(\w+)(:)(\/\/)?/); + const protocol = header && header[1]; + const colon = protocol && header[2]; + const slashes = colon && header[3]; + + // Construct the URL prefix. + const prefix = shorthand + ? '//' + : [protocol, colon, slashes].filter(str => str).join(''); + + // Construct the URL body omitting the prefix of the top value. + const body = [top.indexOf(prefix) === 0 ? top.replace(prefix, '') : top] + // Filter out top value if empty. + .filter(str => str) + // Remove leading slashes in all subsequent URL body elements. + .concat(parts.slice(1).map(str => str.replace(/^\//, ''))) + .join('/') + // Replace multiple slashes with one. + .replace(/\/+/g, '/'); + + return prefix + body; + } + + /** + * Encode the components of a multi-segment url. + * + * @param url - The url to encode. + * + * @returns the encoded url. + * + * #### Notes + * Preserves the `'/'` separators. + * Should not include the base url, since all parts are escaped. + */ + export function encodeParts(url: string): string { + return join(...url.split('/').map(encodeURIComponent)); + } + + /** + * Return a serialized object string suitable for a query. + * + * @param object - The source object. + * + * @returns an encoded url query. + * + * #### Notes + * Modified version of [stackoverflow](http://stackoverflow.com/a/30707423). + */ + export function objectToQueryString(value: JSONObject): string { + const keys = Object.keys(value); + + if (!keys.length) { + return ''; + } + + return ( + '?' + + keys + .map(key => { + const content = encodeURIComponent(String(value[key])); + + return key + (content ? '=' + content : ''); + }) + .join('&') + ); + } + + /** + * Return a parsed object that represents the values in a query string. + */ + export function queryStringToObject( + value: string + ): { [key: string]: string } { + return value + .replace(/^\?/, '') + .split('&') + .reduce( + (acc, val) => { + const [key, value] = val.split('='); + + acc[key] = decodeURIComponent(value || ''); + + return acc; + }, + {} as { [key: string]: string } + ); + } + + /** + * Test whether the url is a local url. + * + * #### Notes + * This function returns `false` for any fully qualified url, including + * `data:`, `file:`, and `//` protocol URLs. + */ + export function isLocal(url: string): boolean { + // If if doesn't have a scheme such as file: or http:// it's local + return !!URI.parse(url).scheme; + } + + /** + * The interface for a URL object + */ + export interface IUrl { + /** + * The full URL string that was parsed with both the protocol and host + * components converted to lower-case. + */ + href?: string; + + /** + * Identifies the URL's lower-cased protocol scheme. + */ + protocol?: string; + + /** + * The full lower-cased host portion of the URL, including the port if + * specified. + */ + host?: string; + + /** + * The lower-cased host name portion of the host component without the + * port included. + */ + hostname?: string; + + /** + * The numeric port portion of the host component. + */ + port?: string; + + /** + * The entire path section of the URL. + */ + pathname?: string; + + /** + * The "fragment" portion of the URL including the leading ASCII hash + * `(#)` character + */ + hash?: string; + + /** + * The search element, including leading question mark (`'?'`), if any, + * of the URL. + */ + search?: string; + } +} diff --git a/src/sql/parts/notebook/outputs/factories.ts b/src/sql/parts/notebook/outputs/factories.ts new file mode 100644 index 0000000000..a34d1cffa1 --- /dev/null +++ b/src/sql/parts/notebook/outputs/factories.ts @@ -0,0 +1,94 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + +import * as widgets from './widgets'; +import { IRenderMime } from './common/renderMimeInterfaces'; + +/** + * A mime renderer factory for raw html. + */ +export const htmlRendererFactory: IRenderMime.IRendererFactory = { + safe: true, + mimeTypes: ['text/html'], + defaultRank: 50, + createRenderer: options => new widgets.RenderedHTML(options) +}; + +/** + * A mime renderer factory for images. + */ +export const imageRendererFactory: IRenderMime.IRendererFactory = { + safe: true, + mimeTypes: ['image/bmp', 'image/png', 'image/jpeg', 'image/gif'], + defaultRank: 90, + createRenderer: options => new widgets.RenderedImage(options) +}; + +// /** +// * A mime renderer factory for LaTeX. +// */ +// export const latexRendererFactory: IRenderMime.IRendererFactory = { +// safe: true, +// mimeTypes: ['text/latex'], +// defaultRank: 70, +// createRenderer: options => new widgets.RenderedLatex(options) +// }; + +// /** +// * A mime renderer factory for Markdown. +// */ +// export const markdownRendererFactory: IRenderMime.IRendererFactory = { +// safe: true, +// mimeTypes: ['text/markdown'], +// defaultRank: 60, +// createRenderer: options => new widgets.RenderedMarkdown(options) +// }; + +/** + * A mime renderer factory for svg. + */ +export const svgRendererFactory: IRenderMime.IRendererFactory = { + safe: false, + mimeTypes: ['image/svg+xml'], + defaultRank: 80, + createRenderer: options => new widgets.RenderedSVG(options) +}; + +/** + * A mime renderer factory for plain and jupyter console text data. + */ +export const textRendererFactory: IRenderMime.IRendererFactory = { + safe: true, + mimeTypes: [ + 'text/plain', + 'application/vnd.jupyter.stdout', + 'application/vnd.jupyter.stderr' + ], + defaultRank: 120, + createRenderer: options => new widgets.RenderedText(options) +}; + +/** + * A placeholder factory for deprecated rendered JavaScript. + */ +export const javaScriptRendererFactory: IRenderMime.IRendererFactory = { + safe: false, + mimeTypes: ['text/javascript', 'application/javascript'], + defaultRank: 110, + createRenderer: options => new widgets.RenderedJavaScript(options) +}; + +/** + * The standard factories provided by the rendermime package. + */ +export const standardRendererFactories: ReadonlyArray = [ + htmlRendererFactory, + // markdownRendererFactory, + // latexRendererFactory, + svgRendererFactory, + imageRendererFactory, + javaScriptRendererFactory, + textRendererFactory +]; diff --git a/src/sql/parts/notebook/outputs/registry.ts b/src/sql/parts/notebook/outputs/registry.ts new file mode 100644 index 0000000000..b35004e696 --- /dev/null +++ b/src/sql/parts/notebook/outputs/registry.ts @@ -0,0 +1,352 @@ + +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ +import { IRenderMime } from './common/renderMimeInterfaces'; +import { MimeModel } from './common/mimemodel'; +import { ReadonlyJSONObject } from './common/jsonext'; +import { defaultSanitizer } from './sanitizer'; + +/** + * An object which manages mime renderer factories. + * + * This object is used to render mime models using registered mime + * renderers, selecting the preferred mime renderer to render the + * model into a widget. + * + * #### Notes + * This class is not intended to be subclassed. + */ +export class RenderMimeRegistry { + /** + * Construct a new rendermime. + * + * @param options - The options for initializing the instance. + */ + constructor(options: RenderMimeRegistry.IOptions = {}) { + // Parse the options. + this.resolver = options.resolver || null; + this.linkHandler = options.linkHandler || null; + this.latexTypesetter = options.latexTypesetter || null; + this.sanitizer = options.sanitizer || defaultSanitizer; + + // Add the initial factories. + if (options.initialFactories) { + for (let factory of options.initialFactories) { + this.addFactory(factory); + } + } + } + + /** + * The sanitizer used by the rendermime instance. + */ + readonly sanitizer: IRenderMime.ISanitizer; + + /** + * The object used to resolve relative urls for the rendermime instance. + */ + readonly resolver: IRenderMime.IResolver | null; + + /** + * The object used to handle path opening links. + */ + readonly linkHandler: IRenderMime.ILinkHandler | null; + + /** + * The LaTeX typesetter for the rendermime. + */ + readonly latexTypesetter: IRenderMime.ILatexTypesetter | null; + + /** + * The ordered list of mimeTypes. + */ + get mimeTypes(): ReadonlyArray { + return this._types || (this._types = Private.sortedTypes(this._ranks)); + } + + /** + * Find the preferred mime type for a mime bundle. + * + * @param bundle - The bundle of mime data. + * + * @param safe - How to consider safe/unsafe factories. If 'ensure', + * it will only consider safe factories. If 'any', any factory will be + * considered. If 'prefer', unsafe factories will be considered, but + * only after the safe options have been exhausted. + * + * @returns The preferred mime type from the available factories, + * or `undefined` if the mime type cannot be rendered. + */ + preferredMimeType( + bundle: ReadonlyJSONObject, + safe: 'ensure' | 'prefer' | 'any' = 'ensure' + ): string | undefined { + // Try to find a safe factory first, if preferred. + if (safe === 'ensure' || safe === 'prefer') { + for (let mt of this.mimeTypes) { + if (mt in bundle && this._factories[mt].safe) { + return mt; + } + } + } + + if (safe !== 'ensure') { + // Otherwise, search for the best factory among all factories. + for (let mt of this.mimeTypes) { + if (mt in bundle) { + return mt; + } + } + } + + // Otherwise, no matching mime type exists. + return undefined; + } + + /** + * Create a renderer for a mime type. + * + * @param mimeType - The mime type of interest. + * + * @returns A new renderer for the given mime type. + * + * @throws An error if no factory exists for the mime type. + */ + createRenderer(mimeType: string): IRenderMime.IRenderer { + // Throw an error if no factory exists for the mime type. + if (!(mimeType in this._factories)) { + throw new Error(`No factory for mime type: '${mimeType}'`); + } + + // Invoke the best factory for the given mime type. + return this._factories[mimeType].createRenderer({ + mimeType, + resolver: this.resolver, + sanitizer: this.sanitizer, + linkHandler: this.linkHandler, + latexTypesetter: this.latexTypesetter + }); + } + + /** + * Create a new mime model. This is a convenience method. + * + * @options - The options used to create the model. + * + * @returns A new mime model. + */ + createModel(options: MimeModel.IOptions = {}): MimeModel { + return new MimeModel(options); + } + + /** + * Create a clone of this rendermime instance. + * + * @param options - The options for configuring the clone. + * + * @returns A new independent clone of the rendermime. + */ + clone(options: RenderMimeRegistry.ICloneOptions = {}): RenderMimeRegistry { + // Create the clone. + let clone = new RenderMimeRegistry({ + resolver: options.resolver || this.resolver || undefined, + sanitizer: options.sanitizer || this.sanitizer || undefined, + linkHandler: options.linkHandler || this.linkHandler || undefined, + latexTypesetter: options.latexTypesetter || this.latexTypesetter + }); + + // Clone the internal state. + clone._factories = { ...this._factories }; + clone._ranks = { ...this._ranks }; + clone._id = this._id; + + // Return the cloned object. + return clone; + } + + /** + * Get the renderer factory registered for a mime type. + * + * @param mimeType - The mime type of interest. + * + * @returns The factory for the mime type, or `undefined`. + */ + getFactory(mimeType: string): IRenderMime.IRendererFactory | undefined { + return this._factories[mimeType]; + } + + /** + * Add a renderer factory to the rendermime. + * + * @param factory - The renderer factory of interest. + * + * @param rank - The rank of the renderer. A lower rank indicates + * a higher priority for rendering. If not given, the rank will + * defer to the `defaultRank` of the factory. If no `defaultRank` + * is given, it will default to 100. + * + * #### Notes + * The renderer will replace an existing renderer for the given + * mimeType. + */ + addFactory(factory: IRenderMime.IRendererFactory, rank?: number): void { + if (rank === undefined) { + rank = factory.defaultRank; + if (rank === undefined) { + rank = 100; + } + } + for (let mt of factory.mimeTypes) { + this._factories[mt] = factory; + this._ranks[mt] = { rank, id: this._id++ }; + } + this._types = null; + } + + /** + * Remove a mime type. + * + * @param mimeType - The mime type of interest. + */ + removeMimeType(mimeType: string): void { + delete this._factories[mimeType]; + delete this._ranks[mimeType]; + this._types = null; + } + + /** + * Get the rank for a given mime type. + * + * @param mimeType - The mime type of interest. + * + * @returns The rank of the mime type or undefined. + */ + getRank(mimeType: string): number | undefined { + let rank = this._ranks[mimeType]; + return rank && rank.rank; + } + + /** + * Set the rank of a given mime type. + * + * @param mimeType - The mime type of interest. + * + * @param rank - The new rank to assign. + * + * #### Notes + * This is a no-op if the mime type is not registered. + */ + setRank(mimeType: string, rank: number): void { + if (!this._ranks[mimeType]) { + return; + } + let id = this._id++; + this._ranks[mimeType] = { rank, id }; + this._types = null; + } + + private _id = 0; + private _ranks: Private.RankMap = {}; + private _types: string[] | null = null; + private _factories: Private.FactoryMap = {}; +} + +/** + * The namespace for `RenderMimeRegistry` class statics. + */ +export namespace RenderMimeRegistry { + /** + * The options used to initialize a rendermime instance. + */ + export interface IOptions { + /** + * Initial factories to add to the rendermime instance. + */ + initialFactories?: ReadonlyArray; + + /** + * The sanitizer used to sanitize untrusted html inputs. + * + * If not given, a default sanitizer will be used. + */ + sanitizer?: IRenderMime.ISanitizer; + + /** + * The initial resolver object. + * + * The default is `null`. + */ + resolver?: IRenderMime.IResolver; + + /** + * An optional path handler. + */ + linkHandler?: IRenderMime.ILinkHandler; + + /** + * An optional LaTeX typesetter. + */ + latexTypesetter?: IRenderMime.ILatexTypesetter; + } + + /** + * The options used to clone a rendermime instance. + */ + export interface ICloneOptions { + /** + * The new sanitizer used to sanitize untrusted html inputs. + */ + sanitizer?: IRenderMime.ISanitizer; + + /** + * The new resolver object. + */ + resolver?: IRenderMime.IResolver; + + /** + * The new path handler. + */ + linkHandler?: IRenderMime.ILinkHandler; + + /** + * The new LaTeX typesetter. + */ + latexTypesetter?: IRenderMime.ILatexTypesetter; + } +} + +/** + * The namespace for the module implementation details. + */ +namespace Private { + /** + * A type alias for a mime rank and tie-breaking id. + */ + export type RankPair = { readonly id: number; readonly rank: number }; + + /** + * A type alias for a mapping of mime type -> rank pair. + */ + export type RankMap = { [key: string]: RankPair }; + + /** + * A type alias for a mapping of mime type -> ordered factories. + */ + export type FactoryMap = { [key: string]: IRenderMime.IRendererFactory }; + + /** + * Get the mime types in the map, ordered by rank. + */ + export function sortedTypes(map: RankMap): string[] { + return Object.keys(map).sort((a, b) => { + let p1 = map[a]; + let p2 = map[b]; + if (p1.rank !== p2.rank) { + return p1.rank - p2.rank; + } + return p1.id - p2.id; + }); + } +} diff --git a/src/sql/parts/notebook/outputs/renderers.ts b/src/sql/parts/notebook/outputs/renderers.ts new file mode 100644 index 0000000000..498ebe0435 --- /dev/null +++ b/src/sql/parts/notebook/outputs/renderers.ts @@ -0,0 +1,629 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + +import { default as AnsiUp } from 'ansi_up'; +import { IRenderMime } from './common/renderMimeInterfaces'; +import { URLExt } from './common/url'; +import URI from 'vs/base/common/uri'; + + +/** + * Render HTML into a host node. + * + * @params options - The options for rendering. + * + * @returns A promise which resolves when rendering is complete. + */ +export function renderHTML(options: renderHTML.IOptions): Promise { + // Unpack the options. + let { + host, + source, + trusted, + sanitizer, + resolver, + linkHandler, + shouldTypeset, + latexTypesetter + } = options; + + let originalSource = source; + + // Bail early if the source is empty. + if (!source) { + host.textContent = ''; + return Promise.resolve(undefined); + } + + // Sanitize the source if it is not trusted. This removes all + // `