Added Unified connection support (#3785)

* Added Unified connection support

* Use generic way to do expandNode.
Cleanup the ported code and removed unreference code. Added as needed later.
Resolved PR comments.

* Minor fixes and removed timer for all expanders for now. If any providers can't response, the tree node will spin and wait. We may improve later.

* Change handSessionClose to not thenable.
Added a node to OE to show error message instead of reject. So we could show partial expanded result if get any.
Resolve PR comments

* Minor fixes of PR comments
This commit is contained in:
Yurong He
2019-01-29 14:37:14 -08:00
committed by GitHub
parent b439ea45ec
commit 3ddc5e7846
37 changed files with 3087 additions and 80 deletions

View File

@@ -18,10 +18,15 @@
"update-grammar": "node ../../build/npm/update-grammar.js Microsoft/vscode-mssql syntaxes/SQL.plist ./syntaxes/sql.tmLanguage.json"
},
"dependencies": {
"clipboardy": "^1.2.3",
"dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#0.2.15",
"opener": "^1.4.3",
"service-downloader": "github:anthonydresser/service-downloader#0.1.5",
"vscode-extension-telemetry": "^0.0.15"
"stream-meter": "^1.0.4",
"uri-js": "^4.2.2",
"vscode-extension-telemetry": "^0.0.15",
"vscode-nls": "2.0.2",
"webhdfs": "^1.1.1"
},
"devDependencies": {
},

View File

@@ -0,0 +1,93 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
/**
* Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into
* this API from our code
*
* @export
* @class ApiWrapper
*/
export class ApiWrapper {
// Data APIs
public registerConnectionProvider(provider: sqlops.ConnectionProvider): vscode.Disposable {
return sqlops.dataprotocol.registerConnectionProvider(provider);
}
public registerObjectExplorerNodeProvider(provider: sqlops.ObjectExplorerNodeProvider): vscode.Disposable {
return sqlops.dataprotocol.registerObjectExplorerNodeProvider(provider);
}
public registerTaskServicesProvider(provider: sqlops.TaskServicesProvider): vscode.Disposable {
return sqlops.dataprotocol.registerTaskServicesProvider(provider);
}
public registerFileBrowserProvider(provider: sqlops.FileBrowserProvider): vscode.Disposable {
return sqlops.dataprotocol.registerFileBrowserProvider(provider);
}
public registerTaskHandler(taskId: string, handler: (profile: sqlops.IConnectionProfile) => void): void {
sqlops.tasks.registerTask(taskId, handler);
}
// VSCode APIs
public executeCommand(command: string, ...rest: any[]): Thenable<any> {
return vscode.commands.executeCommand(command, ...rest);
}
public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable {
return vscode.commands.registerCommand(command, callback, thisArg);
}
public showOpenDialog(options: vscode.OpenDialogOptions): Thenable<vscode.Uri[] | undefined> {
return vscode.window.showOpenDialog(options);
}
public showSaveDialog(options: vscode.SaveDialogOptions): Thenable<vscode.Uri> {
return vscode.window.showSaveDialog(options);
}
public openTextDocument(uri: vscode.Uri): Thenable<vscode.TextDocument>;
public openTextDocument(options: { language?: string; content?: string; }): Thenable<vscode.TextDocument>;
public openTextDocument(uriOrOptions): Thenable<vscode.TextDocument> {
return vscode.workspace.openTextDocument(uriOrOptions);
}
public showTextDocument(document: vscode.TextDocument, column?: vscode.ViewColumn, preserveFocus?: boolean, preview?: boolean): Thenable<vscode.TextEditor> {
let options: vscode.TextDocumentShowOptions = {
viewColumn: column,
preserveFocus: preserveFocus,
preview: preview
};
return vscode.window.showTextDocument(document, options);
}
public showErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
return vscode.window.showErrorMessage(message, ...items);
}
public showWarningMessage(message: string, ...items: string[]): Thenable<string | undefined> {
return vscode.window.showWarningMessage(message, ...items);
}
public showInformationMessage(message: string, ...items: string[]): Thenable<string | undefined> {
return vscode.window.showInformationMessage(message, ...items);
}
public createStatusBarItem(alignment?: vscode.StatusBarAlignment, priority?: number): vscode.StatusBarItem {
return vscode.window.createStatusBarItem(alignment, priority);
}
public get workspaceFolders(): vscode.WorkspaceFolder[] {
return vscode.workspace.workspaceFolders;
}
}

View File

@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import { ApiWrapper } from './apiWrapper';
/**
* Global context for the application
*/
export class AppContext {
private serviceMap: Map<string, any> = new Map();
constructor(public readonly extensionContext: vscode.ExtensionContext, public readonly apiWrapper: ApiWrapper) {
this.apiWrapper = apiWrapper || new ApiWrapper();
}
public getService<T>(serviceName: string): T {
return this.serviceMap.get(serviceName) as T;
}
public registerService<T>(serviceName: string, service: T): void {
this.serviceMap.set(serviceName, service);
}
}

View File

@@ -10,3 +10,51 @@ export const serviceCrashMessage = 'SQL Tools Service component exited unexpecte
export const serviceCrashButton = 'View Known Issues';
export const serviceCrashLink = 'https://github.com/Microsoft/vscode-mssql/wiki/SqlToolsService-Known-Issues';
export const extensionConfigSectionName = 'mssql';
// DATA PROTOCOL VALUES ///////////////////////////////////////////////////////////
export const mssqlClusterProviderName = 'mssqlCluster';
export const hadoopKnoxEndpointName = 'Knox';
export const protocolVersion = '1.0';
export const hostPropName = 'host';
export const userPropName = 'user';
export const knoxPortPropName = 'knoxport';
export const passwordPropName = 'password';
export const groupIdPropName = 'groupId';
export const defaultKnoxPort = '30443';
export const groupIdName = 'groupId';
export const sqlProviderName = 'MSSQL';
export const dataService = 'Data Services';
export const hdfsHost = 'host';
export const hdfsUser = 'user';
export const UNTITLED_SCHEMA = 'untitled';
export const hadoopConnectionTimeoutSeconds = 15;
export const hdfsRootPath = '/';
export const clusterEndpointsProperty = 'clusterEndpoints';
export const isBigDataClusterProperty = 'isBigDataCluster';
// SERVICE NAMES //////////////////////////////////////////////////////////
export const ObjectExplorerService = 'objectexplorer';
export const objectExplorerPrefix: string = 'objectexplorer://';
export const ViewType = 'view';
export enum BuiltInCommands {
SetContext = 'setContext'
}
export enum CommandContext {
WizardServiceEnabled = 'wizardservice:enabled'
}
export enum HdfsItems {
Connection = 'hdfs:connection',
Folder = 'hdfs:folder',
File = 'hdfs:file',
Message = 'hdfs:message'
}
export enum HdfsItemsSubType {
Spark = 'hdfs:spark'
}

View File

@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
// HDFS Constants //////////////////////////////////////////////////////////
export const msgMissingNodeContext = localize('msgMissingNodeContext', 'Node Command called without any node passed');
export const msgTimeout = localize('connectionTimeout', 'connection timed out. Host name or port may be incorrect');

View File

@@ -5,6 +5,7 @@
'use strict';
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
import * as path from 'path';
import { SqlOpsDataClient, ClientOptions } from 'dataprotocol-client';
import { IConfig, ServerProvider, Events } from 'service-downloader';
@@ -17,6 +18,9 @@ import { AzureResourceProvider } from './resourceProvider/resourceProvider';
import * as Utils from './utils';
import { Telemetry, LanguageClientErrorHandler } from './telemetry';
import { TelemetryFeature, AgentServicesFeature, DacFxServicesFeature } from './features';
import { AppContext } from './appContext';
import { ApiWrapper } from './apiWrapper';
import { MssqlObjectExplorerNodeProvider } from './objectExplorerNodeProvider/objectExplorerNodeProvider';
const baseConfig = require('./config.json');
const outputChannel = vscode.window.createOutputChannel(Constants.serviceName);
@@ -85,6 +89,8 @@ export async function activate(context: vscode.ExtensionContext) {
languageClient.start();
credentialsStore.start();
resourceProvider.start();
let nodeProvider = new MssqlObjectExplorerNodeProvider(new AppContext(context, new ApiWrapper()));
sqlops.dataprotocol.registerObjectExplorerNodeProvider(nodeProvider);
}, e => {
Telemetry.sendTelemetryEvent('ServiceInitializingFailed');
vscode.window.showErrorMessage('Failed to start Sql tools service');

View File

@@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Transform } from 'stream';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export class CancelableStream extends Transform {
constructor(private cancelationToken: vscode.CancellationTokenSource) {
super();
}
public _transform(chunk: any, encoding: string, callback: Function): void {
if (this.cancelationToken && this.cancelationToken.token.isCancellationRequested) {
callback(new Error(localize('streamCanceled', 'Stream operation canceled by the user')));
} else {
this.push(chunk);
callback();
}
}
}

View File

@@ -0,0 +1,176 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ApiWrapper } from '../apiWrapper';
import { TreeNode } from './treeNodes';
import { QuestionTypes, IPrompter, IQuestion } from '../prompts/question';
import * as utils from '../utils';
import * as constants from '../constants';
import { AppContext } from '../appContext';
export interface ICommandContextParsingOptions {
editor: boolean;
uri: boolean;
}
export interface ICommandBaseContext {
command: string;
editor?: vscode.TextEditor;
uri?: vscode.Uri;
}
export interface ICommandUnknownContext extends ICommandBaseContext {
type: 'unknown';
}
export interface ICommandUriContext extends ICommandBaseContext {
type: 'uri';
}
export interface ICommandViewContext extends ICommandBaseContext {
type: 'view';
node: TreeNode;
}
export interface ICommandObjectExplorerContext extends ICommandBaseContext {
type: 'objectexplorer';
explorerContext: sqlops.ObjectExplorerContext;
}
export type CommandContext = ICommandObjectExplorerContext | ICommandViewContext | ICommandUriContext | ICommandUnknownContext;
function isTextEditor(editor: any): editor is vscode.TextEditor {
if (editor === undefined) { return false; }
return editor.id !== undefined && ((editor as vscode.TextEditor).edit !== undefined || (editor as vscode.TextEditor).document !== undefined);
}
export abstract class Command extends vscode.Disposable {
protected readonly contextParsingOptions: ICommandContextParsingOptions = { editor: false, uri: false };
private disposable: vscode.Disposable;
constructor(command: string | string[], protected appContext: AppContext) {
super(() => this.dispose());
if (typeof command === 'string') {
this.disposable = this.apiWrapper.registerCommand(command, (...args: any[]) => this._execute(command, ...args), this);
return;
}
const subscriptions = command.map(cmd => this.apiWrapper.registerCommand(cmd, (...args: any[]) => this._execute(cmd, ...args), this));
this.disposable = vscode.Disposable.from(...subscriptions);
}
dispose(): void {
this.disposable && this.disposable.dispose();
}
protected get apiWrapper(): ApiWrapper {
return this.appContext.apiWrapper;
}
protected async preExecute(...args: any[]): Promise<any> {
return this.execute(...args);
}
abstract execute(...args: any[]): any;
protected _execute(command: string, ...args: any[]): any {
// TODO consider using Telemetry.trackEvent(command);
const [context, rest] = Command.parseContext(command, this.contextParsingOptions, ...args);
return this.preExecute(context, ...rest);
}
private static parseContext(command: string, options: ICommandContextParsingOptions, ...args: any[]): [CommandContext, any[]] {
let editor: vscode.TextEditor | undefined = undefined;
let firstArg = args[0];
if (options.editor && (firstArg === undefined || isTextEditor(firstArg))) {
editor = firstArg;
args = args.slice(1);
firstArg = args[0];
}
if (options.uri && (firstArg === undefined || firstArg instanceof vscode.Uri)) {
const [uri, ...rest] = args as [vscode.Uri, any];
return [{ command: command, type: 'uri', editor: editor, uri: uri }, rest];
}
if (firstArg instanceof TreeNode) {
const [node, ...rest] = args as [TreeNode, any];
return [{ command: command, type: constants.ViewType, node: node }, rest];
}
if (firstArg && utils.isObjectExplorerContext(firstArg)) {
const [explorerContext, ...rest] = args as [sqlops.ObjectExplorerContext, any];
return [{ command: command, type: constants.ObjectExplorerService, explorerContext: explorerContext }, rest];
}
return [{ command: command, type: 'unknown', editor: editor }, args];
}
}
export abstract class ProgressCommand extends Command {
static progressId = 0;
constructor(private command: string, protected prompter: IPrompter, appContext: AppContext) {
super(command, appContext);
}
protected async executeWithProgress(
execution: (cancelToken: vscode.CancellationTokenSource) => Promise<void>,
label: string,
isCancelable: boolean = false,
onCanceled?: () => void
): Promise<void> {
let disposables: vscode.Disposable[] = [];
const tokenSource = new vscode.CancellationTokenSource();
const statusBarItem = this.apiWrapper.createStatusBarItem(vscode.StatusBarAlignment.Left);
disposables.push(vscode.Disposable.from(statusBarItem));
statusBarItem.text = localize('progress', '$(sync~spin) {0}...', label);
if (isCancelable) {
const cancelCommandId = `cancelProgress${ProgressCommand.progressId++}`;
disposables.push(this.apiWrapper.registerCommand(cancelCommandId, async () => {
if (await this.confirmCancel()) {
tokenSource.cancel();
}
}));
statusBarItem.tooltip = localize('cancelTooltip', 'Cancel');
statusBarItem.command = cancelCommandId;
}
statusBarItem.show();
try {
await execution(tokenSource);
} catch (error) {
if (isCancelable && onCanceled && tokenSource.token.isCancellationRequested) {
// The error can be assumed to be due to cancelation occurring. Do the callback
onCanceled();
} else {
throw error;
}
} finally {
disposables.forEach(d => d.dispose());
}
}
private async confirmCancel(): Promise<boolean> {
return await this.prompter.promptSingle<boolean>(<IQuestion>{
type: QuestionTypes.confirm,
message: localize('cancel', 'Cancel operation?'),
default: true
});
}
}

View File

