Integrate notebook service with notebook UI (#3143)

Port notebookView code over to notebook.component.ts.
Integrate loading of notebook contents into the UI
This commit is contained in:
Kevin Cunnane
2018-11-06 16:31:37 -08:00
committed by GitHub
parent 5da89ac05b
commit ecd40de7ec
13 changed files with 168 additions and 43 deletions

View File

@@ -55,7 +55,7 @@ export function convertEditorInput(input: EditorInput, options: IQueryEditorOpti
if(uri){ if(uri){
//TODO: We need to pass in notebook data either through notebook input or notebook service //TODO: We need to pass in notebook data either through notebook input or notebook service
let fileName: string = input? input.getName() : 'untitled'; 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. //TO DO: Second paramter has to be the content.
let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel); let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel);
return notebookInput; return notebookInput;

View File

@@ -5,7 +5,7 @@
import 'vs/css!./loadingComponent'; import 'vs/css!./loadingComponent';
import { import {
Component, Input, Inject, ChangeDetectorRef, forwardRef, OnDestroy, AfterViewInit, ViewChild, ElementRef Component, Input, Inject, ChangeDetectorRef, forwardRef, OnDestroy, AfterViewInit, ElementRef
} from '@angular/core'; } from '@angular/core';
import * as sqlops from 'sqlops'; import * as sqlops from 'sqlops';
@@ -31,9 +31,6 @@ export default class LoadingComponent extends ComponentBase implements IComponen
@Input() descriptor: IComponentDescriptor; @Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore; @Input() modelStore: IModelStore;
@ViewChild('spinnerElement', { read: ElementRef }) private _spinnerElement: ElementRef;
@ViewChild('childElement', { read: ElementRef }) private _childElement: ElementRef;
constructor( constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef, @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) el: ElementRef) { @Inject(forwardRef(() => ElementRef)) el: ElementRef) {

View File

@@ -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: `
<div class="modelview-loadingComponent-container" *ngIf="loading">
<div class="modelview-loadingComponent-spinner" *ngIf="loading" [title]=_loadingTitle #spinnerElement></div>
</div>
`
})
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) {
}
}

View File

