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