@@ -0,0 +1,222 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
import * as nls from 'vscode-nls';
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';
function appendIfExists(uri: string, propName: string, propValue: string): string {
if (propValue) {
uri = `${uri};${propName}=${propValue}`;
}
return uri;
}
interface IValidationResult {
isValid: boolean;
errors: string;
}
export class Connection {
private _host: string;
private _knoxPort: string;
constructor(private connectionInfo: sqlops.ConnectionInfo, private connectionUri?: string, private _connectionId?: string) {
if (!this.connectionInfo) {
throw new Error(localize('connectionInfoMissing', 'connectionInfo is required'));
}
if (!this._connectionId) {
this._connectionId = UUID.generateUuid();
}
}
public get uri(): string {
return this.connectionUri;
}
public saveUriWithPrefix(prefix: string): string {
let uri = `${prefix}${this.host}`;
uri = appendIfExists(uri, constants.knoxPortPropName, this.knoxport);
uri = appendIfExists(uri, constants.userPropName, this.user);
uri = appendIfExists(uri, constants.groupIdPropName, this.connectionInfo.options[constants.groupIdPropName]);
this.connectionUri = uri;
return this.connectionUri;
}
public async tryConnect(factory?: FileSourceFactory): Promise<sqlops.ConnectionInfoSummary> {
let fileSource = this.createHdfsFileSource(factory, {
timeout: this.connecttimeout
});
let summary: sqlops.ConnectionInfoSummary = undefined;
try {
await fileSource.enumerateFiles(constants.hdfsRootPath);
summary = {
ownerUri: this.connectionUri,
connectionId: this.connectionId,
connectionSummary: {
serverName: this.host,
databaseName: undefined,
userName: this.user
},
errorMessage: undefined,
errorNumber: undefined,
messages: undefined,
serverInfo: this.getEmptyServerInfo()
};
} catch (error) {
summary = {
ownerUri: this.connectionUri,
connectionId: undefined,
connectionSummary: undefined,
errorMessage: this.getConnectError(error),
errorNumber: undefined,
messages: undefined,
serverInfo: undefined
};
}
return summary;
}
private getConnectError(error: string | Error): string {
let errorMsg = utils.getErrorMessage(error);
if (errorMsg.indexOf('ETIMEDOUT') > -1) {
errorMsg = LocalizedConstants.msgTimeout;
} else if (errorMsg.indexOf('ENOTFOUND') > -1) {
errorMsg = LocalizedConstants.msgTimeout;
}
return localize('connectError', 'Connection failed with error: {0}', errorMsg);
}
private getEmptyServerInfo(): sqlops.ServerInfo {
let info: sqlops.ServerInfo = {
serverMajorVersion: 0,
serverMinorVersion: 0,
serverReleaseVersion: 0,
engineEditionId: 0,
serverVersion: '',
serverLevel: '',
serverEdition: '',
isCloud: false,
azureVersion: 0,
osVersion: '',
options: { isBigDataCluster: false, clusterEndpoints: []}
};
return info;
}
public get connectionId(): string {
return this._connectionId;
}
public get host(): string {
if (!this._host) {
this.ensureHostAndPort();
}
return this._host;
}
/**
* Sets host and port values, using any ',' or ':' delimited port in the hostname in
* preference to the built in port.
*/
private ensureHostAndPort(): void {
this._host = this.connectionInfo.options[constants.hostPropName];
this._knoxPort = Connection.getKnoxPortOrDefault(this.connectionInfo);
// determine whether the host has either a ',' or ':' in it
this.setHostAndPort(',');
this.setHostAndPort(':');
}
// set port and host correctly after we've identified that a delimiter exists in the host name
private setHostAndPort(delimeter: string): void {
let originalHost = this._host;
let index = originalHost.indexOf(delimeter);
if (index > -1) {
this._host = originalHost.slice(0, index);
this._knoxPort = originalHost.slice(index + 1);
}
}
public get user(): string {
return this.connectionInfo.options[constants.userPropName];
}
public get password(): string {
return this.connectionInfo.options[constants.passwordPropName];
}
public get knoxport(): string {
if (!this._knoxPort) {
this.ensureHostAndPort();
}
return this._knoxPort;
}
private static getKnoxPortOrDefault(connInfo: sqlops.ConnectionInfo): string {
let port = connInfo.options[constants.knoxPortPropName];
if (!port) {
port = constants.defaultKnoxPort;
}
return port;
}
public get connecttimeout(): number {
let timeoutSeconds: number = this.connectionInfo.options['connecttimeout'];
if (!timeoutSeconds) {
timeoutSeconds = constants.hadoopConnectionTimeoutSeconds;
}
// connect timeout is in milliseconds
return timeoutSeconds * 1000;
}
public get sslverification(): string {
return this.connectionInfo.options['sslverification'];
}
public get groupId(): string {
return this.connectionInfo.options[constants.groupIdName];
}
public isMatch(connectionInfo: sqlops.ConnectionInfo): 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;
}
public createHdfsFileSource(factory?: FileSourceFactory, additionalRequestParams?: IRequestParams): IFileSource {
factory = factory || FileSourceFactory.instance;
let options: IHdfsOptions = {
protocol: 'https',
host: this.host,
port: this.knoxport,
user: this.user,
path: 'gateway/default/webhdfs/v1',
requestParams: {
auth: {
user: this.user,
pass: this.password
}
}
};
if (additionalRequestParams) {
options.requestParams = Object.assign(options.requestParams, additionalRequestParams);
}
return factory.createHdfsFileSource(options);
}
}

View File

@@ -0,0 +1,371 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as fspath from 'path';
import * as webhdfs from 'webhdfs';
import * as fs from 'fs';
import * as meter from 'stream-meter';
import * as bytes from 'bytes';
import * as https from 'https';
import * as readline from 'readline';
import * as os from 'os';
import * as constants from '../constants';
import * as utils from '../utils';
export function joinHdfsPath(parent: string, child: string): string {
if (parent === constants.hdfsRootPath) {
return `/${child}`;
}
return `${parent}/${child}`;
}
export interface IFile {
path: string;
isDirectory: boolean;
}
export class File implements IFile {
constructor(public path: string, public isDirectory: boolean) {
}
public static createPath(path: string, fileName: string): string {
return joinHdfsPath(path, fileName);
}
public static createChild(parent: IFile, fileName: string, isDirectory: boolean): IFile {
return new File(File.createPath(parent.path, fileName), isDirectory);
}
public static createFile(parent: IFile, fileName: string): File {
return File.createChild(parent, fileName, false);
}
public static createDirectory(parent: IFile, fileName: string): IFile {
return File.createChild(parent, fileName, true);
}
public static getBasename(file: IFile): string {
return fspath.basename(file.path);
}
}
export interface IFileSource {
enumerateFiles(path: string): Promise<IFile[]>;
mkdir(dirName: string, remoteBasePath: string): Promise<void>;
createReadStream(path: string): fs.ReadStream;
readFile(path: string, maxBytes?: number): Promise<Buffer>;
readFileLines(path: string, maxLines: number): Promise<Buffer>;
writeFile(localFile: IFile, remoteDir: string): Promise<string>;
delete(path: string, recursive?: boolean): Promise<void>;
exists(path: string): Promise<boolean>;
}
export interface IHttpAuthentication {
user: string;
pass: string;
}
export interface IHdfsOptions {
host?: string;
port?: string;
protocol?: string;
user?: string;
path?: string;
requestParams?: IRequestParams;
}
export interface IRequestParams {
auth?: IHttpAuthentication;
/**
* Timeout in milliseconds to wait for response
*/
timeout?: number;
agent?: https.Agent;
}
export interface IHdfsFileStatus {
type: 'FILE' | 'DIRECTORY';
pathSuffix: string;
}
export interface IHdfsClient {
readdir(path: string, callback: (err: Error, files: any[]) => void): void;
/**
* Create readable stream for given path
*
* @method createReadStream
* @fires Request#data
* @fires WebHDFS#finish
*
* @param {String} path
* @param {Object} [opts]
*
* @returns {Object}
*/
createReadStream (path: string, opts?: object): fs.ReadStream;
/**
* Create writable stream for given path
*
* @example
*
* var WebHDFS = require('webhdfs');
* var hdfs = WebHDFS.createClient();
*
* var localFileStream = fs.createReadStream('/path/to/local/file');
* var remoteFileStream = hdfs.createWriteStream('/path/to/remote/file');
*
* localFileStream.pipe(remoteFileStream);
*
* remoteFileStream.on('error', function onError (err) {
* // Do something with the error
* });
*
* remoteFileStream.on('finish', function onFinish () {
* // Upload is done
* });
*
* @method createWriteStream
* @fires WebHDFS#finish
*
* @param {String} path
* @param {Boolean} [append] If set to true then append data to the file
* @param {Object} [opts]
*
* @returns {Object}
*/
createWriteStream(path: string, append?: boolean, opts?: object): fs.WriteStream;
/**
* Make new directory
*
* @method mkdir
*
* @param {String} path
* @param {String} [mode=0777]
* @param {Function} callback
*
* @returns {Object}
*/
mkdir (path: string, callback: Function): void;
mkdir (path: string, mode: string, callback: Function): void;
/**
* Delete directory or file path
*
* @method unlink
*
* @param {String} path
* @param {Boolean} [recursive=false]
* @param {Function} callback
*
* @returns {Object}
*/
rmdir (path: string, recursive: boolean, callback: Function): void;
/**
* Check file existence
* Wraps stat method
*
* @method stat
* @see WebHDFS.stat
*
* @param {String} path
* @param {Function} callback
*
* @returns {Object}
*/
exists (path: string, callback: Function): boolean;
}
export class FileSourceFactory {
private static _instance: FileSourceFactory;
public static get instance(): FileSourceFactory {
if (!FileSourceFactory._instance) {
FileSourceFactory._instance = new FileSourceFactory();
}
return FileSourceFactory._instance;
}
public createHdfsFileSource(options: IHdfsOptions): IFileSource {
options = options && options.host ? FileSourceFactory.removePortFromHost(options) : options;
let requestParams: IRequestParams = options.requestParams ? options.requestParams : {};
if (requestParams.auth) {
// TODO Remove handling of unsigned cert once we have real certs in our Knox service
let agentOptions = {
host: options.host,
port: options.port,
path: constants.hdfsRootPath,
rejectUnauthorized: false
};
let agent = new https.Agent(agentOptions);
requestParams['agent'] = agent;
}
return new HdfsFileSource(webhdfs.createClient(options, requestParams));
}
// remove port from host when port is specified after a comma or colon
private static removePortFromHost(options: IHdfsOptions): IHdfsOptions {
// determine whether the host has either a ',' or ':' in it
options = this.setHostAndPort(options, ',');
options = this.setHostAndPort(options, ':');
return options;
}
// set port and host correctly after we've identified that a delimiter exists in the host name
private static setHostAndPort(options: IHdfsOptions, delimeter: string): IHdfsOptions {
let optionsHost: string = options.host;
if (options.host.indexOf(delimeter) > -1) {
options.host = options.host.slice(0, options.host.indexOf(delimeter));
options.port = optionsHost.replace(options.host + delimeter, '');
}
return options;
}
}
export class HdfsFileSource implements IFileSource {
constructor(private client: IHdfsClient) {
}
public enumerateFiles(path: string): Promise<IFile[]> {
return new Promise((resolve, reject) => {
this.client.readdir(path, (error, files) => {
if (error) {
reject(error.message);
} else {
let hdfsFiles: IFile[] = files.map(file => {
let hdfsFile = <IHdfsFileStatus> file;
return new File(File.createPath(path, hdfsFile.pathSuffix), hdfsFile.type === 'DIRECTORY');
});
resolve(hdfsFiles);
}
});
});
}
public mkdir(dirName: string, remoteBasePath: string): Promise<void> {
return new Promise((resolve, reject) => {
let remotePath = joinHdfsPath(remoteBasePath, dirName);
this.client.mkdir(remotePath, (err) => {
if (err) {
reject(err);
} else {
resolve(undefined);
}
});
});
}
public createReadStream(path: string): fs.ReadStream {
return this.client.createReadStream(path);
}
public readFile(path: string, maxBytes?: number): Promise<Buffer> {
return new Promise((resolve, reject) => {
let remoteFileStream = this.client.createReadStream(path);
if (maxBytes) {
remoteFileStream = remoteFileStream.pipe(meter(maxBytes));
}
let data = [];
let error = undefined;
remoteFileStream.on('error', (err) => {
error = err.toString();
if (error.includes('Stream exceeded specified max')) {
error = `File exceeds max size of ${bytes(maxBytes)}`;
}
reject(error);
});
remoteFileStream.on('data', (chunk) => {
data.push(chunk);
});
remoteFileStream.once('finish', () => {
if (!error) {
resolve(Buffer.concat(data));
}
});
});
}
public readFileLines(path: string, maxLines: number): Promise<Buffer> {
return new Promise((resolve, reject) => {
let lineReader = readline.createInterface({
input: this.client.createReadStream(path)
});
let lineCount = 0;
let lineData: string[] = [];
let errorMsg = undefined;
lineReader.on('line', (line: string) => {
lineCount++;
lineData.push(line);
if (lineCount >= maxLines) {
resolve(Buffer.from(lineData.join(os.EOL)));
lineReader.close();
}
})
.on('error', (err) => {
errorMsg = utils.getErrorMessage(err);
reject(errorMsg);
})
.on('close', () => {
if (!errorMsg) {
resolve(Buffer.from(lineData.join(os.EOL)));
}
});
});
}
public writeFile(localFile: IFile, remoteDirPath: string): Promise<string> {
return new Promise((resolve, reject) => {
let fileName = fspath.basename(localFile.path);
let remotePath = joinHdfsPath(remoteDirPath, fileName);
let writeStream = this.client.createWriteStream(remotePath);
let readStream = fs.createReadStream(localFile.path);
readStream.pipe(writeStream);
let error: string | Error = undefined;
// API always calls finish, so catch error then handle exit in the finish event
writeStream.on('error', (err => {
error = err;
reject(error);
}));
writeStream.on('finish', (location) => {
if (!error) {
resolve(location);
}
});
});
}
public delete(path: string, recursive: boolean = false): Promise<void> {
return new Promise((resolve, reject) => {
this.client.rmdir(path, recursive, (error) => {
if (error) {
reject(error);
} else {
resolve(undefined);
}
});
});
}
public exists(path: string): Promise<boolean> {
return new Promise((resolve, reject) => {
this.client.exists(path, (result) => {
resolve(result);
});
});
}
}

View File

