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