Kusto extension for ADS (#11752)

* Kusto extension

* Add kusto to extensions.ts

* Remove objectExplorerNodeProvider

* Removed some BDC items + CR cleanup

* Cleanup unused strings in package.nls.json

* Remove unused svgs, and some cleanup

* Bringing objectExplorerNode back and hygiene changes

* rename to KustoObjectExplorerNodeProvider

* rename to KustoIconProvider

* Cleanup SQL code

* Fix compilation error

* Clean up in objectExplorerNodeProvider folder

* Some more clean up based on comments

* apiWrapper add

* changed to camelCase

* Remove unused functions/files

* Removed AgentServicesFeature

* dacfx, cms, schemacompare clean up

* Clean up and addressed few comments

* Remove apWrapper from kusto extension

* Addressed few comments

* credentialstore and escapeexception changes

* Added strict check for Kusto extension

* Fix error and addressed comment

* Saving Kusto files shoulf default to .kql

* package.json changes

* Fix objectExplorerNodeProvider

* Amir/kusto fix (#11972)

* Add the compiled extensions.js

* Fix strict compile rules

Co-authored-by: Monica Gupta <mogupt@microsoft.com>
Co-authored-by: Amir Omidi <amomidi@microsoft.com>
This commit is contained in:
Shafiq Ur Rahman
2020-08-26 14:13:31 -07:00
committed by GitHub
parent f7279cb1f5
commit 2f94307635
60 changed files with 4660 additions and 26 deletions

View File

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

View File

@@ -0,0 +1,187 @@
/*---------------------------------------------------------------------------------------------
* 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 azdata from 'azdata';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
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: azdata.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 = vscode.commands.registerCommand(command, (...args: any[]) => this._execute(command, ...args), this);
return;
}
const subscriptions = command.map(cmd => vscode.commands.registerCommand(cmd, (...args: any[]) => this._execute(cmd, ...args), this));
this.disposable = vscode.Disposable.from(...subscriptions);
}
dispose(): void {
if (this.disposable) {
this.disposable.dispose();
}
}
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 [azdata.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(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 = vscode.window.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(vscode.commands.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
}))!;
}
}
export function registerSearchServerCommand(): void {
vscode.commands.registerCommand('kusto.searchServers', () => {
vscode.window.showInputBox({
placeHolder: localize('kusto.searchServers', 'Search Server Names')
}).then((stringSearch) => {
if (stringSearch) {
vscode.commands.executeCommand('registeredServers.searchServer', (stringSearch));
}
});
});
vscode.commands.registerCommand('kusto.clearSearchServerResult', () => {
vscode.commands.executeCommand('registeredServers.clearSearchServerResult');
});
}

View File

@@ -0,0 +1,82 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import * as constants from '../constants';
export class KustoClusterConnection {
private _connection: azdata.connection.Connection;
private _profile!: azdata.IConnectionProfile;
private _user: string;
private _password: string;
constructor(connectionInfo: azdata.connection.Connection | azdata.IConnectionProfile) {
this.validate(connectionInfo);
if ('id' in connectionInfo) {
this._profile = connectionInfo;
this._connection = this.toConnection(this._profile);
} else {
this._connection = connectionInfo;
}
this._user = this._connection.options[constants.userPropName];
this._password = this._connection.options[constants.passwordPropName];
}
public get connection(): azdata.connection.Connection { return this._connection; }
public get user(): string { return this._user; }
public get password(): string { return this._password; }
public isMatch(connection: KustoClusterConnection | azdata.ConnectionInfo): boolean {
if (!connection) { return false; }
let options1 = connection instanceof KustoClusterConnection ?
connection._connection.options : connection.options;
let options2 = this._connection.options;
return [constants.serverPropName, constants.userPropName]
.every(e => options1[e] === options2[e]);
}
public isIntegratedAuth(): boolean {
let authType: string = this._connection.options[constants.authenticationTypePropName];
return authType?.toLowerCase() === constants.integratedAuth;
}
public updatePassword(password: string): void {
if (password) {
this._password = password;
}
}
private validate(connectionInfo: azdata.ConnectionInfo): void {
if (!connectionInfo) {
throw new Error(localize('connectionInfoUndefined', 'ConnectionInfo is undefined.'));
}
if (!connectionInfo.options) {
throw new Error(localize('connectionInfoOptionsUndefined', 'ConnectionInfo.options is undefined.'));
}
let missingProperties: string[] = this.getMissingProperties(connectionInfo)!;
if (missingProperties && missingProperties.length > 0) {
throw new Error(localize('connectionInfoOptionsMissingProperties',
'Some missing properties in connectionInfo.options: {0}',
missingProperties.join(', ')));
}
}
private getMissingProperties(connectionInfo: azdata.ConnectionInfo): string[] | undefined {
if (!connectionInfo || !connectionInfo.options) { return undefined; }
let requiredProps = [constants.serverPropName];
requiredProps.push(constants.userPropName);
return requiredProps.filter(e => connectionInfo.options[e] === undefined);
}
private toConnection(connProfile: azdata.IConnectionProfile): azdata.connection.Connection {
let connection: azdata.connection.Connection = Object.assign(connProfile,
{ connectionId: this._profile.id });
return connection;
}
}

View File

@@ -0,0 +1,220 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ProviderBase } from './providerBase';
import { KustoClusterConnection } from './connection';
import { TreeNode } from './treeNodes';
import { AppContext } from '../appContext';
import * as constants from '../constants';
import { ICommandObjectExplorerContext } from './command';
export const kustoOutputChannel = vscode.window.createOutputChannel(constants.providerId);
export interface ITreeChangeHandler {
notifyNodeChanged(node: TreeNode): void;
}
export class KustoObjectExplorerNodeProvider extends ProviderBase implements azdata.ObjectExplorerNodeProvider, ITreeChangeHandler {
public readonly supportedProviderId: string = constants.providerId;
private expandCompleteEmitter = new vscode.EventEmitter<azdata.ObjectExplorerExpandInfo>();
constructor(private appContext: AppContext) {
super();
this.appContext.registerService<KustoObjectExplorerNodeProvider>(constants.ObjectExplorerService, this);
}
handleSessionOpen(session: azdata.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(session: azdata.ObjectExplorerSession): Promise<boolean> {
if (!session || !session.sessionId) { return false; }
let connProfile = await azdata.objectexplorer.getSessionConnectionProfile(session.sessionId);
if (!connProfile) { return false; }
return true;
}
expandNode(nodeInfo: azdata.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: azdata.ExpandNodeInfo, isRefresh: boolean = false): Promise<boolean> {
let response = {
sessionId: nodeInfo.sessionId!,
nodePath: nodeInfo.nodePath!,
errorMessage: undefined,
nodes: []
};
this.expandCompleteEmitter.fire(response);
return true;
}
refreshNode(nodeInfo: azdata.ExpandNodeInfo): Thenable<boolean> {
return this.expandNode(nodeInfo, true);
}
handleSessionClose(closeSessionInfo: azdata.ObjectExplorerCloseSessionInfo): void {
}
findNodes(findNodesInfo: azdata.FindNodesInfo): Thenable<azdata.ObjectExplorerFindNodesResponse> {
let response: azdata.ObjectExplorerFindNodesResponse = {
nodes: []
};
return Promise.resolve(response);
}
registerOnExpandCompleted(handler: (response: azdata.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.getSqlClusterSessionForNode(node);
if (!session) {
vscode.window.showErrorMessage(localize('sessionNotFound', "Session for node {0} does not exist", node.nodePathValue));
} else {
let nodeInfo = node.getNodeInfo();
let expandInfo: azdata.ExpandNodeInfo = {
nodePath: nodeInfo.nodePath,
sessionId: session.sessionId
};
await this.refreshNode(expandInfo);
}
} catch (err) {
kustoOutputChannel.appendLine(localize('notifyError', "Error notifying of node change: {0}", err));
}
}
private getSqlClusterSessionForNode(node?: TreeNode): SqlClusterSession | undefined {
let sqlClusterSession: SqlClusterSession | undefined = undefined;
while (node !== undefined) {
if (node instanceof SqlClusterRootNode) {
sqlClusterSession = node.session;
break;
} else {
node = node.parent;
}
}
return sqlClusterSession;
}
async findSqlClusterNodeByContext<T extends TreeNode>(context: ICommandObjectExplorerContext | azdata.ObjectExplorerContext): Promise<T | undefined> {
let node: T | undefined = undefined;
let explorerContext = 'explorerContext' in context ? context.explorerContext : context;
let sqlConnProfile = explorerContext.connectionProfile;
let session = this.findSqlClusterSessionBySqlConnProfile(sqlConnProfile!);
if (session) {
if (explorerContext.isConnectionNode) {
// Note: ideally fix so we verify T matches RootNode and go from there
node = <T><any>session.rootNode;
} else {
// Find the node under the session
node = <T><any>await session.rootNode.findNodeByPath(explorerContext?.nodeInfo?.nodePath!, true);
}
}
return node;
}
public findSqlClusterSessionBySqlConnProfile(connectionProfile: azdata.IConnectionProfile): SqlClusterSession | undefined {
return undefined;
}
}
export class SqlClusterSession {
private _rootNode: SqlClusterRootNode;
constructor(
private _sqlClusterConnection: KustoClusterConnection,
private _sqlSession: azdata.ObjectExplorerSession,
private _sqlConnectionProfile: azdata.IConnectionProfile
) {
this._rootNode = new SqlClusterRootNode(this,
this._sqlSession.rootNode.nodePath!);
}
public get sqlClusterConnection(): KustoClusterConnection { return this._sqlClusterConnection; }
public get sqlSession(): azdata.ObjectExplorerSession { return this._sqlSession; }
public get sqlConnectionProfile(): azdata.IConnectionProfile { return this._sqlConnectionProfile; }
public get sessionId(): string { return this._sqlSession.sessionId!; }
public get rootNode(): SqlClusterRootNode { return this._rootNode; }
public isMatchedSqlConnection(sqlConnProfile: azdata.IConnectionProfile): boolean {
return this._sqlConnectionProfile.id === sqlConnProfile.id;
}
}
class SqlClusterRootNode extends TreeNode {
private _children: TreeNode[] = [];
constructor(
private _session: SqlClusterSession,
private _nodePathValue: string
) {
super();
}
public get session(): SqlClusterSession {
return this._session;
}
public get nodePathValue(): string {
return this._nodePathValue;
}
public getChildren(refreshChildren: boolean): TreeNode[] | Promise<TreeNode[]> {
if (refreshChildren || !this._children) {
return this.refreshChildren();
}
return this._children;
}
private async refreshChildren(): Promise<TreeNode[]> {
this._children = [];
return this._children;
}
getTreeItem(): vscode.TreeItem | Promise<vscode.TreeItem> {
throw new Error('Not intended for use in a file explorer view.');
}
getNodeInfo(): azdata.NodeInfo {
let nodeInfo: azdata.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;
}
}

View File

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

View File

@@ -0,0 +1,87 @@
/*---------------------------------------------------------------------------------------------
* 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 azdata from 'azdata';
import * as vscode from 'vscode';
import { ITreeNode } from './types';
type TreeNodePredicate = (node: TreeNode) => boolean;
export abstract class TreeNode implements ITreeNode {
private _parent?: TreeNode;
private _errorStatusCode?: number;
public get parent(): TreeNode | undefined {
return this._parent;
}
public set parent(node: TreeNode | undefined) {
this._parent = node;
}
public get errorStatusCode(): number | undefined {
return this._errorStatusCode;
}
public set errorStatusCode(error: number | undefined) {
this._errorStatusCode = error;
}
public generateNodePath(): string | undefined {
let path: string | 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 | undefined> {
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 | undefined> {
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(): azdata.NodeInfo;
}

View File

@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* 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 azdata from 'azdata';
/**
* A tree node in the object explorer tree
*
* @export
*/
export interface ITreeNode {
getNodeInfo(): azdata.NodeInfo;
getChildren(refreshChildren: boolean): ITreeNode[] | Promise<ITreeNode[]>;
}