@@ -0,0 +1,437 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
import * as fs from 'fs';
import * as fspath from 'path';
import * as clipboardy from 'clipboardy';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ApiWrapper } from '../apiWrapper';
import { Command, ICommandViewContext, ProgressCommand, ICommandObjectExplorerContext } from './command';
import { IHdfsOptions, HdfsFileSource, File, IFile, joinHdfsPath, FileSourceFactory } from './fileSources';
import { HdfsProvider, FolderNode, FileNode, HdfsFileSourceNode } from './hdfsProvider';
import { IPrompter, IQuestion, QuestionTypes } from '../prompts/question';
import * as constants from '../constants';
import * as LocalizedConstants from '../localizedConstants';
import * as utils from '../utils';
import { Connection } from './connection';
import { AppContext } from '../appContext';
import { TreeNode } from './treeNodes';
import { MssqlObjectExplorerNodeProvider } from './objectExplorerNodeProvider';
function getSaveableUri(apiWrapper: ApiWrapper, fileName: string, isPreview?: boolean): vscode.Uri {
let root = utils.getUserHome();
let workspaceFolders = apiWrapper.workspaceFolders;
if (workspaceFolders && workspaceFolders.length > 0) {
root = workspaceFolders[0].uri.fsPath;
}
// Cannot preview with a file path that already exists, so keep looking for a valid path that does not exist
if (isPreview) {
let fileNum = 1;
let fileNameWithoutExtension = fspath.parse(fileName).name;
let fileExtension = fspath.parse(fileName).ext;
while (fs.existsSync(fspath.join(root, fileName))) {
fileName = `${fileNameWithoutExtension}-${fileNum}${fileExtension}`;
fileNum++;
}
}
return vscode.Uri.file(fspath.join(root, fileName));
}
export async function getNode<T extends TreeNode>(context: ICommandViewContext |ICommandObjectExplorerContext, appContext: AppContext): Promise<T> {
let node: T = undefined;
if (context && context.type === constants.ViewType && context.node) {
node = context.node as T;
} else if (context && context.type === constants.ObjectExplorerService) {
let oeProvider = appContext.getService<MssqlObjectExplorerNodeProvider>(constants.ObjectExplorerService);
if (oeProvider) {
node = await oeProvider.findNodeForContext<T>(context.explorerContext);
}
} else {
throw new Error(LocalizedConstants.msgMissingNodeContext);
}
return node;
}
export class UploadFilesCommand extends ProgressCommand {
constructor(prompter: IPrompter, appContext: AppContext) {
super('hdfs.uploadFiles', prompter, appContext);
}
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
return this.execute(context, args);
}
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
try {
let folderNode = await getNode<FolderNode>(context, this.appContext);
const allFilesFilter = localize('allFiles', 'All Files');
let filter = {};
filter[allFilesFilter] = '*';
if (folderNode) {
let options: vscode.OpenDialogOptions = {
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: true,
openLabel: localize('lblUploadFiles', 'Upload'),
filters: filter
};
let fileUris: vscode.Uri[] = await this.apiWrapper.showOpenDialog(options);
if (fileUris) {
let files: IFile[] = fileUris.map(uri => uri.fsPath).map(this.mapPathsToFiles());
await this.executeWithProgress(
async (cancelToken: vscode.CancellationTokenSource) => this.writeFiles(files, folderNode, cancelToken),
localize('uploading', 'Uploading files to HDFS'), true,
() => this.apiWrapper.showInformationMessage(localize('uploadCanceled', 'Upload operation was canceled')));
if (context.type === constants.ObjectExplorerService) {
let objectExplorerNode = await sqlops.objectexplorer.getNode(context.explorerContext.connectionProfile.id, folderNode.getNodeInfo().nodePath);
await objectExplorerNode.refresh();
}
}
}
} catch (err) {
this.apiWrapper.showErrorMessage(localize('uploadError', 'Error uploading files: {0}', utils.getErrorMessage(err)));
}
}
private mapPathsToFiles(): (value: string, index: number, array: string[]) => File {
return (path: string) => {
let isDir = fs.lstatSync(path).isDirectory();
return new File(path, isDir);
};
}
private async writeFiles(files: IFile[], folderNode: FolderNode, cancelToken: vscode.CancellationTokenSource): Promise<void> {
for (let file of files) {
if (cancelToken.token.isCancellationRequested) {
// Throw here so that all recursion is ended
throw new Error('Upload canceled');
}
if (file.isDirectory) {
let dirName = fspath.basename(file.path);
let subFolder = await folderNode.mkdir(dirName);
let children: IFile[] = fs.readdirSync(file.path)
.map(childFileName => joinHdfsPath(file.path, childFileName))
.map(this.mapPathsToFiles());
this.writeFiles(children, subFolder, cancelToken);
} else {
await folderNode.writeFile(file);
}
}
}
}
export class MkDirCommand extends ProgressCommand {
constructor(prompter: IPrompter, appContext: AppContext) {
super('hdfs.mkdir', prompter, appContext);
}
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
return this.execute(context, args);
}
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
try {
let folderNode = await getNode<FolderNode>(context, this.appContext);
if (folderNode) {
let fileName: string = await this.getDirName();
if (fileName && fileName.length > 0) {
await this.executeWithProgress(
async (cancelToken: vscode.CancellationTokenSource) => this.mkDir(fileName, folderNode, cancelToken),
localize('makingDir', 'Creating directory'), true,
() => this.apiWrapper.showInformationMessage(localize('mkdirCanceled', 'Operation was canceled')));
if (context.type === constants.ObjectExplorerService) {
let objectExplorerNode = await sqlops.objectexplorer.getNode(context.explorerContext.connectionProfile.id, folderNode.getNodeInfo().nodePath);
await objectExplorerNode.refresh();
}
}
}
} catch (err) {
this.apiWrapper.showErrorMessage(localize('uploadError', 'Error uploading files: {0}', utils.getErrorMessage(err)));
}
}
private async getDirName(): Promise<string> {
return await this.prompter.promptSingle(<IQuestion>{
type: QuestionTypes.input,
name: 'enterDirName',
message: localize('enterDirName', 'Enter directory name'),
default: ''
}).then(confirmed => <string>confirmed);
}
private async mkDir(fileName, folderNode: FolderNode, cancelToken: vscode.CancellationTokenSource): Promise<void> {
let subFolder = await folderNode.mkdir(fileName);
}
}
export class DeleteFilesCommand extends Command {
constructor(private prompter: IPrompter, appContext: AppContext) {
super('hdfs.deleteFiles', appContext);
}
protected async preExecute(context: ICommandViewContext |ICommandObjectExplorerContext, args: object = {}): Promise<any> {
return this.execute(context, args);
}
async execute(context: ICommandViewContext |ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
try {
let node = await getNode<TreeNode>(context, this.appContext);
if (node) {
// TODO ideally would let node define if it's deletable
// TODO also, would like to change this to getNodeInfo as OE is the primary use case now
let treeItem = await node.getTreeItem();
let oeNodeToRefresh: sqlops.objectexplorer.ObjectExplorerNode = undefined;
if (context.type === constants.ObjectExplorerService) {
let oeNodeToDelete = await sqlops.objectexplorer.getNode(context.explorerContext.connectionProfile.id, node.getNodeInfo().nodePath);
oeNodeToRefresh = await oeNodeToDelete.getParent();
}
switch (treeItem.contextValue) {
case constants.HdfsItems.Folder:
await this.deleteFolder(<FolderNode>node);
break;
case constants.HdfsItems.File:
await this.deleteFile(<FileNode>node);
break;
default:
return;
}
if (oeNodeToRefresh) {
await oeNodeToRefresh.refresh();
}
} else {
this.apiWrapper.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
}
} catch (err) {
this.apiWrapper.showErrorMessage(localize('deleteError', 'Error deleting files {0}', err));
}
}
private async confirmDelete(deleteMsg: string): Promise<boolean> {
return await this.prompter.promptSingle(<IQuestion>{
type: QuestionTypes.confirm,
message: deleteMsg,
default: false
}).then(confirmed => <boolean>confirmed);
}
private async deleteFolder(node: FolderNode): Promise<void> {
if (node) {
let confirmed = await this.confirmDelete(localize('msgDeleteFolder', 'Are you sure you want to delete this folder and its contents?'));
if (confirmed) {
// TODO prompt for recursive delete if non-empty?
await node.delete(true);
}
}
}
private async deleteFile(node: FileNode): Promise<void> {
if (node) {
let confirmed = await this.confirmDelete(localize('msgDeleteFile', 'Are you sure you want to delete this file?'));
if (confirmed) {
await node.delete();
}
}
}
}
export class SaveFileCommand extends ProgressCommand {
constructor(prompter: IPrompter, appContext: AppContext) {
super('hdfs.saveFile', prompter, appContext);
}
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
return this.execute(context, args);
}
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
try {
let fileNode = await getNode<FileNode>(context, this.appContext);
if (fileNode) {
let defaultUri = getSaveableUri(this.apiWrapper, fspath.basename(fileNode.hdfsPath));
let fileUri: vscode.Uri = await this.apiWrapper.showSaveDialog({
defaultUri: defaultUri
});
if (fileUri) {
await this.executeWithProgress(
async (cancelToken: vscode.CancellationTokenSource) => this.doSaveAndOpen(fileUri, fileNode, cancelToken),
localize('saving', 'Saving HDFS Files'), true,
() => this.apiWrapper.showInformationMessage(localize('saveCanceled', 'Save operation was canceled')));
}
} else {
this.apiWrapper.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
}
} catch (err) {
this.apiWrapper.showErrorMessage(localize('saveError', 'Error saving file: {0}', err));
}
}
private async doSaveAndOpen(fileUri: vscode.Uri, fileNode: FileNode, cancelToken: vscode.CancellationTokenSource): Promise<void> {
await fileNode.writeFileContentsToDisk(fileUri.fsPath, cancelToken);
await this.apiWrapper.executeCommand('vscode.open', fileUri);
}
}
export class PreviewFileCommand extends ProgressCommand {
public static readonly DefaultMaxSize = 30 * 1024 * 1024;
constructor(prompter: IPrompter, appContext: AppContext) {
super('hdfs.previewFile', prompter, appContext);
}
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
return this.execute(context, args);
}
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
try {
let fileNode = await getNode<FileNode>(context, this.appContext);
if (fileNode) {
await this.executeWithProgress(
async (cancelToken: vscode.CancellationTokenSource) => {
let contents = await fileNode.getFileContentsAsString(PreviewFileCommand.DefaultMaxSize);
let doc = await this.openTextDocument(fspath.basename(fileNode.hdfsPath));
let editor = await this.apiWrapper.showTextDocument(doc, vscode.ViewColumn.Active, false);
await editor.edit(edit => {
edit.insert(new vscode.Position(0, 0), contents);
});
},
localize('previewing', 'Generating preview'),
false);
} else {
this.apiWrapper.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
}
} catch (err) {
this.apiWrapper.showErrorMessage(localize('previewError', 'Error previewing file: {0}', err));
}
}
private async openTextDocument(fileName: string): Promise<vscode.TextDocument> {
let docUri: vscode.Uri = getSaveableUri(this.apiWrapper, fileName, true);
if (docUri) {
docUri = docUri.with({ scheme: constants.UNTITLED_SCHEMA });
return await this.apiWrapper.openTextDocument(docUri);
} else {
// Can't reliably create a filename to save as so just use untitled
let language = fspath.extname(fileName);
if (language && language.length > 0) {
// trim the '.'
language = language.substring(1);
}
return await this.apiWrapper.openTextDocument({
language: language
});
}
}
}
export class CopyPathCommand extends Command {
public static readonly DefaultMaxSize = 30 * 1024 * 1024;
constructor(appContext: AppContext) {
super('hdfs.copyPath', appContext);
}
protected async preExecute(context: ICommandViewContext | ICommandObjectExplorerContext, args: object = {}): Promise<any> {
return this.execute(context, args);
}
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
try {
let node = await getNode<HdfsFileSourceNode>(context, this.appContext);
if (node) {
let path = node.hdfsPath;
clipboardy.writeSync(path);
} else {
this.apiWrapper.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
}
} catch (err) {
this.apiWrapper.showErrorMessage(localize('copyPathError', 'Error copying path: {0}', err));
}
}
}
/**
* The connect task is only expected to work in the file-tree based APIs, not Object Explorer
*/
export class ConnectTask {
constructor(private hdfsProvider: HdfsProvider, private prompter: IPrompter, private apiWrapper: ApiWrapper) {
}
async execute(profile: sqlops.IConnectionProfile, ...args: any[]): Promise<void> {
if (profile) {
return this.createFromProfile(profile);
}
return this.createHdfsConnection();
}
private createFromProfile(profile: sqlops.IConnectionProfile): Promise<void> {
let connection = new Connection(profile);
if (profile.providerName === constants.mssqlClusterProviderName && connection.host) {
// TODO need to get the actual port and auth to be used since this will be non-default
// in future versions
this.hdfsProvider.addHdfsConnection(<IHdfsOptions> {
protocol: 'https',
host: connection.host,
port: connection.knoxport,
user: connection.user,
path: 'gateway/default/webhdfs/v1',
requestParams: {
auth: {
user: connection.user,
pass: connection.password
}
}
});
}
return Promise.resolve(undefined);
}
private addConnection(options: IHdfsOptions): void {
let display: string = `${options.user}@${options.host}:${options.port}`;
this.hdfsProvider.addConnection(display, FileSourceFactory.instance.createHdfsFileSource(options));
}
private async createHdfsConnection(profile?: sqlops.IConnectionProfile): Promise<void> {
let questions: IQuestion[] = [
{
type: QuestionTypes.input,
name: constants.hdfsHost,
message: localize('msgSetWebHdfsHost', 'HDFS URL and port'),
default: 'localhost:50070'
},
{
type: QuestionTypes.input,
name: constants.hdfsUser,
message: localize('msgSetWebHdfsUser', 'User Name'),
default: 'root'
}];
let answers = await this.prompter.prompt(questions);
if (answers) {
let hostAndPort: string = answers[constants.hdfsHost];
let parts = hostAndPort.split(':');
let host: string = parts[0];
let port: string = parts.length > 1 ? parts[1] : undefined;
let user: string = answers[constants.hdfsUser];
let options: IHdfsOptions = {
host: host,
port: port,
user: user
};
this.addConnection(options);
}
}
}

View File

