From ecac6201d09ee472bc9f27fc6fe272f02ac3ea91 Mon Sep 17 00:00:00 2001 From: Yurong He <43652751+YurongHe@users.noreply.github.com> Date: Thu, 31 Jan 2019 13:34:59 -0800 Subject: [PATCH] Rename nodeType name in order to have file context menu in both mssql and SqlOpsStudio (#3862) * Added data service context menu: file related operations. All new files are ported from SqlOpsStudio. Will remove these functionality from SqlOpsStudio. * Used the existing constant hadoopKnoxEndpointName * Rename nodeType name from hdfs to bdc. So we can have file context menu in both mssql and SqlOpsStudio. Need to add "Create External Table from CSV" support for bdc nodeType * Rename bdc to mssqlcluster --- extensions/mssql/package.json | 88 +++++++++++++- extensions/mssql/package.nls.json | 8 +- extensions/mssql/src/constants.ts | 18 +-- extensions/mssql/src/escapeException.ts | 3 + extensions/mssql/src/main.ts | 14 ++- .../objectExplorerNodeProvider/connection.ts | 25 ++-- .../hdfsCommands.ts | 16 +-- .../hdfsProvider.ts | 14 +-- .../objectExplorerNodeProvider.ts | 8 +- extensions/mssql/src/prompts/adapter.ts | 111 ++++++++++++++++++ extensions/mssql/src/prompts/checkbox.ts | 52 ++++++++ extensions/mssql/src/prompts/confirm.ts | 36 ++++++ extensions/mssql/src/prompts/expand.ts | 78 ++++++++++++ extensions/mssql/src/prompts/factory.ts | 35 ++++++ extensions/mssql/src/prompts/input.ts | 59 ++++++++++ extensions/mssql/src/prompts/list.ts | 33 ++++++ extensions/mssql/src/prompts/password.ts | 15 +++ .../mssql/src/prompts/progressIndicator.ts | 70 +++++++++++ extensions/mssql/src/prompts/prompt.ts | 33 ++++++ extensions/mssql/src/utils.ts | 55 ++++++--- 20 files changed, 715 insertions(+), 56 deletions(-) create mode 100644 extensions/mssql/src/escapeException.ts create mode 100644 extensions/mssql/src/prompts/adapter.ts create mode 100644 extensions/mssql/src/prompts/checkbox.ts create mode 100644 extensions/mssql/src/prompts/confirm.ts create mode 100644 extensions/mssql/src/prompts/expand.ts create mode 100644 extensions/mssql/src/prompts/factory.ts create mode 100644 extensions/mssql/src/prompts/input.ts create mode 100644 extensions/mssql/src/prompts/list.ts create mode 100644 extensions/mssql/src/prompts/password.ts create mode 100644 extensions/mssql/src/prompts/progressIndicator.ts create mode 100644 extensions/mssql/src/prompts/prompt.ts diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index ae592f1a50..f0b888d778 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -42,6 +42,32 @@ ] } ], + "commands": [ + { + "command": "mssqlCluster.uploadFiles", + "title": "%mssqlCluster.uploadFiles%" + }, + { + "command": "mssqlCluster.mkdir", + "title": "%mssqlCluster.mkdir%" + }, + { + "command": "mssqlCluster.deleteFiles", + "title": "%mssqlCluster.deleteFiles%" + }, + { + "command": "mssqlCluster.previewFile", + "title": "%mssqlCluster.previewFile%" + }, + { + "command": "mssqlCluster.saveFile", + "title": "%mssqlCluster.saveFile%" + }, + { + "command": "mssqlCluster.copyPath", + "title": "%mssqlCluster.copyPath%" + } + ], "outputChannels": [ "MSSQL" ], @@ -131,6 +157,66 @@ } } }, + "menus": { + "commandPalette": [ + { + "command": "mssqlCluster.uploadFiles", + "when": "false" + }, + { + "command": "mssqlCluster.mkdir", + "when": "false" + }, + { + "command": "mssqlCluster.deleteFiles", + "when": "false" + }, + { + "command": "mssqlCluster.previewFile", + "when": "false" + }, + { + "command": "mssqlCluster.saveFile", + "when": "false" + }, + { + "command": "mssqlCluster.copyPath", + "when": "false" + } + ], + "objectExplorer/item/context": [ + { + "command": "mssqlCluster.uploadFiles", + "when": "nodeType=~/^mssqlCluster/ && nodeType != mssqlCluster:message && nodeType != mssqlCluster:file", + "group": "1mssqlCluster@1" + }, + { + "command": "mssqlCluster.mkdir", + "when": "nodeType=~/^mssqlCluster/ && nodeType != mssqlCluster:message && nodeType != mssqlCluster:file", + "group": "1mssqlCluster@1" + }, + { + "command": "mssqlCluster.saveFile", + "when": "nodeType == mssqlCluster:file", + "group": "1mssqlCluster@1" + }, + { + "command": "mssqlCluster.previewFile", + "when": "nodeType == mssqlCluster:file", + "group": "1mssqlCluster@2" + }, + { + "command": "mssqlCluster.copyPath", + "when": "nodeType=~/^mssqlCluster/ && nodeType != mssqlCluster:connection && nodeType != mssqlCluster:message", + "group": "1mssqlCluster@3" + }, + { + "command": "mssqlCluster.deleteFiles", + "when": "nodeType=~/^mssqlCluster/ && viewItem != mssqlCluster:connection && nodeType != mssqlCluster:message", + "group": "1mssqlCluster@4" + } + ] + }, "dashboard": { "provider": "MSSQL", "flavors": [ @@ -712,4 +798,4 @@ ] } } -} +} \ No newline at end of file diff --git a/extensions/mssql/package.nls.json b/extensions/mssql/package.nls.json index 46fe51b221..dd30e3362e 100644 --- a/extensions/mssql/package.nls.json +++ b/extensions/mssql/package.nls.json @@ -4,5 +4,11 @@ "json.schemas.fileMatch.desc": "An array of file patterns to match against when resolving JSON files to schemas.", "json.schemas.fileMatch.item.desc": "A file pattern that can contain '*' to match against when resolving JSON files to schemas.", "json.schemas.schema.desc": "The schema definition for the given URL. The schema only needs to be provided to avoid accesses to the schema URL.", - "json.format.enable.desc": "Enable/disable default JSON formatter (requires restart)" + "json.format.enable.desc": "Enable/disable default JSON formatter (requires restart)", + "mssqlCluster.uploadFiles": "Upload files", + "mssqlCluster.mkdir": "New directory", + "mssqlCluster.deleteFiles": "Delete", + "mssqlCluster.previewFile": "Preview", + "mssqlCluster.saveFile": "Save", + "mssqlCluster.copyPath": "Copy Path" } \ No newline at end of file diff --git a/extensions/mssql/src/constants.ts b/extensions/mssql/src/constants.ts index 5baec591b4..e693de94a3 100644 --- a/extensions/mssql/src/constants.ts +++ b/extensions/mssql/src/constants.ts @@ -41,20 +41,20 @@ export const objectExplorerPrefix: string = 'objectexplorer://'; export const ViewType = 'view'; export enum BuiltInCommands { - SetContext = 'setContext' + SetContext = 'setContext' } export enum CommandContext { - WizardServiceEnabled = 'wizardservice:enabled' + WizardServiceEnabled = 'wizardservice:enabled' } -export enum HdfsItems { - Connection = 'hdfs:connection', - Folder = 'hdfs:folder', - File = 'hdfs:file', - Message = 'hdfs:message' +export enum MssqlClusterItems { + Connection = 'mssqlCluster:connection', + Folder = 'mssqlCluster:folder', + File = 'mssqlCluster:file', + Message = 'mssqlCluster:message' } -export enum HdfsItemsSubType { - Spark = 'hdfs:spark' +export enum MssqlClusterItemsSubType { + Spark = 'mssqlCluster:spark' } \ No newline at end of file diff --git a/extensions/mssql/src/escapeException.ts b/extensions/mssql/src/escapeException.ts new file mode 100644 index 0000000000..e405084cbe --- /dev/null +++ b/extensions/mssql/src/escapeException.ts @@ -0,0 +1,3 @@ +'use strict'; + +export default require('error-ex')('EscapeException'); diff --git a/extensions/mssql/src/main.ts b/extensions/mssql/src/main.ts index a376dc5968..01c933f1ea 100644 --- a/extensions/mssql/src/main.ts +++ b/extensions/mssql/src/main.ts @@ -21,6 +21,9 @@ import { TelemetryFeature, AgentServicesFeature, DacFxServicesFeature } from './ import { AppContext } from './appContext'; import { ApiWrapper } from './apiWrapper'; import { MssqlObjectExplorerNodeProvider } from './objectExplorerNodeProvider/objectExplorerNodeProvider'; +import { UploadFilesCommand, MkDirCommand, SaveFileCommand, PreviewFileCommand, CopyPathCommand, DeleteFilesCommand } from './objectExplorerNodeProvider/hdfsCommands'; +import { IPrompter } from './prompts/question'; +import CodeAdapter from './prompts/adapter'; const baseConfig = require('./config.json'); const outputChannel = vscode.window.createOutputChannel(Constants.serviceName); @@ -65,6 +68,9 @@ export async function activate(context: vscode.ExtensionContext) { outputChannel: new CustomOutputChannel() }; + let prompter: IPrompter = new CodeAdapter(); + let appContext = new AppContext(context, new ApiWrapper()); + const installationStart = Date.now(); serverdownloader.getOrDownloadServer().then(e => { const installationComplete = Date.now(); @@ -89,7 +95,7 @@ export async function activate(context: vscode.ExtensionContext) { languageClient.start(); credentialsStore.start(); resourceProvider.start(); - let nodeProvider = new MssqlObjectExplorerNodeProvider(new AppContext(context, new ApiWrapper())); + let nodeProvider = new MssqlObjectExplorerNodeProvider(appContext); sqlops.dataprotocol.registerObjectExplorerNodeProvider(nodeProvider); }, e => { Telemetry.sendTelemetryEvent('ServiceInitializingFailed'); @@ -100,6 +106,12 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(contextProvider); context.subscriptions.push(credentialsStore); context.subscriptions.push(resourceProvider); + context.subscriptions.push(new UploadFilesCommand(prompter, appContext)); + context.subscriptions.push(new MkDirCommand(prompter, appContext)); + context.subscriptions.push(new SaveFileCommand(prompter, appContext)); + context.subscriptions.push(new PreviewFileCommand(prompter, appContext)); + context.subscriptions.push(new CopyPathCommand(appContext)); + context.subscriptions.push(new DeleteFilesCommand(prompter, appContext)); context.subscriptions.push({ dispose: () => languageClient.stop() }); } diff --git a/extensions/mssql/src/objectExplorerNodeProvider/connection.ts b/extensions/mssql/src/objectExplorerNodeProvider/connection.ts index 5c5f4991cb..4b2748b913 100644 --- a/extensions/mssql/src/objectExplorerNodeProvider/connection.ts +++ b/extensions/mssql/src/objectExplorerNodeProvider/connection.ts @@ -13,7 +13,8 @@ const localize = nls.loadMessageBundle(); import * as constants from '../constants'; import * as LocalizedConstants from '../localizedConstants'; import * as utils from '../utils'; -import { IFileSource, HdfsFileSource, IHdfsOptions, IRequestParams, FileSourceFactory } from './fileSources'; +import { IFileSource, IHdfsOptions, IRequestParams, FileSourceFactory } from './fileSources'; +import { IEndpoint } from './objectExplorerNodeProvider'; function appendIfExists(uri: string, propName: string, propValue: string): string { if (propValue) { @@ -110,7 +111,7 @@ export class Connection { isCloud: false, azureVersion: 0, osVersion: '', - options: { isBigDataCluster: false, clusterEndpoints: []} + options: {} }; return info; } @@ -188,15 +189,23 @@ export class Connection { return this.connectionInfo.options[constants.groupIdName]; } - public isMatch(connectionInfo: sqlops.ConnectionInfo): boolean { + public async isMatch(connectionInfo: sqlops.ConnectionInfo): Promise { if (!connectionInfo) { return false; } - let otherConnection = new Connection(connectionInfo); - return otherConnection.groupId === this.groupId - && otherConnection.host === this.host - && otherConnection.knoxport === this.knoxport - && otherConnection.user === this.user; + let profile = connectionInfo as sqlops.IConnectionProfile; + if (profile) { + let result: IEndpoint = await utils.getClusterEndpoint(profile.id, constants.hadoopKnoxEndpointName); + if (result === undefined || !result.ipAddress || !result.port) { + return false; + } + return connectionInfo.options.groupId === this.groupId + && result.ipAddress === this.host + && String(result.port).startsWith(this.knoxport) + && String(result.port).endsWith(this.knoxport); + // TODO: enable the user check when the unified user is used + //&& connectionInfo.options.user === this.user; + } } public createHdfsFileSource(factory?: FileSourceFactory, additionalRequestParams?: IRequestParams): IFileSource { diff --git a/extensions/mssql/src/objectExplorerNodeProvider/hdfsCommands.ts b/extensions/mssql/src/objectExplorerNodeProvider/hdfsCommands.ts index eb80d8593a..07437ff10d 100644 --- a/extensions/mssql/src/objectExplorerNodeProvider/hdfsCommands.ts +++ b/extensions/mssql/src/objectExplorerNodeProvider/hdfsCommands.ts @@ -63,7 +63,7 @@ export async function getNode(context: ICommandViewContext | export class UploadFilesCommand extends ProgressCommand { constructor(prompter: IPrompter, appContext: AppContext) { - super('hdfs.uploadFiles', prompter, appContext); + super('mssqlCluster.uploadFiles', prompter, appContext); } protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise { @@ -131,7 +131,7 @@ export class UploadFilesCommand extends ProgressCommand { export class MkDirCommand extends ProgressCommand { constructor(prompter: IPrompter, appContext: AppContext) { - super('hdfs.mkdir', prompter, appContext); + super('mssqlCluster.mkdir', prompter, appContext); } protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise { @@ -177,7 +177,7 @@ export class MkDirCommand extends ProgressCommand { export class DeleteFilesCommand extends Command { constructor(private prompter: IPrompter, appContext: AppContext) { - super('hdfs.deleteFiles', appContext); + super('mssqlCluster.deleteFiles', appContext); } protected async preExecute(context: ICommandViewContext |ICommandObjectExplorerContext, args: object = {}): Promise { @@ -197,10 +197,10 @@ export class DeleteFilesCommand extends Command { oeNodeToRefresh = await oeNodeToDelete.getParent(); } switch (treeItem.contextValue) { - case constants.HdfsItems.Folder: + case constants.MssqlClusterItems.Folder: await this.deleteFolder(node); break; - case constants.HdfsItems.File: + case constants.MssqlClusterItems.File: await this.deleteFile(node); break; default: @@ -248,7 +248,7 @@ export class DeleteFilesCommand extends Command { export class SaveFileCommand extends ProgressCommand { constructor(prompter: IPrompter, appContext: AppContext) { - super('hdfs.saveFile', prompter, appContext); + super('mssqlCluster.saveFile', prompter, appContext); } protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise { @@ -286,7 +286,7 @@ export class PreviewFileCommand extends ProgressCommand { public static readonly DefaultMaxSize = 30 * 1024 * 1024; constructor(prompter: IPrompter, appContext: AppContext) { - super('hdfs.previewFile', prompter, appContext); + super('mssqlCluster.previewFile', prompter, appContext); } protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise { @@ -338,7 +338,7 @@ export class CopyPathCommand extends Command { public static readonly DefaultMaxSize = 30 * 1024 * 1024; constructor(appContext: AppContext) { - super('hdfs.copyPath', appContext); + super('mssqlCluster.copyPath', appContext); } protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise { diff --git a/extensions/mssql/src/objectExplorerNodeProvider/hdfsProvider.ts b/extensions/mssql/src/objectExplorerNodeProvider/hdfsProvider.ts index 83aabb70e7..b90383a127 100644 --- a/extensions/mssql/src/objectExplorerNodeProvider/hdfsProvider.ts +++ b/extensions/mssql/src/objectExplorerNodeProvider/hdfsProvider.ts @@ -108,7 +108,7 @@ export class FolderNode extends HdfsFileSourceNode { protected _nodeType: string; constructor(context: TreeDataContext, path: string, fileSource: IFileSource, nodeType?: string) { super(context, path, fileSource); - this._nodeType = nodeType ? nodeType : Constants.HdfsItems.Folder; + this._nodeType = nodeType ? nodeType : Constants.MssqlClusterItems.Folder; } private ensureChildrenExist(): void { @@ -209,7 +209,7 @@ export class FolderNode extends HdfsFileSourceNode { export class ConnectionNode extends FolderNode { constructor(context: TreeDataContext, private displayName: string, fileSource: IFileSource) { - super(context, '/', fileSource, Constants.HdfsItems.Connection); + super(context, '/', fileSource, Constants.MssqlClusterItems.Connection); } getDisplayName(): string { @@ -247,7 +247,7 @@ export class FileNode extends HdfsFileSourceNode implements IFileNode { dark: this.context.extensionContext.asAbsolutePath('resources/dark/file_inverse.svg'), light: this.context.extensionContext.asAbsolutePath('resources/light/file.svg') }; - item.contextValue = Constants.HdfsItems.File; + item.contextValue = Constants.MssqlClusterItems.File; return item; } @@ -261,7 +261,7 @@ export class FileNode extends HdfsFileSourceNode implements IFileNode { metadata: undefined, nodePath: this.generateNodePath(), nodeStatus: undefined, - nodeType: Constants.HdfsItems.File, + nodeType: Constants.MssqlClusterItems.File, nodeSubType: this.getSubType(), iconType: 'FileGroupFile' }; @@ -306,7 +306,7 @@ export class FileNode extends HdfsFileSourceNode implements IFileNode { private getSubType(): string { if (this.getDisplayName().toLowerCase().endsWith('.jar') || this.getDisplayName().toLowerCase().endsWith('.py')) { - return Constants.HdfsItemsSubType.Spark; + return Constants.MssqlClusterItemsSubType.Spark; } return undefined; @@ -344,7 +344,7 @@ export class MessageNode extends TreeNode { public getTreeItem(): vscode.TreeItem | Promise { let item = new vscode.TreeItem(this.message, vscode.TreeItemCollapsibleState.None); - item.contextValue = Constants.HdfsItems.Message; + item.contextValue = Constants.MssqlClusterItems.Message; return item; } @@ -357,7 +357,7 @@ export class MessageNode extends TreeNode { metadata: undefined, nodePath: this.generateNodePath(), nodeStatus: undefined, - nodeType: Constants.HdfsItems.Message, + nodeType: Constants.MssqlClusterItems.Message, nodeSubType: undefined, iconType: 'MessageType' }; diff --git a/extensions/mssql/src/objectExplorerNodeProvider/objectExplorerNodeProvider.ts b/extensions/mssql/src/objectExplorerNodeProvider/objectExplorerNodeProvider.ts index 9e77cf27de..64aa23ea07 100644 --- a/extensions/mssql/src/objectExplorerNodeProvider/objectExplorerNodeProvider.ts +++ b/extensions/mssql/src/objectExplorerNodeProvider/objectExplorerNodeProvider.ts @@ -21,7 +21,7 @@ import { AppContext } from '../appContext'; import * as constants from '../constants'; const outputChannel = vscode.window.createOutputChannel(constants.providerId); -interface IEndpoint { +export interface IEndpoint { serviceName: string; ipAddress: string; port: number; @@ -209,7 +209,7 @@ export class MssqlObjectExplorerNodeProvider extends ProviderBase implements sql async findNodeForContext(explorerContext: sqlops.ObjectExplorerContext): Promise { let node: T = undefined; - let session = this.findSessionForConnection(explorerContext.connectionProfile); + let session = await this.findSessionForConnection(explorerContext.connectionProfile); if (session) { if (explorerContext.isConnectionNode) { // Note: ideally fix so we verify T matches RootNode and go from there @@ -222,9 +222,9 @@ export class MssqlObjectExplorerNodeProvider extends ProviderBase implements sql return node; } - private findSessionForConnection(connectionProfile: sqlops.IConnectionProfile): Session { + private async findSessionForConnection(connectionProfile: sqlops.IConnectionProfile): Promise { for (let session of this.sessionMap.values()) { - if (session.connection && session.connection.isMatch(connectionProfile)) { + if (session.connection && await session.connection.isMatch(connectionProfile)) { return session; } } diff --git a/extensions/mssql/src/prompts/adapter.ts b/extensions/mssql/src/prompts/adapter.ts new file mode 100644 index 0000000000..0417177505 --- /dev/null +++ b/extensions/mssql/src/prompts/adapter.ts @@ -0,0 +1,111 @@ +'use strict'; + +// This code is originally from https://github.com/DonJayamanne/bowerVSCode +// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE + +import {window, OutputChannel } from 'vscode'; +import * as nodeUtil from 'util'; +import PromptFactory from './factory'; +import EscapeException from '../escapeException'; +import { IQuestion, IPrompter, IPromptCallback } from './question'; + +// Supports simple pattern for prompting for user input and acting on this +export default class CodeAdapter implements IPrompter { + + private outChannel: OutputChannel; + private outBuffer: string = ''; + private messageLevelFormatters = {}; + constructor() { + // TODO Decide whether output channel logging should be saved here? + this.outChannel = window.createOutputChannel('test'); + // this.outChannel.clear(); + } + + public logError(message: any): void { + let line = `error: ${message.message}\n Code - ${message.code}`; + + this.outBuffer += `${line}\n`; + this.outChannel.appendLine(line); + } + + private formatMessage(message: any): string { + const prefix = `${message.level}: (${message.id}) `; + return `${prefix}${message.message}`; + } + + public clearLog(): void { + this.outChannel.clear(); + } + + public showLog(): void { + this.outChannel.show(); + } + + // TODO define question interface + private fixQuestion(question: any): any { + if (question.type === 'checkbox' && Array.isArray(question.choices)) { + // For some reason when there's a choice of checkboxes, they aren't formatted properly + // Not sure where the issue is + question.choices = question.choices.map(item => { + if (typeof (item) === 'string') { + return { checked: false, name: item, value: item }; + } else { + return item; + } + }); + } + } + + public promptSingle(question: IQuestion, ignoreFocusOut?: boolean): Promise { + let questions: IQuestion[] = [question]; + return this.prompt(questions, ignoreFocusOut).then( (answers: {[key: string]: T}) => { + if (answers) { + let response: T = answers[question.name]; + return response || undefined; + } + }); + } + + public prompt(questions: IQuestion[], ignoreFocusOut?: boolean): Promise<{[key: string]: T}> { + let answers: {[key: string]: T} = {}; + + // Collapse multiple questions into a set of prompt steps + let promptResult: Promise<{[key: string]: T}> = questions.reduce((promise: Promise<{[key: string]: T}>, question: IQuestion) => { + this.fixQuestion(question); + + return promise.then(() => { + return PromptFactory.createPrompt(question, ignoreFocusOut); + }).then(prompt => { + if (!question.shouldPrompt || question.shouldPrompt(answers) === true) { + return prompt.render().then(result => { + answers[question.name] = result; + + if (question.onAnswered) { + question.onAnswered(result); + } + return answers; + }); + } + return answers; + }); + }, Promise.resolve()); + + return promptResult.catch(err => { + if (err instanceof EscapeException || err instanceof TypeError) { + return undefined; + } + + window.showErrorMessage(err.message); + }); + } + + // Helper to make it possible to prompt using callback pattern. Generally Promise is a preferred flow + public promptCallback(questions: IQuestion[], callback: IPromptCallback): void { + // Collapse multiple questions into a set of prompt steps + this.prompt(questions).then(answers => { + if (callback) { + callback(answers); + } + }); + } +} diff --git a/extensions/mssql/src/prompts/checkbox.ts b/extensions/mssql/src/prompts/checkbox.ts new file mode 100644 index 0000000000..806199203e --- /dev/null +++ b/extensions/mssql/src/prompts/checkbox.ts @@ -0,0 +1,52 @@ +'use strict'; + +// This code is originally from https://github.com/DonJayamanne/bowerVSCode +// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE + +import { window } from 'vscode'; +import Prompt from './prompt'; +import EscapeException from '../escapeException'; + +const figures = require('figures'); + +export default class CheckboxPrompt extends Prompt { + + constructor(question: any, ignoreFocusOut?: boolean) { + super(question, ignoreFocusOut); + } + + public render(): any { + let choices = this._question.choices.reduce((result, choice) => { + let choiceName = choice.name || choice; + result[`${choice.checked === true ? figures.radioOn : figures.radioOff} ${choiceName}`] = choice; + return result; + }, {}); + + let options = this.defaultQuickPickOptions; + options.placeHolder = this._question.message; + + let quickPickOptions = Object.keys(choices); + quickPickOptions.push(figures.tick); + + return window.showQuickPick(quickPickOptions, options) + .then(result => { + if (result === undefined) { + throw new EscapeException(); + } + + if (result !== figures.tick) { + choices[result].checked = !choices[result].checked; + + return this.render(); + } + + return this._question.choices.reduce((result2, choice) => { + if (choice.checked === true) { + result2.push(choice.value); + } + + return result2; + }, []); + }); + } +} diff --git a/extensions/mssql/src/prompts/confirm.ts b/extensions/mssql/src/prompts/confirm.ts new file mode 100644 index 0000000000..974b2b754c --- /dev/null +++ b/extensions/mssql/src/prompts/confirm.ts @@ -0,0 +1,36 @@ +'use strict'; + +// This code is originally from https://github.com/DonJayamanne/bowerVSCode +// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE + +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { window } from 'vscode'; +import Prompt from './prompt'; +import EscapeException from '../escapeException'; + +export default class ConfirmPrompt extends Prompt { + + constructor(question: any, ignoreFocusOut?: boolean) { + super(question, ignoreFocusOut); + } + + public render(): any { + let choices: { [id: string]: boolean } = {}; + choices[localize('msgYes', 'Yes')] = true; + choices[localize('msgNo', 'No')] = false; + + let options = this.defaultQuickPickOptions; + options.placeHolder = this._question.message; + + return window.showQuickPick(Object.keys(choices), options) + .then(result => { + if (result === undefined) { + throw new EscapeException(); + } + + return choices[result] || false; + }); + } +} diff --git a/extensions/mssql/src/prompts/expand.ts b/extensions/mssql/src/prompts/expand.ts new file mode 100644 index 0000000000..df98b4dbed --- /dev/null +++ b/extensions/mssql/src/prompts/expand.ts @@ -0,0 +1,78 @@ +'use strict'; + +// This code is originally from https://github.com/DonJayamanne/bowerVSCode +// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE + +import vscode = require('vscode'); +import Prompt from './prompt'; +import EscapeException from '../escapeException'; +import { INameValueChoice } from './question'; + +const figures = require('figures'); + +export default class ExpandPrompt extends Prompt { + + constructor(question: any, ignoreFocusOut?: boolean) { + super(question, ignoreFocusOut); + } + + public render(): any { + // label indicates this is a quickpick item. Otherwise it's a name-value pair + if (this._question.choices[0].label) { + return this.renderQuickPick(this._question.choices); + } else { + return this.renderNameValueChoice(this._question.choices); + } + } + + private renderQuickPick(choices: vscode.QuickPickItem[]): any { + let options = this.defaultQuickPickOptions; + options.placeHolder = this._question.message; + + return vscode.window.showQuickPick(choices, options) + .then(result => { + if (result === undefined) { + throw new EscapeException(); + } + + return this.validateAndReturn(result || false); + }); + } + private renderNameValueChoice(choices: INameValueChoice[]): any { + const choiceMap = this._question.choices.reduce((result, choice) => { + result[choice.name] = choice.value; + return result; + }, {}); + + let options = this.defaultQuickPickOptions; + options.placeHolder = this._question.message; + + return vscode.window.showQuickPick(Object.keys(choiceMap), options) + .then(result => { + if (result === undefined) { + throw new EscapeException(); + } + + // Note: cannot be used with 0 or false responses + let returnVal = choiceMap[result] || false; + return this.validateAndReturn(returnVal); + }); + } + + private validateAndReturn(value: any): any { + if (!this.validate(value)) { + return this.render(); + } + return value; + } + + private validate(value: any): boolean { + const validationError = this._question.validate ? this._question.validate(value || '') : undefined; + + if (validationError) { + this._question.message = `${figures.warning} ${validationError}`; + return false; + } + return true; + } +} diff --git a/extensions/mssql/src/prompts/factory.ts b/extensions/mssql/src/prompts/factory.ts new file mode 100644 index 0000000000..1b9db3dda5 --- /dev/null +++ b/extensions/mssql/src/prompts/factory.ts @@ -0,0 +1,35 @@ +'use strict'; + +// This code is originally from https://github.com/DonJayamanne/bowerVSCode +// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE + +import Prompt from './prompt'; +import InputPrompt from './input'; +import PasswordPrompt from './password'; +import ListPrompt from './list'; +import ConfirmPrompt from './confirm'; +import CheckboxPrompt from './checkbox'; +import ExpandPrompt from './expand'; + +export default class PromptFactory { + + public static createPrompt(question: any, ignoreFocusOut?: boolean): Prompt { + switch (question.type || 'input') { + case 'string': + case 'input': + return new InputPrompt(question, ignoreFocusOut); + case 'password': + return new PasswordPrompt(question, ignoreFocusOut); + case 'list': + return new ListPrompt(question, ignoreFocusOut); + case 'confirm': + return new ConfirmPrompt(question, ignoreFocusOut); + case 'checkbox': + return new CheckboxPrompt(question, ignoreFocusOut); + case 'expand': + return new ExpandPrompt(question, ignoreFocusOut); + default: + throw new Error(`Could not find a prompt for question type ${question.type}`); + } + } +} diff --git a/extensions/mssql/src/prompts/input.ts b/extensions/mssql/src/prompts/input.ts new file mode 100644 index 0000000000..41bedc2eb2 --- /dev/null +++ b/extensions/mssql/src/prompts/input.ts @@ -0,0 +1,59 @@ +'use strict'; + +// This code is originally from https://github.com/DonJayamanne/bowerVSCode +// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE + +import { window, InputBoxOptions } from 'vscode'; +import Prompt from './prompt'; +import EscapeException from '../escapeException'; + +const figures = require('figures'); + +export default class InputPrompt extends Prompt { + + protected _options: InputBoxOptions; + + constructor(question: any, ignoreFocusOut?: boolean) { + super(question, ignoreFocusOut); + + this._options = this.defaultInputBoxOptions; + this._options.prompt = this._question.message; + } + + // Helper for callers to know the right type to get from the type factory + public static get promptType(): string { return 'input'; } + + public render(): any { + // Prefer default over the placeHolder, if specified + let placeHolder = this._question.default ? this._question.default : this._question.placeHolder; + + if (this._question.default instanceof Error) { + placeHolder = this._question.default.message; + this._question.default = undefined; + } + + this._options.placeHolder = placeHolder; + + return window.showInputBox(this._options) + .then(result => { + if (result === undefined) { + throw new EscapeException(); + } + + if (result === '') { + // Use the default value, if defined + result = this._question.default || ''; + } + + const validationError = this._question.validate ? this._question.validate(result || '') : undefined; + + if (validationError) { + this._question.default = new Error(`${figures.warning} ${validationError}`); + + return this.render(); + } + + return result; + }); + } +} diff --git a/extensions/mssql/src/prompts/list.ts b/extensions/mssql/src/prompts/list.ts new file mode 100644 index 0000000000..536f35be55 --- /dev/null +++ b/extensions/mssql/src/prompts/list.ts @@ -0,0 +1,33 @@ +'use strict'; + +// This code is originally from https://github.com/DonJayamanne/bowerVSCode +// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE + +import { window } from 'vscode'; +import Prompt from './prompt'; +import EscapeException from '../escapeException'; + +export default class ListPrompt extends Prompt { + constructor(question: any, ignoreFocusOut?: boolean) { + super(question, ignoreFocusOut); + } + + public render(): any { + const choices = this._question.choices.reduce((result, choice) => { + result[choice.name] = choice.value; + return result; + }, {}); + + let options = this.defaultQuickPickOptions; + options.placeHolder = this._question.message; + + return window.showQuickPick(Object.keys(choices), options) + .then(result => { + if (result === undefined) { + throw new EscapeException(); + } + + return choices[result]; + }); + } +} diff --git a/extensions/mssql/src/prompts/password.ts b/extensions/mssql/src/prompts/password.ts new file mode 100644 index 0000000000..8a19e8c643 --- /dev/null +++ b/extensions/mssql/src/prompts/password.ts @@ -0,0 +1,15 @@ +'use strict'; + +// This code is originally from https://github.com/DonJayamanne/bowerVSCode +// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE + +import InputPrompt from './input'; + +export default class PasswordPrompt extends InputPrompt { + + constructor(question: any, ignoreFocusOut?: boolean) { + super(question, ignoreFocusOut); + + this._options.password = true; + } +} diff --git a/extensions/mssql/src/prompts/progressIndicator.ts b/extensions/mssql/src/prompts/progressIndicator.ts new file mode 100644 index 0000000000..4346d00da8 --- /dev/null +++ b/extensions/mssql/src/prompts/progressIndicator.ts @@ -0,0 +1,70 @@ +'use strict'; + +// This code is originally from https://github.com/DonJayamanne/bowerVSCode +// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE + +import {window, StatusBarItem, StatusBarAlignment} from 'vscode'; + +export default class ProgressIndicator { + + private _statusBarItem: StatusBarItem; + + constructor() { + this._statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left); + } + + private _tasks: string[] = []; + public beginTask(task: string): void { + this._tasks.push(task); + this.displayProgressIndicator(); + } + + public endTask(task: string): void { + if (this._tasks.length > 0) { + this._tasks.pop(); + } + + this.setMessage(); + } + + private setMessage(): void { + if (this._tasks.length === 0) { + this._statusBarItem.text = ''; + this.hideProgressIndicator(); + return; + } + + this._statusBarItem.text = this._tasks[this._tasks.length - 1]; + this._statusBarItem.show(); + } + + private _interval: any; + private displayProgressIndicator(): void { + this.setMessage(); + this.hideProgressIndicator(); + this._interval = setInterval(() => this.onDisplayProgressIndicator(), 100); + } + private hideProgressIndicator(): void { + if (this._interval) { + clearInterval(this._interval); + this._interval = undefined; + } + this.ProgressCounter = 0; + } + + private ProgressText = ['|', '/', '-', '\\', '|', '/', '-', '\\']; + private ProgressCounter = 0; + private onDisplayProgressIndicator(): void { + if (this._tasks.length === 0) { + return; + } + + let txt = this.ProgressText[this.ProgressCounter]; + this._statusBarItem.text = this._tasks[this._tasks.length - 1] + ' ' + txt; + this.ProgressCounter++; + + if (this.ProgressCounter >= this.ProgressText.length - 1) { + this.ProgressCounter = 0; + } + } +} diff --git a/extensions/mssql/src/prompts/prompt.ts b/extensions/mssql/src/prompts/prompt.ts new file mode 100644 index 0000000000..700f577d66 --- /dev/null +++ b/extensions/mssql/src/prompts/prompt.ts @@ -0,0 +1,33 @@ +'use strict'; + +// This code is originally from https://github.com/DonJayamanne/bowerVSCode +// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE + +import { InputBoxOptions, QuickPickOptions } from 'vscode'; + +abstract class Prompt { + + protected _question: any; + protected _ignoreFocusOut?: boolean; + + constructor(question: any, ignoreFocusOut?: boolean) { + this._question = question; + this._ignoreFocusOut = ignoreFocusOut ? ignoreFocusOut : false; + } + + public abstract render(): any; + + protected get defaultQuickPickOptions(): QuickPickOptions { + return { + ignoreFocusOut: this._ignoreFocusOut + }; + } + + protected get defaultInputBoxOptions(): InputBoxOptions { + return { + ignoreFocusOut: this._ignoreFocusOut + }; + } +} + +export default Prompt; diff --git a/extensions/mssql/src/utils.ts b/extensions/mssql/src/utils.ts index 0f87bd117c..bf4f613744 100644 --- a/extensions/mssql/src/utils.ts +++ b/extensions/mssql/src/utils.ts @@ -9,8 +9,10 @@ import * as sqlops from 'sqlops'; import * as path from 'path'; import * as crypto from 'crypto'; import * as os from 'os'; -import {workspace, WorkspaceConfiguration} from 'vscode'; +import { workspace, WorkspaceConfiguration } from 'vscode'; import * as findRemoveSync from 'find-remove'; +import { IEndpoint } from './objectExplorerNodeProvider/objectExplorerNodeProvider'; +import * as constants from './constants'; const configTracingLevel = 'tracingLevel'; const configLogRetentionMinutes = 'logRetentionMinutes'; @@ -29,56 +31,53 @@ export function getAppDataPath() { } } -export function removeOldLogFiles(prefix: string) : JSON { - return findRemoveSync(getDefaultLogDir(), {prefix: `${prefix}_`, age: {seconds: getConfigLogRetentionSeconds()}, limit: getConfigLogFilesRemovalLimit()}); +export function removeOldLogFiles(prefix: string): JSON { + return findRemoveSync(getDefaultLogDir(), { prefix: `${prefix}_`, age: { seconds: getConfigLogRetentionSeconds() }, limit: getConfigLogFilesRemovalLimit() }); } -export function getConfiguration(config: string = extensionConfigSectionName) : WorkspaceConfiguration { +export function getConfiguration(config: string = extensionConfigSectionName): WorkspaceConfiguration { return workspace.getConfiguration(extensionConfigSectionName); } -export function getConfigLogFilesRemovalLimit() : number { +export function getConfigLogFilesRemovalLimit(): number { let config = getConfiguration(); if (config) { return Number((config[configLogFilesRemovalLimit]).toFixed(0)); } - else - { + else { return undefined; } } -export function getConfigLogRetentionSeconds() : number { +export function getConfigLogRetentionSeconds(): number { let config = getConfiguration(); if (config) { return Number((config[configLogRetentionMinutes] * 60).toFixed(0)); } - else - { + else { return undefined; } } -export function getConfigTracingLevel() : string { +export function getConfigTracingLevel(): string { let config = getConfiguration(); if (config) { return config[configTracingLevel]; } - else - { + else { return undefined; } } -export function getDefaultLogDir() : string { - return path.join(process.env['VSCODE_LOGS'], '..', '..','mssql'); +export function getDefaultLogDir(): string { + return path.join(process.env['VSCODE_LOGS'], '..', '..', 'mssql'); } -export function getDefaultLogFile(prefix: string, pid: number) : string { +export function getDefaultLogFile(prefix: string, pid: number): string { return path.join(getDefaultLogDir(), `${prefix}_${pid}.log`); } -export function getCommonLaunchArgsAndCleanupOldLogFiles(prefix: string, executablePath: string) : string [] { +export function getCommonLaunchArgsAndCleanupOldLogFiles(prefix: string, executablePath: string): string[] { let launchArgs = []; launchArgs.push('--log-file'); let logFile = getDefaultLogFile(prefix, process.pid); @@ -183,3 +182,25 @@ export function isObjectExplorerContext(object: any): object is sqlops.ObjectExp export function getUserHome(): string { return process.env.HOME || process.env.USERPROFILE; } + +export async function getClusterEndpoint(profileId: string, serviceName: string): Promise { + + let serverInfo: sqlops.ServerInfo = await sqlops.connection.getServerInfo(profileId); + if (!serverInfo || !serverInfo.options) { + return undefined; + } + let endpoints: IEndpoint[] = serverInfo.options[constants.clusterEndpointsProperty]; + if (!endpoints || endpoints.length === 0) { + return undefined; + } + let index = endpoints.findIndex(ep => ep.serviceName === serviceName); + if (index === -1) { + return undefined; + } + let clusterEndpoint: IEndpoint = { + serviceName: endpoints[index].serviceName, + ipAddress: endpoints[index].ipAddress, + port: endpoints[index].port + }; + return clusterEndpoint; +} \ No newline at end of file