mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
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:
@@ -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": {
|
||||
},
|
||||
|
||||
93
extensions/mssql/src/apiWrapper.ts
Normal file
93
extensions/mssql/src/apiWrapper.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
28
extensions/mssql/src/appContext.ts
Normal file
28
extensions/mssql/src/appContext.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
13
extensions/mssql/src/localizedConstants.ts
Normal file
13
extensions/mssql/src/localizedConstants.ts
Normal 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');
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
176
extensions/mssql/src/objectExplorerNodeProvider/command.ts
Normal file
176
extensions/mssql/src/objectExplorerNodeProvider/command.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
222
extensions/mssql/src/objectExplorerNodeProvider/connection.ts
Normal file
222
extensions/mssql/src/objectExplorerNodeProvider/connection.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
371
extensions/mssql/src/objectExplorerNodeProvider/fileSources.ts
Normal file
371
extensions/mssql/src/objectExplorerNodeProvider/fileSources.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
437
extensions/mssql/src/objectExplorerNodeProvider/hdfsCommands.ts
Normal file
437
extensions/mssql/src/objectExplorerNodeProvider/hdfsCommands.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
366
extensions/mssql/src/objectExplorerNodeProvider/hdfsProvider.ts
Normal file
366
extensions/mssql/src/objectExplorerNodeProvider/hdfsProvider.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
78
extensions/mssql/src/objectExplorerNodeProvider/treeNodes.ts
Normal file
78
extensions/mssql/src/objectExplorerNodeProvider/treeNodes.ts
Normal 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;
|
||||
}
|
||||
30
extensions/mssql/src/objectExplorerNodeProvider/types.d.ts
vendored
Normal file
30
extensions/mssql/src/objectExplorerNodeProvider/types.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
68
extensions/mssql/src/prompts/question.ts
Normal file
68
extensions/mssql/src/prompts/question.ts
Normal 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;
|
||||
}
|
||||
1
extensions/mssql/src/typings/refs.d.ts
vendored
1
extensions/mssql/src/typings/refs.d.ts
vendored
@@ -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'/>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user