@@ -0,0 +1,366 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import * as fspath from 'path';
import * as fs from 'fs';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ApiWrapper } from '../apiWrapper';
import * as Constants from '../constants';
import { IFileSource, IHdfsOptions, HdfsFileSource, IFile, File, FileSourceFactory } from './fileSources';
import { CancelableStream } from './cancelableStream';
import { TreeNode } from './treeNodes';
import * as utils from '../utils';
import { IFileNode } from './types';
export interface ITreeChangeHandler {
notifyNodeChanged(node: TreeNode): void;
}
export class TreeDataContext {
constructor(public extensionContext: vscode.ExtensionContext, public changeHandler: ITreeChangeHandler) {
}
}
export class HdfsProvider implements vscode.TreeDataProvider<TreeNode>, ITreeChangeHandler {
static readonly NoConnectionsMessage = 'No connections added';
static readonly ConnectionsLabel = 'Connections';
private connections: ConnectionNode[];
private _onDidChangeTreeData = new vscode.EventEmitter<TreeNode>();
private context: TreeDataContext;
constructor(extensionContext: vscode.ExtensionContext, private vscodeApi: ApiWrapper) {
this.connections = [];
this.context = new TreeDataContext(extensionContext, this);
}
public get onDidChangeTreeData(): vscode.Event<TreeNode> {
return this._onDidChangeTreeData.event;
}
getTreeItem(element: TreeNode): vscode.TreeItem | Thenable<vscode.TreeItem> {
return element.getTreeItem();
}
getChildren(element?: TreeNode): vscode.ProviderResult<TreeNode[]> {
if (element) {
return element.getChildren(false);
} else {
return this.connections.length > 0 ? this.connections : [MessageNode.create(HdfsProvider.NoConnectionsMessage, element)];
}
}
addConnection(displayName: string, fileSource: IFileSource): void {
if (!this.connections.find(c => c.getDisplayName() === displayName)) {
this.connections.push(new ConnectionNode(this.context, displayName, fileSource));
this._onDidChangeTreeData.fire();
}
}
addHdfsConnection(options: IHdfsOptions): void {
let displayName = `${options.user}@${options.host}:${options.port}`;
let fileSource = FileSourceFactory.instance.createHdfsFileSource(options);
this.addConnection(displayName, fileSource);
}
notifyNodeChanged(node: TreeNode): void {
this._onDidChangeTreeData.fire(node);
}
}
export abstract class HdfsFileSourceNode extends TreeNode {
constructor(protected context: TreeDataContext, protected _path: string, protected fileSource: IFileSource) {
super();
}
public get hdfsPath(): string {
return this._path;
}
public get nodePathValue(): string {
return this.getDisplayName();
}
getDisplayName(): string {
return fspath.basename(this._path);
}
public async delete(recursive: boolean = false): Promise<void> {
await this.fileSource.delete(this.hdfsPath, recursive);
// Notify parent should be updated. If at top, will return undefined which will refresh whole tree
(<HdfsFileSourceNode>this.parent).onChildRemoved();
this.context.changeHandler.notifyNodeChanged(this.parent);
}
public abstract onChildRemoved(): void;
}
export class FolderNode extends HdfsFileSourceNode {
private children: TreeNode[];
protected _nodeType: string;
constructor(context: TreeDataContext, path: string, fileSource: IFileSource, nodeType?: string) {
super(context, path, fileSource);
this._nodeType = nodeType ? nodeType : Constants.HdfsItems.Folder;
}
private ensureChildrenExist(): void {
if (!this.children) {
this.children = [];
}
}
public onChildRemoved(): void {
this.children = undefined;
}
async getChildren(refreshChildren: boolean): Promise<TreeNode[]> {
if (refreshChildren || !this.children) {
this.ensureChildrenExist();
try {
let files: IFile[] = await this.fileSource.enumerateFiles(this._path);
if (files) {
// Note: for now, assuming HDFS-provided sorting is sufficient
this.children = files.map((file) => {
let node: TreeNode = file.isDirectory ? new FolderNode(this.context, file.path, this.fileSource)
: new FileNode(this.context, file.path, this.fileSource);
node.parent = this;
return node;
});
}
} catch (error) {
this.children = [MessageNode.create(localize('errorExpanding', 'Error: {0}', utils.getErrorMessage(error)), this)];
}
}
return this.children;
}
getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
let item = new vscode.TreeItem(this.getDisplayName(), vscode.TreeItemCollapsibleState.Collapsed);
// For now, folder always looks the same. We're using SQL icons to differentiate remote vs local files
item.iconPath = {
dark: this.context.extensionContext.asAbsolutePath('resources/light/Folder.svg'),
light: this.context.extensionContext.asAbsolutePath('resources/light/Folder.svg')
};
item.contextValue = this._nodeType;
return item;
}
getNodeInfo(): sqlops.NodeInfo {
// TODO handle error message case by returning it in the OE API
// TODO support better mapping of node type
let nodeInfo: sqlops.NodeInfo = {
label: this.getDisplayName(),
isLeaf: false,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: this._nodeType,
nodeSubType: undefined,
iconType: 'Folder'
};
return nodeInfo;
}
public async writeFile(localFile: IFile): Promise<FileNode> {
return this.runChildAddAction<FileNode>(() => this.writeFileAsync(localFile));
}
private async writeFileAsync(localFile: IFile): Promise<FileNode> {
await this.fileSource.writeFile(localFile, this._path);
let fileNode = new FileNode(this.context, File.createPath(this._path, File.getBasename(localFile)), this.fileSource);
return fileNode;
}
public async mkdir(name: string): Promise<FolderNode> {
return this.runChildAddAction<FolderNode>(() => this.mkdirAsync(name));
}
private async mkdirAsync(name: string): Promise<FolderNode> {
await this.fileSource.mkdir(name, this._path);
let subDir = new FolderNode(this.context, File.createPath(this._path, name), this.fileSource);
return subDir;
}
private async runChildAddAction<T extends TreeNode>(action: () => Promise<T>): Promise<T> {
let node = await action();
await this.getChildren(true);
if (this.children) {
// Find the child matching the node. This is necessary
// since writing can add duplicates.
node = this.children.find(n => n.nodePathValue === node.nodePathValue) as T;
this.context.changeHandler.notifyNodeChanged(this);
} else {
// Failed to retrieve children from server so something went wrong
node = undefined;
}
return node;
}
}
export class ConnectionNode extends FolderNode {
constructor(context: TreeDataContext, private displayName: string, fileSource: IFileSource) {
super(context, '/', fileSource, Constants.HdfsItems.Connection);
}
getDisplayName(): string {
return this.displayName;
}
public async delete(): Promise<void> {
throw new Error(localize('errDeleteConnectionNode', 'Cannot delete a connection. Only subfolders and files can be deleted.'));
}
async getTreeItem(): Promise<vscode.TreeItem> {
let item = await super.getTreeItem();
item.contextValue = this._nodeType;
return item;
}
}
export class FileNode extends HdfsFileSourceNode implements IFileNode {
constructor(context: TreeDataContext, path: string, fileSource: IFileSource) {
super(context, path, fileSource);
}
public onChildRemoved(): void {
// do nothing
}
getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]> {
return [];
}
getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
let item = new vscode.TreeItem(this.getDisplayName(), vscode.TreeItemCollapsibleState.None);
item.iconPath = {
dark: this.context.extensionContext.asAbsolutePath('resources/dark/file_inverse.svg'),
light: this.context.extensionContext.asAbsolutePath('resources/light/file.svg')
};
item.contextValue = Constants.HdfsItems.File;
return item;
}
getNodeInfo(): sqlops.NodeInfo {
// TODO improve node type handling so it's not tied to SQL Server types
let nodeInfo: sqlops.NodeInfo = {
label: this.getDisplayName(),
isLeaf: true,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: Constants.HdfsItems.File,
nodeSubType: this.getSubType(),
iconType: 'FileGroupFile'
};
return nodeInfo;
}
public async getFileContentsAsString(maxBytes?: number): Promise<string> {
let contents: Buffer = await this.fileSource.readFile(this.hdfsPath, maxBytes);
return contents ? contents.toString('utf8') : '';
}
public async getFileLinesAsString(maxLines: number): Promise<string> {
let contents: Buffer = await this.fileSource.readFileLines(this.hdfsPath, maxLines);
return contents ? contents.toString('utf8') : '';
}
public writeFileContentsToDisk(localPath: string, cancelToken?: vscode.CancellationTokenSource): Promise<vscode.Uri> {
return new Promise((resolve, reject) => {
let readStream: fs.ReadStream = this.fileSource.createReadStream(this.hdfsPath);
let writeStream = fs.createWriteStream(localPath, {
encoding: 'utf8'
});
let cancelable = new CancelableStream(cancelToken);
cancelable.on('error', (err) => {
reject(err);
});
readStream.pipe(cancelable).pipe(writeStream);
let error: string | Error = undefined;
writeStream.on('error', (err) => {
error = err;
reject(error);
});
writeStream.on('finish', (location) => {
if (!error) {
resolve(vscode.Uri.file(localPath));
}
});
});
}
private getSubType(): string {
if (this.getDisplayName().toLowerCase().endsWith('.jar') || this.getDisplayName().toLowerCase().endsWith('.py')) {
return Constants.HdfsItemsSubType.Spark;
}
return undefined;
}
}
export class MessageNode extends TreeNode {
static messageNum: number = 0;
private _nodePathValue: string;
constructor(private message: string) {
super();
}
public static create(message: string, parent: TreeNode): MessageNode {
let node = new MessageNode(message);
node.parent = parent;
return node;
}
private ensureNodePathValue(): void {
if (!this._nodePathValue) {
this._nodePathValue = `message_${MessageNode.messageNum++}`;
}
}
public get nodePathValue(): string {
this.ensureNodePathValue();
return this._nodePathValue;
}
public getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]> {
return [];
}
public getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
let item = new vscode.TreeItem(this.message, vscode.TreeItemCollapsibleState.None);
item.contextValue = Constants.HdfsItems.Message;
return item;
}
getNodeInfo(): sqlops.NodeInfo {
let nodeInfo: sqlops.NodeInfo = {
label: this.message,
isLeaf: false,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: Constants.HdfsItems.Message,
nodeSubType: undefined,
iconType: 'MessageType'
};
return nodeInfo;
}
}

View File

@@ -0,0 +1,347 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
import { ProviderBase } from './providerBase';
import { Connection } from './connection';
import * as utils from '../utils';
import { TreeNode } from './treeNodes';
import { ConnectionNode, TreeDataContext, ITreeChangeHandler } from './hdfsProvider';
import { IFileSource } from './fileSources';
import { AppContext } from '../appContext';
import * as constants from '../constants';
const outputChannel = vscode.window.createOutputChannel(constants.providerId);
interface IEndpoint {
serviceName: string;
ipAddress: string;
port: number;
}
export class MssqlObjectExplorerNodeProvider extends ProviderBase implements sqlops.ObjectExplorerNodeProvider, ITreeChangeHandler {
public readonly supportedProviderId: string = constants.providerId;
private sessionMap: Map<string, Session>;
private expandCompleteEmitter = new vscode.EventEmitter<sqlops.ObjectExplorerExpandInfo>();
constructor(private appContext: AppContext) {
super();
this.sessionMap = new Map();
this.appContext.registerService<MssqlObjectExplorerNodeProvider>(constants.ObjectExplorerService, this);
}
handleSessionOpen(session: sqlops.ObjectExplorerSession): Thenable<boolean> {
return new Promise((resolve, reject) => {
if (!session) {
reject('handleSessionOpen requires a session object to be passed');
} else {
resolve(this.doSessionOpen(session));
}
});
}
private async doSessionOpen(sessionInfo: sqlops.ObjectExplorerSession): Promise<boolean> {
let connectionProfile = await sqlops.objectexplorer.getSessionConnectionProfile(sessionInfo.sessionId);
if (!connectionProfile) {
return false;
} else {
let credentials = await sqlops.connection.getCredentials(connectionProfile.id);
let serverInfo = await sqlops.connection.getServerInfo(connectionProfile.id);
if (!serverInfo || !credentials || !serverInfo.options) {
return false;
}
let endpoints: IEndpoint[] = serverInfo.options[constants.clusterEndpointsProperty];
if (!endpoints || endpoints.length === 0) {
return false;
}
let index = endpoints.findIndex(ep => ep.serviceName === constants.hadoopKnoxEndpointName);
if (index === -1) {
return false;
}
let connInfo: sqlops.connection.Connection = {
options: {
'host': endpoints[index].ipAddress,
'groupId': connectionProfile.options.groupId,
'knoxport': endpoints[index].port,
'user': 'root', //connectionProfile.options.userName cluster setup has to have the same user for master and big data cluster
'password': credentials.password,
},
providerName: constants.mssqlClusterProviderName,
connectionId: UUID.generateUuid()
};
let connection = new Connection(connInfo);
connection.saveUriWithPrefix(constants.objectExplorerPrefix);
let session = new Session(connection, sessionInfo.sessionId);
session.root = new RootNode(session, new TreeDataContext(this.appContext.extensionContext, this), sessionInfo.rootNode.nodePath);
this.sessionMap.set(sessionInfo.sessionId, session);
return true;
}
}
expandNode(nodeInfo: sqlops.ExpandNodeInfo, isRefresh: boolean = false): Thenable<boolean> {
return new Promise((resolve, reject) => {
if (!nodeInfo) {
reject('expandNode requires a nodeInfo object to be passed');
} else {
resolve(this.doExpandNode(nodeInfo, isRefresh));
}
});
}
private async doExpandNode(nodeInfo: sqlops.ExpandNodeInfo, isRefresh: boolean = false): Promise<boolean> {
let session = this.sessionMap.get(nodeInfo.sessionId);
let response = {
sessionId: nodeInfo.sessionId,
nodePath: nodeInfo.nodePath,
errorMessage: undefined,
nodes: []
};
if (!session) {
// This is not an error case. Just fire reponse with empty nodes for example: request from standalone SQL instance
this.expandCompleteEmitter.fire(response);
return false;
} else {
setTimeout(() => {
// Running after promise resolution as we need the Ops Studio-side map to have been updated
// Intentionally not awaiting or catching errors.
// Any failure in startExpansion should be emitted in the expand complete result
// We want this to be async and ideally return true before it completes
this.startExpansion(session, nodeInfo, isRefresh);
}, 10);
}
return true;
}
private async startExpansion(session: Session, nodeInfo: sqlops.ExpandNodeInfo, isRefresh: boolean = false): Promise<void> {
let expandResult: sqlops.ObjectExplorerExpandInfo = {
sessionId: session.uri,
nodePath: nodeInfo.nodePath,
errorMessage: undefined,
nodes: []
};
try {
let node = await session.root.findNodeByPath(nodeInfo.nodePath, true);
if (node) {
expandResult.errorMessage = node.getNodeInfo().errorMessage;
let children = await node.getChildren(true);
if (children) {
expandResult.nodes = children.map(c => c.getNodeInfo());
}
}
} catch (error) {
expandResult.errorMessage = utils.getErrorMessage(error);
}
this.expandCompleteEmitter.fire(expandResult);
}
refreshNode(nodeInfo: sqlops.ExpandNodeInfo): Thenable<boolean> {
// TODO #3815 implement properly
return this.expandNode(nodeInfo, true);
}
handleSessionClose(closeSessionInfo: sqlops.ObjectExplorerCloseSessionInfo): void {
this.sessionMap.delete(closeSessionInfo.sessionId);
}
findNodes(findNodesInfo: sqlops.FindNodesInfo): Thenable<sqlops.ObjectExplorerFindNodesResponse> {
// TODO #3814 implement
let response: sqlops.ObjectExplorerFindNodesResponse = {
nodes: []
};
return Promise.resolve(response);
}
registerOnExpandCompleted(handler: (response: sqlops.ObjectExplorerExpandInfo) => any): void {
this.expandCompleteEmitter.event(handler);
}
notifyNodeChanged(node: TreeNode): void {
this.notifyNodeChangesAsync(node);
}
private async notifyNodeChangesAsync(node: TreeNode): Promise<void> {
try {
let session = this.getSessionForNode(node);
if (!session) {
this.appContext.apiWrapper.showErrorMessage(localize('sessionNotFound', 'Session for node {0} does not exist', node.nodePathValue));
} else {
let nodeInfo = node.getNodeInfo();
let expandInfo: sqlops.ExpandNodeInfo = {
nodePath: nodeInfo.nodePath,
sessionId: session.uri
};
await this.refreshNode(expandInfo);
}
} catch (err) {
outputChannel.appendLine(localize('notifyError', 'Error notifying of node change: {0}', err));
}
}
private getSessionForNode(node: TreeNode): Session {
let rootNode: DataServicesNode = undefined;
while (rootNode === undefined && node !== undefined) {
if (node instanceof DataServicesNode) {
rootNode = node;
break;
} else {
node = node.parent;
}
}
if (rootNode) {
return rootNode.session;
}
// Not found
return undefined;
}
async findNodeForContext<T extends TreeNode>(explorerContext: sqlops.ObjectExplorerContext): Promise<T> {
let node: T = undefined;
let session = this.findSessionForConnection(explorerContext.connectionProfile);
if (session) {
if (explorerContext.isConnectionNode) {
// Note: ideally fix so we verify T matches RootNode and go from there
node = <T><any>session.root;
} else {
// Find the node under the session
node = <T><any>await session.root.findNodeByPath(explorerContext.nodeInfo.nodePath, true);
}
}
return node;
}
private findSessionForConnection(connectionProfile: sqlops.IConnectionProfile): Session {
for (let session of this.sessionMap.values()) {
if (session.connection && session.connection.isMatch(connectionProfile)) {
return session;
}
}
return undefined;
}
}
export class Session {
private _root: RootNode;
constructor(private _connection: Connection, private sessionId?: string) {
}
public get uri(): string {
return this.sessionId || this._connection.uri;
}
public get connection(): Connection {
return this._connection;
}
public set root(node: RootNode) {
this._root = node;
}
public get root(): RootNode {
return this._root;
}
}
class RootNode extends TreeNode {
private children: TreeNode[];
constructor(private _session: Session, private context: TreeDataContext, private nodePath: string) {
super();
}
public get session(): Session {
return this._session;
}
public get nodePathValue(): string {
return this.nodePath;
}
public getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]> {
if (refreshChildren || !this.children) {
this.children = [];
let dataServicesNode = new DataServicesNode(this._session, this.context, this.nodePath);
dataServicesNode.parent = this;
this.children.push(dataServicesNode);
}
return this.children;
}
getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
throw new Error('Not intended for use in a file explorer view.');
}
getNodeInfo(): sqlops.NodeInfo {
let nodeInfo: sqlops.NodeInfo = {
label: localize('rootLabel', 'Root'),
isLeaf: false,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: 'sqlCluster:root',
nodeSubType: undefined,
iconType: 'folder'
};
return nodeInfo;
}
}
class DataServicesNode extends TreeNode {
private children: TreeNode[];
constructor(private _session: Session, private context: TreeDataContext, private nodePath: string) {
super();
}
public get session(): Session {
return this._session;
}
public get nodePathValue(): string {
return this.nodePath;
}
public getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]> {
if (refreshChildren || !this.children) {
this.children = [];
let hdfsNode = new ConnectionNode(this.context, localize('hdfsFolder', 'HDFS'), this.createHdfsFileSource());
hdfsNode.parent = this;
this.children.push(hdfsNode);
}
return this.children;
}
private createHdfsFileSource(): IFileSource {
return this.session.connection.createHdfsFileSource();
}
getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
throw new Error('Not intended for use in a file explorer view.');
}
getNodeInfo(): sqlops.NodeInfo {
let nodeInfo: sqlops.NodeInfo = {
label: localize('dataServicesLabel', 'Data Services'),
isLeaf: false,
errorMessage: undefined,
metadata: undefined,
nodePath: this.generateNodePath(),
nodeStatus: undefined,
nodeType: 'dataservices',
nodeSubType: undefined,
iconType: 'folder'
};
return nodeInfo;
}
}

