/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; import { Action } from 'vs/base/common/actions'; import { TPromise } from 'vs/base/common/winjs.base'; import { localize } from 'vs/nls'; 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 } from 'sql/parts/notebook/models/modelInterfaces'; import { CellType } from 'sql/parts/notebook/models/contracts'; import { NotebookComponent } from 'sql/parts/notebook/notebook.component'; import { getErrorMessage, formatServerNameWithDatabaseNameForAttachTo, getServerFromFormattedAttachToName, getDatabaseFromFormattedAttachToName } from 'sql/parts/notebook/notebookUtils'; import { IConnectionManagementService } 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'; import { IConnectionDialogService } from 'sql/workbench/services/connection/common/connectionDialogService'; import { NotebookModel } from 'sql/parts/notebook/models/notebookModel'; const msgLoading = localize('loading', "Loading kernels..."); const msgChanging = localize('changing', "Changing kernel..."); const kernelLabel: string = localize('Kernel', "Kernel: "); 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 HIDE_ICON_CLASS = ' hideIcon'; // Action to add a cell to notebook based on cell type(code/markdown). export class AddCellAction extends Action { public cellType: CellType; constructor( id: string, label: string, cssClass: string ) { super(id, label, cssClass); } public run(context: NotebookComponent): TPromise { return new TPromise((resolve, reject) => { try { context.addCell(this.cellType); resolve(true); } catch (e) { reject(e); } }); } } export interface IToggleableState { baseClass?: string; shouldToggleTooltip?: boolean; toggleOnClass: string; toggleOnLabel: string; toggleOffLabel: string; toggleOffClass: string; isOn: boolean; } export abstract class ToggleableAction extends Action { constructor(id: string, protected state: IToggleableState) { super(id, ''); this.updateLabelAndIcon(); } private updateLabelAndIcon() { if (this.state.shouldToggleTooltip) { this.tooltip = this.state.isOn ? this.state.toggleOnLabel : this.state.toggleOffLabel; } else { this.label = this.state.isOn ? this.state.toggleOnLabel : this.state.toggleOffLabel; } let classes = this.state.baseClass ? `${this.state.baseClass} ` : ''; classes += this.state.isOn ? this.state.toggleOnClass : this.state.toggleOffClass; this.class = classes; } protected toggle(isOn: boolean): void { this.state.isOn = isOn; this.updateLabelAndIcon(); } } export interface IActionStateData { className?: string; label?: string; tooltip?: string; hideIcon?: boolean; } export class IMultiStateData { private _stateMap = new Map(); constructor(mappings: { key: T, value: IActionStateData }[], private _state: T, private _baseClass?: string) { if (mappings) { mappings.forEach(s => this._stateMap.set(s.key, s.value)); } } public set state(value: T) { if (!this._stateMap.has(value)) { throw new Error('State value must be in stateMap'); } this._state = value; } public updateStateData(state: T, updater: (data: IActionStateData) => void): void { let data = this._stateMap.get(state); if (data) { updater(data); } } public get classes(): string { let classVal = this.getStateValueOrDefault((data) => data.className, ''); let classes = this._baseClass ? `${this._baseClass} ` : ''; classes += classVal; if (this.getStateValueOrDefault((data) => data.hideIcon, false)) { classes += HIDE_ICON_CLASS; } return classes; } public get label(): string { return this.getStateValueOrDefault((data) => data.label, ''); } public get tooltip(): string { return this.getStateValueOrDefault((data) => data.tooltip, ''); } private getStateValueOrDefault(getter: (data: IActionStateData) => U, defaultVal?: U): U { let data = this._stateMap.get(this._state); return data ? getter(data) : defaultVal; } } export abstract class MultiStateAction extends Action { constructor(id: string, protected states: IMultiStateData) { super(id, ''); this.updateLabelAndIcon(); } private updateLabelAndIcon() { this.label = this.states.label; this.tooltip = this.states.tooltip; this.class = this.states.classes; } protected updateState(state: T): void { this.states.state = state; this.updateLabelAndIcon(); } } export class TrustedAction extends ToggleableAction { // Constants private static readonly trustedLabel = localize('trustLabel', 'Trusted'); private static readonly notTrustedLabel = localize('untrustLabel', 'Not Trusted'); private static readonly alreadyTrustedMsg = localize('alreadyTrustedMsg', 'Notebook is already trusted.'); private static readonly baseClass = 'notebook-button'; private static readonly trustedCssClass = 'icon-trusted'; private static readonly notTrustedCssClass = 'icon-notTrusted'; // Properties constructor( id: string, @INotificationService private _notificationService: INotificationService ) { super(id, { baseClass: TrustedAction.baseClass, toggleOnLabel: TrustedAction.trustedLabel, toggleOnClass: TrustedAction.trustedCssClass, toggleOffLabel: TrustedAction.notTrustedLabel, toggleOffClass: TrustedAction.notTrustedCssClass, isOn: false }); } public get trusted(): boolean { return this.state.isOn; } public set trusted(value: boolean) { this.toggle(value); } public run(context: NotebookComponent): TPromise { let self = this; return new TPromise((resolve, reject) => { try { if (self.trusted) { const actions: INotificationActions = { primary: [] }; self._notificationService.notify({ severity: Severity.Info, message: TrustedAction.alreadyTrustedMsg, actions }); } else { self.trusted = !self.trusted; context.updateModelTrustDetails(self.trusted); } resolve(true); } catch (e) { reject(e); } }); } } export class KernelsDropdown extends SelectBox { private model: NotebookModel; constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, modelReady: Promise) { super([msgLoading], msgLoading, contextViewProvider, container, { labelText: kernelLabel, labelOnTop: false } as ISelectBoxOptionsWithLabel); if (modelReady) { modelReady .then((model) => this.updateModel(model)) .catch((err) => { // No-op for now }); } this.onDidSelect(e => this.doChangeKernel(e.selected)); } updateModel(model: INotebookModel): void { this.model = model as NotebookModel; this._register(this.model.kernelChanged((changedArgs: azdata.nb.IKernelChangedArgs) => { this.updateKernel(changedArgs.newValue); })); } // Update SelectBox values public updateKernel(kernel: azdata.nb.IKernel) { if (kernel) { let standardKernel = this.model.getStandardKernelFromName(kernel.name); let kernels: string[] = this.model.standardKernelsDisplayName(); if (kernels && standardKernel) { let index = kernels.findIndex((kernel => kernel === standardKernel.displayName)); this.setOptions(kernels, index); } } } public doChangeKernel(displayName: string): void { this.setOptions([msgChanging], 0); this.model.changeKernel(displayName); } } export class AttachToDropdown extends SelectBox { private model: NotebookModel; constructor(container: HTMLElement, contextViewProvider: IContextViewProvider, modelReady: Promise, @IConnectionManagementService private _connectionManagementService: IConnectionManagementService, @IConnectionDialogService private _connectionDialogService: IConnectionDialogService, @INotificationService private _notificationService: INotificationService, @ICapabilitiesService private _capabilitiesService: ICapabilitiesService) { super([msgLoadingContexts], msgLoadingContexts, contextViewProvider, container, { labelText: attachToLabel, labelOnTop: false } as ISelectBoxOptionsWithLabel); if (modelReady) { modelReady .then(model => { this.updateModel(model); this.updateAttachToDropdown(model); }) .catch(err => { // No-op for now }); } this.onDidSelect(e => { this.doChangeContext(new ConnectionProfile(this._capabilitiesService, this.getConnectionWithServerAndDatabaseNames(e.selected))); }); } public updateModel(model: INotebookModel): void { this.model = model as NotebookModel; this._register(model.contextsChanged(() => { let kernelDisplayName: string = this.getKernelDisplayName(); if (kernelDisplayName) { this.loadAttachToDropdown(this.model, kernelDisplayName); } })); this._register(this.model.contextsLoading(() => { this.setOptions([msgLoadingContexts], 0); })); } private updateAttachToDropdown(model: INotebookModel): void { model.onValidConnectionSelected(validConnection => { let kernelDisplayName: string = this.getKernelDisplayName(); if (kernelDisplayName) { this.loadAttachToDropdown(this.model, kernelDisplayName, !validConnection); } }); } private getKernelDisplayName(): string { let kernelDisplayName: string; if (this.model.clientSession && this.model.clientSession.kernel && this.model.clientSession.kernel.name) { let currentKernelName = this.model.clientSession.kernel.name.toLowerCase(); let currentKernelSpec = this.model.specs.kernels.find(kernel => kernel.name && kernel.name.toLowerCase() === currentKernelName); if (currentKernelSpec) { kernelDisplayName = currentKernelSpec.display_name; } } return kernelDisplayName; } // Load "Attach To" dropdown with the values corresponding to Kernel dropdown public async loadAttachToDropdown(model: INotebookModel, currentKernel: string, showSelectConnection?: boolean): Promise { let connProviderIds = this.model.getApplicableConnectionProviderIds(currentKernel); if ((connProviderIds && connProviderIds.length === 0) || currentKernel === noKernel) { this.setOptions([msgLocalHost]); } else { let connections = this.getConnections(model); this.enable(); if (showSelectConnection) { connections = this.loadWithSelectConnection(connections); } else { if (connections.length === 1 && connections[0] === msgAddNewConnection) { connections.unshift(msgSelectConnection); this.selectWithOptionName(msgSelectConnection); } else { connections.push(msgAddNewConnection); } } this.setOptions(connections); } } private loadWithSelectConnection(connections: string[]): string[] { if (connections && connections.length > 0) { connections.unshift(msgSelectConnection); this.selectWithOptionName(msgSelectConnection); connections.push(msgAddNewConnection); this.setOptions(connections); } return connections; } //Get connections from context public getConnections(model: INotebookModel): string[] { let otherConnections: ConnectionProfile[] = []; model.contexts.otherConnections.forEach((conn) => { otherConnections.push(conn); }); // If current connection connects to master, select the option in the dropdown that doesn't specify a database if (!model.contexts.defaultConnection.databaseName) { this.selectWithOptionName(model.contexts.defaultConnection.serverName); } else { if (model.contexts.defaultConnection) { this.selectWithOptionName(formatServerNameWithDatabaseNameForAttachTo(model.contexts.defaultConnection)); } else { this.select(0); } } otherConnections = this.setConnectionsList(model.contexts.defaultConnection, model.contexts.otherConnections); let connections = otherConnections.map((context) => context.databaseName ? context.serverName + ' (' + context.databaseName + ')' : context.serverName); return connections; } private setConnectionsList(defaultConnection: ConnectionProfile, otherConnections: ConnectionProfile[]) { if (defaultConnection.serverName !== msgSelectConnection) { otherConnections = otherConnections.filter(conn => conn.id !== defaultConnection.id); otherConnections.unshift(defaultConnection); if (otherConnections.length > 1) { otherConnections = otherConnections.filter(val => val.serverName !== msgSelectConnection); } } return otherConnections; } public getConnectionWithServerAndDatabaseNames(selection: string): ConnectionProfile { // Find all connections with the the same server as the selected option let connections = this.model.contexts.otherConnections.filter((c) => selection === c.serverName); // If only one connection exists with the same server name, use that one if (connections.length === 1) { return connections[0]; } else { // Extract server and database name let serverName = getServerFromFormattedAttachToName(selection); let databaseName = getDatabaseFromFormattedAttachToName(selection); return this.model.contexts.otherConnections.find((c) => serverName === c.serverName && databaseName === c.databaseName); } } public doChangeContext(connection?: ConnectionProfile, hideErrorMessage?: boolean): void { if (this.value === msgAddNewConnection) { this.openConnectionDialog(); } else { this.model.changeContext(this.value, connection, hideErrorMessage).then(ok => undefined, err => this._notificationService.error(getErrorMessage(err))); } } /** * Open connection dialog * Enter server details and connect to a server from the dialog * Bind the server value to 'Attach To' drop down * Connected server is displayed at the top of drop down **/ public async openConnectionDialog(): Promise { try { 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.getKernelDisplayName()); this.doChangeContext(undefined, true); return; } let connectionProfile = new ConnectionProfile(this._capabilitiesService, connection); let connectedServer = formatServerNameWithDatabaseNameForAttachTo(connectionProfile); //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.getKernelDisplayName()); this.doChangeContext(); return; } else { attachToConnections.unshift(connectedServer); } //To ignore n/a after we have at least one valid connection attachToConnections = attachToConnections.filter(val => val !== msgSelectConnection); let index = attachToConnections.findIndex((connection => connection === connectedServer)); this.setOptions([]); this.setOptions(attachToConnections); if (!index || index < 0 || index >= attachToConnections.length) { index = 0; } this.select(index); this.model.addAttachToConnectionsToBeDisposed(connectionProfile); // Call doChangeContext to set the newly chosen connection in the model this.doChangeContext(connectionProfile); }); } catch (error) { const actions: INotificationActions = { primary: [] }; this._notificationService.notify({ severity: Severity.Error, message: getErrorMessage(error), actions }); } } }