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
This commit is contained in:
Yurong He
2019-01-31 13:34:59 -08:00
committed by GitHub
parent 90d8c37f91
commit ecac6201d0
20 changed files with 715 additions and 56 deletions

View File

@@ -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 @@
]
}
}
}
}

View File

@@ -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"
}

View File

@@ -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'
}

View File

@@ -0,0 +1,3 @@
'use strict';
export default require('error-ex')('EscapeException');

View File

@@ -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() });
}

View File

@@ -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<boolean> {
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 {

View File

@@ -63,7 +63,7 @@ export async function getNode<T extends TreeNode>(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<any> {
@@ -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<any> {
@@ -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<any> {
@@ -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(<FolderNode>node);
break;
case constants.HdfsItems.File:
case constants.MssqlClusterItems.File:
await this.deleteFile(<FileNode>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<any> {
@@ -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<any> {
@@ -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<any> {

View File

@@ -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<vscode.TreeItem> {
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'
};

View File

@@ -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<T extends TreeNode>(explorerContext: sqlops.ObjectExplorerContext): Promise<T> {
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<Session> {
for (let session of this.sessionMap.values()) {
if (session.connection && session.connection.isMatch(connectionProfile)) {
if (session.connection && await session.connection.isMatch(connectionProfile)) {
return session;
}
}

View File

@@ -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<T>(question: IQuestion, ignoreFocusOut?: boolean): Promise<T> {
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<T>(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);
}
});
}
}

View File

@@ -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;
}, []);
});
}
}

View File

@@ -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;
});
}
}

View File

@@ -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;
}
}

View File

@@ -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}`);
}
}
}

View File

@@ -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;
});
}
}

View File

@@ -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];
});
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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<IEndpoint> {
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;
}