View File

@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as constants from '../constants';
export abstract class ProviderBase {
public readonly providerId: string = constants.mssqlClusterProviderName;
public handle: number;
}

View File

@@ -0,0 +1,78 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as vscode from 'vscode';
import { ITreeNode } from './types';
type TreeNodePredicate = (node: TreeNode) => boolean;
export abstract class TreeNode implements ITreeNode {
private _parent: TreeNode = undefined;
public get parent(): TreeNode {
return this._parent;
}
public set parent(node: TreeNode) {
this._parent = node;
}
public generateNodePath(): string {
let path = undefined;
if (this.parent) {
path = this.parent.generateNodePath();
}
path = path ? `${path}/${this.nodePathValue}` : this.nodePathValue;
return path;
}
public findNodeByPath(path: string, expandIfNeeded: boolean = false): Promise<TreeNode> {
let condition: TreeNodePredicate = (node: TreeNode) => node.getNodeInfo().nodePath === path || node.getNodeInfo().nodePath.startsWith(path);
let filter: TreeNodePredicate = (node: TreeNode) => path.startsWith(node.getNodeInfo().nodePath);
return TreeNode.findNode(this, condition, filter, true);
}
public static async findNode(node: TreeNode, condition: TreeNodePredicate, filter: TreeNodePredicate, expandIfNeeded: boolean): Promise<TreeNode> {
if (!node) {
return undefined;
}
if (condition(node)) {
return node;
}
let nodeInfo = node.getNodeInfo();
if (nodeInfo.isLeaf) {
return undefined;
}
// TODO #3813 support filtering by already expanded / not yet expanded
let children = await node.getChildren(false);
if (children) {
for (let child of children) {
if (filter && filter(child)) {
let childNode = await this.findNode(child, condition, filter, expandIfNeeded);
if (childNode) {
return childNode;
}
}
}
}
return undefined;
}
/**
* The value to use for this node in the node path
*/
public abstract get nodePathValue(): string;
abstract getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]>;
abstract getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem>;
abstract getNodeInfo(): sqlops.NodeInfo;
}

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as sqlops from 'sqlops';
/**
* A tree node in the object explorer tree
*
* @export
* @interface ITreeNode
*/
export interface ITreeNode {
getNodeInfo(): sqlops.NodeInfo;
getChildren(refreshChildren: boolean): ITreeNode[] | Promise<ITreeNode[]>;
}
/**
* A HDFS file node. This is a leaf node in the object explorer tree, and its contents
* can be queried
*
* @export
* @interface IFileNode
* @extends {ITreeNode}
*/
export interface IFileNode extends ITreeNode {
getFileContentsAsString(maxBytes?: number): Promise<string>;
}

View File

@@ -0,0 +1,68 @@
'use strict';
import vscode = require('vscode');
export class QuestionTypes {
public static get input(): string { return 'input'; }
public static get password(): string { return 'password'; }
public static get list(): string { return 'list'; }
public static get confirm(): string { return 'confirm'; }
public static get checkbox(): string { return 'checkbox'; }
public static get expand(): string { return 'expand'; }
}
// Question interface to clarify how to use the prompt feature
// based on Bower Question format: https://github.com/bower/bower/blob/89069784bb46bfd6639b4a75e98a0d7399a8c2cb/packages/bower-logger/README.md
export interface IQuestion {
// Type of question (see QuestionTypes)
type: string;
// Name of the question for disambiguation
name: string;
// Message to display to the user
message: string;
// Optional placeHolder to give more detailed information to the user
placeHolder?: any;
// Optional default value - this will be used instead of placeHolder
default?: any;
// optional set of choices to be used. Can be QuickPickItems or a simple name-value pair
choices?: Array<vscode.QuickPickItem | INameValueChoice>;
// Optional validation function that returns an error string if validation fails
validate?: (value: any) => string;
// Optional pre-prompt function. Takes in set of answers so far, and returns true if prompt should occur
shouldPrompt?: (answers: {[id: string]: any}) => boolean;
// Optional action to take on the question being answered
onAnswered?: (value: any) => void;
// Optional set of options to support matching choices.
matchOptions?: vscode.QuickPickOptions;
}
// Pair used to display simple choices to the user
export interface INameValueChoice {
name: string;
value: any;
}
// Generic object that can be used to define a set of questions and handle the result
export interface IQuestionHandler {
// Set of questions to be answered
questions: IQuestion[];
// Optional callback, since questions may handle themselves
callback?: IPromptCallback;
}
export interface IPrompter {
promptSingle<T>(question: IQuestion, ignoreFocusOut?: boolean): Promise<T>;
/**
* Prompts for multiple questions
*
* @returns {[questionId: string]: T} Map of question IDs to results, or undefined if
* the user canceled the question session
*/
prompt<T>(questions: IQuestion[], ignoreFocusOut?: boolean): Promise<{[questionId: string]: any}>;
promptCallback(questions: IQuestion[], callback: IPromptCallback): void;
}
export interface IPromptCallback {
(answers: {[id: string]: any}): void;
}

View File

@@ -4,4 +4,5 @@
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/sql/sqlops.d.ts'/>
/// <reference path='../../../../src/sql/sqlops.proposed.d.ts'/>
/// <reference path='../../../../src/vs/vscode.d.ts'/>

View File

@@ -4,6 +4,8 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sqlops from 'sqlops';
import * as path from 'path';
import * as crypto from 'crypto';
import * as os from 'os';
@@ -169,3 +171,15 @@ export function verifyPlatform(): Thenable<boolean> {
return Promise.resolve(true);
}
}
export function getErrorMessage(error: Error | string): string {
return (error instanceof Error) ? error.message : error;
}
export function isObjectExplorerContext(object: any): object is sqlops.ObjectExplorerContext {
return 'connectionProfile' in object && 'isConnectionNode' in object;
}
export function getUserHome(): string {
return process.env.HOME || process.env.USERPROFILE;
}

View File

@@ -8,6 +8,15 @@ agent-base@4, agent-base@^4.1.0:
dependencies:
es6-promisify "^5.0.0"
ajv@^6.5.5:
version "6.7.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.7.0.tgz#e3ce7bb372d6577bb1839f1dfdfcbf5ad2948d96"
dependencies:
fast-deep-equal "^2.0.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
applicationinsights@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.1.tgz#53446b830fe8d5d619eee2a278b31d3d25030927"
@@ -16,10 +25,42 @@ applicationinsights@1.0.1:
diagnostic-channel-publishers "0.2.1"
zone.js "0.7.6"
arch@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.1.tgz#8f5c2731aa35a30929221bb0640eed65175ec84e"
asn1@~0.2.3:
version "0.2.4"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
dependencies:
safer-buffer "~2.1.0"
assert-plus@1.0.0, assert-plus@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
aws4@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
base64-js@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978"
bcrypt-pbkdf@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
dependencies:
tweetnacl "^0.14.3"
bl@^1.0.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c"
@@ -46,6 +87,10 @@ buffer-fill@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
buffer-stream-reader@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/buffer-stream-reader/-/buffer-stream-reader-0.1.1.tgz#ca8bf93631deedd8b8f8c3bb44991cc30951e259"
buffer@^3.0.1:
version "3.6.0"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-3.6.0.tgz#a72c936f77b96bf52f5f7e7b467180628551defb"
@@ -54,16 +99,47 @@ buffer@^3.0.1:
ieee754 "^1.1.4"
isarray "^1.0.0"
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
clipboardy@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-1.2.3.tgz#0526361bf78724c1f20be248d428e365433c07ef"
dependencies:
arch "^2.1.0"
execa "^0.8.0"
combined-stream@^1.0.6, combined-stream@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
dependencies:
delayed-stream "~1.0.0"
commander@~2.8.1:
version "2.8.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
dependencies:
graceful-readlink ">= 1.0.0"
core-util-is@~1.0.0:
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
cross-spawn@^5.0.1:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
dependencies:
lru-cache "^4.0.1"
shebang-command "^1.2.0"
which "^1.2.9"
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
dependencies:
assert-plus "^1.0.0"
"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#0.2.15":
version "0.2.15"
resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/a2cd2db109de882f0959f7b6421c86afa585f460"
@@ -130,6 +206,10 @@ decompress@^4.2.0:
pify "^2.3.0"
strip-dirs "^2.0.0"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
diagnostic-channel-publishers@0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3"
@@ -140,6 +220,13 @@ diagnostic-channel@0.2.0:
dependencies:
semver "^5.3.0"
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
dependencies:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
end-of-stream@^1.0.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
@@ -160,6 +247,38 @@ eventemitter2@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-5.0.1.tgz#6197a095d5fb6b57e8942f6fd7eaad63a09c9452"
execa@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da"
dependencies:
cross-spawn "^5.0.1"
get-stream "^3.0.0"
is-stream "^1.1.0"
npm-run-path "^2.0.0"
p-finally "^1.0.0"
signal-exit "^3.0.0"
strip-eof "^1.0.0"
extend@^3.0.0, extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
extsprintf@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
fast-deep-equal@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
fast-json-stable-stringify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
@@ -178,6 +297,18 @@ file-type@^6.1.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919"
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
form-data@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.6"
mime-types "^2.1.12"
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
@@ -189,6 +320,16 @@ get-stream@^2.2.0:
object-assign "^4.0.1"
pinkie-promise "^2.0.0"
get-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
getpass@^0.1.1:
version "0.1.7"
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
dependencies:
assert-plus "^1.0.0"
graceful-fs@^4.1.10:
version "4.1.15"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
@@ -197,6 +338,17 @@ graceful-fs@^4.1.10:
version "1.0.1"
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
har-validator@~5.1.0:
version "5.1.3"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
dependencies:
ajv "^6.5.5"
har-schema "^2.0.0"
http-proxy-agent@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405"
@@ -204,6 +356,14 @@ http-proxy-agent@^2.1.0:
agent-base "4"
debug "3.1.0"
http-signature@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
dependencies:
assert-plus "^1.0.0"
jsprim "^1.2.2"
sshpk "^1.7.0"
https-proxy-agent@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
@@ -227,16 +387,70 @@ is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
json-schema@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
dependencies:
assert-plus "1.0.0"
extsprintf "1.3.0"
json-schema "0.2.3"
verror "1.10.0"
lru-cache@^4.0.1:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
dependencies:
pseudomap "^1.0.2"
yallist "^2.1.2"
make-dir@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
dependencies:
pify "^3.0.0"
mime-db@~1.37.0:
version "1.37.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8"
mime-types@^2.1.12, mime-types@~2.1.19:
version "2.1.21"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96"
dependencies:
mime-db "~1.37.0"
minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
@@ -255,6 +469,16 @@ ms@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
dependencies:
path-key "^2.0.0"
oauth-sign@~0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
object-assign@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -273,10 +497,22 @@ os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
path-key@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -299,7 +535,27 @@ process-nextick-args@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
readable-stream@^2.3.0, readable-stream@^2.3.5:
pseudomap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
psl@^1.1.24:
version "1.1.31"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
punycode@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
readable-stream@^2.1.4, readable-stream@^2.3.0, readable-stream@^2.3.5:
version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
dependencies:
@@ -311,10 +567,39 @@ readable-stream@^2.3.0, readable-stream@^2.3.5:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
request@^2.74.0:
version "2.88.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.8.0"
caseless "~0.12.0"
combined-stream "~1.0.6"
extend "~3.0.2"
forever-agent "~0.6.1"
form-data "~2.3.2"
har-validator "~5.1.0"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
json-stringify-safe "~5.0.1"
mime-types "~2.1.19"
oauth-sign "~0.9.0"
performance-now "^2.1.0"
qs "~6.5.2"
safe-buffer "^5.1.2"
tough-cookie "~2.4.3"
tunnel-agent "^0.6.0"
uuid "^3.3.2"
safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
seek-bzip@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.5.tgz#cfe917cb3d274bcffac792758af53173eb1fabdc"
@@ -336,6 +621,40 @@ semver@^5.3.0:
mkdirp "^0.5.1"
tmp "^0.0.33"
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
dependencies:
shebang-regex "^1.0.0"
shebang-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
signal-exit@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
sshpk@^1.7.0:
version "1.16.0"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.0.tgz#1d4963a2fbffe58050aa9084ca20be81741c07de"
dependencies:
asn1 "~0.2.3"
assert-plus "^1.0.0"
bcrypt-pbkdf "^1.0.0"
dashdash "^1.12.0"
ecc-jsbn "~0.1.1"
getpass "^0.1.1"
jsbn "~0.1.0"
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
stream-meter@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/stream-meter/-/stream-meter-1.0.4.tgz#52af95aa5ea760a2491716704dbff90f73afdd1d"
dependencies:
readable-stream "^2.1.4"
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
@@ -348,6 +667,10 @@ strip-dirs@^2.0.0:
dependencies:
is-natural-number "^4.0.1"
strip-eof@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
tar-stream@^1.5.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
@@ -374,6 +697,23 @@ to-buffer@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80"
tough-cookie@~2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
dependencies:
psl "^1.1.24"
punycode "^1.4.1"
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
dependencies:
safe-buffer "^5.0.1"
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
unbzip2-stream@^1.0.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.1.tgz#7854da51622a7e63624221196357803b552966a1"
@@ -381,10 +721,28 @@ unbzip2-stream@^1.0.9:
buffer "^3.0.1"
through "^2.3.6"
uri-js@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
dependencies:
punycode "^2.1.0"
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
uuid@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
verror@1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
dependencies:
assert-plus "^1.0.0"
core-util-is "1.0.2"
extsprintf "^1.2.0"
vscode-extension-telemetry@^0.0.15:
version "0.0.15"
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.0.15.tgz#685c32f3b67e8fb85ba689c1d7f88ff90ff87856"
@@ -412,6 +770,24 @@ vscode-languageserver-types@3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.5.0.tgz#e48d79962f0b8e02de955e3f524908e2b19c0374"
vscode-nls@2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-2.0.2.tgz#808522380844b8ad153499af5c3b03921aea02da"
webhdfs@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/webhdfs/-/webhdfs-1.2.0.tgz#c41b08ae33944a0220863bfd4b6719b9aaec1d37"
dependencies:
buffer-stream-reader "^0.1.1"
extend "^3.0.0"
request "^2.74.0"
which@^1.2.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
dependencies:
isexe "^2.0.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@@ -420,6 +796,10 @@ xtend@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
yauzl@^2.4.2:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"

