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.
This commit is contained in:
Kevin Cunnane
2018-10-31 22:01:40 -07:00
committed by GitHub
parent ac0ffab99c
commit fc3bf45a7f
29 changed files with 2973 additions and 53 deletions

View File

@@ -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;

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';
}

View File

@@ -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';

View File

@@ -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<string, string>;
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<ReadonlyArray<nb.ICellOutput>>();
private _onCellModeChanged = new Emitter<boolean>();
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<ReadonlyArray<nb.ICellOutput>> {
return this._onOutputsChanged.event;
}
public get onCellModeChanged(): Event<boolean> {
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<nb.ICellOutput> {
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<nb.ICell> = {
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<string, string>();
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, '');
}
}
}

View File

@@ -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<void>();
private _kernelChangedEmitter = new Emitter<nb.IKernelChangedArgs>();
private _statusChangedEmitter = new Emitter<nb.ISession>();
private _iopubMessageEmitter = new Emitter<nb.IMessage>();
private _unhandledMessageEmitter = new Emitter<nb.IMessage>();
private _propertyChangedEmitter = new Emitter<'path' | 'name' | 'type'>();
private _path: string;
private _type: string;
private _name: string;
private _isReady: boolean;
private _ready: Deferred<void>;
private _kernelChangeCompleted: Deferred<void>;
private _kernelPreference: IKernelPreference;
private _kernelDisplayName: string;
private _errorMessage: string;
//#endregion
private _serverLoadFinished: Promise<void>;
private _session: nb.ISession;
private isServerStarted: boolean;
private notebookManager: INotebookManager;
private _connection: NotebookConnection;
private _kernelConfigActions: ((kernelName: string) => Promise<any>)[] = [];
constructor(private options: IClientSessionOptions) {
this._path = options.path;
this.notebookManager = options.notebookManager;
this._isReady = false;
this._ready = new Deferred<void>();
this._kernelChangeCompleted = new Deferred<void>();
}
public async initialize(connection?: NotebookConnection): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
return this._serverLoadFinished;
}
//#region IClientSession Properties
public get terminated(): Event<void> {
return this._terminatedEmitter.event;
}
public get kernelChanged(): Event<nb.IKernelChangedArgs> {
return this._kernelChangedEmitter.event;
}
public get statusChanged(): Event<nb.ISession> {
return this._statusChangedEmitter.event;
}
public get iopubMessage(): Event<nb.IMessage> {
return this._iopubMessageEmitter.event;
}
public get unhandledMessage(): Event<nb.IMessage> {
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<void> {
return this._ready.promise;
}
public get kernelChangeCompleted(): Promise<void> {
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<nb.IKernel> {
this._kernelChangeCompleted = new Deferred<void>();
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<nb.IKernel> {
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<void> {
// 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<void> {
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<void> {
// 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<void> {
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<boolean> {
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<void> {
throw new Error('Not implemented');
}
/**
* Change the session name.
*/
setName(name: string): Promise<void> {
throw new Error('Not implemented');
}
/**
* Change the session type.
*/
setType(type: string): Promise<void> {
throw new Error('Not implemented');
}
//#endregion
}

View File

@@ -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
}

View File

@@ -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);
}
}

View File

@@ -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<void>;
/**
* A signal emitted when the kernel changes.
*/
readonly kernelChanged: Event<nb.IKernelChangedArgs>;
/**
* A signal emitted when the kernel status changes.
*/
readonly statusChanged: Event<nb.ISession>;
/**
* A signal emitted for a kernel messages.
*/
readonly iopubMessage: Event<nb.IMessage>;
/**
* A signal emitted for an unhandled kernel message.
*/
readonly unhandledMessage: Event<nb.IMessage>;
/**
* 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<void>;
/**
* A promise that is fulfilled when the session completes a kernel change.
*/
readonly kernelChangeCompleted: Promise<void>;
/**
* 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<void>;
/**
* Change the current kernel associated with the document.
*/
changeKernel(
options: nb.IKernelSpec
): Promise<nb.IKernel>;
/**
* Kill the kernel and shutdown the session.
*
* @returns A promise that resolves when the session is shut down.
*/
shutdown(): Promise<void>;
/**
* Select a kernel for the session.
*/
selectKernel(): Promise<void>;
/**
* 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<boolean>;
/**
* 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<void>;
/**
* Change the session name.
*/
setName(name: string): Promise<void>;
/**
* Change the session type.
*/
setType(type: string): Promise<void>;
/**
* 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<nb.IKernelChangedArgs>;
/**
* Event fired on first initialization of the kernels and
* on subsequent change events
*/
readonly kernelsChanged: Event<nb.IKernelSpec>;
/**
* Default kernel
*/
defaultKernel?: nb.IKernelSpec;
/**
* Event fired on first initialization of the contexts and
* on subsequent change events
*/
readonly contextsChanged: Event<void>;
/**
* 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<boolean>;
/**
* 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';
}

View File

@@ -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;
}
}

View File

@@ -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<void>();
private _contentChangedEmitter = new Emitter<NotebookContentChange>();
private _kernelsChangedEmitter = new Emitter<nb.IKernelSpec>();
private _inErrorState: boolean = false;
private _clientSession: IClientSession;
private _sessionLoadFinished: Promise<void>;
private _onClientSessionReady = new Emitter<IClientSession>();
private _activeContexts: IDefaultConnection;
private _trustedMode: boolean;
private _cells: ICellModel[];
private _defaultLanguageInfo: nb.ILanguageInfo;
private onErrorEmitter = new Emitter<ErrorInfo>();
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<NotebookContentChange> {
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<nb.IKernelChangedArgs> {
return this.clientSession.kernelChanged;
}
public get kernelsChanged(): Event<nb.IKernelSpec> {
return this._kernelsChangedEmitter.event;
}
public get defaultKernel(): nb.IKernelSpec {
return this._defaultKernel;
}
public get contextsChanged(): Event<void> {
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<ErrorInfo> {
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<void> {
return this._sessionLoadFinished;
}
/**
* Notifies when the client session is ready for use
*/
public get onClientSessionReady(): Event<IClientSession> {
return this._onClientSessionReady.event;
}
public async requestModelLoad(isTrusted: boolean = false): Promise<void> {
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<void> {
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<void> {
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 <kernel> (<ip address>)
* 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<boolean> {
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<void> {
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);
}
}

View File

@@ -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 = <any> {
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<IDefaultConnection> {
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<IDefaultConnection> {
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 = <IConnectionProfile> <any>{
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<object> {
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;
}

View File

@@ -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);
}

View File

@@ -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<void> {
return new TPromise<void>((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 });
});

View File

@@ -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;
}

View File

@@ -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
);
}
}

View File

@@ -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<boolean>;
export class NotebookInputModel extends EditorModel {
private dirty: boolean;
private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());
get onDidChangeDirty(): Event<void> { 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<void> {
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 {

View File

@@ -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<void> {
let exists = await pfs.dirExists(dirPath);
if (!exists) {
if (outputChannel) {
outputChannel.append(localize('mkdirOutputMsg', '... Creating {0}', dirPath) + os.EOL);
}
await pfs.mkdirp(dirPath);
}
}

View File

@@ -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/';
}

View File

@@ -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<INotebookService>(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<INotebookManager>;
shutdown(): void;
}
export interface INotebookProvider {
readonly providerId: string;
getNotebookManager(notebookUri: URI): Thenable<INotebookManager>;
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;
}

View File

@@ -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<string, INotebookProvider> = new Map();
private _managers: Map<URI, INotebookManager> = 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<INotebookManager> {
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<T>(providerId: string, op: (provider: INotebookProvider) => Thenable<T>): Thenable<T> {
// 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);
}
}

View File

@@ -52,8 +52,8 @@ declare module 'sqlops' {
}
export interface TreeComponentView<T> extends vscode.Disposable {
onNodeCheckedChanged: vscode.Event<NodeCheckedEventParameters<T>>;
onDidChangeSelection: vscode.Event<vscode.TreeViewSelectionChangeEvent<T>>;
onNodeCheckedChanged: vscode.Event<NodeCheckedEventParameters<T>>;
onDidChangeSelection: vscode.Event<vscode.TreeViewSelectionChangeEvent<T>>;
}
export class TreeComponentItem extends vscode.TreeItem {
@@ -1365,4 +1365,620 @@ declare module 'sqlops' {
*/
export function openConnectionDialog(providers?: string[], initialConnectionProfile?: IConnectionProfile, connectionCompletionOptions?: IConnectionCompletionOptions): Thenable<connection.Connection>;
}
export namespace nb {
export function registerNotebookProvider(provider: NotebookProvider): vscode.Disposable;
export interface NotebookProvider {
handle: number;
readonly providerId: string;
getNotebookManager(notebookUri: vscode.Uri): Thenable<NotebookManager>;
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<void>;
/**
* Starts the server. Some server types may not support or require this.
* Should no-op if server is already started
*/
startServer(): Thenable<void>;
/**
* Stops the server. Some server types may not support or require this
*/
stopServer(): Thenable<void>;
}
//#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<INotebook>;
/**
* 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<INotebook>;
}
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<void>;
readonly specs: IAllKernels | undefined;
startNew(options: ISessionOptions): Thenable<ISession>;
shutdown(id: string): Thenable<void>;
}
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<IKernel>;
}
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<void>;
/**
* 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<IKernelSpec>;
/**
* 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<ICompleteReplyMsg>;
}
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<T extends IMessage> {
handle(message: T): void | Thenable<void>;
}
/**
* 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<IShellMessage | undefined>;
/**
* 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<IShellMessage>): 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<IStdinMessage>): 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<IIOPubMessage>): 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<boolean>
): 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<boolean>
): 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
}
}

View File

@@ -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<number, sqlops.nb.NotebookProvider>();
constructor(private _mainContext: IMainContext) {
this._proxy = _mainContext.getProxy(SqlMainContext.MainThreadNotebook);
}
//#region APIs called by main thread
getNotebookManager(notebookUri: vscode.Uri): Thenable<number> {
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<A, R>(handle: number, ctor: { new(...args: any[]): A }, callback: (adapter: A) => TPromise<R>): TPromise<R> {
let provider = this._providers.get(handle);
if (!(provider instanceof ctor)) {
return TPromise.wrapError<R>(new Error('no adapter found'));
}
return callback(<any>provider);
}
private _addNewProvider(adapter: sqlops.nb.NotebookProvider): number {
const handle = this._nextHandle();
this._providers.set(handle, adapter);
return handle;
}
//#endregion
}

View File

@@ -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<INotebookManager> {
// 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;
}

View File

@@ -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
};
}
};

View File

@@ -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 {

View File

@@ -545,6 +545,7 @@ export const SqlMainContext = {
MainThreadDashboard: createMainId<MainThreadDashboardShape>('MainThreadDashboard'),
MainThreadModelViewDialog: createMainId<MainThreadModelViewDialogShape>('MainThreadModelViewDialog'),
MainThreadQueryEditor: createMainId<MainThreadQueryEditorShape>('MainThreadQueryEditor'),
MainThreadNotebook: createMainId<MainThreadNotebookShape>('MainThreadNotebook')
};
export const SqlExtHostContext = {
@@ -563,7 +564,8 @@ export const SqlExtHostContext = {
ExtHostModelViewTreeViews: createExtId<ExtHostModelViewTreeViewsShape>('ExtHostModelViewTreeViews'),
ExtHostDashboard: createExtId<ExtHostDashboardShape>('ExtHostDashboard'),
ExtHostModelViewDialog: createExtId<ExtHostModelViewDialogShape>('ExtHostModelViewDialog'),
ExtHostQueryEditor: createExtId<ExtHostQueryEditorShape>('ExtHostQueryEditor')
ExtHostQueryEditor: createExtId<ExtHostQueryEditorShape>('ExtHostQueryEditor'),
ExtHostNotebook: createExtId<ExtHostNotebookShape>('ExtHostNotebook')
};
export interface MainThreadDashboardShape extends IDisposable {
@@ -703,4 +705,21 @@ export interface ExtHostQueryEditorShape {
export interface MainThreadQueryEditorShape extends IDisposable {
$connect(fileUri: string, connectionId: string): Thenable<void>;
$runQuery(fileUri: string): void;
}
export interface ExtHostNotebookShape {
/**
* Looks up a notebook manager for a given notebook URI
* @param {vscode.Uri} notebookUri
* @returns {Thenable<string>} handle of the manager to be used when sending
*/
getNotebookManager(notebookUri: vscode.Uri): Thenable<number>;
handleNotebookClosed(notebookUri: vscode.Uri): void;
}
export interface MainThreadNotebookShape extends IDisposable {
$registerNotebookProvider(providerId: string, handle: number): void;
$unregisterNotebookProvider(handle: number): void;
}