@@ -10,4 +10,5 @@ text-cell-component {
text-cell-component .notebook-preview { text-cell-component .notebook-preview {
border-top-width: 1px; border-top-width: 1px;
border-top-style: solid; border-top-style: solid;
user-select: initial;
} }

View File

@@ -234,6 +234,10 @@ export interface IKernelPreference {
} }
export interface INotebookModel { export interface INotebookModel {
/**
* Cell List for this model
*/
readonly cells: ReadonlyArray<ICellModel>;
/** /**
* Client Session in the notebook, used for sending requests to the notebook service * Client Session in the notebook, used for sending requests to the notebook service
*/ */

View File

@@ -20,6 +20,7 @@ import { INotebookManager } from 'sql/services/notebook/notebookService';
import { SparkMagicContexts } from 'sql/parts/notebook/models/sparkMagicContexts'; import { SparkMagicContexts } from 'sql/parts/notebook/models/sparkMagicContexts';
import { IConnectionProfile } from 'sql/parts/connection/common/interfaces'; import { IConnectionProfile } from 'sql/parts/connection/common/interfaces';
import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection'; 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, * 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 _cells: ICellModel[];
private _defaultLanguageInfo: nb.ILanguageInfo; private _defaultLanguageInfo: nb.ILanguageInfo;
private onErrorEmitter = new Emitter<ErrorInfo>(); private onErrorEmitter = new Emitter<INotification>();
private _savedKernelInfo: nb.IKernelInfo; private _savedKernelInfo: nb.IKernelInfo;
private readonly _nbformat: number = nbversion.MAJOR_VERSION; private readonly _nbformat: number = nbversion.MAJOR_VERSION;
private readonly _nbformatMinor: number = nbversion.MINOR_VERSION; private readonly _nbformatMinor: number = nbversion.MINOR_VERSION;
@@ -147,7 +148,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
return this._inErrorState; return this._inErrorState;
} }
public get onError(): Event<ErrorInfo> { public get onError(): Event<INotification> {
return this.onErrorEmitter.event; return this.onErrorEmitter.event;
} }
@@ -242,7 +243,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
} }
private notifyError(error: string): void { private notifyError(error: string): void {
this.onErrorEmitter.fire(new ErrorInfo(error, MessageLevel.Error)); this.onErrorEmitter.fire({ message: error, severity: Severity.Error });
} }
public backgroundStartSession(): void { public backgroundStartSession(): void {

View File

@@ -10,7 +10,8 @@
PlaceHolder for Toolbar PlaceHolder for Toolbar
</div> </div>
</div> </div>
<div style="flex: 1 1 auto; position: relative"> <div class="scrollable" style="flex: 1 1 auto; position: relative">
<loading-spinner [loading]="isLoading"></loading-spinner>
<div class="notebook-cell" *ngFor="let cell of cells" (click)="selectCell(cell)" [class.active]="cell.active" (keydown)="onKeyDown($event)"> <div class="notebook-cell" *ngFor="let cell of cells" (click)="selectCell(cell)" [class.active]="cell.active" (keydown)="onKeyDown($event)">
<code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell"> <code-cell-component *ngIf="cell.cellType === 'code'" [cellModel]="cell">
</code-cell-component> </code-cell-component>

View File

@@ -12,36 +12,25 @@ import { OnInit, Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, V
import URI from 'vs/base/common/uri'; import URI from 'vs/base/common/uri';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as themeColors from 'vs/workbench/common/theme'; 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 { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service';
import { AngularDisposable } from 'sql/base/common/lifecycle'; import { AngularDisposable } from 'sql/base/common/lifecycle';
import { CellTypes, CellType } from 'sql/parts/notebook/models/contracts'; import { CellTypes, CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts';
import { ICellModel } from 'sql/parts/notebook/models/modelInterfaces'; import { ICellModel, INotebookModel, IModelFactory, INotebookModelOptions } from 'sql/parts/notebook/models/modelInterfaces';
import { IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement'; 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 { 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'; 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({ @Component({
selector: NOTEBOOK_SELECTOR, selector: NOTEBOOK_SELECTOR,
@@ -49,8 +38,16 @@ class CellModelStub implements ICellModel {
}) })
export class NotebookComponent extends AngularDisposable implements OnInit { export class NotebookComponent extends AngularDisposable implements OnInit {
@ViewChild('toolbar', { read: ElementRef }) private toolbar: ElementRef; @ViewChild('toolbar', { read: ElementRef }) private toolbar: ElementRef;
protected cells: Array<ICellModel> = []; private _model: NotebookModel;
private _isInErrorState: boolean = false;
private _errorMessage: string;
private _activeCell: ICellModel; private _activeCell: ICellModel;
protected isLoading: boolean;
private notebookManager: INotebookManager;
private _modelReadyDeferred = new Deferred<NotebookModel>();
private profile: IConnectionProfile;
constructor( constructor(
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface, @Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@@ -61,17 +58,18 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
@Inject(IBootstrapParams) private notebookParams: INotebookParams @Inject(IBootstrapParams) private notebookParams: INotebookParams
) { ) {
super(); super();
this.profile = this.notebookParams!.profile;
// TODO NOTEBOOK REFACTOR: This is mock data for cells. Will remove this code when we have a service this.isLoading = true;
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);
} }
ngOnInit() { ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this)); this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme()); this.updateTheme(this.themeService.getColorTheme());
this.doLoad();
}
protected get cells(): ReadonlyArray<ICellModel> {
return this._model ? this._model.cells : [];
} }
private updateTheme(theme: IColorTheme): void { private updateTheme(theme: IColorTheme): void {
@@ -110,7 +108,84 @@ export class NotebookComponent extends AngularDisposable implements OnInit {
} }
} }
findCellIndex(cellModel: ICellModel): number { private async doLoad(): Promise<void> {
return this.cells.findIndex((cell) => cell.id === cellModel.id); 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<void> {
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<boolean> {
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;
// }
}
} }

View File

@@ -40,8 +40,8 @@ export class OpenNotebookAction extends Action {
public run(): TPromise<void> { public run(): TPromise<void> {
return new TPromise<void>((resolve, reject) => { return new TPromise<void>((resolve, reject) => {
let untitledUri = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter++}`}); let untitledUri = URI.from({ scheme: Schemas.untitled, path: `Untitled-${counter++}`});
let model = new NotebookInputModel(untitledUri, undefined, undefined); let model = new NotebookInputModel(untitledUri, undefined, false, undefined);
let input = new NotebookInput('modelViewId', model); let input = new NotebookInput('modelViewId', model,);
this._editorService.openEditor(input, { pinned: true }); this._editorService.openEditor(input, { pinned: true });
}); });
} }

View File

@@ -26,6 +26,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { CodeComponent } from 'sql/parts/notebook/cellViews/code.component'; import { CodeComponent } from 'sql/parts/notebook/cellViews/code.component';
import { CodeCellComponent } from 'sql/parts/notebook/cellViews/codeCell.component'; import { CodeCellComponent } from 'sql/parts/notebook/cellViews/codeCell.component';
import { TextCellComponent } from 'sql/parts/notebook/cellViews/textCell.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 => { export const NotebookModule = (params, selector: string, instantiationService: IInstantiationService): any => {
@NgModule({ @NgModule({
@@ -34,6 +35,7 @@ export const NotebookModule = (params, selector: string, instantiationService: I
SelectBox, SelectBox,
EditableDropDown, EditableDropDown,
InputBox, InputBox,
LoadingSpinner,
CodeComponent, CodeComponent,
CodeCellComponent, CodeCellComponent,
TextCellComponent, TextCellComponent,

View File

@@ -86,7 +86,8 @@ export class NotebookEditor extends BaseEditor {
input.hasBootstrapped = true; input.hasBootstrapped = true;
let params: INotebookParams = { let params: INotebookParams = {
notebookUri: input.notebookUri, 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, bootstrapAngular(this.instantiationService,
NotebookModule, NotebookModule,

View File

@@ -17,7 +17,7 @@ export class NotebookInputModel extends EditorModel {
private dirty: boolean; private dirty: boolean;
private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>()); private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());
private _providerId: string; 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(); super();
this.dirty = false; this.dirty = false;
} }
@@ -30,6 +30,10 @@ export class NotebookInputModel extends EditorModel {
this._providerId = value; this._providerId = value;
} }
get isTrusted(): boolean {
return this._isTrusted;
}
get onDidChangeDirty(): Event<void> { get onDidChangeDirty(): Event<void> {
return this._onDidChangeDirty.event; return this._onDidChangeDirty.event;
} }
@@ -93,6 +97,10 @@ export class NotebookInput extends EditorInput {
return this._title; return this._title;
} }
public get isTrusted(): boolean {
return this._model.isTrusted;
}
public dispose(): void { public dispose(): void {
this._disposeContainer(); this._disposeContainer();
super.dispose(); super.dispose();

View File

@@ -9,6 +9,8 @@ import * as sqlops from 'sqlops';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import URI from 'vs/base/common/uri'; import URI from 'vs/base/common/uri';
import { IBootstrapParams } from 'sql/services/bootstrap/bootstrapService'; 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 SERVICE_ID = 'notebookService';
export const INotebookService = createDecorator<INotebookService>(SERVICE_ID); export const INotebookService = createDecorator<INotebookService>(SERVICE_ID);
@@ -56,4 +58,7 @@ export interface INotebookManager {
export interface INotebookParams extends IBootstrapParams { export interface INotebookParams extends IBootstrapParams {
notebookUri: URI; notebookUri: URI;
providerId: string; providerId: string;
isTrusted: boolean;
profile?: IConnectionProfile;
modelFactory?: ModelFactory;
} }