View File

@@ -26,6 +26,10 @@ export const SERVICE_ID = 'ObjectExplorerService';
export const IObjectExplorerService = createDecorator<IObjectExplorerService>(SERVICE_ID);
export interface NodeExpandInfoWithProviderId extends sqlops.ObjectExplorerExpandInfo {
providerId: string;
}
export interface IObjectExplorerService {
_serviceBrand: any;
@@ -45,13 +49,15 @@ export interface IObjectExplorerService {
onSessionDisconnected(handle: number, sessionResponse: sqlops.ObjectExplorerSession);
onNodeExpanded(handle: number, sessionResponse: sqlops.ObjectExplorerExpandInfo);
onNodeExpanded(sessionResponse: NodeExpandInfoWithProviderId);
/**
* Register a ObjectExplorer provider
*/
registerProvider(providerId: string, provider: sqlops.ObjectExplorerProvider): void;
registerNodeProvider(expander: sqlops.ObjectExplorerNodeProvider): void;
getObjectExplorerNode(connection: IConnectionProfile): TreeNode;
updateObjectExplorerNodes(connectionProfile: IConnectionProfile): Promise<void>;
@@ -82,15 +88,18 @@ export interface IObjectExplorerService {
* For Testing purpose only. Get the context menu actions for an object explorer node.
*/
getNodeActions(connectionId: string, nodePath: string): Thenable<string[]>;
getSessionConnectionProfile(sessionId: string): sqlops.IConnectionProfile;
}
interface SessionStatus {
nodes: { [nodePath: string]: NodeStatus };
connection: ConnectionProfile;
expandNodeTimer?: number;
}
interface NodeStatus {
expandEmitter: Emitter<sqlops.ObjectExplorerExpandInfo>;
expandEmitter: Emitter<NodeExpandInfoWithProviderId>;
}
export interface ObjectExplorerNodeEventArgs {
@@ -103,6 +112,14 @@ export interface NodeInfoWithConnection {
nodeInfo: sqlops.NodeInfo;
}
export interface TopLevelChildrenPath {
providerId: string;
supportedProviderId: string;
groupingId: number;
path: string[];
providerObject: sqlops.ObjectExplorerNodeProvider | sqlops.ObjectExplorerProvider;
}
export class ObjectExplorerService implements IObjectExplorerService {
public _serviceBrand: any;
@@ -111,6 +128,8 @@ export class ObjectExplorerService implements IObjectExplorerService {
private _providers: { [handle: string]: sqlops.ObjectExplorerProvider; } = Object.create(null);
private _nodeProviders: { [handle: string]: sqlops.ObjectExplorerNodeProvider[]; } = Object.create(null);
private _activeObjectExplorerNodes: { [id: string]: TreeNode };
private _sessions: { [sessionId: string]: SessionStatus };
@@ -129,6 +148,7 @@ export class ObjectExplorerService implements IObjectExplorerService {
this._activeObjectExplorerNodes = {};
this._sessions = {};
this._providers = {};
this._nodeProviders = {};
this._onSelectionOrFocusChange = new Emitter<void>();
}
@@ -166,7 +186,7 @@ export class ObjectExplorerService implements IObjectExplorerService {
/**
* Gets called when expanded node response is ready
*/
public onNodeExpanded(handle: number, expandResponse: sqlops.ObjectExplorerExpandInfo) {
public onNodeExpanded(expandResponse: NodeExpandInfoWithProviderId) {
if (expandResponse.errorMessage) {
error(expandResponse.errorMessage);
@@ -189,7 +209,12 @@ export class ObjectExplorerService implements IObjectExplorerService {
/**
* Gets called when session is created
*/
public onSessionCreated(handle: number, session: sqlops.ObjectExplorerSession) {
public onSessionCreated(handle: number, session: sqlops.ObjectExplorerSession): void {
this.handleSessionCreated(session);
}
private async handleSessionCreated(session: sqlops.ObjectExplorerSession): Promise<void> {
try {
let connection: ConnectionProfile = undefined;
let errorMessage: string = undefined;
if (this._sessions[session.sessionId]) {
@@ -200,17 +225,27 @@ export class ObjectExplorerService implements IObjectExplorerService {
server.connection = connection;
server.session = session;
this._activeObjectExplorerNodes[connection.id] = server;
} else {
}
else {
errorMessage = session && session.errorMessage ? session.errorMessage :
nls.localize('OeSessionFailedError', 'Failed to create Object Explorer session');
error(errorMessage);
}
} else {
// Send on session created about the session to all node providers so they can prepare for node expansion
let nodeProviders = this._nodeProviders[connection.providerName];
if (nodeProviders) {
let promises: Thenable<boolean>[] = nodeProviders.map(p => p.handleSessionOpen(session));
await Promise.all(promises);
}
}
else {
warn(`cannot find session ${session.sessionId}`);
}
this.sendUpdateNodeEvent(connection, errorMessage);
} catch (error) {
warn(`cannot handle the session ${session.sessionId} in all nodeProviders`);
}
}
/**
@@ -291,7 +326,7 @@ export class ObjectExplorerService implements IObjectExplorerService {
let provider = this._providers[providerId];
if (provider) {
TelemetryUtils.addTelemetry(this._telemetryService, TelemetryKeys.ObjectExplorerExpand, { refresh: 0, provider: providerId });
this.expandOrRefreshNode(provider, session, nodePath).then(result => {
this.expandOrRefreshNode(providerId, session, nodePath).then(result => {
resolve(result);
}, error => {
reject(error);
@@ -301,7 +336,8 @@ export class ObjectExplorerService implements IObjectExplorerService {
}
});
}
private callExpandOrRefreshFromProvider(provider: sqlops.ObjectExplorerProvider, nodeInfo: sqlops.ExpandNodeInfo, refresh: boolean = false) {
private callExpandOrRefreshFromProvider(provider: sqlops.ObjectExplorerProviderBase, nodeInfo: sqlops.ExpandNodeInfo, refresh: boolean = false) {
if (refresh) {
return provider.refreshNode(nodeInfo);
} else {
@@ -310,7 +346,7 @@ export class ObjectExplorerService implements IObjectExplorerService {
}
private expandOrRefreshNode(
provider: sqlops.ObjectExplorerProvider,
providerId: string,
session: sqlops.ObjectExplorerSession,
nodePath: string,
refresh: boolean = false): Thenable<sqlops.ObjectExplorerExpandInfo> {
@@ -320,29 +356,60 @@ export class ObjectExplorerService implements IObjectExplorerService {
let newRequest = false;
if (!self._sessions[session.sessionId].nodes[nodePath]) {
self._sessions[session.sessionId].nodes[nodePath] = {
expandEmitter: new Emitter<sqlops.ObjectExplorerExpandInfo>()
expandEmitter: new Emitter<NodeExpandInfoWithProviderId>()
};
newRequest = true;
}
self._sessions[session.sessionId].nodes[nodePath].expandEmitter.event(((expandResult) => {
if (expandResult && !expandResult.errorMessage) {
resolve(expandResult);
let provider = this._providers[providerId];
if (provider) {
let resultMap: Map<string, sqlops.ObjectExplorerExpandInfo> = new Map<string, sqlops.ObjectExplorerExpandInfo>();
let allProviders: sqlops.ObjectExplorerProviderBase[] = [provider];
let nodeProviders = this._nodeProviders[providerId];
if (nodeProviders) {
nodeProviders = nodeProviders.sort((a, b) => a.group.toLowerCase().localeCompare(b.group.toLowerCase()));
allProviders.push(...nodeProviders);
}
else {
reject(expandResult ? expandResult.errorMessage : undefined);
self._sessions[session.sessionId].nodes[nodePath].expandEmitter.event((expandResult) => {
if (expandResult && expandResult.providerId) {
resultMap.set(expandResult.providerId, expandResult);
} else {
console.log('OE provider returns empty result or providerId');
}
// When get all responses from all providers, merge results
if (resultMap.size === allProviders.length) {
resolve(self.mergeResults(allProviders, resultMap, nodePath));
// Have to delete it after get all reponses otherwise couldn't find session for not the first response
if (newRequest) {
delete self._sessions[session.sessionId].nodes[nodePath];
}
}));
}
});
if (newRequest) {
allProviders.forEach(provider => {
TelemetryUtils.addTelemetry(this._telemetryService, TelemetryKeys.ObjectExplorerExpand, { refresh: 0, provider: providerId });
self.callExpandOrRefreshFromProvider(provider, {
sessionId: session.sessionId,
nodePath: nodePath
}, refresh).then(result => {
}, refresh).then(isExpanding => {
if (!isExpanding) {
// The provider stated it's not going to expand the node, therefore do not need to track when merging results
let emptyResult: sqlops.ObjectExplorerExpandInfo = {
errorMessage: undefined,
nodePath: nodePath,
nodes: [],
sessionId: session.sessionId
};
resultMap.set(provider.providerId, emptyResult);
}
}, error => {
reject(error);
});
});
}
}
} else {
reject(`session cannot find to expand node. id: ${session.sessionId} nodePath: ${nodePath}`);
@@ -350,11 +417,48 @@ export class ObjectExplorerService implements IObjectExplorerService {
});
}
private mergeResults(allProviders: sqlops.ObjectExplorerProviderBase[], resultMap: Map<string, sqlops.ObjectExplorerExpandInfo>, nodePath: string): sqlops.ObjectExplorerExpandInfo {
let finalResult: sqlops.ObjectExplorerExpandInfo;
let allNodes: sqlops.NodeInfo[] = [];
let errorNode: sqlops.NodeInfo = {
nodePath: nodePath,
label: 'Error',
errorMessage: '',
nodeType: 'folder',
isLeaf: true,
nodeSubType: '',
nodeStatus: '',
metadata: null
};
for (let provider of allProviders) {
if (resultMap.has(provider.providerId)) {
let result = resultMap.get(provider.providerId);
if (result) {
if (!result.errorMessage) {
finalResult = result;
allNodes = allNodes.concat(result.nodes);
} else {
errorNode.errorMessage += provider.providerId + 'returns ' + result.errorMessage + ' ';
}
}
}
}
if (finalResult) {
if (errorNode.errorMessage && errorNode.errorMessage.length > 0) {
allNodes = allNodes.concat([errorNode]);
}
finalResult.nodes = allNodes;
}
return finalResult;
}
public refreshNode(providerId: string, session: sqlops.ObjectExplorerSession, nodePath: string): Thenable<sqlops.ObjectExplorerExpandInfo> {
let provider = this._providers[providerId];
if (provider) {
TelemetryUtils.addTelemetry(this._telemetryService, TelemetryKeys.ObjectExplorerExpand, { refresh: 1, provider: providerId });
return this.expandOrRefreshNode(provider, session, nodePath, true);
return this.expandOrRefreshNode(providerId, session, nodePath, true);
}
return Promise.resolve(undefined);
}
@@ -369,7 +473,8 @@ export class ObjectExplorerService implements IObjectExplorerService {
sessionId: session.sessionId,
nodes: [],
nodePath: nodePath,
errorMessage: undefined
errorMessage: undefined,
providerId: providerId
});
}
});
@@ -377,6 +482,14 @@ export class ObjectExplorerService implements IObjectExplorerService {
let provider = this._providers[providerId];
if (provider) {
let nodeProviders = this._nodeProviders[providerId];
if (nodeProviders) {
for (let nodeProvider of nodeProviders) {
nodeProvider.handleSessionClose({
sessionId: session ? session.sessionId : undefined
});
}
}
return provider.closeSession({
sessionId: session ? session.sessionId : undefined
});
@@ -392,6 +505,12 @@ export class ObjectExplorerService implements IObjectExplorerService {
this._providers[providerId] = provider;
}
public registerNodeProvider(nodeProvider: sqlops.ObjectExplorerNodeProvider): void {
let nodeProviders = this._nodeProviders[nodeProvider.supportedProviderId] || [];
nodeProviders.push(nodeProvider);
this._nodeProviders[nodeProvider.supportedProviderId] = nodeProviders;
}
public dispose(): void {
this._disposables = dispose(this._disposables);
}
@@ -552,6 +671,10 @@ export class ObjectExplorerService implements IObjectExplorerService {
return treeNode;
}
public getSessionConnectionProfile(sessionId: string): sqlops.IConnectionProfile {
return this._sessions[sessionId].connection.toIConnectionProfile();
}
private async setNodeExpandedState(treeNode: TreeNode, expandedState: TreeItemCollapsibleState): Promise<void> {
treeNode = await this.getUpdatedTreeNode(treeNode);
let expandNode = this.getTreeItem(treeNode);

View File

@@ -256,6 +256,13 @@ export interface IConnectionManagementService {
*/
getActiveConnectionCredentials(profileId: string): { [name: string]: string };
/**
* Get the ServerInfo for a connected connection profile
* @param {string} profileId The id of the connection profile to get the password for
* @returns ServerInfo
*/
getServerInfo(profileId: string): sqlops.ServerInfo;
/**
* Get the connection string for the provided connection ID
*/

View File

@@ -1367,6 +1367,17 @@ export class ConnectionManagementService extends Disposable implements IConnecti
return credentials;
}
public getServerInfo(profileId: string): sqlops.ServerInfo {
let profile = this._connectionStatusManager.findConnectionByProfileId(profileId);
if (!profile) {
return undefined;
}
let serverInfo = profile.serverInfo;
return serverInfo;
}
/**
* Get the connection string for the provided connection ID
*/

51
src/sql/sqlops.d.ts vendored
View File

@@ -21,6 +21,8 @@ declare module 'sqlops' {
export function registerObjectExplorerProvider(provider: ObjectExplorerProvider): vscode.Disposable;
export function registerObjectExplorerNodeProvider(provider: ObjectExplorerNodeProvider): vscode.Disposable;
export function registerTaskServicesProvider(provider: TaskServicesProvider): vscode.Disposable;
export function registerFileBrowserProvider(provider: FileBrowserProvider): vscode.Disposable;
@@ -101,6 +103,13 @@ declare module 'sqlops' {
*/
export function getCredentials(connectionId: string): Thenable<{ [name: string]: string }>;
/**
* Get ServerInfo for a connectionId
* @param {string} connectionId The id of the connection
* @returns ServerInfo
*/
export function getServerInfo(connectionId: string): Thenable<ServerInfo>;
/**
* Interface for representing a connection when working with connection APIs
*/
@@ -150,6 +159,13 @@ declare module 'sqlops' {
*/
export function findNodes(connectionId: string, type: string, schema: string, name: string, database: string, parentObjectNames: string[]): Thenable<ObjectExplorerNode[]>;
/**
* Get connectionProfile from sessionId
* *@param {string} sessionId The id of the session that the node exists on
* @returns {IConnectionProfile} The IConnecitonProfile for the session
*/
export function getSessionConnectionProfile(sessionId: string): Thenable<IConnectionProfile>;
/**
* Interface for representing and interacting with items in Object Explorer
*/
@@ -347,6 +363,10 @@ declare module 'sqlops' {
* The Operating System version string of the machine running the instance.
*/
osVersion: string;
/**
* options for all new server properties.
*/
options: {};
}
export interface DataProvider {
@@ -1163,22 +1183,41 @@ declare module 'sqlops' {
nodes: NodeInfo[];
}
export interface ObjectExplorerProvider extends DataProvider {
createNewSession(connInfo: ConnectionInfo): Thenable<ObjectExplorerSessionResponse>;
export interface ObjectExplorerProviderBase extends DataProvider {
expandNode(nodeInfo: ExpandNodeInfo): Thenable<boolean>;
refreshNode(nodeInfo: ExpandNodeInfo): Thenable<boolean>;
closeSession(closeSessionInfo: ObjectExplorerCloseSessionInfo): Thenable<ObjectExplorerCloseSessionResponse>;
findNodes(findNodesInfo: FindNodesInfo): Thenable<ObjectExplorerFindNodesResponse>;
registerOnExpandCompleted(handler: (response: ObjectExplorerExpandInfo) => any): void;
}
export interface ObjectExplorerProvider extends ObjectExplorerProviderBase {
createNewSession(connInfo: ConnectionInfo): Thenable<ObjectExplorerSessionResponse>;
closeSession(closeSessionInfo: ObjectExplorerCloseSessionInfo): Thenable<ObjectExplorerCloseSessionResponse>;
registerOnSessionCreated(handler: (response: ObjectExplorerSession) => any): void;
registerOnSessionDisconnected?(handler: (response: ObjectExplorerSession) => any): void;
}
registerOnExpandCompleted(handler: (response: ObjectExplorerExpandInfo) => any): void;
export interface ObjectExplorerNodeProvider extends ObjectExplorerProviderBase {
/**
* The providerId for whichever type of ObjectExplorer connection this can add folders and objects to
*/
readonly supportedProviderId: string;
/**
* Optional group name used to sort nodes in the tree. If not defined, the node order will be added in order based on provider ID, with
* nodes from the main ObjectExplorerProvider for this provider type added first
*/
readonly group?: string;
handleSessionOpen(session: ObjectExplorerSession): Thenable<boolean>;
handleSessionClose(closeSessionInfo: ObjectExplorerCloseSessionInfo): void;
}
// Admin Services interfaces -----------------------------------------------------------------------

View File

@@ -1240,6 +1240,7 @@ declare module 'sqlops' {
AgentServicesProvider = 'AgentServicesProvider',
CapabilitiesProvider = 'CapabilitiesProvider',
DacFxServicesProvider = 'DacFxServicesProvider',
ObjectExplorerNodeProvider = 'ObjectExplorerNodeProvider',
}
export namespace dataprotocol {

View File

@@ -288,6 +288,7 @@ export enum DataProviderType {
AgentServicesProvider = 'AgentServicesProvider',
CapabilitiesProvider = 'CapabilitiesProvider',
DacFxServicesProvider = 'DacFxServicesProvider',
ObjectExplorerNodeProvider = 'ObjectExplorerNodeProvider',
}
export enum DeclarativeDataType {
@@ -320,6 +321,17 @@ export enum AzureResource {
Sql = 1
}
export interface ServerInfoOption {
isBigDataCluster: boolean;
clusterEndpoints: ClusterEndpoint;
}
export interface ClusterEndpoint {
serviceName: string;
ipAddress: string;
port: number;
}
export class SqlThemeIcon {
static readonly Folder = new SqlThemeIcon('Folder');

View File

@@ -32,6 +32,10 @@ export class ExtHostConnectionManagement extends ExtHostConnectionManagementShap
return this._proxy.$getCredentials(connectionId);
}
public $getServerInfo(connectionId: string): Thenable<sqlops.ServerInfo> {
return this._proxy.$getServerInfo(connectionId);
}
public $openConnectionDialog(providers?: string[], initialConnectionProfile?: sqlops.IConnectionProfile, connectionCompletionOptions?: sqlops.IConnectionCompletionOptions): Thenable<sqlops.connection.Connection> {
return this._proxy.$openConnectionDialog(providers, initialConnectionProfile, connectionCompletionOptions);
}

View File

@@ -132,6 +132,12 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape {
return rt;
}
$registerObjectExplorerNodeProvider(provider: sqlops.ObjectExplorerNodeProvider): vscode.Disposable {
let rt = this.registerProvider(provider, DataProviderType.ObjectExplorerNodeProvider);
this._proxy.$registerObjectExplorerNodeProvider(provider.providerId, provider.supportedProviderId, provider.group, provider.handle);
return rt;
}
$registerProfilerProvider(provider: sqlops.ProfilerProvider): vscode.Disposable {
let rt = this.registerProvider(provider, DataProviderType.ProfilerProvider);
this._proxy.$registerProfilerProvider(provider.providerId, provider.handle);
@@ -342,20 +348,28 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape {
return this._resolveProvider<sqlops.ObjectExplorerProvider>(handle).createNewSession(connInfo);
}
public $createObjectExplorerNodeProviderSession(handle: number, session: sqlops.ObjectExplorerSession): Thenable<boolean> {
return this._resolveProvider<sqlops.ObjectExplorerNodeProvider>(handle).handleSessionOpen(session);
}
public $expandObjectExplorerNode(handle: number, nodeInfo: sqlops.ExpandNodeInfo): Thenable<boolean> {
return this._resolveProvider<sqlops.ObjectExplorerProvider>(handle).expandNode(nodeInfo);
return this._resolveProvider<sqlops.ObjectExplorerProviderBase> (handle).expandNode(nodeInfo);
}
public $refreshObjectExplorerNode(handle: number, nodeInfo: sqlops.ExpandNodeInfo): Thenable<boolean> {
return this._resolveProvider<sqlops.ObjectExplorerProvider>(handle).refreshNode(nodeInfo);
return this._resolveProvider<sqlops.ObjectExplorerProviderBase> (handle).refreshNode(nodeInfo);
}
public $closeObjectExplorerSession(handle: number, closeSessionInfo: sqlops.ObjectExplorerCloseSessionInfo): Thenable<sqlops.ObjectExplorerCloseSessionResponse> {
return this._resolveProvider<sqlops.ObjectExplorerProvider>(handle).closeSession(closeSessionInfo);
}
public $handleSessionClose(handle: number, closeSessionInfo: sqlops.ObjectExplorerCloseSessionInfo): void {
return this._resolveProvider<sqlops.ObjectExplorerNodeProvider>(handle).handleSessionClose(closeSessionInfo);
}
public $findNodes(handle: number, findNodesInfo: sqlops.FindNodesInfo): Thenable<sqlops.ObjectExplorerFindNodesResponse> {
return this._resolveProvider<sqlops.ObjectExplorerProvider>(handle).findNodes(findNodesInfo);
return this._resolveProvider<sqlops.ObjectExplorerProviderBase>(handle).findNodes(findNodesInfo);
}
public $onObjectExplorerSessionCreated(handle: number, response: sqlops.ObjectExplorerSession): void {
@@ -366,8 +380,8 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape {
this._proxy.$onObjectExplorerSessionDisconnected(handle, response);
}
public $onObjectExplorerNodeExpanded(handle: number, response: sqlops.ObjectExplorerExpandInfo): void {
this._proxy.$onObjectExplorerNodeExpanded(handle, response);
public $onObjectExplorerNodeExpanded(providerId: string, response: sqlops.ObjectExplorerExpandInfo): void {
this._proxy.$onObjectExplorerNodeExpanded(providerId, response);
}
// Task Service

View File

@@ -34,6 +34,10 @@ export class ExtHostObjectExplorer implements ExtHostObjectExplorerShape {
public $getNodeActions(connectionId: string, nodePath: string): Thenable<string[]> {
return this._proxy.$getNodeActions(connectionId, nodePath);
}
public $getSessionConnectionProfile(sessionId: string): Thenable<sqlops.IConnectionProfile> {
return this._proxy.$getSessionConnectionProfile(sessionId);
}
}
class ExtHostObjectExplorerNode implements sqlops.objectexplorer.ObjectExplorerNode {

View File

@@ -55,6 +55,9 @@ export class MainThreadConnectionManagement implements MainThreadConnectionManag
return Promise.resolve(this._connectionManagementService.getActiveConnectionCredentials(connectionId));
}
public $getServerInfo(connectionId: string): Thenable<sqlops.ServerInfo> {
return Promise.resolve(this._connectionManagementService.getServerInfo(connectionId));
}
public async $openConnectionDialog(providers: string[], initialConnectionProfile?: IConnectionProfile, connectionCompletionOptions?: sqlops.IConnectionCompletionOptions): Promise<sqlops.connection.Connection> {
let connectionProfile = await this._connectionDialogService.openDialogAndWait(this._connectionManagementService, { connectionType: 1, providers: providers }, initialConnectionProfile);

View File

@@ -15,7 +15,7 @@ import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilit
import { IQueryManagementService } from 'sql/platform/query/common/queryManagement';
import * as sqlops from 'sqlops';
import { IMetadataService } from 'sql/platform/metadata/common/metadataService';
import { IObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
import { IObjectExplorerService, NodeExpandInfoWithProviderId } from 'sql/parts/objectExplorer/common/objectExplorerService';
import { IScriptingService } from 'sql/platform/scripting/common/scriptingService';
import { IAdminService } from 'sql/workbench/services/admin/common/adminService';
import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces';
@@ -231,6 +231,7 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape {
public $registerObjectExplorerProvider(providerId: string, handle: number): TPromise<any> {
const self = this;
this._objectExplorerService.registerProvider(providerId, <sqlops.ObjectExplorerProvider>{
providerId: providerId,
createNewSession(connection: sqlops.ConnectionInfo): Thenable<sqlops.ObjectExplorerSessionResponse> {
return self._proxy.$createObjectExplorerSession(handle, connection);
},
@@ -251,6 +252,32 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape {
return undefined;
}
public $registerObjectExplorerNodeProvider(providerId: string, supportedProviderId: string, group: string, handle: number): TPromise<any> {
const self = this;
this._objectExplorerService.registerNodeProvider(<sqlops.ObjectExplorerNodeProvider> {
supportedProviderId: supportedProviderId,
providerId: providerId,
group: group,
expandNode(nodeInfo: sqlops.ExpandNodeInfo): Thenable<boolean> {
return self._proxy.$expandObjectExplorerNode(handle, nodeInfo);
},
refreshNode(nodeInfo: sqlops.ExpandNodeInfo): Thenable<boolean> {
return self._proxy.$refreshObjectExplorerNode(handle, nodeInfo);
},
findNodes(findNodesInfo: sqlops.FindNodesInfo): Thenable<sqlops.ObjectExplorerFindNodesResponse> {
return self._proxy.$findNodes(handle, findNodesInfo);
},
handleSessionOpen(session: sqlops.ObjectExplorerSession): Thenable<boolean> {
return self._proxy.$createObjectExplorerNodeProviderSession(handle, session);
},
handleSessionClose(closeSessionInfo: sqlops.ObjectExplorerCloseSessionInfo): void {
return self._proxy.$handleSessionClose(handle, closeSessionInfo);
}
});
return undefined;
}
public $registerTaskServicesProvider(providerId: string, handle: number): TPromise<any> {
const self = this;
this._taskService.registerProvider(providerId, <sqlops.TaskServicesProvider>{
@@ -474,8 +501,9 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape {
this._objectExplorerService.onSessionDisconnected(handle, sessionResponse);
}
public $onObjectExplorerNodeExpanded(handle: number, expandResponse: sqlops.ObjectExplorerExpandInfo): void {
this._objectExplorerService.onNodeExpanded(handle, expandResponse);
public $onObjectExplorerNodeExpanded(providerId: string, expandResponse: sqlops.ObjectExplorerExpandInfo): void {
let expandInfo: NodeExpandInfoWithProviderId = Object.assign({ providerId: providerId }, expandResponse);
this._objectExplorerService.onNodeExpanded(expandInfo);
}
//Tasks handlers

View File

@@ -82,4 +82,8 @@ export class MainThreadObjectExplorer implements MainThreadObjectExplorerShape {
public $getNodeActions(connectionId: string, nodePath: string): Thenable<string[]> {
return this._objectExplorerService.getNodeActions(connectionId, nodePath);
}
public $getSessionConnectionProfile(sessionId: string): Thenable<sqlops.IConnectionProfile> {
return Promise.resolve(this._objectExplorerService.getSessionConnectionProfile(sessionId));
}
}

View File

@@ -120,6 +120,9 @@ export function createApiFactory(
getCredentials(connectionId: string): Thenable<{ [name: string]: string }> {
return extHostConnectionManagement.$getCredentials(connectionId);
},
getServerInfo(connectionId: string): Thenable<sqlops.ServerInfo> {
return extHostConnectionManagement.$getServerInfo(connectionId);
},
openConnectionDialog(providers?: string[], initialConnectionProfile?: sqlops.IConnectionProfile, connectionCompletionOptions?: sqlops.IConnectionCompletionOptions): Thenable<sqlops.connection.Connection> {
return extHostConnectionManagement.$openConnectionDialog(providers, initialConnectionProfile, connectionCompletionOptions);
},
@@ -160,6 +163,9 @@ export function createApiFactory(
},
getNodeActions(connectionId: string, nodePath: string): Thenable<string[]> {
return extHostObjectExplorer.$getNodeActions(connectionId, nodePath);
},
getSessionConnectionProfile(sessionId: string): Thenable<sqlops.IConnectionProfile> {
return extHostObjectExplorer.$getSessionConnectionProfile(sessionId);
}
};
@@ -238,12 +244,20 @@ export function createApiFactory(
}
provider.registerOnExpandCompleted((response: sqlops.ObjectExplorerExpandInfo) => {
extHostDataProvider.$onObjectExplorerNodeExpanded(provider.handle, response);
extHostDataProvider.$onObjectExplorerNodeExpanded(provider.providerId, response);
});
return extHostDataProvider.$registerObjectExplorerProvider(provider);
};
let registerObjectExplorerNodeProvider = (provider: sqlops.ObjectExplorerNodeProvider): vscode.Disposable => {
provider.registerOnExpandCompleted((response: sqlops.ObjectExplorerExpandInfo) => {
extHostDataProvider.$onObjectExplorerNodeExpanded(provider.providerId, response);
});
return extHostDataProvider.$registerObjectExplorerNodeProvider(provider);
};
let registerTaskServicesProvider = (provider: sqlops.TaskServicesProvider): vscode.Disposable => {
provider.registerOnTaskCreated((response: sqlops.TaskInfo) => {
extHostDataProvider.$onTaskCreated(provider.handle, response);
@@ -335,6 +349,7 @@ export function createApiFactory(
registerFileBrowserProvider,
registerMetadataProvider,
registerObjectExplorerProvider,
registerObjectExplorerNodeProvider,
registerProfilerProvider,
registerRestoreProvider,
registerScriptingProvider,

View File

@@ -128,6 +128,10 @@ export abstract class ExtHostDataProtocolShape {
$findNodes(handle: number, findNodesInfo: sqlops.FindNodesInfo): Thenable<sqlops.ObjectExplorerFindNodesResponse> { throw ni(); }
$createObjectExplorerNodeProviderSession(handle: number, sessionInfo: sqlops.ObjectExplorerSession): Thenable<boolean> { throw ni(); }
$handleSessionClose(handle: number, closeSessionInfo: sqlops.ObjectExplorerCloseSessionInfo): void { throw ni(); }
/**
* Tasks
*/
@@ -507,6 +511,7 @@ export interface MainThreadDataProtocolShape extends IDisposable {
$registerQueryProvider(providerId: string, handle: number): TPromise<any>;
$registerProfilerProvider(providerId: string, handle: number): TPromise<any>;
$registerObjectExplorerProvider(providerId: string, handle: number): TPromise<any>;
$registerObjectExplorerNodeProvider(providerId: string, supportedProviderId: string, group: string, handle: number): TPromise<any>;
$registerMetadataProvider(providerId: string, handle: number): TPromise<any>;
$registerTaskServicesProvider(providerId: string, handle: number): TPromise<any>;
$registerFileBrowserProvider(providerId: string, handle: number): TPromise<any>;
@@ -526,7 +531,7 @@ export interface MainThreadDataProtocolShape extends IDisposable {
$onQueryMessage(handle: number, message: sqlops.QueryExecuteMessageParams): void;
$onObjectExplorerSessionCreated(handle: number, message: sqlops.ObjectExplorerSession): void;
$onObjectExplorerSessionDisconnected(handle: number, message: sqlops.ObjectExplorerSession): void;
$onObjectExplorerNodeExpanded(handle: number, message: sqlops.ObjectExplorerExpandInfo): void;
$onObjectExplorerNodeExpanded(providerId: string, message: sqlops.ObjectExplorerExpandInfo): void;
$onTaskCreated(handle: number, sessionResponse: sqlops.TaskInfo): void;
$onTaskStatusChanged(handle: number, sessionResponse: sqlops.TaskProgressInfo): void;
$onFileBrowserOpened(handle: number, response: sqlops.FileBrowserOpenedParams): void;
@@ -548,6 +553,7 @@ export interface MainThreadConnectionManagementShape extends IDisposable {
$getActiveConnections(): Thenable<sqlops.connection.Connection[]>;
$getCurrentConnection(): Thenable<sqlops.connection.Connection>;
$getCredentials(connectionId: string): Thenable<{ [name: string]: string }>;
$getServerInfo(connectedId: string): Thenable<sqlops.ServerInfo>;
$openConnectionDialog(providers: string[], initialConnectionProfile?: sqlops.IConnectionProfile, connectionCompletionOptions?: sqlops.IConnectionCompletionOptions): Thenable<sqlops.connection.Connection>;
$listDatabases(connectionId: string): Thenable<string[]>;
$getConnectionString(connectionId: string, includePassword: boolean): Thenable<string>;
@@ -717,6 +723,7 @@ export interface MainThreadObjectExplorerShape extends IDisposable {
$findNodes(connectionId: string, type: string, schema: string, name: string, database: string, parentObjectNames: string[]): Thenable<sqlops.NodeInfo[]>;
$refresh(connectionId: string, nodePath: string): Thenable<sqlops.NodeInfo>;
$getNodeActions(connectionId: string, nodePath: string): Thenable<string[]>;
$getSessionConnectionProfile(sessionId: string): Thenable<sqlops.IConnectionProfile>;
}
export interface ExtHostModelViewDialogShape {

View File

@@ -8,7 +8,7 @@ import { ObjectExplorerProviderTestService } from 'sqltest/stubs/objectExplorerP
import { TestConnectionManagementService } from 'sqltest/stubs/connectionManagementService.test';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup';
import { ObjectExplorerService } from 'sql/parts/objectExplorer/common/objectExplorerService';
import { ObjectExplorerService, NodeExpandInfoWithProviderId } from 'sql/parts/objectExplorer/common/objectExplorerService';
import { NodeType } from 'sql/parts/objectExplorer/common/nodeType';
import { TreeNode, TreeItemCollapsibleState, ObjectExplorerCallbacks } from 'sql/parts/objectExplorer/common/treeNode';
@@ -32,12 +32,13 @@ suite('SQL Object Explorer Service tests', () => {
let objectExplorerSession: sqlops.ObjectExplorerSession;
let objectExplorerFailedSession: sqlops.ObjectExplorerSession;
let objectExplorerCloseSessionResponse: sqlops.ObjectExplorerCloseSessionResponse;
let objectExplorerExpandInfo: sqlops.ObjectExplorerExpandInfo;
let objectExplorerExpandInfoRefresh: sqlops.ObjectExplorerExpandInfo;
let objectExplorerExpandInfo: NodeExpandInfoWithProviderId;
let objectExplorerExpandInfoRefresh: NodeExpandInfoWithProviderId;
let sessionId = '1234';
let failedSessionId = '12345';
let numberOfFailedSession: number = 0;
let serverTreeView: TypeMoq.Mock<ServerTreeView>;
const providerId = 'MSSQL';
setup(() => {
@@ -105,14 +106,16 @@ suite('SQL Object Explorer Service tests', () => {
sessionId: sessionId,
nodes: [NodeInfoTable1, NodeInfoTable2],
errorMessage: '',
nodePath: objectExplorerSession.rootNode.nodePath
nodePath: objectExplorerSession.rootNode.nodePath,
providerId: providerId
};
objectExplorerExpandInfoRefresh = {
sessionId: sessionId,
nodes: [NodeInfoTable1, NodeInfoTable3],
errorMessage: '',
nodePath: objectExplorerSession.rootNode.nodePath
nodePath: objectExplorerSession.rootNode.nodePath,
providerId: providerId
};
let response: sqlops.ObjectExplorerSessionResponse = {
sessionId: objectExplorerSession.sessionId
@@ -126,9 +129,8 @@ suite('SQL Object Explorer Service tests', () => {
sqlOEProvider.callBase = true;
let onCapabilitiesRegistered = new Emitter<string>();
let sqlProvider = {
providerId: 'MSSQL',
providerId: providerId,
displayName: 'MSSQL',
connectionOptions: [
{
@@ -279,10 +281,10 @@ suite('SQL Object Explorer Service tests', () => {
resolve(failedResponse);
}));
sqlOEProvider.setup(x => x.expandNode(TypeMoq.It.isAny())).callback(() => {
objectExplorerService.onNodeExpanded(1, objectExplorerExpandInfo);
objectExplorerService.onNodeExpanded(objectExplorerExpandInfo);
}).returns(() => TPromise.as(true));
sqlOEProvider.setup(x => x.refreshNode(TypeMoq.It.isAny())).callback(() => {
objectExplorerService.onNodeExpanded(1, objectExplorerExpandInfoRefresh);
objectExplorerService.onNodeExpanded(objectExplorerExpandInfoRefresh);
}).returns(() => TPromise.as(true));
sqlOEProvider.setup(x => x.closeSession(TypeMoq.It.isAny())).returns(() => TPromise.as(objectExplorerCloseSessionResponse));
@@ -539,7 +541,8 @@ suite('SQL Object Explorer Service tests', () => {
sessionId: sessionId,
nodes: [],
errorMessage: '',
nodePath: table1NodePath
nodePath: table1NodePath,
providerId: providerId
};
serverTreeView.setup(x => x.isExpanded(TypeMoq.It.isAny())).returns(treeNode => {
return treeNode === connection || treeNode.nodePath === table1NodePath;
@@ -549,7 +552,7 @@ suite('SQL Object Explorer Service tests', () => {
objectExplorerService.onSessionCreated(1, objectExplorerSession);
objectExplorerService.resolveTreeNodeChildren(objectExplorerSession, objectExplorerService.getObjectExplorerNode(connection)).then(childNodes => {
sqlOEProvider.setup(x => x.expandNode(TypeMoq.It.isAny())).callback(() => {
objectExplorerService.onNodeExpanded(1, tableExpandInfo);
objectExplorerService.onNodeExpanded(tableExpandInfo);
}).returns(() => TPromise.as(true));
let tableNode = childNodes.find(node => node.nodePath === table1NodePath);
objectExplorerService.resolveTreeNodeChildren(objectExplorerSession, tableNode).then(() => {
@@ -596,7 +599,8 @@ suite('SQL Object Explorer Service tests', () => {
sessionId: sessionId,
nodes: [],
errorMessage: '',
nodePath: table1NodePath
nodePath: table1NodePath,
providerId: providerId
};
serverTreeView.setup(x => x.isExpanded(TypeMoq.It.isAny())).returns(treeNode => {
return treeNode.nodePath === table1NodePath;
@@ -606,7 +610,7 @@ suite('SQL Object Explorer Service tests', () => {
objectExplorerService.onSessionCreated(1, objectExplorerSession);
objectExplorerService.resolveTreeNodeChildren(objectExplorerSession, objectExplorerService.getObjectExplorerNode(connection)).then(childNodes => {
sqlOEProvider.setup(x => x.expandNode(TypeMoq.It.isAny())).callback(() => {
objectExplorerService.onNodeExpanded(1, tableExpandInfo);
objectExplorerService.onNodeExpanded(tableExpandInfo);
}).returns(() => TPromise.as(true));
objectExplorerService.resolveTreeNodeChildren(objectExplorerSession, childNodes.find(node => node.nodePath === table1NodePath)).then(() => {
// If I check whether the table is expanded, the answer should be yes
@@ -630,11 +634,12 @@ suite('SQL Object Explorer Service tests', () => {
sessionId: sessionId,
nodes: [],
errorMessage: '',
nodePath: table1NodePath
nodePath: table1NodePath,
providerId: providerId
};
// Set up the OE provider so that the second expand call expands the table
sqlOEProvider.setup(x => x.expandNode(TypeMoq.It.is(nodeInfo => nodeInfo.nodePath === table1NodePath))).callback(() => {
objectExplorerService.onNodeExpanded(1, tableExpandInfo);
objectExplorerService.onNodeExpanded(tableExpandInfo);
}).returns(() => TPromise.as(true));
serverTreeView.setup(x => x.setExpandedState(TypeMoq.It.isAny(), TypeMoq.It.is(state => state === TreeItemCollapsibleState.Expanded))).returns(treeNode => {
if (treeNode instanceof ConnectionProfile) {

View File

@@ -58,6 +58,7 @@ suite('Dashboard Properties Widget Tests', () => {
serverEdition: undefined,
azureVersion: undefined,
osVersion: undefined,
options: {},
};
let databaseInfo = {

View File

@@ -254,6 +254,10 @@ export class TestConnectionManagementService implements IConnectionManagementSer
return undefined;
}
getServerInfo(profileId: string): sqlops.ServerInfo {
return undefined;
}
getConnectionString(connectionId: string): Thenable<string> {
return undefined;
}