mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-13 03:28:33 -05:00
More Layering (#9139)
* move handling generated files to the serilization classes * remove unneeded methods * add more folders to strictire compile, add more strict compile options * update ci * wip * add more layering and fix issues * add more strictness * remove unnecessary assertion * add missing checks * fix indentation * wip * remove jsdoc * fix layering * fix compile * fix compile errors * wip * wip * finish layering * fix css * more layering * rip * reworking results serializer * move some files around * move capabilities to platform wip * implement capabilities register provider * fix capabilities service * fix usage of the regist4ry * add contribution * wip * wip * wip * remove no longer good parts * fix strict-nulls * fix issues with startup * another try * fix startup * fix imports * fix tests * fix tests * fix more tests * fix tests * fix more tests * fix broken test * fix tabbing * fix naming * wip * finished layering * fix imports * fix valid layers * fix layers
This commit is contained in:
23
src/sql/workbench/services/notebook/browser/interfaces.ts
Normal file
23
src/sql/workbench/services/notebook/browser/interfaces.ts
Normal 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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { nb } from 'azdata';
|
||||
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export interface FutureInternal extends nb.IFuture {
|
||||
inProgress: boolean;
|
||||
}
|
||||
|
||||
export namespace notebookConstants {
|
||||
export const SQL = 'SQL';
|
||||
export const SQL_CONNECTION_PROVIDER = mssqlProviderName;
|
||||
export const sqlKernel: string = localize('sqlKernel', "SQL");
|
||||
export const sqlKernelSpec: nb.IKernelSpec = ({
|
||||
name: sqlKernel,
|
||||
language: 'sql',
|
||||
display_name: sqlKernel
|
||||
});
|
||||
}
|
||||
718
src/sql/workbench/services/notebook/browser/models/cell.ts
Normal file
718
src/sql/workbench/services/notebook/browser/models/cell.ts
Normal file
@@ -0,0 +1,718 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { nb, ServerInfo } from 'azdata';
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
import * as notebookUtils from 'sql/workbench/services/notebook/browser/models/notebookUtils';
|
||||
import { CellTypes, CellType, NotebookChangeType } from 'sql/workbench/services/notebook/common/contracts';
|
||||
import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel';
|
||||
import { ICellModel, IOutputChangedEvent, CellExecutionState, ICellModelOptions } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { optional } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { getErrorMessage, onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
|
||||
import { firstIndex, find } from 'vs/base/common/arrays';
|
||||
import { HideInputTag } from 'sql/platform/notebooks/common/outputRegistry';
|
||||
import { FutureInternal, notebookConstants } from 'sql/workbench/services/notebook/browser/interfaces';
|
||||
|
||||
let modelId = 0;
|
||||
|
||||
export class CellModel implements ICellModel {
|
||||
public id: string;
|
||||
|
||||
private _cellType: nb.CellType;
|
||||
private _source: string | string[];
|
||||
private _language: string;
|
||||
private _cellGuid: string;
|
||||
private _future: FutureInternal;
|
||||
private _outputs: nb.ICellOutput[] = [];
|
||||
private _isEditMode: boolean;
|
||||
private _onOutputsChanged = new Emitter<IOutputChangedEvent>();
|
||||
private _onCellModeChanged = new Emitter<boolean>();
|
||||
private _onExecutionStateChanged = new Emitter<CellExecutionState>();
|
||||
private _isTrusted: boolean;
|
||||
private _active: boolean;
|
||||
private _hover: boolean;
|
||||
private _executionCount: number | undefined;
|
||||
private _cellUri: URI;
|
||||
private _connectionManagementService: IConnectionManagementService;
|
||||
private _stdInHandler: nb.MessageHandler<nb.IStdinMessage>;
|
||||
private _onCellLoaded = new Emitter<string>();
|
||||
private _loaded: boolean;
|
||||
private _stdInVisible: boolean;
|
||||
private _metadata: { language?: string; tags?: string[]; cellGuid?: string; };
|
||||
private _isCollapsed: boolean;
|
||||
private _onCollapseStateChanged = new Emitter<boolean>();
|
||||
private _modelContentChangedEvent: IModelContentChangedEvent;
|
||||
private readonly _ariaLabel: string;
|
||||
|
||||
private readonly codeCellLabel = localize('codeCellLabel', "Code Cell");
|
||||
private readonly textCellLabel = localize('textCellLabel', "Text Cell");
|
||||
|
||||
constructor(cellData: nb.ICellContents,
|
||||
private _options: ICellModelOptions,
|
||||
@optional(INotebookService) private _notebookService?: INotebookService
|
||||
) {
|
||||
this.id = `${modelId++}`;
|
||||
if (cellData) {
|
||||
// Read in contents if available
|
||||
this.fromJSON(cellData);
|
||||
} else {
|
||||
this._cellType = CellTypes.Code;
|
||||
this._source = '';
|
||||
}
|
||||
|
||||
if (this._cellType === CellTypes.Code) {
|
||||
this._ariaLabel = this.codeCellLabel;
|
||||
} else {
|
||||
this._ariaLabel = this.textCellLabel;
|
||||
}
|
||||
|
||||
this._isEditMode = this._cellType !== CellTypes.Markdown;
|
||||
this._stdInVisible = false;
|
||||
if (_options && _options.isTrusted) {
|
||||
this._isTrusted = true;
|
||||
} else {
|
||||
this._isTrusted = false;
|
||||
}
|
||||
// if the fromJson() method was already called and _cellGuid was previously set, don't generate another UUID unnecessarily
|
||||
this._cellGuid = this._cellGuid || generateUuid();
|
||||
this.createUri();
|
||||
}
|
||||
|
||||
public equals(other: ICellModel) {
|
||||
return other !== undefined && other.id === this.id;
|
||||
}
|
||||
|
||||
public get ariaLabel(): string {
|
||||
return this._ariaLabel;
|
||||
}
|
||||
|
||||
public get onCollapseStateChanged(): Event<boolean> {
|
||||
return this._onCollapseStateChanged.event;
|
||||
}
|
||||
|
||||
public get onOutputsChanged(): Event<IOutputChangedEvent> {
|
||||
return this._onOutputsChanged.event;
|
||||
}
|
||||
|
||||
public get isEditMode(): boolean {
|
||||
return this._isEditMode;
|
||||
}
|
||||
|
||||
public get future(): FutureInternal {
|
||||
return this._future;
|
||||
}
|
||||
|
||||
public get isCollapsed() {
|
||||
return this._isCollapsed;
|
||||
}
|
||||
|
||||
public set isCollapsed(value: boolean) {
|
||||
if (this.cellType !== CellTypes.Code) {
|
||||
return;
|
||||
}
|
||||
let stateChanged = this._isCollapsed !== value;
|
||||
this._isCollapsed = value;
|
||||
|
||||
let tagIndex = -1;
|
||||
if (this._metadata.tags) {
|
||||
tagIndex = firstIndex(this._metadata.tags, tag => tag === HideInputTag);
|
||||
}
|
||||
|
||||
if (this._isCollapsed) {
|
||||
if (tagIndex === -1) {
|
||||
if (!this._metadata.tags) {
|
||||
this._metadata.tags = [];
|
||||
}
|
||||
this._metadata.tags.push(HideInputTag);
|
||||
}
|
||||
} else {
|
||||
if (tagIndex > -1) {
|
||||
this._metadata.tags.splice(tagIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (stateChanged) {
|
||||
this._onCollapseStateChanged.fire(this._isCollapsed);
|
||||
this.sendChangeToNotebook(NotebookChangeType.CellInputVisibilityChanged);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
let outputEvent: IOutputChangedEvent = {
|
||||
outputs: this._outputs,
|
||||
shouldScroll: false
|
||||
};
|
||||
this._onOutputsChanged.fire(outputEvent);
|
||||
}
|
||||
}
|
||||
|
||||
public get active(): boolean {
|
||||
return this._active;
|
||||
}
|
||||
|
||||
public set active(value: boolean) {
|
||||
this._active = value;
|
||||
this.fireExecutionStateChanged();
|
||||
}
|
||||
|
||||
public get hover(): boolean {
|
||||
return this._hover;
|
||||
}
|
||||
|
||||
public set hover(value: boolean) {
|
||||
this._hover = value;
|
||||
this.fireExecutionStateChanged();
|
||||
}
|
||||
|
||||
public get executionCount(): number | undefined {
|
||||
return this._executionCount;
|
||||
}
|
||||
|
||||
public set executionCount(value: number | undefined) {
|
||||
this._executionCount = value;
|
||||
this.fireExecutionStateChanged();
|
||||
}
|
||||
|
||||
public get cellUri(): URI {
|
||||
return this._cellUri;
|
||||
}
|
||||
|
||||
public get notebookModel(): NotebookModel {
|
||||
return this._options && <NotebookModel>this._options.notebook;
|
||||
}
|
||||
|
||||
public set cellUri(value: URI) {
|
||||
this._cellUri = value;
|
||||
}
|
||||
|
||||
public get cellType(): CellType {
|
||||
return this._cellType;
|
||||
}
|
||||
|
||||
public get source(): string | string[] {
|
||||
return this._source;
|
||||
}
|
||||
|
||||
public set source(newSource: string | string[]) {
|
||||
newSource = this.getMultilineSource(newSource);
|
||||
if (this._source !== newSource) {
|
||||
this._source = newSource;
|
||||
this.sendChangeToNotebook(NotebookChangeType.CellSourceUpdated);
|
||||
}
|
||||
this._modelContentChangedEvent = undefined;
|
||||
}
|
||||
|
||||
public get modelContentChangedEvent(): IModelContentChangedEvent {
|
||||
return this._modelContentChangedEvent;
|
||||
}
|
||||
|
||||
public set modelContentChangedEvent(e: IModelContentChangedEvent) {
|
||||
this._modelContentChangedEvent = e;
|
||||
}
|
||||
|
||||
public get language(): string {
|
||||
if (this._cellType === CellTypes.Markdown) {
|
||||
return 'markdown';
|
||||
}
|
||||
if (this._language) {
|
||||
return this._language;
|
||||
}
|
||||
return this._options.notebook.language;
|
||||
}
|
||||
|
||||
public get cellGuid(): string {
|
||||
return this._cellGuid;
|
||||
}
|
||||
|
||||
public setOverrideLanguage(newLanguage: string) {
|
||||
this._language = newLanguage;
|
||||
}
|
||||
|
||||
public get onExecutionStateChange(): Event<CellExecutionState> {
|
||||
return this._onExecutionStateChanged.event;
|
||||
}
|
||||
|
||||
private fireExecutionStateChanged(): void {
|
||||
this._onExecutionStateChanged.fire(this.executionState);
|
||||
}
|
||||
|
||||
public get onLoaded(): Event<string> {
|
||||
return this._onCellLoaded.event;
|
||||
}
|
||||
|
||||
public get loaded(): boolean {
|
||||
return this._loaded;
|
||||
}
|
||||
|
||||
public set loaded(val: boolean) {
|
||||
this._loaded = val;
|
||||
if (val) {
|
||||
this._onCellLoaded.fire(this._cellType);
|
||||
}
|
||||
}
|
||||
|
||||
public get stdInVisible(): boolean {
|
||||
return this._stdInVisible;
|
||||
}
|
||||
|
||||
public set stdInVisible(val: boolean) {
|
||||
this._stdInVisible = val;
|
||||
}
|
||||
|
||||
private notifyExecutionComplete(): void {
|
||||
if (this._notebookService) {
|
||||
this._notebookService.serializeNotebookStateChange(this.notebookModel.notebookUri, NotebookChangeType.CellExecuted, this)
|
||||
.catch(e => onUnexpectedError(e));
|
||||
}
|
||||
}
|
||||
|
||||
public get executionState(): CellExecutionState {
|
||||
let isRunning = !!(this._future && this._future.inProgress);
|
||||
if (isRunning) {
|
||||
return CellExecutionState.Running;
|
||||
} else if (this.active || this.hover) {
|
||||
return CellExecutionState.Stopped;
|
||||
}
|
||||
// TODO save error state and show the error
|
||||
return CellExecutionState.Hidden;
|
||||
}
|
||||
|
||||
public async runCell(notificationService?: INotificationService, connectionManagementService?: IConnectionManagementService): Promise<boolean> {
|
||||
try {
|
||||
if (!this.active && this !== this.notebookModel.activeCell) {
|
||||
this.notebookModel.updateActiveCell(this);
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
if (connectionManagementService) {
|
||||
this._connectionManagementService = connectionManagementService;
|
||||
}
|
||||
if (this.cellType !== CellTypes.Code) {
|
||||
// TODO should change hidden state to false if we add support
|
||||
// for this property
|
||||
return false;
|
||||
}
|
||||
let kernel = await this.getOrStartKernel(notificationService);
|
||||
if (!kernel) {
|
||||
return false;
|
||||
}
|
||||
// If cell is currently running and user clicks the stop/cancel button, call kernel.interrupt()
|
||||
// This matches the same behavior as JupyterLab
|
||||
if (this.future && this.future.inProgress) {
|
||||
// If stdIn is visible, to prevent a kernel hang, we need to send a dummy input reply
|
||||
if (this._stdInVisible && this._stdInHandler) {
|
||||
this.future.sendInputReply({ value: '' });
|
||||
}
|
||||
this.future.inProgress = false;
|
||||
await kernel.interrupt();
|
||||
this.sendNotification(notificationService, Severity.Info, localize('runCellCancelled', "Cell execution cancelled"));
|
||||
} else {
|
||||
// TODO update source based on editor component contents
|
||||
if (kernel.requiresConnection && !this.notebookModel.context) {
|
||||
let connected = await this.notebookModel.requestConnection();
|
||||
if (!connected) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let content = this.source;
|
||||
if ((Array.isArray(content) && content.length > 0) || (!Array.isArray(content) && content)) {
|
||||
this.notebookModel.trustedMode = true;
|
||||
|
||||
// requestExecute expects a string for the code parameter
|
||||
content = Array.isArray(content) ? content.join('') : content;
|
||||
const future = kernel.requestExecute({
|
||||
code: content,
|
||||
stop_on_error: true
|
||||
}, false);
|
||||
this.setFuture(future as FutureInternal);
|
||||
this.fireExecutionStateChanged();
|
||||
// For now, await future completion. Later we should just track and handle cancellation based on model notifications
|
||||
let result: nb.IExecuteReplyMsg = <nb.IExecuteReplyMsg><any>await future.done;
|
||||
if (result && result.content) {
|
||||
this.executionCount = result.content.execution_count;
|
||||
if (result.content.status !== 'ok') {
|
||||
// TODO track error state
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
let message: string;
|
||||
if (error.message === 'Canceled') {
|
||||
message = localize('executionCanceled', "Query execution was canceled");
|
||||
} else {
|
||||
message = getErrorMessage(error);
|
||||
}
|
||||
this.sendNotification(notificationService, Severity.Error, message);
|
||||
// TODO track error state for the cell
|
||||
} finally {
|
||||
this.disposeFuture();
|
||||
this.fireExecutionStateChanged();
|
||||
this.notifyExecutionComplete();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async getOrStartKernel(notificationService: INotificationService): Promise<nb.IKernel> {
|
||||
let model = this._options.notebook;
|
||||
let clientSession = model && model.clientSession;
|
||||
if (!clientSession) {
|
||||
this.sendNotification(notificationService, Severity.Error, localize('notebookNotReady', "The session for this notebook is not yet ready"));
|
||||
return undefined;
|
||||
} else if (!clientSession.isReady || clientSession.status === 'dead') {
|
||||
|
||||
this.sendNotification(notificationService, Severity.Info, localize('sessionNotReady', "The session for this notebook will start momentarily"));
|
||||
await clientSession.kernelChangeCompleted;
|
||||
}
|
||||
if (!clientSession.kernel) {
|
||||
let defaultKernel = model && model.defaultKernel && model.defaultKernel.name;
|
||||
if (!defaultKernel) {
|
||||
this.sendNotification(notificationService, Severity.Error, localize('noDefaultKernel', "No kernel is available for this notebook"));
|
||||
return undefined;
|
||||
}
|
||||
await clientSession.changeKernel({
|
||||
name: defaultKernel
|
||||
});
|
||||
}
|
||||
return clientSession.kernel;
|
||||
}
|
||||
|
||||
private sendNotification(notificationService: INotificationService, severity: Severity, message: string): void {
|
||||
if (notificationService) {
|
||||
notificationService.notify({ severity: severity, message: message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the future which will be used to update the output
|
||||
* area for this cell
|
||||
*/
|
||||
setFuture(future: FutureInternal): 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) });
|
||||
future.setStdInHandler({ handle: (msg) => this.handleSdtIn(msg) });
|
||||
}
|
||||
|
||||
public clearOutputs(): void {
|
||||
this._outputs = [];
|
||||
this.fireOutputsChanged();
|
||||
|
||||
this.executionCount = undefined;
|
||||
}
|
||||
|
||||
private fireOutputsChanged(shouldScroll: boolean = false): void {
|
||||
let outputEvent: IOutputChangedEvent = {
|
||||
outputs: this.outputs,
|
||||
shouldScroll: !!shouldScroll
|
||||
};
|
||||
this._onOutputsChanged.fire(outputEvent);
|
||||
if (this.outputs.length !== 0) {
|
||||
this.sendChangeToNotebook(NotebookChangeType.CellOutputUpdated);
|
||||
} else {
|
||||
this.sendChangeToNotebook(NotebookChangeType.CellOutputCleared);
|
||||
}
|
||||
}
|
||||
|
||||
private sendChangeToNotebook(change: NotebookChangeType): void {
|
||||
if (this._options && this._options.notebook) {
|
||||
this._options.notebook.onCellChange(this, change);
|
||||
}
|
||||
}
|
||||
|
||||
public get outputs(): Array<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
|
||||
|
||||
if (!this._future.inProgress) {
|
||||
this.disposeFuture();
|
||||
}
|
||||
}
|
||||
|
||||
private handleIOPub(msg: nb.IIOPubMessage): void {
|
||||
let msgType = msg.header.msg_type;
|
||||
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) {
|
||||
// deletes transient node in the serialized JSON
|
||||
delete output['transient'];
|
||||
this._outputs.push(this.rewriteOutputUrls(output));
|
||||
// Only scroll on 1st output being added
|
||||
let shouldScroll = this._outputs.length === 1;
|
||||
this.fireOutputsChanged(shouldScroll);
|
||||
}
|
||||
}
|
||||
|
||||
private rewriteOutputUrls(output: nb.ICellOutput): nb.ICellOutput {
|
||||
const driverLog = '/gateway/default/yarn/container';
|
||||
const yarnUi = '/gateway/default/yarn/proxy';
|
||||
const defaultPort = ':30433';
|
||||
// Only rewrite if this is coming back during execution, not when loading from disk.
|
||||
// A good approximation is that the model has a future (needed for execution)
|
||||
if (this.future) {
|
||||
try {
|
||||
let result = output as nb.IDisplayResult;
|
||||
if (result && result.data && result.data['text/html']) {
|
||||
let model = this._options.notebook as NotebookModel;
|
||||
if (model.context) {
|
||||
let gatewayEndpointInfo = this.getGatewayEndpoint(model.context);
|
||||
if (gatewayEndpointInfo) {
|
||||
let hostAndIp = notebookUtils.getHostAndPortFromEndpoint(gatewayEndpointInfo.endpoint);
|
||||
let host = hostAndIp.host ? hostAndIp.host : model.context.serverName;
|
||||
let port = hostAndIp.port ? ':' + hostAndIp.port : defaultPort;
|
||||
let html = result.data['text/html'];
|
||||
// CTP 3.1 and earlier Spark link
|
||||
html = this.rewriteUrlUsingRegex(/(https?:\/\/master.*\/proxy)(.*)/g, html, host, port, yarnUi);
|
||||
// CTP 3.2 and later spark link
|
||||
html = this.rewriteUrlUsingRegex(/(https?:\/\/sparkhead.*\/proxy)(.*)/g, html, host, port, yarnUi);
|
||||
// Driver link
|
||||
html = this.rewriteUrlUsingRegex(/(https?:\/\/storage.*\/containerlogs)(.*)/g, html, host, port, driverLog);
|
||||
(<nb.IDisplayResult>output).data['text/html'] = html;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) { }
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private rewriteUrlUsingRegex(regex: RegExp, html: string, host: string, port: string, target: string): string {
|
||||
return html.replace(regex, function (a, b, c) {
|
||||
let ret = '';
|
||||
if (b !== '') {
|
||||
ret = 'https://' + host + port + target;
|
||||
}
|
||||
if (c !== '') {
|
||||
ret = ret + c;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
}
|
||||
|
||||
public setStdInHandler(handler: nb.MessageHandler<nb.IStdinMessage>): void {
|
||||
this._stdInHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* StdIn requires user interaction, so this is deferred to upstream UI
|
||||
* components. If one is registered the cell will call and wait on it, if not
|
||||
* it will immediately return to unblock error handling
|
||||
*/
|
||||
private handleSdtIn(msg: nb.IStdinMessage): void | Thenable<void> {
|
||||
let handler = async () => {
|
||||
if (!this._stdInHandler) {
|
||||
// No-op
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this._stdInHandler.handle(msg);
|
||||
} catch (err) {
|
||||
if (this.future) {
|
||||
// TODO should we error out in this case somehow? E.g. send Ctrl+C?
|
||||
this.future.sendInputReply({ value: '' });
|
||||
}
|
||||
}
|
||||
};
|
||||
return handler();
|
||||
}
|
||||
|
||||
public toJSON(): nb.ICellContents {
|
||||
let metadata = this._metadata || {};
|
||||
let cellJson: Partial<nb.ICellContents> = {
|
||||
cell_type: this._cellType,
|
||||
source: this._source,
|
||||
metadata: metadata
|
||||
};
|
||||
cellJson.metadata.azdata_cell_guid = this._cellGuid;
|
||||
if (this._cellType === CellTypes.Code) {
|
||||
cellJson.metadata.language = this._language;
|
||||
cellJson.metadata.tags = metadata.tags;
|
||||
cellJson.outputs = this._outputs;
|
||||
cellJson.execution_count = this.executionCount ? this.executionCount : null;
|
||||
}
|
||||
return cellJson as nb.ICellContents;
|
||||
}
|
||||
|
||||
public fromJSON(cell: nb.ICellContents): void {
|
||||
if (!cell) {
|
||||
return;
|
||||
}
|
||||
this._cellType = cell.cell_type;
|
||||
this.executionCount = cell.execution_count;
|
||||
this._source = this.getMultilineSource(cell.source);
|
||||
this._metadata = cell.metadata || {};
|
||||
|
||||
if (this._metadata.tags && this._metadata.tags.some(x => x === HideInputTag) && this._cellType === CellTypes.Code) {
|
||||
this._isCollapsed = true;
|
||||
} else {
|
||||
this._isCollapsed = false;
|
||||
}
|
||||
|
||||
this._cellGuid = cell.metadata && cell.metadata.azdata_cell_guid ? cell.metadata.azdata_cell_guid : generateUuid();
|
||||
this.setLanguageFromContents(cell);
|
||||
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 setLanguageFromContents(cell: nb.ICellContents): void {
|
||||
if (cell.cell_type === CellTypes.Markdown) {
|
||||
this._language = 'markdown';
|
||||
} else if (cell.metadata && cell.metadata.language) {
|
||||
this._language = cell.metadata.language;
|
||||
}
|
||||
// else skip, we set default language anyhow
|
||||
}
|
||||
|
||||
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 createUri(): void {
|
||||
let uri = URI.from({ scheme: Schemas.untitled, path: `notebook-editor-${this.id}` });
|
||||
// Use this to set the internal (immutable) and public (shared with extension) uri properties
|
||||
this.cellUri = uri;
|
||||
}
|
||||
|
||||
// Get Knox endpoint from IConnectionProfile
|
||||
// TODO: this will be refactored out into the notebooks extension as a contribution point
|
||||
private getGatewayEndpoint(activeConnection: IConnectionProfile): notebookUtils.IEndpoint {
|
||||
let endpoint;
|
||||
if (this._connectionManagementService && activeConnection && activeConnection.providerName.toLowerCase() === notebookConstants.SQL_CONNECTION_PROVIDER.toLowerCase()) {
|
||||
let serverInfo: ServerInfo = this._connectionManagementService.getServerInfo(activeConnection.id);
|
||||
if (serverInfo) {
|
||||
let endpoints: notebookUtils.IEndpoint[] = notebookUtils.getClusterEndpoints(serverInfo);
|
||||
if (endpoints && endpoints.length > 0) {
|
||||
endpoint = find(endpoints, ep => ep.serviceName.toLowerCase() === notebookUtils.hadoopEndpointNameGateway);
|
||||
}
|
||||
}
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
|
||||
private getMultilineSource(source: string | string[]): string | string[] {
|
||||
if (source === undefined) {
|
||||
return [];
|
||||
}
|
||||
if (typeof source === 'string') {
|
||||
let sourceMultiline = source.split('\n');
|
||||
// If source is one line (i.e. no '\n'), return it immediately
|
||||
if (sourceMultiline.length === 1) {
|
||||
return [source];
|
||||
} else if (sourceMultiline.length === 0) {
|
||||
return [];
|
||||
}
|
||||
// Otherwise, add back all of the newlines here
|
||||
// Note: for Windows machines that require '/r/n',
|
||||
// splitting on '\n' and putting back the '\n' will still
|
||||
// retain the '\r', so that isn't lost in the process
|
||||
// Note: the last line will not include a newline at the end
|
||||
for (let i = 0; i < sourceMultiline.length - 1; i++) {
|
||||
sourceMultiline[i] += '\n';
|
||||
}
|
||||
return sourceMultiline;
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
// Dispose and set current future to undefined
|
||||
private disposeFuture() {
|
||||
if (this._future) {
|
||||
this._future.dispose();
|
||||
}
|
||||
this._future = undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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
|
||||
|
||||
import { nb } from 'azdata';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { localize } from 'vs/nls';
|
||||
import { getErrorMessage } from 'vs/base/common/errors';
|
||||
|
||||
import { IClientSession, IKernelPreference, IClientSessionOptions } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
import { INotebookManager } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
|
||||
type KernelChangeHandler = (kernel: nb.IKernelChangedArgs) => Promise<void>;
|
||||
/**
|
||||
* 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 _notebookUri: URI;
|
||||
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;
|
||||
private _cachedKernelSpec: nb.IKernelSpec;
|
||||
private _kernelChangeHandlers: KernelChangeHandler[] = [];
|
||||
private _defaultKernel: nb.IKernelSpec;
|
||||
|
||||
//#endregion
|
||||
|
||||
private _serverLoadFinished: Promise<void>;
|
||||
private _session: nb.ISession;
|
||||
private isServerStarted: boolean;
|
||||
private notebookManager: INotebookManager;
|
||||
private _kernelConfigActions: ((kernelName: string) => Promise<any>)[] = [];
|
||||
|
||||
constructor(private options: IClientSessionOptions) {
|
||||
this._notebookUri = options.notebookUri;
|
||||
this.notebookManager = options.notebookManager;
|
||||
this._isReady = false;
|
||||
this._ready = new Deferred<void>();
|
||||
this._kernelChangeCompleted = new Deferred<void>();
|
||||
this._defaultKernel = options.kernelSpec;
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
try {
|
||||
this._serverLoadFinished = this.startServer();
|
||||
await this._serverLoadFinished;
|
||||
await this.initializeSession();
|
||||
await this.updateCachedKernelSpec();
|
||||
} catch (err) {
|
||||
this._errorMessage = getErrorMessage(err) || localize('clientSession.unknownError', "An error occurred while starting the notebook session");
|
||||
}
|
||||
// Always resolving for now. It's up to callers to check for error case
|
||||
this._isReady = true;
|
||||
this._ready.resolve();
|
||||
if (!this.isInErrorState && this._session && this._session.kernel) {
|
||||
await this.notifyKernelChanged(undefined, this._session.kernel);
|
||||
}
|
||||
}
|
||||
|
||||
private async startServer(): Promise<void> {
|
||||
let serverManager = this.notebookManager.serverManager;
|
||||
if (serverManager) {
|
||||
await serverManager.startServer();
|
||||
if (!serverManager.isStarted) {
|
||||
throw new Error(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._defaultKernel) {
|
||||
await this.startSessionInstance(this._defaultKernel.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async startSessionInstance(kernelName: string): Promise<void> {
|
||||
let session: nb.ISession;
|
||||
try {
|
||||
// TODO #3164 should use URI instead of path for startNew
|
||||
session = await this.notebookManager.sessionManager.startNew({
|
||||
path: this.notebookUri.fsPath,
|
||||
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(localize('kernelRequiresConnection', "Kernel {0} was not found. The default kernel will be used instead.", kernelName));
|
||||
session = await this.notebookManager.sessionManager.startNew({
|
||||
path: this.notebookUri.fsPath,
|
||||
kernelName: undefined
|
||||
});
|
||||
session.defaultKernelLoaded = false;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
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 onKernelChanging(changeHandler: (kernel: nb.IKernelChangedArgs) => Promise<void>): void {
|
||||
if (changeHandler) {
|
||||
this._kernelChangeHandlers.push(changeHandler);
|
||||
}
|
||||
}
|
||||
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 notebookUri(): URI {
|
||||
return this._notebookUri;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
public get cachedKernelSpec(): nb.IKernelSpec {
|
||||
return this._cachedKernelSpec;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Not Yet Implemented
|
||||
/**
|
||||
* Change the current kernel associated with the document.
|
||||
*/
|
||||
async changeKernel(options: nb.IKernelSpec, oldValue?: nb.IKernel): Promise<nb.IKernel> {
|
||||
this._kernelChangeCompleted = new Deferred<void>();
|
||||
this._isReady = false;
|
||||
let oldKernel = oldValue ? oldValue : 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;
|
||||
}
|
||||
let newKernel = this._session ? kernel : this._session.kernel;
|
||||
this._isReady = kernel.isReady;
|
||||
await this.updateCachedKernelSpec();
|
||||
// Send resolution events to listeners
|
||||
await this.notifyKernelChanged(oldKernel, newKernel);
|
||||
return kernel;
|
||||
}
|
||||
|
||||
private async notifyKernelChanged(oldKernel: nb.IKernel, newKernel: nb.IKernel): Promise<void> {
|
||||
let changeArgs: nb.IKernelChangedArgs = {
|
||||
oldValue: oldKernel,
|
||||
newValue: newKernel
|
||||
};
|
||||
let changePromises = this._kernelChangeHandlers.map(handler => handler(changeArgs));
|
||||
await Promise.all(changePromises);
|
||||
// Wait on connection configuration to complete before resolving full kernel change
|
||||
this._kernelChangeCompleted.resolve();
|
||||
this._kernelChangedEmitter.fire(changeArgs);
|
||||
}
|
||||
|
||||
private async updateCachedKernelSpec(): Promise<void> {
|
||||
this._cachedKernelSpec = undefined;
|
||||
let kernel = this.kernel;
|
||||
if (kernel) {
|
||||
await kernel.ready;
|
||||
if (kernel.isReady) {
|
||||
this._cachedKernelSpec = await kernel.getSpec();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to either call ChangeKernel on current session, or start a new session
|
||||
*/
|
||||
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 configureKernel(options: nb.IKernelSpec): Promise<void> {
|
||||
if (this._session) {
|
||||
await this._session.configureKernel(options);
|
||||
}
|
||||
}
|
||||
|
||||
public async updateConnection(connection: IConnectionProfile): Promise<void> {
|
||||
if (!this.kernel) {
|
||||
// TODO is there any case where skipping causes errors? So far it seems like it gets called twice
|
||||
return;
|
||||
}
|
||||
if (connection.id !== '-1') {
|
||||
await this._session.configureConnection(connection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 && this.notebookManager.sessionManager) {
|
||||
await this.notebookManager.sessionManager.shutdown(this._session.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { nb } from 'azdata';
|
||||
|
||||
import { CellModel } from 'sql/workbench/services/notebook/browser/models/cell';
|
||||
import { IClientSession, IClientSessionOptions, ICellModelOptions, ICellModel, IModelFactory } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
|
||||
import { ClientSession } from 'sql/workbench/services/notebook/browser/models/clientSession';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export class ModelFactory implements IModelFactory {
|
||||
|
||||
constructor(private instantiationService: IInstantiationService) {
|
||||
|
||||
}
|
||||
public createCell(cell: nb.ICellContents, options: ICellModelOptions): ICellModel {
|
||||
return this.instantiationService.createInstance(CellModel, cell, options);
|
||||
}
|
||||
|
||||
public createClientSession(options: IClientSessionOptions): IClientSession {
|
||||
return new ClientSession(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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
|
||||
|
||||
import { nb } from 'azdata';
|
||||
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/workbench/services/notebook/common/contracts';
|
||||
import { INotebookManager, ILanguageMagic } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { IStandardKernelWithProvider } from 'sql/workbench/services/notebook/browser/models/notebookUtils';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel';
|
||||
import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
|
||||
import type { FutureInternal } from 'sql/workbench/services/notebook/browser/interfaces';
|
||||
|
||||
export interface IClientSessionOptions {
|
||||
notebookUri: URI;
|
||||
notebookManager: INotebookManager;
|
||||
notificationService: INotificationService;
|
||||
kernelSpec: nb.IKernelSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 notebookUri: URI;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
readonly cachedKernelSpec: nb.IKernelSpec;
|
||||
|
||||
/**
|
||||
* 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(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Change the current kernel associated with the document.
|
||||
*/
|
||||
changeKernel(
|
||||
options: nb.IKernelSpec,
|
||||
oldKernel?: nb.IKernel
|
||||
): Promise<nb.IKernel>;
|
||||
|
||||
/**
|
||||
* Configure the current kernel associated with the document.
|
||||
*/
|
||||
configureKernel(
|
||||
options: nb.IKernelSpec
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* 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: IConnectionProfile): Promise<void>;
|
||||
|
||||
/**
|
||||
* Supports registering a handler to run during kernel change and implement any calls needed to configure
|
||||
* the kernel before actions such as run should be allowed
|
||||
*/
|
||||
onKernelChanging(changeHandler: ((kernel: nb.IKernelChangedArgs) => Promise<void>)): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
/**
|
||||
* Cell List for this model
|
||||
*/
|
||||
readonly cells: ReadonlyArray<ICellModel>;
|
||||
|
||||
/**
|
||||
* The active cell for this model. May be undefined
|
||||
*/
|
||||
activeCell: ICellModel | undefined;
|
||||
|
||||
/**
|
||||
* Client Session in the notebook, used for sending requests to the notebook service
|
||||
*/
|
||||
readonly clientSession: IClientSession;
|
||||
/**
|
||||
* LanguageInfo saved in the notebook
|
||||
*/
|
||||
readonly languageInfo: nb.ILanguageInfo;
|
||||
/**
|
||||
* Current default language for the notebook
|
||||
*/
|
||||
readonly language: string;
|
||||
|
||||
/**
|
||||
* All notebook managers applicable for a given notebook
|
||||
*/
|
||||
readonly notebookManagers: INotebookManager[];
|
||||
|
||||
/**
|
||||
* Event fired on first initialization of the kernel and
|
||||
* on subsequent change events
|
||||
*/
|
||||
readonly kernelChanged: Event<nb.IKernelChangedArgs>;
|
||||
|
||||
/**
|
||||
* Fired on notifications that notebook components should be re-laid out.
|
||||
*/
|
||||
readonly layoutChanged: Event<void>;
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
|
||||
/**
|
||||
* Event fired on when switching kernel and should show loading context
|
||||
*/
|
||||
readonly contextsLoading: 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 context, or undefined if this has
|
||||
* not been loaded yet
|
||||
*/
|
||||
readonly context: ConnectionProfile | undefined;
|
||||
|
||||
/**
|
||||
* Event fired on first initialization of the cells and
|
||||
* on subsequent change events
|
||||
*/
|
||||
readonly contentChanged: Event<NotebookContentChange>;
|
||||
|
||||
/**
|
||||
* Event fired on notebook provider change
|
||||
*/
|
||||
readonly onProviderIdChange: Event<string>;
|
||||
|
||||
/**
|
||||
* Event fired on active cell change
|
||||
*/
|
||||
readonly onActiveCellChanged: Event<ICellModel>;
|
||||
|
||||
/**
|
||||
* The trusted mode of the Notebook
|
||||
*/
|
||||
trustedMode: boolean;
|
||||
|
||||
/**
|
||||
* Current notebook provider id
|
||||
*/
|
||||
providerId: string;
|
||||
|
||||
/**
|
||||
* 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, connection?: IConnectionProfile, hideErrorMessage?: boolean): Promise<void>;
|
||||
|
||||
/**
|
||||
* Find a cell's index given its model
|
||||
*/
|
||||
findCellIndex(cellModel: ICellModel): number;
|
||||
|
||||
/**
|
||||
* Adds a cell to the index of the model
|
||||
*/
|
||||
addCell(cellType: CellType, index?: number): void;
|
||||
|
||||
/**
|
||||
* Deletes a cell
|
||||
*/
|
||||
deleteCell(cellModel: ICellModel): void;
|
||||
|
||||
/**
|
||||
* Serialize notebook cell content to JSON
|
||||
*/
|
||||
toJSON(type?: NotebookChangeType): nb.INotebookContents;
|
||||
|
||||
/**
|
||||
* Notifies the notebook of a change in the cell
|
||||
*/
|
||||
onCellChange(cell: ICellModel, change: NotebookChangeType): void;
|
||||
|
||||
|
||||
/**
|
||||
* Push edit operations, basically editing the model. This is the preferred way of
|
||||
* editing the model. Long-term, this will ensure edit operations can be added to the undo stack
|
||||
* @param edits The edit operations to perform
|
||||
*/
|
||||
pushEditOperations(edits: ISingleNotebookEditOperation[]): void;
|
||||
|
||||
getApplicableConnectionProviderIds(kernelName: string): string[];
|
||||
|
||||
updateActiveCell(cell: ICellModel): void;
|
||||
|
||||
/**
|
||||
* Get the standardKernelWithProvider by name
|
||||
* @param name The kernel name
|
||||
*/
|
||||
getStandardKernelFromName(name: string): IStandardKernelWithProvider;
|
||||
|
||||
/** Event fired once we get call back from ConfigureConnection method in sqlops extension */
|
||||
readonly onValidConnectionSelected: Event<boolean>;
|
||||
|
||||
serializationStateChanged(changeType: NotebookChangeType, cell?: ICellModel): void;
|
||||
|
||||
standardKernels: IStandardKernelWithProvider[];
|
||||
|
||||
requestConnection(): Promise<boolean>;
|
||||
|
||||
}
|
||||
|
||||
export interface NotebookContentChange {
|
||||
/**
|
||||
* The type of 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
|
||||
*/
|
||||
isDirty?: boolean;
|
||||
|
||||
/**
|
||||
* Text content changed event for cell edits
|
||||
*/
|
||||
modelContentChangedEvent?: IModelContentChangedEvent;
|
||||
}
|
||||
|
||||
export interface ICellModelOptions {
|
||||
notebook: INotebookModel;
|
||||
isTrusted: boolean;
|
||||
}
|
||||
|
||||
export enum CellExecutionState {
|
||||
Hidden = 0,
|
||||
Stopped = 1,
|
||||
Running = 2,
|
||||
Error = 3
|
||||
}
|
||||
|
||||
export interface IOutputChangedEvent {
|
||||
outputs: ReadonlyArray<nb.ICellOutput>;
|
||||
shouldScroll: boolean;
|
||||
}
|
||||
|
||||
export interface ICellModel {
|
||||
cellUri: URI;
|
||||
id: string;
|
||||
readonly language: string;
|
||||
readonly cellGuid: string;
|
||||
source: string | string[];
|
||||
cellType: CellType;
|
||||
trustedMode: boolean;
|
||||
active: boolean;
|
||||
hover: boolean;
|
||||
executionCount: number | undefined;
|
||||
readonly future: FutureInternal;
|
||||
readonly outputs: ReadonlyArray<nb.ICellOutput>;
|
||||
readonly onOutputsChanged: Event<IOutputChangedEvent>;
|
||||
readonly onExecutionStateChange: Event<CellExecutionState>;
|
||||
readonly executionState: CellExecutionState;
|
||||
readonly notebookModel: NotebookModel;
|
||||
setFuture(future: FutureInternal): void;
|
||||
setStdInHandler(handler: nb.MessageHandler<nb.IStdinMessage>): void;
|
||||
runCell(notificationService?: INotificationService, connectionManagementService?: IConnectionManagementService): Promise<boolean>;
|
||||
setOverrideLanguage(language: string);
|
||||
equals(cellModel: ICellModel): boolean;
|
||||
toJSON(): nb.ICellContents;
|
||||
loaded: boolean;
|
||||
stdInVisible: boolean;
|
||||
readonly onLoaded: Event<string>;
|
||||
isCollapsed: boolean;
|
||||
readonly onCollapseStateChanged: Event<boolean>;
|
||||
modelContentChangedEvent: IModelContentChangedEvent;
|
||||
isEditMode: boolean;
|
||||
readonly ariaLabel: string;
|
||||
}
|
||||
|
||||
export interface IModelFactory {
|
||||
|
||||
createCell(cell: nb.ICellContents, options: ICellModelOptions): ICellModel;
|
||||
createClientSession(options: IClientSessionOptions): IClientSession;
|
||||
}
|
||||
|
||||
export interface IContentManager {
|
||||
/**
|
||||
* This is a specialized method intended to load for a default context - just the current Notebook's URI
|
||||
*/
|
||||
loadContent(): Promise<nb.INotebookContents>;
|
||||
}
|
||||
|
||||
export interface INotebookModelOptions {
|
||||
/**
|
||||
* Path to the local or remote notebook
|
||||
*/
|
||||
notebookUri: URI;
|
||||
|
||||
/**
|
||||
* Factory for creating cells and client sessions
|
||||
*/
|
||||
factory: IModelFactory;
|
||||
|
||||
contentManager: IContentManager;
|
||||
notebookManagers: INotebookManager[];
|
||||
providerId: string;
|
||||
defaultKernel: nb.IKernelSpec;
|
||||
cellMagicMapper: ICellMagicMapper;
|
||||
|
||||
layoutChanged: Event<void>;
|
||||
|
||||
notificationService: INotificationService;
|
||||
connectionService: IConnectionManagementService;
|
||||
capabilitiesService: ICapabilitiesService;
|
||||
editorLoadedTimestamp?: number;
|
||||
}
|
||||
|
||||
export interface ICellMagicMapper {
|
||||
/**
|
||||
* Tries to find a language mapping for an identified cell magic
|
||||
* @param magic a string defining magic. For example for %%sql the magic text is sql
|
||||
* @param kernelId the name of the current kernel to use when looking up magics
|
||||
*/
|
||||
toLanguageMagic(magic: string, kernelId: string): ILanguageMagic | undefined;
|
||||
}
|
||||
|
||||
export interface INotebookContentsEditable {
|
||||
cells: nb.ICellContents[];
|
||||
metadata: nb.INotebookMetadata;
|
||||
nbformat: number;
|
||||
nbformat_minor: number;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||
|
||||
|
||||
export class NotebookContexts {
|
||||
|
||||
public static get DefaultContext(): ConnectionProfile {
|
||||
return <any>{
|
||||
providerName: mssqlProviderName,
|
||||
id: '-1',
|
||||
serverName: localize('selectConnection', "Select Connection")
|
||||
};
|
||||
}
|
||||
|
||||
public static get LocalContext(): ConnectionProfile {
|
||||
return <any>{
|
||||
providerName: mssqlProviderName,
|
||||
id: '-1',
|
||||
serverName: localize('localhost', "localhost")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the applicable context for a given kernel
|
||||
* @param context current connection profile
|
||||
* @param connProviderIds array of connection provider ids applicable for a kernel
|
||||
*/
|
||||
public static getContextForKernel(context: ConnectionProfile, connProviderIds: string[]): ConnectionProfile {
|
||||
// If no connection provider ids exist for a given kernel, the attach to should show localhost
|
||||
if (connProviderIds.length === 0) {
|
||||
return NotebookContexts.LocalContext;
|
||||
}
|
||||
if (context && context.providerName && connProviderIds.filter(p => p === context.providerName).length > 0) {
|
||||
return context;
|
||||
}
|
||||
return NotebookContexts.DefaultContext;
|
||||
}
|
||||
|
||||
}
|
||||
1007
src/sql/workbench/services/notebook/browser/models/notebookModel.ts
Normal file
1007
src/sql/workbench/services/notebook/browser/models/notebookModel.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,127 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'vs/base/common/path';
|
||||
import { nb, ServerInfo } from 'azdata';
|
||||
import { DEFAULT_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_FILETYPE, INotebookService } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
|
||||
export const clusterEndpointsProperty = 'clusterEndpoints';
|
||||
export const hadoopEndpointNameGateway = 'gateway';
|
||||
/**
|
||||
* 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 getProvidersForFileName(fileName: string, notebookService: INotebookService): string[] {
|
||||
let fileExt = path.extname(fileName);
|
||||
let providers: string[];
|
||||
// First try to get provider for actual file type
|
||||
if (fileExt && startsWith(fileExt, '.')) {
|
||||
fileExt = fileExt.slice(1, fileExt.length);
|
||||
providers = notebookService.getProvidersForFileType(fileExt);
|
||||
}
|
||||
// Fallback to provider for default file type (assume this is a global handler)
|
||||
if (!providers) {
|
||||
providers = notebookService.getProvidersForFileType(DEFAULT_NOTEBOOK_FILETYPE);
|
||||
}
|
||||
// Finally if all else fails, use the built-in handler
|
||||
if (!providers) {
|
||||
providers = [DEFAULT_NOTEBOOK_PROVIDER];
|
||||
}
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function getStandardKernelsForProvider(providerId: string, notebookService: INotebookService): IStandardKernelWithProvider[] {
|
||||
if (!providerId || !notebookService) {
|
||||
return [];
|
||||
}
|
||||
let standardKernels = notebookService.getStandardKernelsForProvider(providerId);
|
||||
standardKernels.forEach(kernel => {
|
||||
assign(<IStandardKernelWithProvider>kernel, {
|
||||
name: kernel.name,
|
||||
connectionProviderIds: kernel.connectionProviderIds,
|
||||
notebookProvider: providerId
|
||||
});
|
||||
});
|
||||
return <IStandardKernelWithProvider[]>(standardKernels);
|
||||
}
|
||||
|
||||
export interface IStandardKernelWithProvider {
|
||||
readonly name: string;
|
||||
readonly displayName: string;
|
||||
readonly connectionProviderIds: string[];
|
||||
readonly notebookProvider: string;
|
||||
}
|
||||
|
||||
export interface IEndpoint {
|
||||
serviceName: string;
|
||||
description: string;
|
||||
endpoint: string;
|
||||
protocol: string;
|
||||
}
|
||||
|
||||
export async function asyncForEach(array: any[], callback: Function): Promise<any> {
|
||||
if (array && callback) {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getClusterEndpoints(serverInfo: ServerInfo): IEndpoint[] | undefined {
|
||||
let endpoints: RawEndpoint[] = serverInfo.options[clusterEndpointsProperty];
|
||||
if (!endpoints || endpoints.length === 0) { return []; }
|
||||
|
||||
return endpoints.map(e => {
|
||||
// If endpoint is missing, we're on CTP bits. All endpoints from the CTP serverInfo should be treated as HTTPS
|
||||
let endpoint = e.endpoint ? e.endpoint : `https://${e.ipAddress}:${e.port}`;
|
||||
let updatedEndpoint: IEndpoint = {
|
||||
serviceName: e.serviceName,
|
||||
description: e.description,
|
||||
endpoint: endpoint,
|
||||
protocol: e.protocol
|
||||
};
|
||||
return updatedEndpoint;
|
||||
});
|
||||
}
|
||||
|
||||
export type HostAndIp = { host: string, port: string };
|
||||
|
||||
export function getHostAndPortFromEndpoint(endpoint: string): HostAndIp {
|
||||
let authority = URI.parse(endpoint).authority;
|
||||
let hostAndPortRegex = /^(.*)([,:](\d+))/g;
|
||||
let match = hostAndPortRegex.exec(authority);
|
||||
if (match) {
|
||||
return {
|
||||
host: match[1],
|
||||
port: match[3]
|
||||
};
|
||||
}
|
||||
return {
|
||||
host: authority,
|
||||
port: undefined
|
||||
};
|
||||
}
|
||||
|
||||
export interface RawEndpoint {
|
||||
serviceName: string;
|
||||
description?: string;
|
||||
endpoint?: string;
|
||||
protocol?: string;
|
||||
ipAddress?: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
export interface IEndpoint {
|
||||
serviceName: string;
|
||||
description: string;
|
||||
endpoint: string;
|
||||
protocol: string;
|
||||
}
|
||||
@@ -5,19 +5,19 @@
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
import * as vsEvent from 'vs/base/common/event';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { RenderMimeRegistry } from 'sql/workbench/contrib/notebook/browser/outputs/registry';
|
||||
import { ModelFactory } from 'sql/workbench/contrib/notebook/browser/models/modelFactory';
|
||||
import { RenderMimeRegistry } from 'sql/workbench/services/notebook/browser/outputs/registry';
|
||||
import { ModelFactory } from 'sql/workbench/services/notebook/browser/models/modelFactory';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput';
|
||||
import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
import { ICellModel, INotebookModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { ICellModel, INotebookModel, IContentManager } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
|
||||
import { NotebookChangeType, CellType } from 'sql/workbench/services/notebook/common/contracts';
|
||||
import { IBootstrapParams } from 'sql/workbench/services/bootstrap/common/bootstrapParams';
|
||||
import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor';
|
||||
import { NotebookRange } from 'sql/workbench/contrib/notebook/find/notebookFindDecorations';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { IStandardKernelWithProvider } from 'sql/workbench/services/notebook/browser/models/notebookUtils';
|
||||
|
||||
export const SERVICE_ID = 'notebookService';
|
||||
export const INotebookService = createDecorator<INotebookService>(SERVICE_ID);
|
||||
@@ -37,9 +37,9 @@ export interface ILanguageMagic {
|
||||
export interface INotebookService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
readonly onNotebookEditorAdd: vsEvent.Event<INotebookEditor>;
|
||||
readonly onNotebookEditorRemove: vsEvent.Event<INotebookEditor>;
|
||||
onNotebookEditorRename: vsEvent.Event<INotebookEditor>;
|
||||
readonly onNotebookEditorAdd: Event<INotebookEditor>;
|
||||
readonly onNotebookEditorRemove: Event<INotebookEditor>;
|
||||
onNotebookEditorRename: Event<INotebookEditor>;
|
||||
|
||||
readonly isRegistrationComplete: boolean;
|
||||
readonly registrationComplete: Promise<void>;
|
||||
@@ -131,9 +131,21 @@ export interface IProviderInfo {
|
||||
providerId: string;
|
||||
providers: string[];
|
||||
}
|
||||
|
||||
export interface INotebookInput {
|
||||
readonly notebookUri: URI;
|
||||
updateModel(): void;
|
||||
isDirty(): boolean;
|
||||
readonly defaultKernel: azdata.nb.IKernelSpec;
|
||||
readonly editorOpenedTimestamp: number;
|
||||
readonly contentManager: IContentManager;
|
||||
readonly standardKernels: IStandardKernelWithProvider[];
|
||||
readonly layoutChanged: Event<void>;
|
||||
}
|
||||
|
||||
export interface INotebookParams extends IBootstrapParams {
|
||||
notebookUri: URI;
|
||||
input: NotebookInput;
|
||||
input: INotebookInput;
|
||||
providerInfo: Promise<IProviderInfo>;
|
||||
profile?: IConnectionProfile;
|
||||
modelFactory?: ModelFactory;
|
||||
@@ -155,6 +167,18 @@ export interface ICellEditorProvider {
|
||||
deltaDecorations(newDecorationRange: NotebookRange, oldDecorationRange: NotebookRange): void;
|
||||
}
|
||||
|
||||
export class NotebookRange extends Range {
|
||||
updateActiveCell(cell: ICellModel) {
|
||||
this.cell = cell;
|
||||
}
|
||||
cell: ICellModel;
|
||||
|
||||
constructor(cell: ICellModel, startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) {
|
||||
super(startLineNumber, startColumn, endLineNumber, endColumn);
|
||||
this.updateActiveCell(cell);
|
||||
}
|
||||
}
|
||||
|
||||
export interface INotebookEditor {
|
||||
readonly notebookParams: INotebookParams;
|
||||
readonly id: string;
|
||||
@@ -173,7 +197,7 @@ export interface INotebookEditor {
|
||||
getSections(): INotebookSection[];
|
||||
navigateToSection(sectionId: string): void;
|
||||
deltaDecorations(newDecorationRange: NotebookRange, oldDecorationRange: NotebookRange): void;
|
||||
addCell(cellType: CellType, index?: number, event?: Event);
|
||||
addCell(cellType: CellType, index?: number, event?: UIEvent);
|
||||
}
|
||||
|
||||
export interface INavigationProvider {
|
||||
|
||||
@@ -10,10 +10,10 @@ import { Registry } from 'vs/platform/registry/common/platform';
|
||||
|
||||
import {
|
||||
INotebookService, INotebookManager, INotebookProvider,
|
||||
DEFAULT_NOTEBOOK_FILETYPE, INotebookEditor, SQL_NOTEBOOK_PROVIDER, OVERRIDE_EDITOR_THEMING_SETTING, INavigationProvider, ILanguageMagic
|
||||
DEFAULT_NOTEBOOK_FILETYPE, INotebookEditor, SQL_NOTEBOOK_PROVIDER, INavigationProvider, ILanguageMagic
|
||||
} from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { RenderMimeRegistry } from 'sql/workbench/contrib/notebook/browser/outputs/registry';
|
||||
import { standardRendererFactories } from 'sql/workbench/contrib/notebook/browser/outputs/factories';
|
||||
import { RenderMimeRegistry } from 'sql/workbench/services/notebook/browser/outputs/registry';
|
||||
import { standardRendererFactories } from 'sql/workbench/services/notebook/browser/outputs/factories';
|
||||
import { Extensions, INotebookProviderRegistry, NotebookProviderRegistration } from 'sql/workbench/services/notebook/common/notebookRegistry';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Memento } from 'vs/workbench/common/memento';
|
||||
@@ -23,17 +23,10 @@ import { IExtensionManagementService, IExtensionIdentifier } from 'vs/platform/e
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { NotebookEditorVisibleContext } from 'sql/workbench/services/notebook/common/notebookContext';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { NotebookEditor } from 'sql/workbench/contrib/notebook/browser/notebookEditor';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { registerNotebookThemes } from 'sql/workbench/contrib/notebook/browser/notebookStyles';
|
||||
import { IQueryManagementService } from 'sql/workbench/services/query/common/queryManagement';
|
||||
import { notebookConstants, ICellModel } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { SqlNotebookProvider } from 'sql/workbench/services/notebook/browser/sql/sqlNotebookProvider';
|
||||
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { keys } from 'vs/base/common/map';
|
||||
import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
@@ -42,6 +35,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { NotebookChangeType } from 'sql/workbench/services/notebook/common/contracts';
|
||||
import { find, firstIndex } from 'vs/base/common/arrays';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { notebookConstants } from 'sql/workbench/services/notebook/browser/interfaces';
|
||||
|
||||
export interface NotebookProviderProperties {
|
||||
provider: string;
|
||||
@@ -109,9 +103,7 @@ export class NotebookService extends Disposable implements INotebookService {
|
||||
private _providerToStandardKernels = new Map<string, nb.IStandardKernel[]>();
|
||||
private _registrationComplete = new Deferred<void>();
|
||||
private _isRegistrationComplete = false;
|
||||
private notebookEditorVisible: IContextKey<boolean>;
|
||||
private _themeParticipant: IDisposable;
|
||||
private _overrideEditorThemeSetting: boolean;
|
||||
private _trustedCacheQueue: URI[] = [];
|
||||
private _unTrustedCacheQueue: URI[] = [];
|
||||
|
||||
@@ -121,10 +113,6 @@ export class NotebookService extends Disposable implements INotebookService {
|
||||
@IExtensionService private _extensionService: IExtensionService,
|
||||
@IExtensionManagementService extensionManagementService: IExtensionManagementService,
|
||||
@IInstantiationService private _instantiationService: IInstantiationService,
|
||||
@IContextKeyService private _contextKeyService: IContextKeyService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IQueryManagementService private readonly _queryManagementService: IQueryManagementService,
|
||||
@@ -166,9 +154,6 @@ export class NotebookService extends Disposable implements INotebookService {
|
||||
}
|
||||
|
||||
lifecycleService.onWillShutdown(() => this.shutdown());
|
||||
this.hookContextKeyListeners();
|
||||
this.hookNotebookThemesAndConfigListener();
|
||||
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
@@ -178,34 +163,6 @@ export class NotebookService extends Disposable implements INotebookService {
|
||||
}
|
||||
}
|
||||
|
||||
private hookContextKeyListeners(): void {
|
||||
const updateEditorContextKeys = () => {
|
||||
const visibleEditors = this._editorService.visibleControls;
|
||||
this.notebookEditorVisible.set(visibleEditors.some(control => control.getId() === NotebookEditor.ID));
|
||||
};
|
||||
if (this._contextKeyService) {
|
||||
this.notebookEditorVisible = NotebookEditorVisibleContext.bindTo(this._contextKeyService);
|
||||
}
|
||||
if (this._editorService) {
|
||||
this._register(this._editorService.onDidActiveEditorChange(() => updateEditorContextKeys()));
|
||||
this._register(this._editorService.onDidVisibleEditorsChange(() => updateEditorContextKeys()));
|
||||
this._register(this._editorGroupsService.onDidAddGroup(() => updateEditorContextKeys()));
|
||||
this._register(this._editorGroupsService.onDidRemoveGroup(() => updateEditorContextKeys()));
|
||||
}
|
||||
}
|
||||
|
||||
private hookNotebookThemesAndConfigListener(): void {
|
||||
if (this._configurationService) {
|
||||
this.updateNotebookThemes();
|
||||
this._register(this._configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration(OVERRIDE_EDITOR_THEMING_SETTING)
|
||||
|| e.affectsConfiguration('resultsGrid')) {
|
||||
this.updateNotebookThemes();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private updateSQLRegistrationWithConnectionProviders() {
|
||||
// Update the SQL extension
|
||||
let sqlNotebookProvider = this._providerToStandardKernels.get(notebookConstants.SQL);
|
||||
@@ -224,19 +181,6 @@ export class NotebookService extends Disposable implements INotebookService {
|
||||
this._registrationComplete.resolve();
|
||||
}
|
||||
|
||||
private updateNotebookThemes() {
|
||||
let overrideEditorSetting = this._configurationService.getValue<boolean>(OVERRIDE_EDITOR_THEMING_SETTING);
|
||||
if (overrideEditorSetting !== this._overrideEditorThemeSetting) {
|
||||
// Re-add the participant since this will trigger update of theming rules, can't just
|
||||
// update something and ask to change
|
||||
if (this._themeParticipant) {
|
||||
this._themeParticipant.dispose();
|
||||
}
|
||||
this._overrideEditorThemeSetting = overrideEditorSetting;
|
||||
this._themeParticipant = registerNotebookThemes(overrideEditorSetting, this._configurationService);
|
||||
}
|
||||
}
|
||||
|
||||
private updateRegisteredProviders(p: { id: string; registration: NotebookProviderRegistration; }) {
|
||||
let registration = p.registration;
|
||||
|
||||
|
||||
105
src/sql/workbench/services/notebook/browser/outputs/factories.ts
Normal file
105
src/sql/workbench/services/notebook/browser/outputs/factories.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
import * as widgets from 'sql/workbench/contrib/notebook/browser/outputs/widgets';
|
||||
import { IRenderMime } from './renderMimeInterfaces';
|
||||
|
||||
/**
|
||||
* A mime renderer factory for raw html.
|
||||
*/
|
||||
export const htmlRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: true,
|
||||
mimeTypes: ['text/html'],
|
||||
defaultRank: 50,
|
||||
createRenderer: options => new widgets.RenderedHTML(options)
|
||||
};
|
||||
|
||||
/**
|
||||
* A mime renderer factory for images.
|
||||
*/
|
||||
export const imageRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: true,
|
||||
mimeTypes: ['image/bmp', 'image/png', 'image/jpeg', 'image/gif'],
|
||||
defaultRank: 90,
|
||||
createRenderer: options => new widgets.RenderedImage(options)
|
||||
};
|
||||
|
||||
// /**
|
||||
// * A mime renderer factory for LaTeX.
|
||||
// */
|
||||
// export const latexRendererFactory: IRenderMime.IRendererFactory = {
|
||||
// safe: true,
|
||||
// mimeTypes: ['text/latex'],
|
||||
// defaultRank: 70,
|
||||
// createRenderer: options => new widgets.RenderedLatex(options)
|
||||
// };
|
||||
|
||||
/**
|
||||
* A mime renderer factory for svg.
|
||||
*/
|
||||
export const svgRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: false,
|
||||
mimeTypes: ['image/svg+xml'],
|
||||
defaultRank: 80,
|
||||
createRenderer: options => new widgets.RenderedSVG(options)
|
||||
};
|
||||
|
||||
/**
|
||||
* A mime renderer factory for plain and jupyter console text data.
|
||||
*/
|
||||
export const textRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: true,
|
||||
mimeTypes: [
|
||||
'text/plain',
|
||||
'application/vnd.jupyter.stdout',
|
||||
'application/vnd.jupyter.stderr'
|
||||
],
|
||||
defaultRank: 120,
|
||||
createRenderer: options => new widgets.RenderedText(options)
|
||||
};
|
||||
|
||||
/**
|
||||
* A placeholder factory for deprecated rendered JavaScript.
|
||||
*/
|
||||
export const javaScriptRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: false,
|
||||
mimeTypes: ['text/javascript', 'application/javascript'],
|
||||
defaultRank: 110,
|
||||
createRenderer: options => new widgets.RenderedJavaScript(options)
|
||||
};
|
||||
|
||||
export const dataResourceRendererFactory: IRenderMime.IRendererFactory = {
|
||||
safe: true,
|
||||
mimeTypes: [
|
||||
'application/vnd.dataresource+json',
|
||||
'application/vnd.dataresource'
|
||||
],
|
||||
defaultRank: 40,
|
||||
createRenderer: options => new widgets.RenderedDataResource(options)
|
||||
};
|
||||
|
||||
export const ipywidgetFactory: IRenderMime.IRendererFactory = {
|
||||
safe: false,
|
||||
mimeTypes: [
|
||||
'application/vnd.jupyter.widget-view',
|
||||
'application/vnd.jupyter.widget-view+json'
|
||||
],
|
||||
defaultRank: 45,
|
||||
createRenderer: options => new widgets.RenderedIPyWidget(options)
|
||||
};
|
||||
|
||||
/**
|
||||
* The standard factories provided by the rendermime package.
|
||||
*/
|
||||
export const standardRendererFactories: ReadonlyArray<IRenderMime.IRendererFactory> = [
|
||||
htmlRendererFactory,
|
||||
// latexRendererFactory,
|
||||
svgRendererFactory,
|
||||
imageRendererFactory,
|
||||
javaScriptRendererFactory,
|
||||
textRendererFactory,
|
||||
dataResourceRendererFactory,
|
||||
ipywidgetFactory
|
||||
];
|
||||
100
src/sql/workbench/services/notebook/browser/outputs/mimemodel.ts
Normal file
100
src/sql/workbench/services/notebook/browser/outputs/mimemodel.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
import { IRenderMime } from 'sql/workbench/services/notebook/browser/outputs/renderMimeInterfaces';
|
||||
import { ReadonlyJSONObject } from 'sql/workbench/services/notebook/common/jsonext';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
/**
|
||||
* The default mime model implementation.
|
||||
*/
|
||||
export class MimeModel implements IRenderMime.IMimeModel {
|
||||
/**
|
||||
* Construct a new mime model.
|
||||
*/
|
||||
constructor(options: MimeModel.IOptions = {}) {
|
||||
this.trusted = !!options.trusted;
|
||||
this._data = options.data || {};
|
||||
this._metadata = options.metadata || {};
|
||||
this._callback = options.callback;
|
||||
this._themeService = options.themeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the model is trusted.
|
||||
*/
|
||||
readonly trusted: boolean;
|
||||
|
||||
/**
|
||||
* The data associated with the model.
|
||||
*/
|
||||
get data(): ReadonlyJSONObject {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
/**
|
||||
* The metadata associated with the model.
|
||||
*/
|
||||
get metadata(): ReadonlyJSONObject {
|
||||
return this._metadata;
|
||||
}
|
||||
|
||||
get themeService(): IThemeService {
|
||||
return this._themeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the data associated with the model.
|
||||
*
|
||||
* #### Notes
|
||||
* Depending on the implementation of the mime model,
|
||||
* this call may or may not have deferred effects,
|
||||
*/
|
||||
setData(options: IRenderMime.ISetDataOptions): void {
|
||||
this._data = options.data || this._data;
|
||||
this._metadata = options.metadata || this._metadata;
|
||||
this._callback(options);
|
||||
}
|
||||
|
||||
private _callback: (options: IRenderMime.ISetDataOptions) => void;
|
||||
private _data: ReadonlyJSONObject;
|
||||
private _metadata: ReadonlyJSONObject;
|
||||
private _themeService: IThemeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for MimeModel class statics.
|
||||
*/
|
||||
export namespace MimeModel {
|
||||
/**
|
||||
* The options used to create a mime model.
|
||||
*/
|
||||
export interface IOptions {
|
||||
/**
|
||||
* Whether the model is trusted. Defaults to `false`.
|
||||
*/
|
||||
trusted?: boolean;
|
||||
|
||||
/**
|
||||
* A callback function for when the data changes.
|
||||
*/
|
||||
callback?: (options: IRenderMime.ISetDataOptions) => void;
|
||||
|
||||
/**
|
||||
* The initial mime data.
|
||||
*/
|
||||
data?: ReadonlyJSONObject;
|
||||
|
||||
/**
|
||||
* The initial mime metadata.
|
||||
*/
|
||||
metadata?: ReadonlyJSONObject;
|
||||
|
||||
/**
|
||||
* Theme service used to react to theme change events
|
||||
*/
|
||||
themeService?: IThemeService;
|
||||
}
|
||||
}
|
||||
352
src/sql/workbench/services/notebook/browser/outputs/registry.ts
Normal file
352
src/sql/workbench/services/notebook/browser/outputs/registry.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
|
||||
import { defaultSanitizer } from './sanitizer';
|
||||
import { ReadonlyJSONObject } from 'sql/workbench/services/notebook/common/jsonext';
|
||||
import { IRenderMime } from 'sql/workbench/services/notebook/browser/outputs/renderMimeInterfaces';
|
||||
import { MimeModel } from 'sql/workbench/services/notebook/browser/outputs/mimemodel';
|
||||
|
||||
/**
|
||||
* An object which manages mime renderer factories.
|
||||
*
|
||||
* This object is used to render mime models using registered mime
|
||||
* renderers, selecting the preferred mime renderer to render the
|
||||
* model into a widget.
|
||||
*
|
||||
* #### Notes
|
||||
* This class is not intended to be subclassed.
|
||||
*/
|
||||
export class RenderMimeRegistry {
|
||||
/**
|
||||
* Construct a new rendermime.
|
||||
*
|
||||
* @param options - The options for initializing the instance.
|
||||
*/
|
||||
constructor(options: RenderMimeRegistry.IOptions = {}) {
|
||||
// Parse the options.
|
||||
this.resolver = options.resolver || null;
|
||||
this.linkHandler = options.linkHandler || null;
|
||||
this.latexTypesetter = options.latexTypesetter || null;
|
||||
this.sanitizer = options.sanitizer || defaultSanitizer;
|
||||
|
||||
// Add the initial factories.
|
||||
if (options.initialFactories) {
|
||||
for (let factory of options.initialFactories) {
|
||||
this.addFactory(factory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The sanitizer used by the rendermime instance.
|
||||
*/
|
||||
readonly sanitizer: IRenderMime.ISanitizer;
|
||||
|
||||
/**
|
||||
* The object used to resolve relative urls for the rendermime instance.
|
||||
*/
|
||||
readonly resolver: IRenderMime.IResolver | null;
|
||||
|
||||
/**
|
||||
* The object used to handle path opening links.
|
||||
*/
|
||||
readonly linkHandler: IRenderMime.ILinkHandler | null;
|
||||
|
||||
/**
|
||||
* The LaTeX typesetter for the rendermime.
|
||||
*/
|
||||
readonly latexTypesetter: IRenderMime.ILatexTypesetter | null;
|
||||
|
||||
/**
|
||||
* The ordered list of mimeTypes.
|
||||
*/
|
||||
get mimeTypes(): ReadonlyArray<string> {
|
||||
return this._types || (this._types = Private.sortedTypes(this._ranks));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the preferred mime type for a mime bundle.
|
||||
*
|
||||
* @param bundle - The bundle of mime data.
|
||||
*
|
||||
* @param safe - How to consider safe/unsafe factories. If 'ensure',
|
||||
* it will only consider safe factories. If 'any', any factory will be
|
||||
* considered. If 'prefer', unsafe factories will be considered, but
|
||||
* only after the safe options have been exhausted.
|
||||
*
|
||||
* @returns The preferred mime type from the available factories,
|
||||
* or `undefined` if the mime type cannot be rendered.
|
||||
*/
|
||||
preferredMimeType(
|
||||
bundle: ReadonlyJSONObject,
|
||||
safe: 'ensure' | 'prefer' | 'any' = 'ensure'
|
||||
): string | undefined {
|
||||
// Try to find a safe factory first, if preferred.
|
||||
if (safe === 'ensure' || safe === 'prefer') {
|
||||
for (let mt of this.mimeTypes) {
|
||||
if (mt in bundle && this._factories[mt].safe) {
|
||||
return mt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (safe !== 'ensure') {
|
||||
// Otherwise, search for the best factory among all factories.
|
||||
for (let mt of this.mimeTypes) {
|
||||
if (mt in bundle) {
|
||||
return mt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, no matching mime type exists.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a renderer for a mime type.
|
||||
*
|
||||
* @param mimeType - The mime type of interest.
|
||||
*
|
||||
* @returns A new renderer for the given mime type.
|
||||
*
|
||||
* @throws An error if no factory exists for the mime type.
|
||||
*/
|
||||
createRenderer(mimeType: string): IRenderMime.IRenderer {
|
||||
// Throw an error if no factory exists for the mime type.
|
||||
if (!(mimeType in this._factories)) {
|
||||
throw new Error(`No factory for mime type: '${mimeType}'`);
|
||||
}
|
||||
|
||||
// Invoke the best factory for the given mime type.
|
||||
return this._factories[mimeType].createRenderer({
|
||||
mimeType,
|
||||
resolver: this.resolver,
|
||||
sanitizer: this.sanitizer,
|
||||
linkHandler: this.linkHandler,
|
||||
latexTypesetter: this.latexTypesetter
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new mime model. This is a convenience method.
|
||||
*
|
||||
* @options - The options used to create the model.
|
||||
*
|
||||
* @returns A new mime model.
|
||||
*/
|
||||
createModel(options: MimeModel.IOptions = {}): MimeModel {
|
||||
return new MimeModel(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a clone of this rendermime instance.
|
||||
*
|
||||
* @param options - The options for configuring the clone.
|
||||
*
|
||||
* @returns A new independent clone of the rendermime.
|
||||
*/
|
||||
clone(options: RenderMimeRegistry.ICloneOptions = {}): RenderMimeRegistry {
|
||||
// Create the clone.
|
||||
let clone = new RenderMimeRegistry({
|
||||
resolver: options.resolver || this.resolver || undefined,
|
||||
sanitizer: options.sanitizer || this.sanitizer || undefined,
|
||||
linkHandler: options.linkHandler || this.linkHandler || undefined,
|
||||
latexTypesetter: options.latexTypesetter || this.latexTypesetter
|
||||
});
|
||||
|
||||
// Clone the internal state.
|
||||
clone._factories = { ...this._factories };
|
||||
clone._ranks = { ...this._ranks };
|
||||
clone._id = this._id;
|
||||
|
||||
// Return the cloned object.
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the renderer factory registered for a mime type.
|
||||
*
|
||||
* @param mimeType - The mime type of interest.
|
||||
*
|
||||
* @returns The factory for the mime type, or `undefined`.
|
||||
*/
|
||||
getFactory(mimeType: string): IRenderMime.IRendererFactory | undefined {
|
||||
return this._factories[mimeType];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a renderer factory to the rendermime.
|
||||
*
|
||||
* @param factory - The renderer factory of interest.
|
||||
*
|
||||
* @param rank - The rank of the renderer. A lower rank indicates
|
||||
* a higher priority for rendering. If not given, the rank will
|
||||
* defer to the `defaultRank` of the factory. If no `defaultRank`
|
||||
* is given, it will default to 100.
|
||||
*
|
||||
* #### Notes
|
||||
* The renderer will replace an existing renderer for the given
|
||||
* mimeType.
|
||||
*/
|
||||
addFactory(factory: IRenderMime.IRendererFactory, rank?: number): void {
|
||||
if (rank === undefined) {
|
||||
rank = factory.defaultRank;
|
||||
if (rank === undefined) {
|
||||
rank = 100;
|
||||
}
|
||||
}
|
||||
for (let mt of factory.mimeTypes) {
|
||||
this._factories[mt] = factory;
|
||||
this._ranks[mt] = { rank, id: this._id++ };
|
||||
}
|
||||
this._types = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a mime type.
|
||||
*
|
||||
* @param mimeType - The mime type of interest.
|
||||
*/
|
||||
removeMimeType(mimeType: string): void {
|
||||
delete this._factories[mimeType];
|
||||
delete this._ranks[mimeType];
|
||||
this._types = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rank for a given mime type.
|
||||
*
|
||||
* @param mimeType - The mime type of interest.
|
||||
*
|
||||
* @returns The rank of the mime type or undefined.
|
||||
*/
|
||||
getRank(mimeType: string): number | undefined {
|
||||
let rank = this._ranks[mimeType];
|
||||
return rank && rank.rank;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the rank of a given mime type.
|
||||
*
|
||||
* @param mimeType - The mime type of interest.
|
||||
*
|
||||
* @param rank - The new rank to assign.
|
||||
*
|
||||
* #### Notes
|
||||
* This is a no-op if the mime type is not registered.
|
||||
*/
|
||||
setRank(mimeType: string, rank: number): void {
|
||||
if (!this._ranks[mimeType]) {
|
||||
return;
|
||||
}
|
||||
let id = this._id++;
|
||||
this._ranks[mimeType] = { rank, id };
|
||||
this._types = null;
|
||||
}
|
||||
|
||||
private _id = 0;
|
||||
private _ranks: Private.RankMap = {};
|
||||
private _types: string[] | null = null;
|
||||
private _factories: Private.FactoryMap = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for `RenderMimeRegistry` class statics.
|
||||
*/
|
||||
export namespace RenderMimeRegistry {
|
||||
/**
|
||||
* The options used to initialize a rendermime instance.
|
||||
*/
|
||||
export interface IOptions {
|
||||
/**
|
||||
* Initial factories to add to the rendermime instance.
|
||||
*/
|
||||
initialFactories?: ReadonlyArray<IRenderMime.IRendererFactory>;
|
||||
|
||||
/**
|
||||
* The sanitizer used to sanitize untrusted html inputs.
|
||||
*
|
||||
* If not given, a default sanitizer will be used.
|
||||
*/
|
||||
sanitizer?: IRenderMime.ISanitizer;
|
||||
|
||||
/**
|
||||
* The initial resolver object.
|
||||
*
|
||||
* The default is `null`.
|
||||
*/
|
||||
resolver?: IRenderMime.IResolver;
|
||||
|
||||
/**
|
||||
* An optional path handler.
|
||||
*/
|
||||
linkHandler?: IRenderMime.ILinkHandler;
|
||||
|
||||
/**
|
||||
* An optional LaTeX typesetter.
|
||||
*/
|
||||
latexTypesetter?: IRenderMime.ILatexTypesetter;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options used to clone a rendermime instance.
|
||||
*/
|
||||
export interface ICloneOptions {
|
||||
/**
|
||||
* The new sanitizer used to sanitize untrusted html inputs.
|
||||
*/
|
||||
sanitizer?: IRenderMime.ISanitizer;
|
||||
|
||||
/**
|
||||
* The new resolver object.
|
||||
*/
|
||||
resolver?: IRenderMime.IResolver;
|
||||
|
||||
/**
|
||||
* The new path handler.
|
||||
*/
|
||||
linkHandler?: IRenderMime.ILinkHandler;
|
||||
|
||||
/**
|
||||
* The new LaTeX typesetter.
|
||||
*/
|
||||
latexTypesetter?: IRenderMime.ILatexTypesetter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The namespace for the module implementation details.
|
||||
*/
|
||||
namespace Private {
|
||||
/**
|
||||
* A type alias for a mime rank and tie-breaking id.
|
||||
*/
|
||||
export type RankPair = { readonly id: number; readonly rank: number };
|
||||
|
||||
/**
|
||||
* A type alias for a mapping of mime type -> rank pair.
|
||||
*/
|
||||
export type RankMap = { [key: string]: RankPair };
|
||||
|
||||
/**
|
||||
* A type alias for a mapping of mime type -> ordered factories.
|
||||
*/
|
||||
export type FactoryMap = { [key: string]: IRenderMime.IRendererFactory };
|
||||
|
||||
/**
|
||||
* Get the mime types in the map, ordered by rank.
|
||||
*/
|
||||
export function sortedTypes(map: RankMap): string[] {
|
||||
return Object.keys(map).sort((a, b) => {
|
||||
let p1 = map[a];
|
||||
let p2 = map[b];
|
||||
if (p1.rank !== p2.rank) {
|
||||
return p1.rank - p2.rank;
|
||||
}
|
||||
return p1.id - p2.id;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/*-----------------------------------------------------------------------------
|
||||
| Copyright (c) Jupyter Development Team.
|
||||
| Distributed under the terms of the Modified BSD License.
|
||||
|----------------------------------------------------------------------------*/
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { ReadonlyJSONObject } from 'sql/workbench/services/notebook/common/jsonext';
|
||||
|
||||
/**
|
||||
* A namespace for rendermime associated interfaces.
|
||||
*/
|
||||
export namespace IRenderMime {
|
||||
/**
|
||||
* A model for mime data.
|
||||
*/
|
||||
export interface IMimeModel {
|
||||
/**
|
||||
* Whether the data in the model is trusted.
|
||||
*/
|
||||
readonly trusted: boolean;
|
||||
|
||||
/**
|
||||
* The data associated with the model.
|
||||
*/
|
||||
readonly data: ReadonlyJSONObject;
|
||||
|
||||
/**
|
||||
* The metadata associated with the model.
|
||||
*/
|
||||
readonly metadata: ReadonlyJSONObject;
|
||||
|
||||
/**
|
||||
* Set the data associated with the model.
|
||||
*
|
||||
* #### Notes
|
||||
* Calling this function may trigger an asynchronous operation
|
||||
* that could cause the renderer to be rendered with a new model
|
||||
* containing the new data.
|
||||
*/
|
||||
setData(options: ISetDataOptions): void;
|
||||
|
||||
/**
|
||||
* Theme service used to react to theme change events
|
||||
*/
|
||||
readonly themeService: IThemeService;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options used to update a mime model.
|
||||
*/
|
||||
export interface ISetDataOptions {
|
||||
/**
|
||||
* The new data object.
|
||||
*/
|
||||
data?: ReadonlyJSONObject;
|
||||
|
||||
/**
|
||||
* The new metadata object.
|
||||
*/
|
||||
metadata?: ReadonlyJSONObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* A widget which displays the contents of a mime model.
|
||||
*/
|
||||
export interface IRenderer {
|
||||
/**
|
||||
* Render a mime model.
|
||||
*
|
||||
* @param model - The mime model to render.
|
||||
*
|
||||
* @returns A promise which resolves when rendering is complete.
|
||||
*
|
||||
* #### Notes
|
||||
* This method may be called multiple times during the lifetime
|
||||
* of the widget to update it if and when new data is available.
|
||||
*/
|
||||
renderModel(model: IRenderMime.IMimeModel): Promise<void>;
|
||||
|
||||
/**
|
||||
* Node to be updated by the renderer
|
||||
*/
|
||||
node: HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface for a renderer factory.
|
||||
*/
|
||||
export interface IRendererFactory {
|
||||
/**
|
||||
* Whether the factory is a "safe" factory.
|
||||
*
|
||||
* #### Notes
|
||||
* A "safe" factory produces renderer widgets which can render
|
||||
* untrusted model data in a usable way. *All* renderers must
|
||||
* handle untrusted data safely, but some may simply failover
|
||||
* with a "Run cell to view output" message. A "safe" renderer
|
||||
* is an indication that its sanitized output will be useful.
|
||||
*/
|
||||
readonly safe: boolean;
|
||||
|
||||
/**
|
||||
* The mime types handled by this factory.
|
||||
*/
|
||||
readonly mimeTypes: ReadonlyArray<string>;
|
||||
|
||||
/**
|
||||
* The default rank of the factory. If not given, defaults to 100.
|
||||
*/
|
||||
readonly defaultRank?: number;
|
||||
|
||||
/**
|
||||
* Create a renderer which displays the mime data.
|
||||
*
|
||||
* @param options - The options used to render the data.
|
||||
*/
|
||||
createRenderer(options: IRendererOptions): IRenderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options used to create a renderer.
|
||||
*/
|
||||
export interface IRendererOptions {
|
||||
/**
|
||||
* The preferred mimeType to render.
|
||||
*/
|
||||
mimeType: string;
|
||||
|
||||
/**
|
||||
* The html sanitizer.
|
||||
*/
|
||||
sanitizer: ISanitizer;
|
||||
|
||||
/**
|
||||
* An optional url resolver.
|
||||
*/
|
||||
resolver?: IResolver | null;
|
||||
|
||||
/**
|
||||
* An optional link handler.
|
||||
*/
|
||||
linkHandler?: ILinkHandler | null;
|
||||
|
||||
/**
|
||||
* The LaTeX typesetter.
|
||||
*/
|
||||
latexTypesetter?: ILatexTypesetter | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that handles html sanitization.
|
||||
*/
|
||||
export interface ISanitizer {
|
||||
/**
|
||||
* Sanitize an HTML string.
|
||||
*/
|
||||
sanitize(dirty: string): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that handles links on a node.
|
||||
*/
|
||||
export interface ILinkHandler {
|
||||
/**
|
||||
* Add the link handler to the node.
|
||||
*
|
||||
* @param node: the node for which to handle the link.
|
||||
*
|
||||
* @param path: the path to open when the link is clicked.
|
||||
*
|
||||
* @param id: an optional element id to scroll to when the path is opened.
|
||||
*/
|
||||
handleLink(node: HTMLElement, path: string, id?: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that resolves relative URLs.
|
||||
*/
|
||||
export interface IResolver {
|
||||
/**
|
||||
* Resolve a relative url to a correct server path.
|
||||
*/
|
||||
resolveUrl(url: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* Get the download url of a given absolute server path.
|
||||
*/
|
||||
getDownloadUrl(path: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* Whether the URL should be handled by the resolver
|
||||
* or not.
|
||||
*
|
||||
* #### Notes
|
||||
* This is similar to the `isLocal` check in `URLExt`,
|
||||
* but can also perform additional checks on whether the
|
||||
* resolver should handle a given URL.
|
||||
*/
|
||||
isLocal?: (url: string) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface for a LaTeX typesetter.
|
||||
*/
|
||||
export interface ILatexTypesetter {
|
||||
/**
|
||||
* Typeset a DOM element.
|
||||
*
|
||||
* @param element - the DOM element to typeset. The typesetting may
|
||||
* happen synchronously or asynchronously.
|
||||
*
|
||||
* #### Notes
|
||||
* The application-wide rendermime object has a settable
|
||||
* `latexTypesetter` property which is used wherever LaTeX
|
||||
* typesetting is required. Extensions wishing to provide their
|
||||
* own typesetter may replace that on the global `lab.rendermime`.
|
||||
*/
|
||||
typeset(element: HTMLElement): void;
|
||||
}
|
||||
}
|
||||
1053
src/sql/workbench/services/notebook/browser/outputs/sanitizer.ts
Normal file
1053
src/sql/workbench/services/notebook/browser/outputs/sanitizer.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,8 @@
|
||||
|
||||
import { nb } from 'azdata';
|
||||
import { localize } from 'vs/nls';
|
||||
import { FutureInternal } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import type { FutureInternal } from 'sql/workbench/services/notebook/browser/interfaces';
|
||||
|
||||
export const noKernel: string = localize('noKernel', "No Kernel");
|
||||
const runNotebookDisabled = localize('runNotebookDisabled', "Cannot run cells as no kernel has been configured");
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { nb, QueryExecuteSubsetResult, IDbColumn, BatchSummary, IResultMessage, ResultSetSummary } from 'azdata';
|
||||
import { localize } from 'vs/nls';
|
||||
import { FutureInternal, notebookConstants } from 'sql/workbench/contrib/notebook/browser/models/modelInterfaces';
|
||||
import QueryRunner from 'sql/workbench/services/query/common/queryRunner';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
@@ -16,7 +15,6 @@ import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMess
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { escape } from 'sql/base/common/strings';
|
||||
import * as notebookUtils from 'sql/workbench/contrib/notebook/browser/models/notebookUtils';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
@@ -28,6 +26,8 @@ import { getUriPrefix, uriPrefixes } from 'sql/platform/connection/common/utils'
|
||||
import { firstIndex } from 'vs/base/common/arrays';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { FutureInternal, notebookConstants } from 'sql/workbench/services/notebook/browser/interfaces';
|
||||
import { tryMatchCellMagic } from 'sql/workbench/services/notebook/browser/utils';
|
||||
|
||||
export const sqlKernelError: string = localize("sqlKernelError", "SQL kernel error");
|
||||
export const MAX_ROWS = 5000;
|
||||
@@ -312,7 +312,7 @@ class SqlKernel extends Disposable implements nb.IKernel {
|
||||
// Strip out the line
|
||||
code = code.substring(firstLineEnd, code.length);
|
||||
// Try and match to an external script magic. If we add more magics later, should handle transforms better
|
||||
let magic = notebookUtils.tryMatchCellMagic(firstLine);
|
||||
let magic = tryMatchCellMagic(firstLine);
|
||||
if (magic) {
|
||||
let executor = this._magicToExecutorMap.get(magic.toLowerCase());
|
||||
if (executor) {
|
||||
|
||||
15
src/sql/workbench/services/notebook/browser/utils.ts
Normal file
15
src/sql/workbench/services/notebook/browser/utils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export function tryMatchCellMagic(input: string): string {
|
||||
if (!input) {
|
||||
return input;
|
||||
}
|
||||
let firstLine = input.trimLeft();
|
||||
let magicRegex = /^%%(\w+)/g;
|
||||
let match = magicRegex.exec(firstLine);
|
||||
let magicName = match && match[1];
|
||||
return magicName;
|
||||
}
|
||||
Reference in New Issue
Block a user