diff --git a/src/sql/parts/common/customInputConverter.ts b/src/sql/parts/common/customInputConverter.ts index 22e88a1b00..4274b7f516 100644 --- a/src/sql/parts/common/customInputConverter.ts +++ b/src/sql/parts/common/customInputConverter.ts @@ -15,7 +15,7 @@ import { IQueryEditorOptions } from 'sql/parts/query/common/queryEditorService'; import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput'; import { NotebookInput, NotebookInputModel, NotebookInputValidator } from 'sql/parts/notebook/notebookInput'; import { DEFAULT_NOTEBOOK_PROVIDER, INotebookService } from 'sql/workbench/services/notebook/common/notebookService'; -import { getProvidersForFileName } from 'sql/parts/notebook/notebookUtils'; +import { getProvidersForFileName, getStandardKernelsForProvider } from 'sql/parts/notebook/notebookUtils'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; const fs = require('fs'); @@ -69,6 +69,10 @@ export function convertEditorInput(input: EditorInput, options: IQueryEditorOpti let notebookInputModel = new NotebookInputModel(uri, undefined, false, undefined); notebookInputModel.providerId = providerIds.filter(provider => provider !== DEFAULT_NOTEBOOK_PROVIDER)[0]; notebookInputModel.providers = providerIds; + notebookInputModel.providers.forEach(provider => { + let standardKernels = getStandardKernelsForProvider(provider, notebookService); + notebookInputModel.standardKernels = standardKernels; + }); let notebookInput: NotebookInput = instantiationService.createInstance(NotebookInput, fileName, notebookInputModel); return notebookInput; }); diff --git a/src/sql/parts/notebook/models/cell.ts b/src/sql/parts/notebook/models/cell.ts index 3d275bd8d2..0d9a37eec5 100644 --- a/src/sql/parts/notebook/models/cell.ts +++ b/src/sql/parts/notebook/models/cell.ts @@ -20,8 +20,6 @@ let modelId = 0; export class CellModel implements ICellModel { - private static LanguageMapping: Map; - private _cellType: nb.CellType; private _source: string; private _language: string; @@ -37,7 +35,6 @@ export class CellModel implements ICellModel { constructor(private factory: IModelFactory, cellData?: nb.ICellContents, private _options?: ICellModelOptions) { this.id = `${modelId++}`; - CellModel.CreateLanguageMappings(); if (cellData) { // Read in contents if available this.fromJSON(cellData); @@ -238,9 +235,9 @@ export class CellModel implements ICellModel { try { let result = output as nb.IDisplayResult; if (result && result.data && result.data['text/html']) { - let nbm = (this as CellModel).options.notebook as NotebookModel; - if (nbm.hadoopConnection) { - let host = nbm.hadoopConnection.host; + let model = (this as CellModel).options.notebook as NotebookModel; + if (model.activeConnection) { + let host = model.activeConnection.serverName; let html = result.data['text/html']; html = html.replace(/(https?:\/\/mssql-master.*\/proxy)(.*)/g, function (a, b, c) { let ret = ''; @@ -321,18 +318,6 @@ export class CellModel implements ICellModel { } } - private static CreateLanguageMappings(): void { - if (CellModel.LanguageMapping) { - return; - } - CellModel.LanguageMapping = new Map(); - CellModel.LanguageMapping['pyspark'] = 'python'; - CellModel.LanguageMapping['pyspark3'] = 'python'; - CellModel.LanguageMapping['python'] = 'python'; - CellModel.LanguageMapping['scala'] = 'scala'; - CellModel.LanguageMapping['sql'] = 'sql'; - } - private get languageInfo(): nb.ILanguageInfo { if (this._options && this._options.notebook && this._options.notebook.languageInfo) { return this._options.notebook.languageInfo; @@ -371,17 +356,15 @@ export class CellModel implements ICellModel { // Otherwise, default to python as the language let languageInfo = this.languageInfo; if (languageInfo) { - if (languageInfo.name) { - // check the LanguageMapping to determine if a mapping is necessary (example 'pyspark' -> 'python') - if (CellModel.LanguageMapping[languageInfo.name]) { - this._language = CellModel.LanguageMapping[languageInfo.name]; + if (languageInfo.codemirror_mode) { + let codeMirrorMode: nb.ICodeMirrorMode = (languageInfo.codemirror_mode); + if (codeMirrorMode && codeMirrorMode.name) { + this._language = codeMirrorMode.name; } - else { - this._language = languageInfo.name; - } - } - else if (languageInfo.mimetype) { + } else if (languageInfo.mimetype) { this._language = languageInfo.mimetype; + } else if (languageInfo.name) { + this._language = languageInfo.name; } } diff --git a/src/sql/parts/notebook/models/clientSession.ts b/src/sql/parts/notebook/models/clientSession.ts index c05dacd808..082f47b497 100644 --- a/src/sql/parts/notebook/models/clientSession.ts +++ b/src/sql/parts/notebook/models/clientSession.ts @@ -17,10 +17,8 @@ import { IClientSession, IKernelPreference, IClientSessionOptions } from './mode import { Deferred } from 'sql/base/common/promise'; import * as notebookUtils from '../notebookUtils'; -import * as sparkUtils from '../spark/sparkUtils'; import { INotebookManager } from 'sql/workbench/services/notebook/common/notebookService'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; -import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection'; /** * Implementation of a client session. This is a model over session operations, @@ -49,7 +47,6 @@ export class ClientSession implements IClientSession { private _session: nb.ISession; private isServerStarted: boolean; private notebookManager: INotebookManager; - private _connection: NotebookConnection; private _kernelConfigActions: ((kernelName: string) => Promise)[] = []; constructor(private options: IClientSessionOptions) { @@ -60,10 +57,8 @@ export class ClientSession implements IClientSession { this._kernelChangeCompleted = new Deferred(); } - public async initialize(connection?: NotebookConnection): Promise { + public async initialize(): Promise { try { - this._kernelConfigActions.push((kernelName: string) => { return this.runTasksBeforeSessionStart(kernelName); }); - this._connection = connection; this._serverLoadFinished = this.startServer(); await this._serverLoadFinished; await this.initializeSession(); @@ -114,7 +109,7 @@ export class ClientSession implements IClientSession { } catch (err) { // TODO move registration if (err && err.response && err.response.status === 501) { - this.options.notificationService.warn(nls.localize('sparkKernelRequiresConnection', 'Kernel {0} was not found. The default kernel will be used instead.', kernelName)); + this.options.notificationService.warn(nls.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 @@ -256,41 +251,22 @@ export class ClientSession implements IClientSession { return kernel; } - public async runTasksBeforeSessionStart(kernelName: string): Promise { - // TODO we should move all Spark-related code to SparkMagicContext - if (this._session && this._connection && this.isSparkKernel(kernelName)) { - // TODO may need to reenable a way to get the credential - // await this._connection.getCredential(); - // %_do_not_call_change_endpoint is a SparkMagic command that lets users change endpoint options, - // such as user/profile/host name/auth type - - let server = URI.parse(sparkUtils.getLivyUrl(this._connection.host, this._connection.knoxport)).toString(); - let doNotCallChangeEndpointParams = - `%_do_not_call_change_endpoint --username=${this._connection.user} --password=${this._connection.password} --server=${server} --auth=Basic_Access`; - let future = this._session.kernel.requestExecute({ - code: doNotCallChangeEndpointParams - }, true); - await future.done; + public async configureKernel(options: nb.IKernelSpec): Promise { + if (this._session) { + await this._session.configureKernel(options); } } - public async updateConnection(connection: NotebookConnection): Promise { + public async updateConnection(connection: IConnectionProfile): Promise { if (!this.kernel) { // TODO is there any case where skipping causes errors? So far it seems like it gets called twice return; } - this._connection = (connection.connectionProfile.id !== '-1') ? connection : this._connection; - // if kernel is not set, don't run kernel config actions - // this should only occur when a cell is cancelled, which interrupts the kernel - if (this.kernel && this.kernel.name) { - await this.runKernelConfigActions(this.kernel.name); + if (connection.id !== '-1') { + await this._session.configureConnection(connection); } } - isSparkKernel(kernelName: string): any { - return kernelName && kernelName.toLowerCase().indexOf('spark') > -1; - } - /** * Kill the kernel and shutdown the session. * diff --git a/src/sql/parts/notebook/models/modelInterfaces.ts b/src/sql/parts/notebook/models/modelInterfaces.ts index 575a15a1eb..3283c0f881 100644 --- a/src/sql/parts/notebook/models/modelInterfaces.ts +++ b/src/sql/parts/notebook/models/modelInterfaces.ts @@ -17,9 +17,11 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { CellType, NotebookChangeType } from 'sql/parts/notebook/models/contracts'; import { INotebookManager } from 'sql/workbench/services/notebook/common/notebookService'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; -import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection'; import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { IStandardKernelWithProvider } from 'sql/parts/notebook/notebookUtils'; +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; +import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; export interface IClientSessionOptions { notebookUri: URI; @@ -131,7 +133,7 @@ export interface IClientSession extends IDisposable { * This will optionally start a session if the kernel preferences * indicate this is desired */ - initialize(connection?: NotebookConnection): Promise; + initialize(connection?: IConnectionProfile): Promise; /** * Change the current kernel associated with the document. @@ -140,6 +142,13 @@ export interface IClientSession extends IDisposable { options: nb.IKernelSpec ): Promise; + /** + * Configure the current kernel associated with the document. + */ + configureKernel( + options: nb.IKernelSpec + ): Promise; + /** * Kill the kernel and shutdown the session. * @@ -191,12 +200,12 @@ export interface IClientSession extends IDisposable { /** * Updates the connection */ - updateConnection(connection: NotebookConnection): void; + updateConnection(connection: IConnectionProfile): void; } export interface IDefaultConnection { - defaultConnection: IConnectionProfile; - otherConnections: IConnectionProfile[]; + defaultConnection: ConnectionProfile; + otherConnections: ConnectionProfile[]; } /** @@ -348,6 +357,8 @@ export interface INotebookModel { * @param edits The edit operations to perform */ pushEditOperations(edits: ISingleNotebookEditOperation[]): void; + + getApplicableConnectionProviderIds(kernelName: string): string[]; } export interface NotebookContentChange { @@ -418,16 +429,14 @@ export interface INotebookModelOptions { notebookManagers: INotebookManager[]; providerId: string; + standardKernels: IStandardKernelWithProvider[]; + defaultKernel: nb.IKernelSpec; notificationService: INotificationService; connectionService: IConnectionManagementService; + capabilitiesService: ICapabilitiesService; } -// TODO would like to move most of these constants to an extension export namespace notebookConstants { - export const hadoopKnoxProviderName = 'HADOOP_KNOX'; - export const python3 = 'python3'; - export const python3DisplayName = 'Python 3'; - export const defaultSparkKernel = 'pyspark3kernel'; - export const hostPropName = 'host'; + export const SQL = 'SQL'; } \ No newline at end of file diff --git a/src/sql/parts/notebook/models/notebookConnection.ts b/src/sql/parts/notebook/models/notebookConnection.ts index c03ca0e6f6..3fdf658daf 100644 --- a/src/sql/parts/notebook/models/notebookConnection.ts +++ b/src/sql/parts/notebook/models/notebookConnection.ts @@ -11,7 +11,6 @@ import { localize } from 'vs/nls'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; export namespace constants { - export const hostPropName = 'host'; export const userPropName = 'user'; export const knoxPortPropName = 'knoxport'; export const clusterPropName = 'clustername'; @@ -52,7 +51,7 @@ export class NotebookConnection { * preference to the built in port. */ private ensureHostAndPort(): void { - this._host = this.connectionProfile.options[constants.hostPropName]; + this._host = this.connectionProfile.serverName; this._knoxPort = NotebookConnection.getKnoxPortOrDefault(this.connectionProfile); // determine whether the host has either a ',' or ':' in it this.setHostAndPort(','); diff --git a/src/sql/parts/notebook/models/notebookContexts.ts b/src/sql/parts/notebook/models/notebookContexts.ts new file mode 100644 index 0000000000..333cd9b095 --- /dev/null +++ b/src/sql/parts/notebook/models/notebookContexts.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { nb } from 'sqlops'; + +import { localize } from 'vs/nls'; +import { IDefaultConnection, notebookConstants, INotebookModelOptions } from 'sql/parts/notebook/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'; + +export class NotebookContexts { + private static MSSQL_PROVIDER = 'MSSQL'; + + private static get DefaultContext(): IDefaultConnection { + let defaultConnection: ConnectionProfile = { + providerName: NotebookContexts.MSSQL_PROVIDER, + 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 = { + providerName: NotebookContexts.MSSQL_PROVIDER, + 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 { + 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 { + 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); + } + } + } + activeConnections = []; + connections.forEach(connection => activeConnections.push(connection)); + } + if (defaultConnection === NotebookContexts.DefaultContext.defaultConnection) { + let newConnection = { + 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 connectionInfo connection profile + * @param savedKernelInfo kernel info loaded from + */ + public static getDefaultKernel(specs: nb.IAllKernels, connectionInfo: IConnectionProfile, savedKernelInfo: nb.IKernelInfo): nb.IKernelSpec { + let defaultKernel: nb.IKernelSpec; + if (specs) { + // find the saved kernel (if it exists) + if (savedKernelInfo) { + defaultKernel = specs.kernels.find((kernel) => kernel.name === savedKernelInfo.name); + } + // 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 = { + name: notebookConstants.SQL, + display_name: notebookConstants.SQL + }; + } + return defaultKernel; + } +} diff --git a/src/sql/parts/notebook/models/notebookModel.ts b/src/sql/parts/notebook/models/notebookModel.ts index 9e5c124d49..aebce2daea 100644 --- a/src/sql/parts/notebook/models/notebookModel.ts +++ b/src/sql/parts/notebook/models/notebookModel.ts @@ -17,13 +17,13 @@ import { NotebookChangeType, CellType } from 'sql/parts/notebook/models/contract import { nbversion } from '../notebookConstants'; import * as notebookUtils from '../notebookUtils'; import { INotebookManager, SQL_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/common/notebookService'; -import { SparkMagicContexts } from 'sql/parts/notebook/models/sparkMagicContexts'; +import { NotebookContexts } from 'sql/parts/notebook/models/notebookContexts'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; -import { NotebookConnection } from 'sql/parts/notebook/models/notebookConnection'; import { INotification, Severity } from 'vs/platform/notification/common/notification'; import { Schemas } from 'vs/base/common/network'; import URI from 'vs/base/common/uri'; import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; /* * Used to control whether a message in a dialog/wizard is displayed as an error, @@ -58,11 +58,12 @@ export class NotebookModel extends Disposable implements INotebookModel { private _savedKernelInfo: nb.IKernelInfo; private readonly _nbformat: number = nbversion.MAJOR_VERSION; private readonly _nbformatMinor: number = nbversion.MINOR_VERSION; - private _hadoopConnection: NotebookConnection; - private _defaultKernel: nb.IKernelSpec; + private _activeConnection: ConnectionProfile; private _activeCell: ICellModel; private _providerId: string; - private _isNewNotebook: boolean = true; + private _defaultKernel: nb.IKernelSpec; + private _kernelDisplayNameToConnectionProviderIds: Map = new Map(); + private _kernelDisplayNameToNotebookProviderIds: Map = new Map(); constructor(private notebookOptions: INotebookModelOptions, startSessionImmediately?: boolean, private connectionProfile?: IConnectionProfile) { super(); @@ -74,6 +75,11 @@ export class NotebookModel extends Disposable implements INotebookModel { } this._trustedMode = false; this._providerId = notebookOptions.providerId; + this.notebookOptions.standardKernels.forEach(kernel => { + this._kernelDisplayNameToConnectionProviderIds.set(kernel.name, kernel.connectionProviderIds); + this._kernelDisplayNameToNotebookProviderIds.set(kernel.name, kernel.notebookProvider); + }); + this._defaultKernel = notebookOptions.defaultKernel; } public get notebookManagers(): INotebookManager[] { @@ -171,10 +177,6 @@ export class NotebookModel extends Disposable implements INotebookModel { return this._trustedMode; } - public get isNewNotebook(): boolean { - return this._isNewNotebook; - } - public get providerId(): string { return this._providerId; } @@ -188,8 +190,8 @@ export class NotebookModel extends Disposable implements INotebookModel { } } - public get hadoopConnection(): NotebookConnection { - return this._hadoopConnection; + public get activeConnection(): IConnectionProfile { + return this._activeConnection; } /** @@ -207,6 +209,14 @@ export class NotebookModel extends Disposable implements INotebookModel { return this._onClientSessionReady.event; } + public getApplicableConnectionProviderIds(kernelDisplayName: string): string[] { + let ids = []; + if (kernelDisplayName) { + ids = this._kernelDisplayNameToConnectionProviderIds.get(kernelDisplayName); + } + return !ids ? [] : ids; + } + public async requestModelLoad(isTrusted: boolean = false): Promise { try { this._trustedMode = isTrusted; @@ -223,9 +233,12 @@ export class NotebookModel extends Disposable implements INotebookModel { version: '' }; if (contents) { - this._isNewNotebook = false; this._defaultLanguageInfo = this.getDefaultLanguageInfo(contents); this._savedKernelInfo = this.getSavedKernelInfo(contents); + this.setProviderIdForKernel(this._savedKernelInfo); + if (this._savedKernelInfo) { + this._defaultKernel = this._savedKernelInfo; + } if (contents.cells && contents.cells.length > 0) { this._cells = contents.cells.map(c => factory.createCell(c, { notebook: this, isTrusted: isTrusted })); } @@ -338,15 +351,15 @@ export class NotebookModel extends Disposable implements INotebookModel { if (!this._activeClientSession) { this._activeClientSession = clientSession; } - let profile = this.connectionProfile as IConnectionProfile; + let profile = new ConnectionProfile(this.notebookOptions.capabilitiesService, this.connectionProfile); - if (this.isValidKnoxConnection(profile)) { - this._hadoopConnection = new NotebookConnection(this.connectionProfile); + if (this.isValidConnection(profile)) { + this._activeConnection = profile; } else { - this._hadoopConnection = undefined; + this._activeConnection = undefined; } - clientSession.initialize(this._hadoopConnection); + clientSession.initialize(this._activeConnection); this._sessionLoadFinished = clientSession.ready.then(async () => { if (clientSession.isInErrorState) { this.setErrorState(clientSession.errorMessage); @@ -360,8 +373,10 @@ export class NotebookModel extends Disposable implements INotebookModel { }); } - private isValidKnoxConnection(profile: IConnectionProfile | connection.Connection) { - return profile && profile.providerName === notebookConstants.hadoopKnoxProviderName && profile.options[notebookConstants.hostPropName] !== undefined; + private isValidConnection(profile: IConnectionProfile | connection.Connection) { + let standardKernels = this.notebookOptions.standardKernels.find(kernel => this._savedKernelInfo && kernel.name === this._savedKernelInfo.display_name); + let connectionProviderIds = standardKernels ? standardKernels.connectionProviderIds : undefined; + return profile && connectionProviderIds && connectionProviderIds.find(provider => provider === profile.providerName) !== undefined; } public get languageInfo(): nb.ILanguageInfo { @@ -380,45 +395,49 @@ export class NotebookModel extends Disposable implements INotebookModel { } public doChangeKernel(kernelSpec: nb.IKernelSpec): Promise { - this.findProviderIdForKernel(kernelSpec); - return this._activeClientSession.changeKernel(kernelSpec) - .then((kernel) => { - kernel.ready.then(() => { - if (kernel.info) { - this.updateLanguageInfo(kernel.info.language_info); - } - }, err => undefined); - return this.updateKernelInfo(kernel); - }).catch((err) => { - this.notifyError(localize('changeKernelFailed', 'Failed to change kernel: {0}', notebookUtils.getErrorMessage(err))); - // TODO should revert kernels dropdown - }); + this.setProviderIdForKernel(kernelSpec); + if (this._activeClientSession && this._activeClientSession.isReady) { + return this._activeClientSession.changeKernel(kernelSpec) + .then((kernel) => { + this.updateKernelInfo(kernel); + kernel.ready.then(() => { + if (kernel.info) { + this.updateLanguageInfo(kernel.info.language_info); + } + }, err => undefined); + }).catch((err) => { + this.notifyError(localize('changeKernelFailed', 'Failed to change kernel: {0}', notebookUtils.getErrorMessage(err))); + // TODO should revert kernels dropdown + }); + } + this.notifyError(localize('noActiveClientSessionFound', 'No active client session was found.')); + return Promise.resolve(); } - public changeContext(host: string, newConnection?: IConnectionProfile): void { + public changeContext(server: string, newConnection?: IConnectionProfile): void { try { if (!newConnection) { - newConnection = this._activeContexts.otherConnections.find((connection) => connection.options['host'] === host); + newConnection = this._activeContexts.otherConnections.find((connection) => connection.serverName === server); } - if (!newConnection && this._activeContexts.defaultConnection.options['host'] === host) { + if (!newConnection && (this._activeContexts.defaultConnection.serverName === server)) { newConnection = this._activeContexts.defaultConnection; } - SparkMagicContexts.configureContext(); - this._hadoopConnection = new NotebookConnection(newConnection); - this.refreshConnections(newConnection); - this._activeClientSession.updateConnection(this._hadoopConnection); + let newConnectionProfile = new ConnectionProfile(this.notebookOptions.capabilitiesService, newConnection); + this._activeConnection = newConnectionProfile; + this.refreshConnections(newConnectionProfile); + this._activeClientSession.updateConnection(this._activeConnection); } catch (err) { let msg = notebookUtils.getErrorMessage(err); this.notifyError(localize('changeContextFailed', 'Changing context failed: {0}', msg)); } } - private refreshConnections(newConnection: IConnectionProfile) { - if (this.isValidKnoxConnection(newConnection) && - this._hadoopConnection.connectionProfile.id !== '-1' && - this._hadoopConnection.connectionProfile.id !== this._activeContexts.defaultConnection.id) { + private refreshConnections(newConnection: ConnectionProfile) { + if (this.isValidConnection(newConnection) && + this._activeConnection.id !== '-1' && + this._activeConnection.id !== this._activeContexts.defaultConnection.id) { // Put the defaultConnection to the head of otherConnections - if (this.isValidKnoxConnection(this._activeContexts.defaultConnection)) { + if (this.isValidConnection(this._activeContexts.defaultConnection)) { this._activeContexts.otherConnections = this._activeContexts.otherConnections.filter(conn => conn.id !== this._activeContexts.defaultConnection.id); this._activeContexts.otherConnections.unshift(this._activeContexts.defaultConnection); } @@ -439,18 +458,15 @@ export class NotebookModel extends Disposable implements INotebookModel { try { let sessionManager = this.notebookManager.sessionManager; if (sessionManager) { - let defaultKernel = SparkMagicContexts.getDefaultKernel(sessionManager.specs, this.connectionProfile, this._savedKernelInfo, this.notebookOptions.notificationService); - this._defaultKernel = defaultKernel; + if (!this._defaultKernel) { + this._defaultKernel = NotebookContexts.getDefaultKernel(sessionManager.specs, this.connectionProfile, this._savedKernelInfo); + } this._clientSessions.forEach(clientSession => { clientSession.statusChanged(async (session) => { - if (session && session.defaultKernelLoaded === true) { - this._kernelsChangedEmitter.fire(defaultKernel); - } else if (session && !session.defaultKernelLoaded) { - this._kernelsChangedEmitter.fire({ name: notebookConstants.python3, display_name: notebookConstants.python3DisplayName }); - } + this._kernelsChangedEmitter.fire(session.kernel); }); }); - this.doChangeKernel(defaultKernel); + this.doChangeKernel(this._defaultKernel); } } catch (err) { let msg = notebookUtils.getErrorMessage(err); @@ -484,6 +500,15 @@ export class NotebookModel extends Disposable implements INotebookModel { return kernel; } + private getDisplayNameFromSpecName(kernelid: string): string { + let newKernel = this.notebookManager.sessionManager.specs.kernels.find(kernel => kernel.name === kernelid); + let newKernelDisplayName; + if (newKernel) { + newKernelDisplayName = newKernel.display_name; + } + return newKernelDisplayName; + } + private setErrorState(errMsg: string): void { this._inErrorState = true; let msg = localize('startSessionFailed', 'Could not start session: {0}', errMsg); @@ -499,6 +524,11 @@ export class NotebookModel extends Disposable implements INotebookModel { public async handleClosed(): Promise { try { if (this._activeClientSession) { + try { + await this._activeClientSession.ready; + } catch(err) { + this.notifyError(localize('shutdownClientSessionError', 'A client session error occurred when closing the notebook: {0}', err)); + } await this._activeClientSession.shutdown(); this._clientSessions = undefined; this._activeClientSession = undefined; @@ -509,11 +539,13 @@ export class NotebookModel extends Disposable implements INotebookModel { } private async loadActiveContexts(kernelChangedArgs: nb.IKernelChangedArgs): Promise { - this._activeContexts = await SparkMagicContexts.getContextsForKernel(this.notebookOptions.connectionService, kernelChangedArgs, this.connectionProfile); - this._contextsChangedEmitter.fire(); - if (this.contexts.defaultConnection !== undefined && this.contexts.defaultConnection.options !== undefined) { - let defaultHadoopConnection = new NotebookConnection(this.contexts.defaultConnection); - this.changeContext(defaultHadoopConnection.host); + if (kernelChangedArgs && kernelChangedArgs.newValue && kernelChangedArgs.newValue.name) { + let kernelDisplayName = this.getDisplayNameFromSpecName(kernelChangedArgs.newValue.name); + this._activeContexts = await NotebookContexts.getContextsForKernel(this.notebookOptions.connectionService, this.getApplicableConnectionProviderIds(kernelDisplayName), kernelChangedArgs, this.connectionProfile); + this._contextsChangedEmitter.fire(); + if (this.contexts.defaultConnection !== undefined && this.contexts.defaultConnection.serverName !== undefined) { + this.changeContext(this.contexts.defaultConnection.serverName); + } } } @@ -555,6 +587,7 @@ export class NotebookModel extends Disposable implements INotebookModel { display_name: spec.display_name, language: spec.language }; + this.clientSession.configureKernel(this._savedKernelInfo); } catch (err) { // Don't worry about this for now. Just use saved values } @@ -565,17 +598,27 @@ export class NotebookModel extends Disposable implements INotebookModel { * Set _providerId and _activeClientSession based on a kernelSpec representing new kernel * @param kernelSpec KernelSpec for new kernel */ - private findProviderIdForKernel(kernelSpec: nb.IKernelSpec): void { + private setProviderIdForKernel(kernelSpec: nb.IKernelSpec): void { + let sessionManagerFound: boolean = false; for (let i = 0; i < this.notebookManagers.length; i++) { if (this.notebookManagers[i].sessionManager && this.notebookManagers[i].sessionManager.specs && this.notebookManagers[i].sessionManager.specs.kernels) { let index = this.notebookManagers[i].sessionManager.specs.kernels.findIndex(kernel => kernel.name === kernelSpec.name); if (index >= 0) { this._activeClientSession = this._clientSessions[i]; this._providerId = this.notebookManagers[i].providerId; + sessionManagerFound = true; break; } } } + + // If no SessionManager exists, utilize passed in StandardKernels to see if we can intelligently set _providerId + if (!sessionManagerFound) { + let provider = this._kernelDisplayNameToNotebookProviderIds.get(kernelSpec.display_name); + if (provider) { + this._providerId = provider; + } + } } /** * Serialize the model to JSON. diff --git a/src/sql/parts/notebook/models/sparkMagicContexts.ts b/src/sql/parts/notebook/models/sparkMagicContexts.ts deleted file mode 100644 index 5c89f84745..0000000000 --- a/src/sql/parts/notebook/models/sparkMagicContexts.ts +++ /dev/null @@ -1,238 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as path from 'path'; -import { nb } from 'sqlops'; - -import * as pfs from 'vs/base/node/pfs'; -import { localize } from 'vs/nls'; -import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; -import { IDefaultConnection, notebookConstants, INotebookModelOptions } from 'sql/parts/notebook/models/modelInterfaces'; -import * as notebookUtils from '../notebookUtils'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; - -const configBase = { - 'kernel_python_credentials': { - 'url': '' - }, - 'kernel_scala_credentials': { - 'url': '' - }, - 'kernel_r_credentials': { - 'url': '' - }, - - 'ignore_ssl_errors': true, - - 'logging_config': { - 'version': 1, - 'formatters': { - 'magicsFormatter': { - 'format': '%(asctime)s\t%(levelname)s\t%(message)s', - 'datefmt': '' - } - }, - 'handlers': { - 'magicsHandler': { - 'class': 'hdijupyterutils.filehandler.MagicsFileHandler', - 'formatter': 'magicsFormatter', - 'home_path': '' - } - }, - 'loggers': { - 'magicsLogger': { - 'handlers': ['magicsHandler'], - 'level': 'DEBUG', - 'propagate': 0 - } - } - } -}; -export class SparkMagicContexts { - - public static get DefaultContext(): IDefaultConnection { - // TODO NOTEBOOK REFACTOR fix default connection handling - let defaultConnection: IConnectionProfile = { - providerName: notebookConstants.hadoopKnoxProviderName, - id: '-1', - options: - { - host: localize('selectConnection', 'Select connection') - } - }; - - return { - // default context if no other contexts are applicable - defaultConnection: defaultConnection, - otherConnections: [defaultConnection] - }; - } - - /** - * Get all of the applicable contexts for a given kernel - * @param apiWrapper ApiWrapper - * @param kernelChangedArgs kernel changed args (both old and new kernel info) - * @param profile current connection profile - */ - public static async getContextsForKernel(connectionService: IConnectionManagementService, kernelChangedArgs?: nb.IKernelChangedArgs, profile?: IConnectionProfile): Promise { - let connections: IDefaultConnection = this.DefaultContext; - if (!profile) { - if (!kernelChangedArgs || !kernelChangedArgs.newValue || - (kernelChangedArgs.oldValue && kernelChangedArgs.newValue.id === kernelChangedArgs.oldValue.id)) { - // nothing to do, kernels are the same or new kernel is undefined - return connections; - } - } - if (kernelChangedArgs && kernelChangedArgs.newValue && kernelChangedArgs.newValue.name) { - switch (kernelChangedArgs.newValue.name) { - case (notebookConstants.python3): - // python3 case, use this.DefaultContext for the only connection - break; - //TO DO: Handle server connections based on kernel type. Right now, we call the same method for all kernel types. - default: - connections = await this.getActiveContexts(connectionService, profile); - } - } else { - connections = await this.getActiveContexts(connectionService, profile); - } - return connections; - } - - /** - * Get all active contexts and sort them - * @param apiWrapper ApiWrapper - * @param profile current connection profile - */ - public static async getActiveContexts(connectionService: IConnectionManagementService, profile: IConnectionProfile): Promise { - let defaultConnection: IConnectionProfile = SparkMagicContexts.DefaultContext.defaultConnection; - let activeConnections: IConnectionProfile[] = await connectionService.getActiveConnections(); - // If no connections exist, only show 'n/a' - if (activeConnections && activeConnections.length > 0) { - // Remove all non-Spark connections - activeConnections = activeConnections.filter(conn => conn.providerName === notebookConstants.hadoopKnoxProviderName); - } - if (activeConnections.length === 0) { - return SparkMagicContexts.DefaultContext; - } - - // If launched from the right click or server dashboard, connection profile data exists, so use that as default - if (profile && profile.options) { - let profileConnection = activeConnections.filter(conn => conn.options['host'] === profile.options['host']); - if (profileConnection) { - defaultConnection = profileConnection[0]; - } - } else { - if (activeConnections.length > 0) { - defaultConnection = activeConnections[0]; - } else { - // TODO NOTEBOOK REFACTOR change this so it's no longer incompatible with IConnectionProfile - defaultConnection = { - providerName: notebookConstants.hadoopKnoxProviderName, - id: '-1', - options: - { - host: localize('addConnection', 'Add new connection') - } - }; - activeConnections.push(defaultConnection); - } - } - return { - otherConnections: activeConnections, - defaultConnection: defaultConnection - }; - } - - public static async configureContext(): Promise { - let sparkmagicConfDir = path.join(notebookUtils.getUserHome(), '.sparkmagic'); - // TODO NOTEBOOK REFACTOR re-enable this or move to extension. Requires config files to be available in order to work - await notebookUtils.mkDir(sparkmagicConfDir); - - // Default to localhost in config file. - let creds: ICredentials = { - 'url': 'http://localhost:8088' - }; - - let config: ISparkMagicConfig = Object.assign({}, configBase); - SparkMagicContexts.updateConfig(config, creds, sparkmagicConfDir); - - let configFilePath = path.join(sparkmagicConfDir, 'config.json'); - await pfs.writeFile(configFilePath, JSON.stringify(config)); - - return { 'SPARKMAGIC_CONF_DIR': sparkmagicConfDir }; - } - /** - * - * @param specs kernel specs (comes from session manager) - * @param connectionInfo connection profile - * @param savedKernelInfo kernel info loaded from - */ - public static getDefaultKernel(specs: nb.IAllKernels, connectionInfo: IConnectionProfile, savedKernelInfo: nb.IKernelInfo, notificationService: INotificationService): nb.IKernelSpec { - let foundSavedKernelInSpecs; - let defaultKernel; - if (specs) { - defaultKernel = specs.kernels.find((kernel) => kernel.name === specs.defaultKernel); - if (savedKernelInfo) { - foundSavedKernelInSpecs = specs.kernels.find((kernel) => kernel.name === savedKernelInfo.name); - } - } - let profile = connectionInfo as IConnectionProfile; - if (specs && connectionInfo && profile.providerName === notebookConstants.hadoopKnoxProviderName) { - // set default kernel to default spark kernel if profile exists - // otherwise, set default to kernel info loaded from existing file - defaultKernel = !foundSavedKernelInSpecs ? specs.kernels.find((spec) => spec.name === notebookConstants.defaultSparkKernel) : foundSavedKernelInSpecs; - } else { - // Handle kernels - if (savedKernelInfo && savedKernelInfo.name.toLowerCase().indexOf('spark') > -1) { - notificationService.warn(localize('sparkKernelRequiresConnection', 'Cannot use kernel {0} as no connection is active. The default kernel of {1} will be used instead.', savedKernelInfo.display_name, defaultKernel.display_name)); - } - } - - // If no default kernel specified (should never happen), default to python3 - if (!defaultKernel) { - defaultKernel = { - name: notebookConstants.python3, - display_name: notebookConstants.python3DisplayName - }; - } - return defaultKernel; - } - - private static updateConfig(config: ISparkMagicConfig, creds: ICredentials, homePath: string): void { - config.kernel_python_credentials = creds; - config.kernel_scala_credentials = creds; - config.kernel_r_credentials = creds; - config.logging_config.handlers.magicsHandler.home_path = homePath; - } -} - -interface ICredentials { - 'url': string; -} - -interface ISparkMagicConfig { - kernel_python_credentials: ICredentials; - kernel_scala_credentials: ICredentials; - kernel_r_credentials: ICredentials; - ignore_ssl_errors?: boolean; - logging_config: { - handlers: { - magicsHandler: { - home_path: string; - class?: string; - formatter?: string - } - } - }; - -} - -export interface IKernelJupyterID { - id: string; - jupyterId: string; -} diff --git a/src/sql/parts/notebook/notebook.component.ts b/src/sql/parts/notebook/notebook.component.ts index c669499f95..f8145d3c30 100644 --- a/src/sql/parts/notebook/notebook.component.ts +++ b/src/sql/parts/notebook/notebook.component.ts @@ -49,6 +49,7 @@ import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHos import { IResourceInput } from 'vs/platform/editor/common/editor'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; +import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; export const NOTEBOOK_SELECTOR: string = 'notebook-component'; @@ -92,7 +93,8 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe @Inject(IWindowService) private windowService: IWindowService, @Inject(IViewletService) private viewletService: IViewletService, @Inject(IUntitledEditorService) private untitledEditorService: IUntitledEditorService, - @Inject(IEditorGroupsService) private editorGroupService: IEditorGroupsService + @Inject(IEditorGroupsService) private editorGroupService: IEditorGroupsService, + @Inject(ICapabilitiesService) private capabilitiesService: ICapabilitiesService ) { super(); this.updateProfile(); @@ -105,11 +107,11 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe // use global connection if possible let profile = TaskUtilities.getCurrentGlobalConnection(this.objectExplorerService, this.connectionManagementService, this.editorService); // TODO use generic method to match kernel with valid connection that's compatible. For now, we only have 1 - if (profile && profile.providerName === notebookConstants.hadoopKnoxProviderName) { + if (profile && profile.providerName) { this.profile = profile; } else { // if not, try 1st active connection that matches our filter - let profiles = this.connectionManagementService.getActiveConnections([notebookConstants.hadoopKnoxProviderName]); + let profiles = this.connectionManagementService.getActiveConnections(); if (profiles && profiles.length > 0) { this.profile = profiles[0]; } @@ -239,7 +241,10 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe connectionService: this.connectionManagementService, notificationService: this.notificationService, notebookManagers: this.notebookManagers, - providerId: notebookUtils.sqlNotebooksEnabled() ? 'sql' : 'jupyter' // this is tricky; really should also depend on the connection profile + standardKernels: this._notebookParams.input.standardKernels, + providerId: notebookUtils.sqlNotebooksEnabled() ? 'sql' : 'jupyter', // this is tricky; really should also depend on the connection profile + defaultKernel: this._notebookParams.input.defaultKernel, + capabilitiesService: this.capabilitiesService }, false, this.profile); model.onError((errInfo: INotification) => this.handleModelError(errInfo)); await model.requestModelLoad(this._notebookParams.isTrusted); @@ -324,7 +329,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe let attachToContainer = document.createElement('div'); let attachTodropdwon = new AttachToDropdown(attachToContainer, this.contextViewService, this.modelRegistered, - this.connectionManagementService, this.connectionDialogService, this.notificationService); + this.connectionManagementService, this.connectionDialogService, this.notificationService, this.capabilitiesService); attachTodropdwon.render(attachToContainer); attachSelectBoxStyler(attachTodropdwon, this.themeService); diff --git a/src/sql/parts/notebook/notebookActions.ts b/src/sql/parts/notebook/notebookActions.ts index 2b653ab4c6..4560414260 100644 --- a/src/sql/parts/notebook/notebookActions.ts +++ b/src/sql/parts/notebook/notebookActions.ts @@ -12,12 +12,13 @@ import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview import { INotificationService, Severity, INotificationActions } from 'vs/platform/notification/common/notification'; import { SelectBox, ISelectBoxOptionsWithLabel } from 'sql/base/browser/ui/selectBox/selectBox'; -import { INotebookModel, notebookConstants } from 'sql/parts/notebook/models/modelInterfaces'; +import { INotebookModel } from 'sql/parts/notebook/models/modelInterfaces'; import { CellType } from 'sql/parts/notebook/models/contracts'; import { NotebookComponent } from 'sql/parts/notebook/notebook.component'; -import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; -import { IConnectionManagementService, IConnectionDialogService } from 'sql/platform/connection/common/connectionManagement'; import { getErrorMessage } from 'sql/parts/notebook/notebookUtils'; +import { IConnectionManagementService, IConnectionDialogService } from 'sql/platform/connection/common/connectionManagement'; +import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { noKernel } from 'sql/workbench/services/notebook/common/sessionManager'; const msgLoading = localize('loading', 'Loading kernels...'); @@ -26,7 +27,7 @@ const attachToLabel: string = localize('AttachTo', 'Attach to: '); const msgLoadingContexts = localize('loadingContexts', 'Loading contexts...'); const msgAddNewConnection = localize('addNewConnection', 'Add new connection'); const msgSelectConnection = localize('selectConnection', 'Select connection'); -const msgLocalHost = localize('localhost', 'Localhost'); +const msgLocalHost = localize('localhost', 'localhost'); // Action to add a cell to notebook based on cell type(code/markdown). export class AddCellAction extends Action { @@ -209,7 +210,8 @@ export class AttachToDropdown extends SelectBox { constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, modelRegistered: Promise, @IConnectionManagementService private _connectionManagementService: IConnectionManagementService, @IConnectionDialogService private _connectionDialogService: IConnectionDialogService, - @INotificationService private _notificationService: INotificationService) { + @INotificationService private _notificationService: INotificationService, + @ICapabilitiesService private _capabilitiesService: ICapabilitiesService) { super([msgLoadingContexts], msgLoadingContexts, contextViewProvider, container, { labelText: attachToLabel, labelOnTop: false } as ISelectBoxOptionsWithLabel); if (modelRegistered) { modelRegistered @@ -219,8 +221,8 @@ export class AttachToDropdown extends SelectBox { }); } this.onDidSelect(e => { - let connection = this.model.contexts.otherConnections.find((c) => c.options.host === e.selected); - this.doChangeContext(connection); + let connection = this.model.contexts.otherConnections.find((c) => c.serverName === e.selected); + this.doChangeContext(new ConnectionProfile(this._capabilitiesService, connection)); }); } @@ -228,54 +230,55 @@ export class AttachToDropdown extends SelectBox { this.model = model; model.contextsChanged(() => { if (this.model.clientSession.kernel && this.model.clientSession.kernel.name) { - let currentKernel = this.model.clientSession.kernel.name; - this.loadAttachToDropdown(this.model, currentKernel); + let currentKernelSpec = this.model.specs.kernels.find(kernel => kernel.name === this.model.clientSession.kernel.name); + this.loadAttachToDropdown(this.model, currentKernelSpec.display_name); } }); } // Load "Attach To" dropdown with the values corresponding to Kernel dropdown public async loadAttachToDropdown(model: INotebookModel, currentKernel: string): Promise { - if (currentKernel === notebookConstants.python3 || currentKernel === noKernel) { + let connProviderIds = this.model.getApplicableConnectionProviderIds(currentKernel); + if ((connProviderIds && connProviderIds.length === 0) || currentKernel === noKernel) { this.setOptions([msgLocalHost]); } else { - let hadoopConnections = this.getHadoopConnections(model); + let connections = this.getConnections(model); this.enable(); - if (hadoopConnections.length === 1 && hadoopConnections[0] === msgAddNewConnection) { - hadoopConnections.unshift(msgSelectConnection); + if (connections.length === 1 && connections[0] === msgAddNewConnection) { + connections.unshift(msgSelectConnection); this.selectWithOptionName(msgSelectConnection); } else { - hadoopConnections.push(msgAddNewConnection); + connections.push(msgAddNewConnection); } - this.setOptions(hadoopConnections); + this.setOptions(connections); } } - //Get hadoop connections from context - public getHadoopConnections(model: INotebookModel): string[] { - let otherHadoopConnections: IConnectionProfile[] = []; - model.contexts.otherConnections.forEach((conn) => { otherHadoopConnections.push(conn); }); - this.selectWithOptionName(model.contexts.defaultConnection.options.host); - otherHadoopConnections = this.setHadoopConnectionsList(model.contexts.defaultConnection, model.contexts.otherConnections); - let hadoopConnections = otherHadoopConnections.map((context) => context.options.host); - return hadoopConnections; + //Get connections from context + public getConnections(model: INotebookModel): string[] { + let otherConnections: ConnectionProfile[] = []; + model.contexts.otherConnections.forEach((conn) => { otherConnections.push(conn); }); + this.selectWithOptionName(model.contexts.defaultConnection.serverName); + otherConnections = this.setConnectionsList(model.contexts.defaultConnection, model.contexts.otherConnections); + let connections = otherConnections.map((context) => context.serverName); + return connections; } - private setHadoopConnectionsList(defaultHadoopConnection: IConnectionProfile, otherHadoopConnections: IConnectionProfile[]) { - if (defaultHadoopConnection.options.host !== msgSelectConnection) { - otherHadoopConnections = otherHadoopConnections.filter(conn => conn.options.host !== defaultHadoopConnection.options.host); - otherHadoopConnections.unshift(defaultHadoopConnection); - if (otherHadoopConnections.length > 1) { - otherHadoopConnections = otherHadoopConnections.filter(val => val.options.host !== msgSelectConnection); + private setConnectionsList(defaultConnection: ConnectionProfile, otherConnections: ConnectionProfile[]) { + if (defaultConnection.serverName !== msgSelectConnection) { + otherConnections = otherConnections.filter(conn => conn.serverName !== defaultConnection.serverName); + otherConnections.unshift(defaultConnection); + if (otherConnections.length > 1) { + otherConnections = otherConnections.filter(val => val.serverName !== msgSelectConnection); } } - return otherHadoopConnections; + return otherConnections; } - public doChangeContext(connection?: IConnectionProfile): void { + public doChangeContext(connection?: ConnectionProfile): void { if (this.value === msgAddNewConnection) { this.openConnectionDialog(); } else { @@ -291,15 +294,15 @@ export class AttachToDropdown extends SelectBox { **/ public async openConnectionDialog(): Promise { try { - //TODO: Figure out how to plumb through the correct provider here - await this._connectionDialogService.openDialogAndWait(this._connectionManagementService, { connectionType: 1, providers: [notebookConstants.hadoopKnoxProviderName] }).then(connection => { + await this._connectionDialogService.openDialogAndWait(this._connectionManagementService, { connectionType: 1, providers: this.model.getApplicableConnectionProviderIds(this.model.clientSession.kernel.name) }).then(connection => { let attachToConnections = this.values; if (!connection) { this.loadAttachToDropdown(this.model, this.model.clientSession.kernel.name); return; } - let connectedServer = connection.options[notebookConstants.hostPropName]; - //Check to see if the same host is already there in dropdown. We only have host names in dropdown + let connectionProfile = new ConnectionProfile(this._capabilitiesService, connection); + let connectedServer = connectionProfile.serverName; + //Check to see if the same server is already there in dropdown. We only have server names in dropdown if (attachToConnections.some(val => val === connectedServer)) { this.loadAttachToDropdown(this.model, this.model.clientSession.kernel.name); this.doChangeContext(); @@ -320,7 +323,7 @@ export class AttachToDropdown extends SelectBox { this.select(index); // Call doChangeContext to set the newly chosen connection in the model - this.doChangeContext(connection); + this.doChangeContext(connectionProfile); }); } catch (error) { diff --git a/src/sql/parts/notebook/notebookInput.ts b/src/sql/parts/notebook/notebookInput.ts index 7b66fe539e..d30d9b0607 100644 --- a/src/sql/parts/notebook/notebookInput.ts +++ b/src/sql/parts/notebook/notebookInput.ts @@ -13,7 +13,9 @@ import { Emitter, Event } from 'vs/base/common/event'; import URI from 'vs/base/common/uri'; import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import * as resources from 'vs/base/common/resources'; +import * as sqlops from 'sqlops'; +import { IStandardKernelWithProvider } from 'sql/parts/notebook/notebookUtils'; import { INotebookService, INotebookEditor } from 'sql/workbench/services/notebook/common/notebookService'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; @@ -27,10 +29,13 @@ export class NotebookInputModel extends EditorModel { private dirty: boolean; private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); private _providerId: string; + private _standardKernels: IStandardKernelWithProvider[]; + private _defaultKernel: sqlops.nb.IKernelSpec; constructor(public readonly notebookUri: URI, private readonly handle: number, private _isTrusted: boolean = false, private saveHandler?: ModeViewSaveHandler, provider?: string, private _providers?: string[]) { super(); this.dirty = false; this._providerId = provider; + this._standardKernels = []; } public get providerId(): string { @@ -49,6 +54,28 @@ export class NotebookInputModel extends EditorModel { this._providers = value; } + public get standardKernels(): IStandardKernelWithProvider[] { + return this._standardKernels; + } + + public set standardKernels(value: IStandardKernelWithProvider[]) { + value.forEach(kernel => { + this._standardKernels.push({ + connectionProviderIds: kernel.connectionProviderIds, + name: kernel.name, + notebookProvider: kernel.notebookProvider + }); + }); + } + + public get defaultKernel(): sqlops.nb.IKernelSpec { + return this._defaultKernel; + } + + public set defaultKernel(kernel: sqlops.nb.IKernelSpec) { + this._defaultKernel = kernel; + } + get isTrusted(): boolean { return this._isTrusted; } @@ -116,6 +143,14 @@ export class NotebookInput extends EditorInput { return this._model.providers; } + public get standardKernels(): IStandardKernelWithProvider[] { + return this._model.standardKernels; + } + + public get defaultKernel(): sqlops.nb.IKernelSpec { + return this._model.defaultKernel; + } + public getTypeId(): string { return NotebookInput.ID; } diff --git a/src/sql/parts/notebook/notebookUtils.ts b/src/sql/parts/notebook/notebookUtils.ts index 16d32a5af9..3a63bd68a3 100644 --- a/src/sql/parts/notebook/notebookUtils.ts +++ b/src/sql/parts/notebook/notebookUtils.ts @@ -58,7 +58,28 @@ export function getProvidersForFileName(fileName: string, notebookService: INote return providers; } +export function getStandardKernelsForProvider(providerId: string, notebookService: INotebookService) : IStandardKernelWithProvider[] { + if (!providerId || !notebookService) { + return []; + } + let standardKernels = notebookService.getStandardKernelsForProvider(providerId); + standardKernels.forEach(kernel => { + Object.assign(kernel, { + name: kernel.name, + connectionProviderIds: kernel.connectionProviderIds, + notebookProvider: providerId + }); + }); + return (standardKernels); +} + // Private feature flag to enable Sql Notebook experience export function sqlNotebooksEnabled() { return process.env['SQLOPS_SQL_NOTEBOOK'] !== undefined; } + +export interface IStandardKernelWithProvider { + readonly name: string; + readonly connectionProviderIds: string[]; + readonly notebookProvider: string; +} \ No newline at end of file diff --git a/src/sql/parts/notebook/outputs/style/index.css b/src/sql/parts/notebook/outputs/style/index.css index c71a80a415..48b7e75ec9 100644 --- a/src/sql/parts/notebook/outputs/style/index.css +++ b/src/sql/parts/notebook/outputs/style/index.css @@ -325,7 +325,7 @@ output-component .jp-RenderedHTMLCommon table { border: none; color: var(--jp-ui-font-color1); font-size: 12px; - table-layout: fixed; + table-layout: auto; margin-left: auto; margin-right: auto; } @@ -338,7 +338,7 @@ output-component .jp-RenderedHTMLCommon thead { output-component .jp-RenderedHTMLCommon td, output-component .jp-RenderedHTMLCommon th, output-component .jp-RenderedHTMLCommon tr { - text-align: right; + text-align: left; vertical-align: middle; padding: 0.5em 0.5em; line-height: normal; @@ -370,6 +370,7 @@ output-component .jp-RenderedHTMLCommon tbody tr:hover { output-component .jp-RenderedHTMLCommon table { margin-bottom: 1em; + display: table-row; } output-component .jp-RenderedHTMLCommon p { diff --git a/src/sql/parts/notebook/spark/sparkUtils.ts b/src/sql/parts/notebook/spark/sparkUtils.ts deleted file mode 100644 index b0c4b59a46..0000000000 --- a/src/sql/parts/notebook/spark/sparkUtils.ts +++ /dev/null @@ -1,16 +0,0 @@ - -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -// TODO: The content of this file should be refactored to an extension -export function getKnoxUrl(host: string, port: string): string { - return `https://${host}:${port}/gateway`; -} - -export function getLivyUrl(serverName: string, port: string): string { - return getKnoxUrl(serverName, port) + '/default/livy/v1/'; -} \ No newline at end of file diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index e0367a5dd7..94dfb730b0 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -1594,6 +1594,11 @@ declare module 'sqlops' { * Optional ID indicating the initial connection to use for this editor */ connectionId?: string; + + /** + * Default kernel for notebook + */ + defaultKernel?: nb.IKernelSpec; } /** @@ -1666,9 +1671,14 @@ declare module 'sqlops' { */ export function registerNotebookProvider(provider: NotebookProvider): vscode.Disposable; + export interface IStandardKernel { + readonly name: string; + readonly connectionProviderIds: string[]; + } + export interface NotebookProvider { readonly providerId: string; - readonly standardKernels: string[]; + readonly standardKernels: IStandardKernel[]; getNotebookManager(notebookUri: vscode.Uri): Thenable; handleNotebookClosed(notebookUri: vscode.Uri): void; } @@ -1945,6 +1955,10 @@ declare module 'sqlops' { defaultKernelLoaded?: boolean; changeKernel(kernelInfo: IKernelSpec): Thenable; + + configureKernel(kernelInfo: IKernelSpec): Thenable; + + configureConnection(connection: IConnectionProfile): Thenable; } export interface ISessionOptions { diff --git a/src/sql/workbench/api/node/extHostNotebook.ts b/src/sql/workbench/api/node/extHostNotebook.ts index 623c8e1c04..7907ddb4bc 100644 --- a/src/sql/workbench/api/node/extHostNotebook.ts +++ b/src/sql/workbench/api/node/extHostNotebook.ts @@ -125,6 +125,16 @@ export class ExtHostNotebook implements ExtHostNotebookShape { return session.changeKernel(kernelInfo).then(kernel => this.saveKernel(kernel)); } + $configureKernel(sessionId: number, kernelInfo: sqlops.nb.IKernelSpec): Thenable { + let session = this._getAdapter(sessionId); + return session.configureKernel(kernelInfo).then(() => null); + } + + $configureConnection(sessionId: number, connection: sqlops.IConnectionProfile): Thenable { + let session = this._getAdapter(sessionId); + return session.configureConnection(connection).then(() => null); + } + $getKernelReadyStatus(kernelId: number): Thenable { let kernel = this._getAdapter(kernelId); return kernel.ready.then(success => kernel.info); diff --git a/src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts index c71f8f33c1..6d22014386 100644 --- a/src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts @@ -168,6 +168,7 @@ export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocume options.position = showOptions.viewColumn; options.providerId = showOptions.providerId; options.connectionId = showOptions.connectionId; + options.defaultKernel = showOptions.defaultKernel; } let id = await this._proxy.$tryShowNotebookDocument(uri, options); let editor = this.getEditor(id); diff --git a/src/sql/workbench/api/node/mainThreadNotebook.ts b/src/sql/workbench/api/node/mainThreadNotebook.ts index 094a1f1645..d444d820bd 100644 --- a/src/sql/workbench/api/node/mainThreadNotebook.ts +++ b/src/sql/workbench/api/node/mainThreadNotebook.ts @@ -288,11 +288,30 @@ class SessionWrapper implements sqlops.nb.ISession { return this.doChangeKernel(kernelInfo); } + configureKernel(kernelInfo: sqlops.nb.IKernelSpec): Thenable { + return this.doConfigureKernel(kernelInfo); + } + + configureConnection(connection: sqlops.IConnectionProfile): Thenable { + if (connection['capabilitiesService'] !== undefined) { + connection['capabilitiesService'] = undefined; + } + return this.doConfigureConnection(connection); + } + private async doChangeKernel(kernelInfo: sqlops.nb.IKernelSpec): Promise { let kernelDetails = await this._proxy.ext.$changeKernel(this.sessionDetails.sessionId, kernelInfo); this._kernel = new KernelWrapper(this._proxy, kernelDetails); return this._kernel; } + + private async doConfigureKernel(kernelInfo: sqlops.nb.IKernelSpec): Promise { + await this._proxy.ext.$configureKernel(this.sessionDetails.sessionId, kernelInfo); + } + + private async doConfigureConnection(connection: sqlops.IConnectionProfile): Promise { + await this._proxy.ext.$configureConnection(this.sessionDetails.sessionId, connection); + } } class KernelWrapper implements sqlops.nb.IKernel { diff --git a/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts index 1e03d02221..d888be3404 100644 --- a/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/node/mainThreadNotebookDocumentsAndEditors.ts @@ -25,7 +25,7 @@ import { import { NotebookInputModel, NotebookInput } from 'sql/parts/notebook/notebookInput'; import { INotebookService, INotebookEditor, DEFAULT_NOTEBOOK_PROVIDER } from 'sql/workbench/services/notebook/common/notebookService'; import { TPromise } from 'vs/base/common/winjs.base'; -import { getProvidersForFileName } from 'sql/parts/notebook/notebookUtils'; +import { getProvidersForFileName, getStandardKernelsForProvider } from 'sql/parts/notebook/notebookUtils'; import { ISingleNotebookEditOperation } from 'sql/workbench/api/common/sqlExtHostTypes'; import { disposed } from 'vs/base/common/errors'; import { ICellModel, NotebookContentChange } from 'sql/parts/notebook/models/modelInterfaces'; @@ -332,6 +332,11 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements } model.providers = providers; model.providerId = providerId; + model.defaultKernel = options && options.defaultKernel; + model.providers.forEach(provider => { + let standardKernels = getStandardKernelsForProvider(provider, this._notebookService); + model.standardKernels = standardKernels; + }); let input = this._instantiationService.createInstance(NotebookInput, undefined, model); let editor = await this._editorService.openEditor(input, editorOptions, viewColumnToEditorGroup(this._editorGroupService, options.position)); diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index f3f0ab0220..2414ae3f71 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -773,6 +773,8 @@ export interface ExtHostNotebookShape { // Session APIs $changeKernel(sessionId: number, kernelInfo: sqlops.nb.IKernelSpec): Thenable; + $configureKernel(sessionId: number, kernelInfo: sqlops.nb.IKernelSpec): Thenable; + $configureConnection(sessionId: number, connection: sqlops.IConnectionProfile): Thenable; // Kernel APIs $getKernelReadyStatus(kernelId: number): Thenable; @@ -829,6 +831,7 @@ export interface INotebookShowOptions { preview?: boolean; providerId?: string; connectionId?: string; + defaultKernel?: sqlops.nb.IKernelSpec; } export interface ExtHostNotebookDocumentsAndEditorsShape { diff --git a/src/sql/workbench/services/notebook/common/notebookRegistry.ts b/src/sql/workbench/services/notebook/common/notebookRegistry.ts index ae909b0e16..0bb4a1add4 100644 --- a/src/sql/workbench/services/notebook/common/notebookRegistry.ts +++ b/src/sql/workbench/services/notebook/common/notebookRegistry.ts @@ -9,6 +9,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { localize } from 'vs/nls'; import * as platform from 'vs/platform/registry/common/platform'; +import * as sqlops from 'sqlops'; import { Event, Emitter } from 'vs/base/common/event'; export const Extensions = { @@ -18,7 +19,7 @@ export const Extensions = { export interface NotebookProviderRegistration { provider: string; fileExtensions: string | string[]; - standardKernels: string | string[]; + standardKernels: sqlops.nb.IStandardKernel | sqlops.nb.IStandardKernel[]; } let notebookProviderType: IJSONSchema = { @@ -44,11 +45,38 @@ let notebookProviderType: IJSONSchema = { standardKernels: { description: localize('carbon.extension.contributes.notebook.standardKernels', 'What kernels should be standard with this notebook provider'), oneOf: [ - { type: 'string' }, + { + type: 'object', + properties: { + name: { + type: 'string', + }, + connectionProviderIds: { + type: 'array', + items: { + type: 'string' + } + } + } + }, { type: 'array', items: { - type: 'string' + type: 'object', + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + connectionProviderIds: { + type: 'array', + items: { + type: 'string' + } + } + } + } } } ] diff --git a/src/sql/workbench/services/notebook/common/notebookService.ts b/src/sql/workbench/services/notebook/common/notebookService.ts index 789da13db5..e2e6dfe43d 100644 --- a/src/sql/workbench/services/notebook/common/notebookService.ts +++ b/src/sql/workbench/services/notebook/common/notebookService.ts @@ -48,6 +48,8 @@ export interface INotebookService { getProvidersForFileType(fileType: string): string[]; + getStandardKernelsForProvider(provider: string): sqlops.nb.IStandardKernel[]; + /** * Initializes and returns a Notebook manager that can handle all important calls to open, display, and * run cells in a notebook. diff --git a/src/sql/workbench/services/notebook/common/notebookServiceImpl.ts b/src/sql/workbench/services/notebook/common/notebookServiceImpl.ts index 2e9743362b..a0c7c2086c 100644 --- a/src/sql/workbench/services/notebook/common/notebookServiceImpl.ts +++ b/src/sql/workbench/services/notebook/common/notebookServiceImpl.ts @@ -80,6 +80,7 @@ export class NotebookService extends Disposable implements INotebookService { private _onNotebookEditorRename = new Emitter(); private _editors = new Map(); private _fileToProviders = new Map(); + private _providerToStandardKernels = new Map(); private _registrationComplete = new Deferred(); private _isRegistrationComplete = false; @@ -121,6 +122,9 @@ export class NotebookService extends Disposable implements INotebookService { this.addFileProvider(registration.fileExtensions, registration); } } + if (registration.standardKernels) { + this.addStandardKernels(registration); + } } registerProvider(providerId: string, instance: INotebookProvider): void { @@ -154,6 +158,26 @@ export class NotebookService extends Disposable implements INotebookService { this._fileToProviders.set(fileType.toUpperCase(), providers); } + // Standard kernels are contributed where a list of kernels are defined that can be shown + // in the kernels dropdown list before a SessionManager has been started; this way, + // every NotebookProvider doesn't need to have an active SessionManager in order to contribute + // kernels to the dropdown + private addStandardKernels(provider: NotebookProviderRegistration) { + let providerUpperCase = provider.provider.toUpperCase(); + let standardKernels = this._providerToStandardKernels.get(providerUpperCase); + if (!standardKernels) { + standardKernels = []; + } + if (Array.isArray(provider.standardKernels)) { + provider.standardKernels.forEach(kernel => { + standardKernels.push(kernel); + }); + } else { + standardKernels.push(provider.standardKernels); + } + this._providerToStandardKernels.set(providerUpperCase, standardKernels); + } + getSupportedFileExtensions(): string[] { return Array.from(this._fileToProviders.keys()); } @@ -165,6 +189,10 @@ export class NotebookService extends Disposable implements INotebookService { return providers ? providers.map(provider => provider.provider) : undefined; } + getStandardKernelsForProvider(provider: string): nb.IStandardKernel[] { + return this._providerToStandardKernels.get(provider.toUpperCase()); + } + public shutdown(): void { this._managersMap.forEach(manager => { manager.forEach(m => { @@ -337,7 +365,7 @@ export class NotebookService extends Disposable implements INotebookService { notebookRegistry.registerNotebookProvider({ provider: sqlProvider.providerId, fileExtensions: DEFAULT_NOTEBOOK_FILETYPE, - standardKernels: ['SQL'] + standardKernels: { name: 'SQL', connectionProviderIds: ['MSSQL'] } }); } } @@ -444,5 +472,4 @@ export class SqlNotebookManager implements INotebookManager { public get sessionManager(): nb.SessionManager { return this._sessionManager; } - } \ No newline at end of file diff --git a/src/sql/workbench/services/notebook/common/sessionManager.ts b/src/sql/workbench/services/notebook/common/sessionManager.ts index eae63f7794..d81ff3a017 100644 --- a/src/sql/workbench/services/notebook/common/sessionManager.ts +++ b/src/sql/workbench/services/notebook/common/sessionManager.ts @@ -7,6 +7,7 @@ import { nb } from 'sqlops'; import { localize } from 'vs/nls'; import { FutureInternal } from 'sql/parts/notebook/models/modelInterfaces'; +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; export const noKernel: string = localize('noKernel', 'No Kernel'); const runNotebookDisabled = localize('runNotebookDisabled', 'Cannot run cells as no kernel has been configured'); @@ -91,6 +92,15 @@ export class EmptySession implements nb.ISession { changeKernel(kernelInfo: nb.IKernelSpec): Thenable { return Promise.resolve(this.kernel); } + + // No kernel config necessary for empty session + configureKernel(kernelInfo: nb.IKernelSpec): Thenable { + return Promise.resolve(); + } + + configureConnection(connection: ConnectionProfile): Thenable { + return Promise.resolve(); + } } class EmptyKernel implements nb.IKernel { diff --git a/src/sql/workbench/services/notebook/common/sqlSessionManager.ts b/src/sql/workbench/services/notebook/common/sqlSessionManager.ts index 6895263d43..2d1ed3efa3 100644 --- a/src/sql/workbench/services/notebook/common/sqlSessionManager.ts +++ b/src/sql/workbench/services/notebook/common/sqlSessionManager.ts @@ -14,8 +14,9 @@ import Severity from 'vs/base/common/severity'; import * as Utils from 'sql/platform/connection/common/utils'; import { Deferred } from 'sql/base/common/promise'; import { Disposable } from 'vs/base/common/lifecycle'; -import { mssqlProviderName } from 'sql/platform/connection/common/constants'; import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService'; +import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; +import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; export const sqlKernel: string = localize('sqlKernel', 'SQL'); export const sqlKernelError: string = localize("sqlKernelError", "SQL kernel error"); @@ -63,6 +64,7 @@ export class SqlSessionManager implements nb.SessionManager { export class SqlSession implements nb.ISession { private _kernel: SqlKernel; private _defaultKernelLoaded = false; + private _currentConnection: IConnectionProfile; public set defaultKernelLoaded(value) { this._defaultKernelLoaded = value; @@ -107,12 +109,25 @@ export class SqlSession implements nb.ISession { changeKernel(kernelInfo: nb.IKernelSpec): Thenable { return Promise.resolve(this.kernel); } + + configureKernel(kernelInfo: nb.IKernelSpec): Thenable { + return Promise.resolve(); + } + + configureConnection(connection: ConnectionProfile): Thenable { + if (this._kernel) { + this._kernel.connection = connection; + } + return Promise.resolve(); + } } class SqlKernel extends Disposable implements nb.IKernel { private _queryRunner: QueryRunner; private _columns: IDbColumn[]; private _rows: DbCellValue[][]; + private _currentConnection: IConnectionProfile; + static kernelId: number = 0; constructor( @IConnectionManagementService private _connectionManagementService: IConnectionManagementService, @IInstantiationService private _instantiationService: IInstantiationService, @@ -121,7 +136,7 @@ class SqlKernel extends Disposable implements nb.IKernel { } public get id(): string { - return '-1'; + return (SqlKernel.kernelId++).toString(); } public get name(): string { @@ -159,6 +174,12 @@ class SqlKernel extends Disposable implements nb.IKernel { return info; } + + public set connection(conn: IConnectionProfile) { + this._currentConnection = conn; + this._queryRunner = undefined; + } + getSpec(): Thenable { return Promise.resolve(sqlKernelSpec); } @@ -167,11 +188,10 @@ class SqlKernel extends Disposable implements nb.IKernel { if (this._queryRunner) { this._queryRunner.runQuery(content.code); } else { - let connections = this._connectionManagementService.getActiveConnections(); - let connectionProfile = connections.find(connection => connection.providerName === mssqlProviderName); - let connectionUri = Utils.generateUri(connectionProfile, 'notebook'); + let connectionUri = Utils.generateUri(this._currentConnection, 'notebook'); this._queryRunner = this._instantiationService.createInstance(QueryRunner, connectionUri, undefined); - this._connectionManagementService.connect(connectionProfile, connectionUri).then((result) => { + this._connectionManagementService.connect(this._currentConnection, connectionUri).then((result) => + { this.addQueryEventListeners(this._queryRunner); this._queryRunner.runQuery(content.code); }); @@ -186,7 +206,9 @@ class SqlKernel extends Disposable implements nb.IKernel { } interrupt(): Thenable { - return Promise.resolve(undefined); + // TODO: figure out what to do with the QueryCancelResult + return this._queryRunner.cancelQuery().then((cancelResult) => { + }); } private addQueryEventListeners(queryRunner: QueryRunner): void { @@ -234,7 +256,11 @@ export class SQLFuture extends Disposable implements FutureInternal { get inProgress(): boolean { return !this._queryRunner.hasCompleted; } - + set inProgress(val: boolean) { + if (this._queryRunner && !val) { + this._queryRunner.cancelQuery(); + } + } get msg(): nb.IMessage { return this._msg; } diff --git a/src/sqltest/parts/notebook/common.ts b/src/sqltest/parts/notebook/common.ts index 3afa623ea1..caaeaddaee 100644 --- a/src/sqltest/parts/notebook/common.ts +++ b/src/sqltest/parts/notebook/common.ts @@ -47,36 +47,42 @@ export class NotebookModelStub implements INotebookModel { get contentChanged(): Event { throw new Error('method not implemented.'); } - get specs(): nb.IAllKernels { - throw new Error('method not implemented.'); - } - get contexts(): IDefaultConnection { - throw new Error('method not implemented.'); - } - get providerId(): string { - throw new Error('method not implemented.'); - } - changeKernel(displayName: string): void { - throw new Error('Method not implemented.'); - } - changeContext(host: string, connection?: IConnectionProfile): void { - throw new Error('Method not implemented.'); - } - findCellIndex(cellModel: ICellModel): number { - throw new Error('Method not implemented.'); - } - addCell(cellType: CellType, index?: number): void { - throw new Error('Method not implemented.'); - } - deleteCell(cellModel: ICellModel): void { - throw new Error('Method not implemented.'); - } - saveModel(): Promise { - throw new Error('Method not implemented.'); - } - pushEditOperations(edits: ISingleNotebookEditOperation[]): void { - throw new Error('Method not implemented.'); - } + get specs(): nb.IAllKernels { + throw new Error('method not implemented.'); + } + get contexts(): IDefaultConnection { + throw new Error('method not implemented.'); + } + get providerId(): string { + throw new Error('method not implemented.'); + } + get applicableConnectionProviderIds(): string[] { + throw new Error('method not implemented.'); + } + changeKernel(displayName: string): void { + throw new Error('Method not implemented.'); + } + changeContext(host: string, connection?: IConnectionProfile): void { + throw new Error('Method not implemented.'); + } + findCellIndex(cellModel: ICellModel): number { + throw new Error('Method not implemented.'); + } + addCell(cellType: CellType, index?: number): void { + throw new Error('Method not implemented.'); + } + deleteCell(cellModel: ICellModel): void { + throw new Error('Method not implemented.'); + } + saveModel(): Promise { + throw new Error('Method not implemented.'); + } + pushEditOperations(edits: ISingleNotebookEditOperation[]): void { + throw new Error('Method not implemented.'); + } + getApplicableConnectionProviderIds(kernelName: string): string[] { + throw new Error('Method not implemented.'); + } } export class NotebookManagerStub implements INotebookManager { diff --git a/src/sqltest/parts/notebook/model/notebookModel.test.ts b/src/sqltest/parts/notebook/model/notebookModel.test.ts index ff1cd95240..4efd49beeb 100644 --- a/src/sqltest/parts/notebook/model/notebookModel.test.ts +++ b/src/sqltest/parts/notebook/model/notebookModel.test.ts @@ -25,6 +25,8 @@ import { Deferred } from 'sql/base/common/promise'; import { ConnectionManagementService } from 'sql/platform/connection/common/connectionManagementService'; import { Memento } from 'vs/workbench/common/memento'; import { Emitter } from 'vs/base/common/event'; +import { CapabilitiesTestService } from 'sqltest/stubs/capabilitiesTestService'; +import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; let expectedNotebookContent: nb.INotebookContents = { cells: [{ @@ -71,182 +73,187 @@ let mockClientSession: TypeMoq.Mock; let sessionReady: Deferred; let mockModelFactory: TypeMoq.Mock; let notificationService: TypeMoq.Mock; +let capabilitiesService: TypeMoq.Mock; -describe('notebook model', function (): void { - let notebookManagers = [new NotebookManagerStub()]; - let memento: TypeMoq.Mock; - let queryConnectionService: TypeMoq.Mock; - let defaultModelOptions: INotebookModelOptions; - beforeEach(() => { - sessionReady = new Deferred(); - notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); - memento = TypeMoq.Mock.ofType(Memento, TypeMoq.MockBehavior.Loose, ''); - memento.setup(x => x.getMemento(TypeMoq.It.isAny())).returns(() => void 0); - queryConnectionService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose, memento.object, undefined); - queryConnectionService.callBase = true; - defaultModelOptions = { - notebookUri: defaultUri, - factory: new ModelFactory(), - notebookManagers, - notificationService: notificationService.object, - connectionService: queryConnectionService.object, - providerId: 'jupyter' - }; - mockClientSession = TypeMoq.Mock.ofType(ClientSession, undefined, defaultModelOptions); - mockClientSession.setup(c => c.initialize(TypeMoq.It.isAny())).returns(() => { - return Promise.resolve(); - }); - mockClientSession.setup(c => c.ready).returns(() => sessionReady.promise); - mockModelFactory = TypeMoq.Mock.ofType(ModelFactory); - mockModelFactory.callBase = true; - mockModelFactory.setup(f => f.createClientSession(TypeMoq.It.isAny())).returns(() => { - return mockClientSession.object; - }); - }); +describe('notebook model', function(): void { + let notebookManagers = [new NotebookManagerStub()]; + let memento: TypeMoq.Mock; + let queryConnectionService: TypeMoq.Mock; + let defaultModelOptions: INotebookModelOptions; + beforeEach(() => { + sessionReady = new Deferred(); + notificationService = TypeMoq.Mock.ofType(TestNotificationService, TypeMoq.MockBehavior.Loose); + capabilitiesService = TypeMoq.Mock.ofType(CapabilitiesTestService); + memento = TypeMoq.Mock.ofType(Memento, TypeMoq.MockBehavior.Loose, ''); + memento.setup(x => x.getMemento(TypeMoq.It.isAny())).returns(() => void 0); + queryConnectionService = TypeMoq.Mock.ofType(ConnectionManagementService, TypeMoq.MockBehavior.Loose, memento.object, undefined); + queryConnectionService.callBase = true; + defaultModelOptions = { + notebookUri: defaultUri, + factory: new ModelFactory(), + notebookManagers, + notificationService: notificationService.object, + connectionService: queryConnectionService.object, + providerId: 'SQL', + standardKernels: [{ name: 'SQL', connectionProviderIds: ['MSSQL'], notebookProvider: 'sql' }], + defaultKernel: undefined, + capabilitiesService: capabilitiesService.object + }; + mockClientSession = TypeMoq.Mock.ofType(ClientSession, undefined, defaultModelOptions); + mockClientSession.setup(c => c.initialize(TypeMoq.It.isAny())).returns(() => { + return Promise.resolve(); + }); + mockClientSession.setup(c => c.ready).returns(() => sessionReady.promise); + mockModelFactory = TypeMoq.Mock.ofType(ModelFactory); + mockModelFactory.callBase = true; + mockModelFactory.setup(f => f.createClientSession(TypeMoq.It.isAny())).returns(() => { + return mockClientSession.object; + }); + }); - it('Should create no cells if model has no contents', async function (): Promise { - // Given an empty notebook - let emptyNotebook: nb.INotebookContents = { - cells: [], - metadata: { - kernelspec: { - name: 'mssql', - language: 'sql' - } - }, - nbformat: 4, - nbformat_minor: 5 - }; + it('Should create no cells if model has no contents', async function(): Promise { + // Given an empty notebook + let emptyNotebook: nb.INotebookContents = { + cells: [], + metadata: { + kernelspec: { + name: 'mssql', + language: 'sql' + } + }, + nbformat: 4, + nbformat_minor: 5 + }; - let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); - mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(emptyNotebook)); - notebookManagers[0].contentManager = mockContentManager.object; + let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); + mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(emptyNotebook)); + notebookManagers[0].contentManager = mockContentManager.object; - // When I initialize the model - let model = new NotebookModel(defaultModelOptions); - await model.requestModelLoad(); + // When I initialize the model + let model = new NotebookModel(defaultModelOptions); + await model.requestModelLoad(); - // Then I expect to have 0 code cell as the contents - should(model.cells).have.length(0); - }); + // Then I expect to have 0 code cell as the contents + should(model.cells).have.length(0); + }); - it('Should throw if model load fails', async function (): Promise { - // Given a call to get Contents fails - let error = new Error('File not found'); - let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); - mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).throws(error); - notebookManagers[0].contentManager = mockContentManager.object; + it('Should throw if model load fails', async function(): Promise { + // Given a call to get Contents fails + let error = new Error('File not found'); + let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); + mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).throws(error); + notebookManagers[0].contentManager = mockContentManager.object; - // When I initalize the model - // Then it should throw - let model = new NotebookModel(defaultModelOptions); - should(model.inErrorState).be.false(); - await testUtils.assertThrowsAsync(() => model.requestModelLoad(), error.message); - should(model.inErrorState).be.true(); - }); + // When I initalize the model + // Then it should throw + let model = new NotebookModel(defaultModelOptions); + should(model.inErrorState).be.false(); + await testUtils.assertThrowsAsync(() => model.requestModelLoad(), error.message); + should(model.inErrorState).be.true(); + }); - it('Should convert cell info to CellModels', async function (): Promise { - // Given a notebook with 2 cells - let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); - mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContent)); - notebookManagers[0].contentManager = mockContentManager.object; + it('Should convert cell info to CellModels', async function(): Promise { + // Given a notebook with 2 cells + let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); + mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContent)); + notebookManagers[0].contentManager = mockContentManager.object; - // When I initalize the model - let model = new NotebookModel(defaultModelOptions); - await model.requestModelLoad(); + // When I initalize the model + let model = new NotebookModel(defaultModelOptions); + await model.requestModelLoad(); - // Then I expect all cells to be in the model - should(model.cells).have.length(2); - should(model.cells[0].source).be.equal(expectedNotebookContent.cells[0].source); - should(model.cells[1].source).be.equal(expectedNotebookContent.cells[1].source); - }); + // Then I expect all cells to be in the model + should(model.cells).have.length(2); + should(model.cells[0].source).be.equal(expectedNotebookContent.cells[0].source); + should(model.cells[1].source).be.equal(expectedNotebookContent.cells[1].source); + }); - it('Should load contents but then go to error state if client session startup fails', async function (): Promise { - let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); - mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContentOneCell)); - notebookManagers[0].contentManager = mockContentManager.object; + it('Should load contents but then go to error state if client session startup fails', async function(): Promise { + let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); + mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContentOneCell)); + notebookManagers[0].contentManager = mockContentManager.object; - // Given I have a session that fails to start - mockClientSession.setup(c => c.isInErrorState).returns(() => true); - mockClientSession.setup(c => c.errorMessage).returns(() => 'Error'); - sessionReady.resolve(); - let sessionFired = false; + // Given I have a session that fails to start + mockClientSession.setup(c => c.isInErrorState).returns(() => true); + mockClientSession.setup(c => c.errorMessage).returns(() => 'Error'); + sessionReady.resolve(); + let sessionFired = false; - let options: INotebookModelOptions = Object.assign({}, defaultModelOptions, >{ - factory: mockModelFactory.object - }); - let model = new NotebookModel(options); - model.onClientSessionReady((session) => sessionFired = true); - await model.requestModelLoad(); - model.backgroundStartSession(); + let options: INotebookModelOptions = Object.assign({}, defaultModelOptions, > { + factory: mockModelFactory.object + }); + let model = new NotebookModel(options); + model.onClientSessionReady((session) => sessionFired = true); + await model.requestModelLoad(); + model.backgroundStartSession(); - // Then I expect load to succeed - shouldHaveOneCell(model); - should(model.clientSession).not.be.undefined(); - // but on server load completion I expect error state to be set - // Note: do not expect serverLoad event to throw even if failed - await model.sessionLoadFinished; - should(model.inErrorState).be.true(); - should(sessionFired).be.false(); - }); + // Then I expect load to succeed + shouldHaveOneCell(model); + should(model.clientSession).not.be.undefined(); + // but on server load completion I expect error state to be set + // Note: do not expect serverLoad event to throw even if failed + await model.sessionLoadFinished; + should(model.inErrorState).be.true(); + should(sessionFired).be.false(); + }); - it('Should not be in error state if client session initialization succeeds', async function (): Promise { - let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); - mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContentOneCell)); - notebookManagers[0].contentManager = mockContentManager.object; - let kernelChangedEmitter: Emitter = new Emitter(); + it('Should not be in error state if client session initialization succeeds', async function(): Promise { + let mockContentManager = TypeMoq.Mock.ofType(LocalContentManager); + mockContentManager.setup(c => c.getNotebookContents(TypeMoq.It.isAny())).returns(() => Promise.resolve(expectedNotebookContentOneCell)); + notebookManagers[0].contentManager = mockContentManager.object; + let kernelChangedEmitter: Emitter = new Emitter(); - mockClientSession.setup(c => c.isInErrorState).returns(() => false); - mockClientSession.setup(c => c.isReady).returns(() => true); - mockClientSession.setup(c => c.kernelChanged).returns(() => kernelChangedEmitter.event); + mockClientSession.setup(c => c.isInErrorState).returns(() => false); + mockClientSession.setup(c => c.isReady).returns(() => true); + mockClientSession.setup(c => c.kernelChanged).returns(() => kernelChangedEmitter.event); - queryConnectionService.setup(c => c.getActiveConnections(TypeMoq.It.isAny())).returns(() => null); + queryConnectionService.setup(c => c.getActiveConnections(TypeMoq.It.isAny())).returns(() => null); - sessionReady.resolve(); - let actualSession: IClientSession = undefined; + sessionReady.resolve(); + let actualSession: IClientSession = undefined; - let options: INotebookModelOptions = Object.assign({}, defaultModelOptions, >{ - factory: mockModelFactory.object - }); - let model = new NotebookModel(options, false); - model.onClientSessionReady((session) => actualSession = session); - await model.requestModelLoad(); - model.backgroundStartSession(); + let options: INotebookModelOptions = Object.assign({}, defaultModelOptions, > { + factory: mockModelFactory.object + }); + let model = new NotebookModel(options, false); + model.onClientSessionReady((session) => actualSession = session); + await model.requestModelLoad(); + model.backgroundStartSession(); - // Then I expect load to succeed - should(model.clientSession).not.be.undefined(); - // but on server load completion I expect error state to be set - // Note: do not expect serverLoad event to throw even if failed - let kernelChangedArg: nb.IKernelChangedArgs = undefined; - model.kernelChanged((kernel) => kernelChangedArg = kernel); - await model.sessionLoadFinished; - should(model.inErrorState).be.false(); - should(actualSession).equal(mockClientSession.object); - should(model.clientSession).equal(mockClientSession.object); - }); + // Then I expect load to succeed + should(model.clientSession).not.be.undefined(); + // but on server load completion I expect error state to be set + // Note: do not expect serverLoad event to throw even if failed + let kernelChangedArg: nb.IKernelChangedArgs = undefined; + model.kernelChanged((kernel) => kernelChangedArg = kernel); + await model.sessionLoadFinished; + should(model.inErrorState).be.false(); + should(actualSession).equal(mockClientSession.object); + should(model.clientSession).equal(mockClientSession.object); + }); - it('Should sanitize kernel display name when IP is included', async function (): Promise { - let model = new NotebookModel(defaultModelOptions); - let displayName = 'PySpark (1.1.1.1)'; - let sanitizedDisplayName = model.sanitizeDisplayName(displayName); - should(sanitizedDisplayName).equal('PySpark'); - }); + it('Should sanitize kernel display name when IP is included', async function(): Promise { + let model = new NotebookModel(defaultModelOptions); + let displayName = 'PySpark (1.1.1.1)'; + let sanitizedDisplayName = model.sanitizeDisplayName(displayName); + should(sanitizedDisplayName).equal('PySpark'); + }); - it('Should sanitize kernel display name properly when IP is not included', async function (): Promise { - let model = new NotebookModel(defaultModelOptions); - let displayName = 'PySpark'; - let sanitizedDisplayName = model.sanitizeDisplayName(displayName); - should(sanitizedDisplayName).equal('PySpark'); - }); + it('Should sanitize kernel display name properly when IP is not included', async function(): Promise { + let model = new NotebookModel(defaultModelOptions); + let displayName = 'PySpark'; + let sanitizedDisplayName = model.sanitizeDisplayName(displayName); + should(sanitizedDisplayName).equal('PySpark'); + }); - function shouldHaveOneCell(model: NotebookModel): void { - should(model.cells).have.length(1); - verifyCellModel(model.cells[0], { cell_type: CellTypes.Code, source: 'insert into t1 values (c1, c2)', metadata: { language: 'python' }, execution_count: 1 }); - } + function shouldHaveOneCell(model: NotebookModel): void { + should(model.cells).have.length(1); + verifyCellModel(model.cells[0], { cell_type: CellTypes.Code, source: 'insert into t1 values (c1, c2)', metadata: { language: 'python' }, execution_count: 1 }); + } - function verifyCellModel(cellModel: ICellModel, expected: nb.ICellContents): void { - should(cellModel.cellType).equal(expected.cell_type); - should(cellModel.source).equal(expected.source); - } + function verifyCellModel(cellModel: ICellModel, expected: nb.ICellContents): void { + should(cellModel.cellType).equal(expected.cell_type); + should(cellModel.source).equal(expected.source); + } }); diff --git a/src/sqltest/workbench/api/exthostNotebook.test.ts b/src/sqltest/workbench/api/exthostNotebook.test.ts index 5343a9bd32..1f6624a392 100644 --- a/src/sqltest/workbench/api/exthostNotebook.test.ts +++ b/src/sqltest/workbench/api/exthostNotebook.test.ts @@ -122,7 +122,7 @@ suite('ExtHostNotebook Tests', () => { class NotebookProviderStub implements sqlops.nb.NotebookProvider { providerId: string = 'TestProvider'; - standardKernels: string[] = ['fakeKernel']; + standardKernels: sqlops.nb.IStandardKernel[] = [{name: 'fakeKernel', connectionProviderIds: ['MSSQL']}]; getNotebookManager(notebookUri: vscode.Uri): Thenable { throw new Error('Method not implemented.'); diff --git a/src/sqltest/workbench/api/mainThreadNotebook.test.ts b/src/sqltest/workbench/api/mainThreadNotebook.test.ts index 9dfd58499b..9cdc8d150a 100644 --- a/src/sqltest/workbench/api/mainThreadNotebook.test.ts +++ b/src/sqltest/workbench/api/mainThreadNotebook.test.ts @@ -149,6 +149,12 @@ class ExtHostNotebookStub implements ExtHostNotebookShape { $changeKernel(sessionId: number, kernelInfo: sqlops.nb.IKernelSpec): Thenable { throw new Error('Method not implemented.'); } + $configureKernel(sessionId: number, kernelInfo: sqlops.nb.IKernelSpec): Thenable { + throw new Error('Method not implemented.'); + } + $configureConnection(sessionId: number, conneection: sqlops.IConnectionProfile): Thenable { + throw new Error('Method not implemented.'); + } $getKernelReadyStatus(kernelId: number): Thenable { throw new Error('Method not implemented.'); }