mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-10 10:12:34 -05:00
No browser from common (#7178)
* no browser from common * clean up some imports
This commit is contained in:
652
src/sql/workbench/parts/notebook/browser/models/cell.ts
Normal file
652
src/sql/workbench/parts/notebook/browser/models/cell.ts
Normal file
@@ -0,0 +1,652 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/parts/notebook/browser/models/notebookUtils';
|
||||
import { CellTypes, CellType, NotebookChangeType } from 'sql/workbench/parts/notebook/common/models/contracts';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/browser/models/notebookModel';
|
||||
import { ICellModel, notebookConstants, IOutputChangedEvent, FutureInternal, CellExecutionState, ICellModelOptions } from 'sql/workbench/parts/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 } from 'vs/base/common/errors';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
|
||||
let modelId = 0;
|
||||
|
||||
|
||||
export class CellModel implements ICellModel {
|
||||
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;
|
||||
public id: string;
|
||||
private _connectionManagementService: IConnectionManagementService;
|
||||
private _stdInHandler: nb.MessageHandler<nb.IStdinMessage>;
|
||||
private _onCellLoaded = new Emitter<string>();
|
||||
private _loaded: boolean;
|
||||
private _stdInVisible: boolean;
|
||||
private _metadata: { language?: string, cellGuid?: string; };
|
||||
private _modelContentChangedEvent: IModelContentChangedEvent;
|
||||
|
||||
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 = '';
|
||||
}
|
||||
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 && other.id === this.id;
|
||||
}
|
||||
|
||||
public get onOutputsChanged(): Event<IOutputChangedEvent> {
|
||||
return this._onOutputsChanged.event;
|
||||
}
|
||||
|
||||
public get onCellModeChanged(): Event<boolean> {
|
||||
return this._onCellModeChanged.event;
|
||||
}
|
||||
|
||||
public get isEditMode(): boolean {
|
||||
return this._isEditMode;
|
||||
}
|
||||
|
||||
public get future(): FutureInternal {
|
||||
return this._future;
|
||||
}
|
||||
|
||||
public set isEditMode(isEditMode: boolean) {
|
||||
this._isEditMode = isEditMode;
|
||||
this._onCellModeChanged.fire(this._isEditMode);
|
||||
// Note: this does not require a notebook update as it does not change overall state
|
||||
}
|
||||
|
||||
public get trustedMode(): boolean {
|
||||
return this._isTrusted;
|
||||
}
|
||||
|
||||
public set trustedMode(isTrusted: boolean) {
|
||||
if (this._isTrusted !== isTrusted) {
|
||||
this._isTrusted = isTrusted;
|
||||
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 <NotebookModel>this.options.notebook;
|
||||
}
|
||||
|
||||
public set cellUri(value: URI) {
|
||||
this._cellUri = value;
|
||||
}
|
||||
|
||||
public get options(): ICellModelOptions {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
public get cellType(): CellType {
|
||||
return this._cellType;
|
||||
}
|
||||
|
||||
public get source(): string | 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);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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.activeConnection) {
|
||||
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)) {
|
||||
// requestExecute expects a string for the code parameter
|
||||
content = Array.isArray(content) ? content.join('') : content;
|
||||
let future = await 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();
|
||||
}
|
||||
|
||||
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
|
||||
let output: nb.ICellOutput = msg.content as nb.ICellOutput;
|
||||
|
||||
if (!this._future.inProgress) {
|
||||
this.disposeFuture();
|
||||
}
|
||||
}
|
||||
|
||||
private handleIOPub(msg: nb.IIOPubMessage): void {
|
||||
let msgType = msg.header.msg_type;
|
||||
let displayId = this.getDisplayId(msg);
|
||||
let output: nb.ICellOutput;
|
||||
switch (msgType) {
|
||||
case 'execute_result':
|
||||
case 'display_data':
|
||||
case 'stream':
|
||||
case 'error':
|
||||
output = msg.content as nb.ICellOutput;
|
||||
output.output_type = msgType;
|
||||
break;
|
||||
case 'clear_output':
|
||||
// TODO wait until next message before clearing
|
||||
// let wait = (msg as KernelMessage.IClearOutputMsg).content.wait;
|
||||
this.clearOutputs();
|
||||
break;
|
||||
case 'update_display_data':
|
||||
output = msg.content as nb.ICellOutput;
|
||||
output.output_type = 'display_data';
|
||||
// TODO #930 handle in-place update of displayed data
|
||||
// targets = this._displayIdMap.get(displayId);
|
||||
// if (targets) {
|
||||
// for (let index of targets) {
|
||||
// model.set(index, output);
|
||||
// }
|
||||
// }
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// TODO handle in-place update of displayed data
|
||||
// if (displayId && msgType === 'display_data') {
|
||||
// targets = this._displayIdMap.get(displayId) || [];
|
||||
// targets.push(model.length - 1);
|
||||
// this._displayIdMap.set(displayId, targets);
|
||||
// }
|
||||
if (output) {
|
||||
// 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 as CellModel).options.notebook as NotebookModel;
|
||||
if (model.activeConnection) {
|
||||
let gatewayEndpointInfo = this.getGatewayEndpoint(model.activeConnection);
|
||||
if (gatewayEndpointInfo) {
|
||||
let hostAndIp = notebookUtils.getHostAndPortFromEndpoint(gatewayEndpointInfo.endpoint);
|
||||
let host = gatewayEndpointInfo && hostAndIp.host ? hostAndIp.host : model.activeConnection.serverName;
|
||||
let port = gatewayEndpointInfo && 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;
|
||||
});
|
||||
}
|
||||
|
||||
private getDisplayId(msg: nb.IIOPubMessage): string | undefined {
|
||||
let transient = (msg.content.transient || {});
|
||||
return transient['display_id'] as string;
|
||||
}
|
||||
|
||||
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 cellJson: Partial<nb.ICellContents> = {
|
||||
cell_type: this._cellType,
|
||||
source: this._source,
|
||||
metadata: this._metadata || {}
|
||||
};
|
||||
cellJson.metadata.azdata_cell_guid = this._cellGuid;
|
||||
if (this._cellType === CellTypes.Code) {
|
||||
cellJson.metadata.language = this._language;
|
||||
cellJson.outputs = this._outputs;
|
||||
cellJson.execution_count = this.executionCount ? this.executionCount : 0;
|
||||
}
|
||||
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;
|
||||
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 = endpoints.find(ep => ep.serviceName.toLowerCase() === notebookUtils.hadoopEndpointNameGateway);
|
||||
}
|
||||
}
|
||||
}
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
|
||||
private getMultilineSource(source: string | string[]): string | string[] {
|
||||
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,53 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ICellMagicMapper } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
|
||||
import { ILanguageMagic } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
|
||||
const defaultKernel = '*';
|
||||
export class CellMagicMapper implements ICellMagicMapper {
|
||||
private kernelToMagicMap = new Map<string, ILanguageMagic[]>();
|
||||
|
||||
constructor(languageMagics: ILanguageMagic[]) {
|
||||
if (languageMagics) {
|
||||
for (let magic of languageMagics) {
|
||||
if (!magic.kernels || magic.kernels.length === 0) {
|
||||
this.addKernelMapping(defaultKernel, magic);
|
||||
}
|
||||
if (magic.kernels) {
|
||||
for (let kernel of magic.kernels) {
|
||||
this.addKernelMapping(kernel.toLowerCase(), magic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addKernelMapping(kernelId: string, magic: ILanguageMagic): void {
|
||||
let magics = this.kernelToMagicMap.get(kernelId) || [];
|
||||
magics.push(magic);
|
||||
this.kernelToMagicMap.set(kernelId, magics);
|
||||
}
|
||||
|
||||
private findMagicForKernel(searchText: string, kernelId: string): ILanguageMagic | undefined {
|
||||
if (kernelId === undefined || !searchText) {
|
||||
return undefined;
|
||||
}
|
||||
searchText = searchText.toLowerCase();
|
||||
let kernelMagics = this.kernelToMagicMap.get(kernelId) || [];
|
||||
if (kernelMagics) {
|
||||
return kernelMagics.find(m => m.magic.toLowerCase() === searchText);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
toLanguageMagic(magic: string, kernelId: string): ILanguageMagic {
|
||||
let languageMagic = this.findMagicForKernel(magic, kernelId.toLowerCase());
|
||||
if (!languageMagic) {
|
||||
languageMagic = this.findMagicForKernel(magic, defaultKernel);
|
||||
}
|
||||
return languageMagic;
|
||||
}
|
||||
}
|
||||
367
src/sql/workbench/parts/notebook/browser/models/clientSession.ts
Normal file
367
src/sql/workbench/parts/notebook/browser/models/clientSession.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/parts/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 && !serverManager.isStarted) {
|
||||
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 newKernel = this.kernel;
|
||||
|
||||
let kernel = await this.doChangeKernel(options);
|
||||
try {
|
||||
await kernel.ready;
|
||||
} catch (error) {
|
||||
// Cleanup some state before re-throwing
|
||||
this._isReady = kernel.isReady;
|
||||
this._kernelChangeCompleted.resolve();
|
||||
throw error;
|
||||
}
|
||||
newKernel = this._session ? kernel : this._session.kernel;
|
||||
this._isReady = kernel.isReady;
|
||||
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/parts/notebook/browser/models/cell';
|
||||
import { IClientSession, IClientSessionOptions, ICellModelOptions, ICellModel, IModelFactory } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
|
||||
import { ClientSession } from 'sql/workbench/parts/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,561 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/parts/notebook/common/models/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/parts/notebook/browser/models/notebookUtils';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService';
|
||||
import { localize } from 'vs/nls';
|
||||
import { NotebookModel } from 'sql/workbench/parts/notebook/browser/models/notebookModel';
|
||||
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||
import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface IDefaultConnection {
|
||||
defaultConnection: ConnectionProfile;
|
||||
otherConnections: ConnectionProfile[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
readonly activeCell: ICellModel;
|
||||
|
||||
/**
|
||||
* 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 contexts, or undefined if these have
|
||||
* not been loaded yet
|
||||
*/
|
||||
readonly contexts: IDefaultConnection | 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[];
|
||||
|
||||
/**
|
||||
* 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[];
|
||||
|
||||
/**
|
||||
* Updates the model's view of an active cell to the new active cell
|
||||
* @param cell New active cell
|
||||
*/
|
||||
updateActiveCell(cell: ICellModel);
|
||||
}
|
||||
|
||||
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>;
|
||||
modelContentChangedEvent: IModelContentChangedEvent;
|
||||
}
|
||||
|
||||
export interface FutureInternal extends nb.IFuture {
|
||||
inProgress: boolean;
|
||||
}
|
||||
|
||||
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 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
|
||||
});
|
||||
}
|
||||
|
||||
export interface INotebookContentsEditable {
|
||||
cells: nb.ICellContents[];
|
||||
metadata: nb.INotebookMetadata;
|
||||
nbformat: number;
|
||||
nbformat_minor: number;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { localize } from 'vs/nls';
|
||||
import { IDefaultConnection, notebookConstants } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
|
||||
import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
|
||||
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { IConnectionProfile } from 'sql/platform/connection/common/interfaces';
|
||||
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||
|
||||
export class NotebookContexts {
|
||||
|
||||
private static get DefaultContext(): IDefaultConnection {
|
||||
let defaultConnection: ConnectionProfile = <any>{
|
||||
providerName: mssqlProviderName,
|
||||
id: '-1',
|
||||
serverName: localize('selectConnection', "Select Connection")
|
||||
};
|
||||
|
||||
return {
|
||||
// default context if no other contexts are applicable
|
||||
defaultConnection: defaultConnection,
|
||||
otherConnections: [defaultConnection]
|
||||
};
|
||||
}
|
||||
|
||||
private static get LocalContext(): IDefaultConnection {
|
||||
let localConnection: ConnectionProfile = <any>{
|
||||
providerName: mssqlProviderName,
|
||||
id: '-1',
|
||||
serverName: localize('localhost', "localhost")
|
||||
};
|
||||
|
||||
return {
|
||||
// default context if no other contexts are applicable
|
||||
defaultConnection: localConnection,
|
||||
otherConnections: [localConnection]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the applicable contexts for a given kernel
|
||||
* @param connectionService connection management service
|
||||
* @param connProviderIds array of connection provider ids applicable for a kernel
|
||||
* @param kernelChangedArgs kernel changed args (both old and new kernel info)
|
||||
* @param profile current connection profile
|
||||
*/
|
||||
public static async getContextsForKernel(connectionService: IConnectionManagementService, connProviderIds: string[], kernelChangedArgs?: nb.IKernelChangedArgs, profile?: IConnectionProfile): Promise<IDefaultConnection> {
|
||||
let connections: IDefaultConnection = this.DefaultContext;
|
||||
if (!profile) {
|
||||
if (!kernelChangedArgs || !kernelChangedArgs.newValue ||
|
||||
(kernelChangedArgs.oldValue && kernelChangedArgs.newValue.id === kernelChangedArgs.oldValue.id)) {
|
||||
// nothing to do, kernels are the same or new kernel is undefined
|
||||
return connections;
|
||||
}
|
||||
}
|
||||
if (kernelChangedArgs && kernelChangedArgs.newValue && kernelChangedArgs.newValue.name && connProviderIds.length < 1) {
|
||||
return connections;
|
||||
} else {
|
||||
connections = await this.getActiveContexts(connectionService, connProviderIds, profile);
|
||||
}
|
||||
return connections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active contexts and sort them
|
||||
* @param apiWrapper ApiWrapper
|
||||
* @param profile current connection profile
|
||||
*/
|
||||
public static async getActiveContexts(connectionService: IConnectionManagementService, connProviderIds: string[], profile: IConnectionProfile): Promise<IDefaultConnection> {
|
||||
let defaultConnection: ConnectionProfile = NotebookContexts.DefaultContext.defaultConnection;
|
||||
let activeConnections: ConnectionProfile[] = await connectionService.getActiveConnections();
|
||||
if (activeConnections && activeConnections.length > 0) {
|
||||
activeConnections = activeConnections.filter(conn => conn.id !== '-1');
|
||||
}
|
||||
// If no connection provider ids exist for a given kernel, the attach to should show localhost
|
||||
if (connProviderIds.length === 0) {
|
||||
return NotebookContexts.LocalContext;
|
||||
}
|
||||
// If no active connections exist, show "Select connection" as the default value
|
||||
if (activeConnections.length === 0) {
|
||||
return NotebookContexts.DefaultContext;
|
||||
}
|
||||
// Filter active connections by their provider ids to match kernel's supported connection providers
|
||||
else if (activeConnections.length > 0) {
|
||||
let connections = activeConnections.filter(connection => {
|
||||
return connProviderIds.includes(connection.providerName);
|
||||
});
|
||||
if (connections && connections.length > 0) {
|
||||
defaultConnection = connections[0];
|
||||
if (profile && profile.options) {
|
||||
if (connections.find(connection => connection.serverName === profile.serverName)) {
|
||||
defaultConnection = connections.find(connection => connection.serverName === profile.serverName);
|
||||
}
|
||||
}
|
||||
} else if (connections.length === 0) {
|
||||
return NotebookContexts.DefaultContext;
|
||||
}
|
||||
activeConnections = [];
|
||||
connections.forEach(connection => activeConnections.push(connection));
|
||||
}
|
||||
if (defaultConnection === NotebookContexts.DefaultContext.defaultConnection) {
|
||||
let newConnection = <ConnectionProfile><any>{
|
||||
providerName: 'SQL',
|
||||
id: '-2',
|
||||
serverName: localize('addConnection', "Add New Connection")
|
||||
};
|
||||
activeConnections.push(newConnection);
|
||||
}
|
||||
|
||||
return {
|
||||
otherConnections: activeConnections,
|
||||
defaultConnection: defaultConnection
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param specs kernel specs (comes from session manager)
|
||||
* @param displayName kernel info loaded from
|
||||
*/
|
||||
public static getDefaultKernel(specs: nb.IAllKernels, displayName: string): nb.IKernelSpec {
|
||||
let defaultKernel: nb.IKernelSpec;
|
||||
if (specs) {
|
||||
// find the saved kernel (if it exists)
|
||||
if (displayName) {
|
||||
defaultKernel = specs.kernels.find((kernel) => kernel.display_name === displayName);
|
||||
}
|
||||
// if no saved kernel exists, use the default KernelSpec
|
||||
if (!defaultKernel) {
|
||||
defaultKernel = specs.kernels.find((kernel) => kernel.name === specs.defaultKernel);
|
||||
}
|
||||
if (defaultKernel) {
|
||||
return defaultKernel;
|
||||
}
|
||||
}
|
||||
|
||||
// If no default kernel specified (should never happen), default to SQL
|
||||
if (!defaultKernel) {
|
||||
defaultKernel = notebookConstants.sqlKernelSpec;
|
||||
}
|
||||
return defaultKernel;
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,11 @@ import { URI } from 'vs/base/common/uri';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import * as azdata from 'azdata';
|
||||
|
||||
import { IStandardKernelWithProvider, getProvidersForFileName, getStandardKernelsForProvider } from 'sql/workbench/parts/notebook/common/models/notebookUtils';
|
||||
import { INotebookService, DEFAULT_NOTEBOOK_PROVIDER, IProviderInfo } from 'sql/workbench/services/notebook/common/notebookService';
|
||||
import { IStandardKernelWithProvider, getProvidersForFileName, getStandardKernelsForProvider } from 'sql/workbench/parts/notebook/browser/models/notebookUtils';
|
||||
import { INotebookService, DEFAULT_NOTEBOOK_PROVIDER, IProviderInfo } from 'sql/workbench/services/notebook/browser/notebookService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { INotebookModel, IContentManager, NotebookContentChange } from 'sql/workbench/parts/notebook/common/models/modelInterfaces';
|
||||
import { INotebookModel, IContentManager, NotebookContentChange } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
|
||||
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
|
||||
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
@@ -26,7 +26,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { NotebookChangeType } from 'sql/workbench/parts/notebook/common/models/contracts';
|
||||
import { Deferred } from 'sql/base/common/promise';
|
||||
import { NotebookTextFileModel } from 'sql/workbench/parts/notebook/common/models/notebookTextFileModel';
|
||||
import { NotebookTextFileModel } from 'sql/workbench/parts/notebook/browser/models/notebookTextFileModel';
|
||||
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
|
||||
export type ModeViewSaveHandler = (handle: number) => Thenable<boolean>;
|
||||
|
||||
1028
src/sql/workbench/parts/notebook/browser/models/notebookModel.ts
Normal file
1028
src/sql/workbench/parts/notebook/browser/models/notebookModel.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,258 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Range, IRange } from 'vs/editor/common/core/range';
|
||||
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
|
||||
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
|
||||
import { FindMatch } from 'vs/editor/common/model';
|
||||
import { NotebookContentChange, INotebookModel } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
|
||||
import { NotebookChangeType } from 'sql/workbench/parts/notebook/common/models/contracts';
|
||||
|
||||
export class NotebookTextFileModel {
|
||||
// save active cell's line/column in editor model for the beginning of the source property
|
||||
private _sourceBeginRange: Range;
|
||||
// save active cell's line/column in editor model for the beginning of the output property
|
||||
private _outputBeginRange: Range;
|
||||
// save active cell guid
|
||||
private _activeCellGuid: string;
|
||||
|
||||
constructor(private _eol: string) {
|
||||
}
|
||||
|
||||
public get activeCellGuid(): string {
|
||||
return this._activeCellGuid;
|
||||
}
|
||||
|
||||
public set activeCellGuid(guid: string) {
|
||||
if (this._activeCellGuid !== guid) {
|
||||
this._sourceBeginRange = undefined;
|
||||
this._outputBeginRange = undefined;
|
||||
this._activeCellGuid = guid;
|
||||
}
|
||||
}
|
||||
|
||||
public transformAndApplyEditForSourceUpdate(contentChange: NotebookContentChange, textEditorModel: TextFileEditorModel | UntitledEditorModel): boolean {
|
||||
let cellGuidRange = this.getCellNodeByGuid(textEditorModel, contentChange.cells[0].cellGuid);
|
||||
|
||||
// convert the range to leverage offsets in the json
|
||||
if (contentChange && contentChange.modelContentChangedEvent && areRangePropertiesPopulated(cellGuidRange)) {
|
||||
contentChange.modelContentChangedEvent.changes.forEach(change => {
|
||||
let convertedRange: IRange = {
|
||||
startLineNumber: change.range.startLineNumber + cellGuidRange.startLineNumber - 1,
|
||||
endLineNumber: change.range.endLineNumber + cellGuidRange.startLineNumber - 1,
|
||||
startColumn: change.range.startColumn + cellGuidRange.startColumn,
|
||||
endColumn: change.range.endColumn + cellGuidRange.startColumn
|
||||
};
|
||||
// Need to subtract one because we're going from 1-based to 0-based
|
||||
let startSpaces: string = ' '.repeat(cellGuidRange.startColumn - 1);
|
||||
// The text here transforms a string from 'This is a string\n this is another string' to:
|
||||
// This is a string
|
||||
// this is another string
|
||||
textEditorModel.textEditorModel.applyEdits([{
|
||||
range: new Range(convertedRange.startLineNumber, convertedRange.startColumn, convertedRange.endLineNumber, convertedRange.endColumn),
|
||||
text: change.text.split(this._eol).join('\\n\",'.concat(this._eol).concat(startSpaces).concat('\"'))
|
||||
}]);
|
||||
});
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public transformAndApplyEditForOutputUpdate(contentChange: NotebookContentChange, textEditorModel: TextFileEditorModel | UntitledEditorModel): boolean {
|
||||
if (Array.isArray(contentChange.cells[0].outputs) && contentChange.cells[0].outputs.length > 0) {
|
||||
let newOutput = JSON.stringify(contentChange.cells[0].outputs[contentChange.cells[0].outputs.length - 1], undefined, ' ');
|
||||
if (contentChange.cells[0].outputs.length > 1) {
|
||||
newOutput = ', '.concat(newOutput);
|
||||
} else {
|
||||
newOutput = '\n'.concat(newOutput).concat('\n');
|
||||
}
|
||||
let range = this.getEndOfOutputs(textEditorModel, contentChange.cells[0].cellGuid);
|
||||
if (range) {
|
||||
textEditorModel.textEditorModel.applyEdits([{
|
||||
range: new Range(range.startLineNumber, range.startColumn, range.startLineNumber, range.startColumn),
|
||||
text: newOutput
|
||||
}]);
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public transformAndApplyEditForCellUpdated(contentChange: NotebookContentChange, textEditorModel: TextFileEditorModel | UntitledEditorModel): boolean {
|
||||
let executionCountMatch = this.getExecutionCountRange(textEditorModel, contentChange.cells[0].cellGuid);
|
||||
if (executionCountMatch && executionCountMatch.range) {
|
||||
// Execution count can be between 0 and n characters long
|
||||
let beginExecutionCountColumn = executionCountMatch.range.endColumn;
|
||||
let endExecutionCountColumn = beginExecutionCountColumn + 1;
|
||||
let lineContent = textEditorModel.textEditorModel.getLineContent(executionCountMatch.range.endLineNumber);
|
||||
while (lineContent[endExecutionCountColumn - 1]) {
|
||||
endExecutionCountColumn++;
|
||||
}
|
||||
if (contentChange.cells[0].executionCount) {
|
||||
textEditorModel.textEditorModel.applyEdits([{
|
||||
range: new Range(executionCountMatch.range.startLineNumber, beginExecutionCountColumn, executionCountMatch.range.endLineNumber, endExecutionCountColumn),
|
||||
text: contentChange.cells[0].executionCount.toString()
|
||||
}]);
|
||||
} else {
|
||||
// This is a special case when cells are canceled; there will be no execution count included
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public transformAndApplyEditForClearOutput(contentChange: NotebookContentChange, textEditorModel: TextFileEditorModel | UntitledEditorModel): boolean {
|
||||
if (!textEditorModel || !contentChange || !contentChange.cells || !contentChange.cells[0] || !contentChange.cells[0].cellGuid) {
|
||||
return false;
|
||||
}
|
||||
if (!this.getOutputNodeByGuid(textEditorModel, contentChange.cells[0].cellGuid)) {
|
||||
this.updateOutputBeginRange(textEditorModel, contentChange.cells[0].cellGuid);
|
||||
}
|
||||
let outputEndRange = this.getEndOfOutputs(textEditorModel, contentChange.cells[0].cellGuid);
|
||||
let outputStartRange = this.getOutputNodeByGuid(textEditorModel, contentChange.cells[0].cellGuid);
|
||||
if (outputStartRange && outputEndRange) {
|
||||
textEditorModel.textEditorModel.applyEdits([{
|
||||
range: new Range(outputStartRange.startLineNumber, outputStartRange.endColumn, outputEndRange.endLineNumber, outputEndRange.endColumn),
|
||||
text: ''
|
||||
}]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public replaceEntireTextEditorModel(notebookModel: INotebookModel, type: NotebookChangeType, textEditorModel: TextFileEditorModel | UntitledEditorModel) {
|
||||
let content = JSON.stringify(notebookModel.toJSON(type), undefined, ' ');
|
||||
let model = textEditorModel.textEditorModel;
|
||||
let endLine = model.getLineCount();
|
||||
let endCol = model.getLineMaxColumn(endLine);
|
||||
textEditorModel.textEditorModel.applyEdits([{
|
||||
range: new Range(1, 1, endLine, endCol),
|
||||
text: content
|
||||
}]);
|
||||
}
|
||||
|
||||
// Find the beginning of a cell's source in the text editor model
|
||||
private updateSourceBeginRange(textEditorModel: TextFileEditorModel | UntitledEditorModel, cellGuid: string): void {
|
||||
if (!cellGuid) {
|
||||
return;
|
||||
}
|
||||
this._sourceBeginRange = undefined;
|
||||
|
||||
let cellGuidMatches = findOrSetCellGuidMatch(textEditorModel, cellGuid);
|
||||
if (cellGuidMatches && cellGuidMatches.length > 0) {
|
||||
let sourceBefore = textEditorModel.textEditorModel.findPreviousMatch('"source": [', { lineNumber: cellGuidMatches[0].range.startLineNumber, column: cellGuidMatches[0].range.startColumn }, false, true, undefined, true);
|
||||
if (!sourceBefore || !sourceBefore.range) {
|
||||
return;
|
||||
}
|
||||
let firstQuoteOfSource = textEditorModel.textEditorModel.findNextMatch('"', { lineNumber: sourceBefore.range.startLineNumber, column: sourceBefore.range.endColumn }, false, true, undefined, true);
|
||||
this._sourceBeginRange = firstQuoteOfSource.range;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the beginning of a cell's outputs in the text editor model
|
||||
private updateOutputBeginRange(textEditorModel: TextFileEditorModel | UntitledEditorModel, cellGuid: string): void {
|
||||
if (!cellGuid) {
|
||||
return undefined;
|
||||
}
|
||||
this._outputBeginRange = undefined;
|
||||
|
||||
let cellGuidMatches = findOrSetCellGuidMatch(textEditorModel, cellGuid);
|
||||
if (cellGuidMatches && cellGuidMatches.length > 0) {
|
||||
let outputsBegin = textEditorModel.textEditorModel.findNextMatch('"outputs": [', { lineNumber: cellGuidMatches[0].range.endLineNumber, column: cellGuidMatches[0].range.endColumn }, false, true, undefined, true);
|
||||
if (!outputsBegin || !outputsBegin.range) {
|
||||
return undefined;
|
||||
}
|
||||
this._outputBeginRange = outputsBegin.range;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the end of a cell's outputs in the text editor model
|
||||
// This will be used as a starting point for any future outputs
|
||||
private getEndOfOutputs(textEditorModel: TextFileEditorModel | UntitledEditorModel, cellGuid: string) {
|
||||
let outputsBegin;
|
||||
if (this._activeCellGuid === cellGuid) {
|
||||
outputsBegin = this._outputBeginRange;
|
||||
}
|
||||
if (!outputsBegin || !textEditorModel.textEditorModel.getLineContent(outputsBegin.startLineNumber).trim().includes('output')) {
|
||||
this.updateOutputBeginRange(textEditorModel, cellGuid);
|
||||
outputsBegin = this._outputBeginRange;
|
||||
if (!outputsBegin) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
let outputsEnd = textEditorModel.textEditorModel.matchBracket({ column: outputsBegin.endColumn - 1, lineNumber: outputsBegin.endLineNumber });
|
||||
if (!outputsEnd || outputsEnd.length < 2) {
|
||||
return undefined;
|
||||
}
|
||||
// single line output [i.e. no outputs exist for a cell]
|
||||
if (outputsBegin.endLineNumber === outputsEnd[1].startLineNumber) {
|
||||
// Adding 1 to startColumn to replace text starting one character after '['
|
||||
return {
|
||||
startColumn: outputsEnd[0].startColumn + 1,
|
||||
startLineNumber: outputsEnd[0].startLineNumber,
|
||||
endColumn: outputsEnd[0].endColumn,
|
||||
endLineNumber: outputsEnd[0].endLineNumber
|
||||
};
|
||||
} else {
|
||||
// Last 2 lines in multi-line output will look like the following:
|
||||
// " }"
|
||||
// " ],"
|
||||
if (textEditorModel.textEditorModel.getLineContent(outputsEnd[1].endLineNumber - 1).trim() === '}') {
|
||||
return {
|
||||
startColumn: textEditorModel.textEditorModel.getLineFirstNonWhitespaceColumn(outputsEnd[1].endLineNumber - 1) + 1,
|
||||
startLineNumber: outputsEnd[1].endLineNumber - 1,
|
||||
endColumn: outputsEnd[1].endColumn - 1,
|
||||
endLineNumber: outputsEnd[1].endLineNumber
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine what text needs to be replaced when execution counts are updated
|
||||
private getExecutionCountRange(textEditorModel: TextFileEditorModel | UntitledEditorModel, cellGuid: string) {
|
||||
let endOutputRange = this.getEndOfOutputs(textEditorModel, cellGuid);
|
||||
if (endOutputRange && endOutputRange.endLineNumber) {
|
||||
return textEditorModel.textEditorModel.findNextMatch('"execution_count": ', { lineNumber: endOutputRange.endLineNumber, column: endOutputRange.endColumn }, false, true, undefined, true);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find a cell's location, given its cellGuid
|
||||
// If it doesn't exist (e.g. it's not the active cell), attempt to find it
|
||||
private getCellNodeByGuid(textEditorModel: TextFileEditorModel | UntitledEditorModel, guid: string) {
|
||||
if (this._activeCellGuid !== guid || !this._sourceBeginRange) {
|
||||
this.updateSourceBeginRange(textEditorModel, guid);
|
||||
}
|
||||
return this._sourceBeginRange;
|
||||
}
|
||||
|
||||
private getOutputNodeByGuid(textEditorModel: TextFileEditorModel | UntitledEditorModel, guid: string) {
|
||||
if (this._activeCellGuid !== guid) {
|
||||
this.updateOutputBeginRange(textEditorModel, guid);
|
||||
}
|
||||
return this._outputBeginRange;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function areRangePropertiesPopulated(range: Range) {
|
||||
return range && range.startLineNumber && range.startColumn && range.endLineNumber && range.endColumn;
|
||||
}
|
||||
|
||||
function findOrSetCellGuidMatch(textEditorModel: TextFileEditorModel | UntitledEditorModel, cellGuid: string): FindMatch[] {
|
||||
if (!textEditorModel || !cellGuid) {
|
||||
return undefined;
|
||||
}
|
||||
return textEditorModel.textEditorModel.findMatches(cellGuid, false, false, true, undefined, true);
|
||||
}
|
||||
183
src/sql/workbench/parts/notebook/browser/models/notebookUtils.ts
Normal file
183
src/sql/workbench/parts/notebook/browser/models/notebookUtils.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ICellModel } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
|
||||
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 && fileExt.startsWith('.')) {
|
||||
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 => {
|
||||
Object.assign(<IStandardKernelWithProvider>kernel, {
|
||||
name: kernel.name,
|
||||
connectionProviderIds: kernel.connectionProviderIds,
|
||||
notebookProvider: providerId
|
||||
});
|
||||
});
|
||||
return <IStandardKernelWithProvider[]>(standardKernels);
|
||||
}
|
||||
|
||||
// In the Attach To dropdown, show the database name (if it exists) using the current connection
|
||||
// Example: myFakeServer (myDatabase)
|
||||
export function formatServerNameWithDatabaseNameForAttachTo(connectionProfile: ConnectionProfile): string {
|
||||
if (connectionProfile && connectionProfile.serverName) {
|
||||
return !connectionProfile.databaseName ? connectionProfile.serverName : connectionProfile.serverName + ' (' + connectionProfile.databaseName + ')';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Extract server name from format used in Attach To: serverName (databaseName)
|
||||
export function getServerFromFormattedAttachToName(name: string): string {
|
||||
return name.substring(0, name.lastIndexOf(' (')) ? name.substring(0, name.lastIndexOf(' (')) : name;
|
||||
}
|
||||
|
||||
// Extract database name from format used in Attach To: serverName (databaseName)
|
||||
export function getDatabaseFromFormattedAttachToName(name: string): string {
|
||||
return name.substring(name.lastIndexOf('(') + 1, name.lastIndexOf(')')) ?
|
||||
name.substring(name.lastIndexOf('(') + 1, name.lastIndexOf(')')) : '';
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
export async function asyncForEach(array: any, callback: any): Promise<any> {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only replace vscode-resource with file when in the same (or a sub) directory
|
||||
* This matches Jupyter Notebook viewer behavior
|
||||
*/
|
||||
export function convertVscodeResourceToFileInSubDirectories(htmlContent: string, cellModel: ICellModel): string {
|
||||
let htmlContentCopy = htmlContent;
|
||||
while (htmlContentCopy.search('(?<=img src=\"vscode-resource:)') > 0) {
|
||||
let pathStartIndex = htmlContentCopy.search('(?<=img src=\"vscode-resource:)');
|
||||
let pathEndIndex = htmlContentCopy.indexOf('\" ', pathStartIndex);
|
||||
let filePath = htmlContentCopy.substring(pathStartIndex, pathEndIndex);
|
||||
// If the asset is in the same folder or a subfolder, replace 'vscode-resource:' with 'file:', so the image is visible
|
||||
if (!path.relative(path.dirname(cellModel.notebookModel.notebookUri.fsPath), filePath).includes('..')) {
|
||||
// ok to change from vscode-resource: to file:
|
||||
htmlContent = htmlContent.replace('vscode-resource:' + filePath, 'file:' + filePath);
|
||||
}
|
||||
htmlContentCopy = htmlContentCopy.slice(pathEndIndex);
|
||||
}
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
export function useInProcMarkdown(configurationService: IConfigurationService): boolean {
|
||||
return configurationService.getValue('notebook.useInProcMarkdown');
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user