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,34 @@
/*---------------------------------------------------------------------------------------------
* 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';
/**
* Global context for the application
*/
export class AppContext {
private serviceMap: Map<string, any> = new Map();
constructor(public readonly extensionContext: vscode.ExtensionContext) { }
public getService<T>(serviceName: string): T | undefined {
const service = this.serviceMap.get(serviceName) as T;
if (!service) {
console.warn('Service ', serviceName, ' is not registered');
}
return service;
}
public registerService<T>(serviceName: string, service: T): void {
if (this.serviceMap.has(serviceName)) {
console.warn('Multiple services ', serviceName, ' registered!');
} else {
this.serviceMap.set(serviceName, service);
}
}
}

View File

@@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const serviceName = 'Kusto service';
export const providerId = 'KUSTO';
export const serviceCrashLink = 'https://github.com/Microsoft/vscode-kusto/wiki/SqlToolsService-Known-Issues';
export const extensionConfigSectionName = 'kusto';
// DATA PROTOCOL VALUES ///////////////////////////////////////////////////////////
export const kustoClusterProviderName = 'kustoCluster';
export const protocolVersion = '1.0';
export const authenticationTypePropName = 'authenticationType';
export const integratedAuth = 'integrated';
export const serverPropName = 'server';
export const userPropName = 'user';
export const passwordPropName = 'password';
export const groupIdPropName = 'groupId';
export const groupIdName = 'groupId';
export const kustoProviderName = 'KUSTO';
export const UNTITLED_SCHEMA = 'untitled';
export const clusterEndpointsProperty = 'clusterEndpoints';
export const hdfsRootPath = '/';
// 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 KustoClusterItems {
Connection = 'kustoCluster:connection',
Folder = 'kustoCluster:folder',
File = 'kustoCluster:file',
Error = 'kustoCluster:error'
}
export const kustoClusterNewNotebookTask = 'kustoCluster.task.newNotebook';
export const kustoClusterOpenNotebookTask = 'kustoCluster.task.openNotebook';

View File

@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* 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 types from './types';
export enum BuiltInCommands {
SetContext = 'setContext',
}
export enum ContextKeys {
ISCLOUD = 'kusto:iscloud',
EDITIONID = 'kusto:engineedition',
ISCLUSTER = 'kusto:iscluster',
SERVERMAJORVERSION = 'kusto:servermajorversion'
}
const isCloudEditions = [
5,
6
];
export function setCommandContext(key: ContextKeys | string, value: any) {
return vscode.commands.executeCommand(BuiltInCommands.SetContext, key, value);
}
export default class ContextProvider {
private _disposables = new Array<vscode.Disposable>();
constructor() {
this._disposables.push(azdata.workspace.onDidOpenDashboard(this.onDashboardOpen, this));
this._disposables.push(azdata.workspace.onDidChangeToDashboard(this.onDashboardOpen, this));
}
public onDashboardOpen(e: azdata.DashboardDocument): void {
let iscloud: boolean = false;
let edition: number | undefined;
let isCluster: boolean = false; // TODO: Do we even need this for Kusto
let serverMajorVersion: number | undefined;
if (e.profile.providerName.toLowerCase() === 'kusto' && !types.isUndefinedOrNull(e.serverInfo) && !types.isUndefinedOrNull(e.serverInfo.engineEditionId)) {
if (isCloudEditions.some(i => i === e.serverInfo.engineEditionId)) {
iscloud = true;
} else {
iscloud = false;
}
edition = e.serverInfo.engineEditionId;
serverMajorVersion = e.serverInfo.serverMajorVersion;
}
if (iscloud === true || iscloud === false) {
setCommandContext(ContextKeys.ISCLOUD, iscloud);
}
if (!types.isUndefinedOrNull(edition)) {
setCommandContext(ContextKeys.EDITIONID, edition);
}
if (!types.isUndefinedOrNull(isCluster)) {
setCommandContext(ContextKeys.ISCLUSTER, isCluster);
}
if (!types.isUndefinedOrNull(serverMajorVersion)) {
setCommandContext(ContextKeys.SERVERMAJORVERSION, serverMajorVersion);
}
}
dispose(): void {
this._disposables = this._disposables.map(i => i.dispose());
}
}

View File

@@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { NotificationType, RequestType } from 'vscode-languageclient';
import { ITelemetryEventProperties, ITelemetryEventMeasures } from './telemetry';
import * as azdata from 'azdata';
// ------------------------------- < Telemetry Sent Event > ------------------------------------
/**
* Event sent when the language service send a telemetry event
*/
export namespace TelemetryNotification {
export const type = new NotificationType<TelemetryParams, void>('telemetry/sqlevent');
}
/**
* Update event parameters
*/
export class TelemetryParams {
public params!: {
eventName: string;
properties: ITelemetryEventProperties;
measures: ITelemetryEventMeasures;
};
}
// ------------------------------- </ Telemetry Sent Event > ----------------------------------
// ------------------------------- <Serialization> -----------------------------
export namespace SerializeDataStartRequest {
export const type = new RequestType<azdata.SerializeDataStartRequestParams, azdata.SerializeDataResult, void, void>('serialize/start');
}
export namespace SerializeDataContinueRequest {
export const type = new RequestType<azdata.SerializeDataContinueRequestParams, azdata.SerializeDataResult, void, void>('serialize/continue');
}
// ------------------------------- <Serialization> -----------------------------
// ------------------------------- < Load Completion Extension Request > ------------------------------------
/**
* Completion extension load parameters
*/
export class CompletionExtensionParams {
/// <summary>
/// Absolute path for the assembly containing the completion extension
/// </summary>
public assemblyPath?: string;
/// <summary>
/// The type name for the completion extension
/// </summary>
public typeName?: string;
/// <summary>
/// Property bag for initializing the completion extension
/// </summary>
public properties?: {};
}
export namespace CompletionExtLoadRequest {
export const type = new RequestType<CompletionExtensionParams, boolean, void, void>('completion/extLoad');
}

View File

@@ -0,0 +1,89 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SqlOpsDataClient, SqlOpsFeature } from 'dataprotocol-client';
import { ClientCapabilities, StaticFeature, RPCMessageType, ServerCapabilities } from 'vscode-languageclient';
import { Disposable } from 'vscode';
import { Telemetry } from './telemetry';
import * as contracts from './contracts';
import * as azdata from 'azdata';
import * as Utils from './utils';
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
export class TelemetryFeature implements StaticFeature {
constructor(private _client: SqlOpsDataClient) { }
fillClientCapabilities(capabilities: ClientCapabilities): void {
Utils.ensure(capabilities, 'telemetry')!.telemetry = true;
}
initialize(): void {
this._client.onNotification(contracts.TelemetryNotification.type, e => {
Telemetry.sendTelemetryEvent(e.params.eventName, e.params.properties, e.params.measures);
});
}
}
export class SerializationFeature extends SqlOpsFeature<undefined> {
private static readonly messageTypes: RPCMessageType[] = [
contracts.SerializeDataStartRequest.type,
contracts.SerializeDataContinueRequest.type,
];
constructor(client: SqlOpsDataClient) {
super(client, SerializationFeature.messageTypes);
}
public fillClientCapabilities(capabilities: ClientCapabilities): void {
}
public initialize(capabilities: ServerCapabilities): void {
this.register(this.messages, {
id: UUID.generateUuid(),
registerOptions: undefined
});
}
protected registerProvider(options: undefined): Disposable {
const client = this._client;
let startSerialization = (requestParams: azdata.SerializeDataStartRequestParams): Thenable<azdata.SerializeDataResult> => {
return client.sendRequest(contracts.SerializeDataStartRequest.type, requestParams).then(
r => {
return r;
},
e => {
client.logFailedRequest(contracts.SerializeDataStartRequest.type, e);
return Promise.resolve(<azdata.SerializeDataResult>{
succeeded: false,
messages: Utils.getErrorMessage(e)
});
}
);
};
let continueSerialization = (requestParams: azdata.SerializeDataContinueRequestParams): Thenable<azdata.SerializeDataResult> => {
return client.sendRequest(contracts.SerializeDataContinueRequest.type, requestParams).then(
r => {
return r;
},
e => {
client.logFailedRequest(contracts.SerializeDataContinueRequest.type, e);
return Promise.resolve(<azdata.SerializeDataResult>{
succeeded: false,
messages: Utils.getErrorMessage(e)
});
}
);
};
return azdata.dataprotocol.registerSerializationProvider({
providerId: client.providerId,
startSerialization,
continueSerialization
});
}
}

View File

@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* 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 constants from './constants';
const cloudIcon = 'kusto:cloud';
export class KustoIconProvider implements azdata.IconProvider {
public readonly providerId: string = constants.kustoProviderName;
public handle?: number;
getConnectionIconId(connection: azdata.IConnectionProfile, serverInfo: azdata.ServerInfo): Thenable<string | undefined> {
let iconName: string | undefined;
if (connection.providerName === this.providerId) {
iconName = cloudIcon;
}
return Promise.resolve(iconName);
}
}

38
extensions/kusto/src/kusto.d.ts vendored Normal file
View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// This is the place for extensions to expose APIs.
import * as azdata from 'azdata';
/**
* The APIs provided by Kusto extension
*/
export interface IExtension {
/**
* Gets the object explorer API that supports querying over the connections supported by this extension
*
*/
getKustoObjectExplorerBrowser(): KustoObjectExplorerBrowser;
}
/**
* A browser supporting actions over the object explorer connections provided by this extension.
* Currently this is the
*/
export interface KustoObjectExplorerBrowser {
/**
* Gets the matching node given a context object, e.g. one from a right-click on a node in Object Explorer
*/
getNode<T extends ITreeNode>(objectExplorerContext: azdata.ObjectExplorerContext): Thenable<T>;
}
/**
* A tree node in the object explorer tree
*/
export interface ITreeNode {
getNodeInfo(): azdata.NodeInfo;
getChildren(refreshChildren: boolean): ITreeNode[] | Thenable<ITreeNode[]>;
}

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AppContext } from './appContext';
import { IExtension, KustoObjectExplorerBrowser } from './kusto';
import * as constants from './constants';
import { KustoObjectExplorerNodeProvider } from './objectExplorerNodeProvider/objectExplorerNodeProvider';
import * as azdata from 'azdata';
export function createKustoApi(context: AppContext): IExtension {
return {
getKustoObjectExplorerBrowser(): KustoObjectExplorerBrowser {
return {
getNode: (explorerContext: azdata.ObjectExplorerContext) => {
let oeProvider = context.getService<KustoObjectExplorerNodeProvider>(constants.ObjectExplorerService);
return <any>oeProvider?.findSqlClusterNodeByContext(explorerContext);
}
};
}
};
}

View File

@@ -0,0 +1,163 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ServerProvider, IConfig, Events } from 'service-downloader';
import { ServerOptions, TransportKind } from 'vscode-languageclient';
import * as Constants from './constants';
import * as vscode from 'vscode';
import * as path from 'path';
import { getCommonLaunchArgsAndCleanupOldLogFiles } from './utils';
import { localize } from './localize';
import { Telemetry, LanguageClientErrorHandler } from './telemetry';
import { SqlOpsDataClient, ClientOptions } from 'dataprotocol-client';
import { TelemetryFeature, SerializationFeature } from './features';
import { AppContext } from './appContext';
import { CompletionExtensionParams, CompletionExtLoadRequest } from './contracts';
import { promises as fs } from 'fs';
const outputChannel = vscode.window.createOutputChannel(Constants.serviceName);
const statusView = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
export class KustoServer {
private client!: SqlOpsDataClient;
private config!: IConfig;
private disposables: vscode.Disposable[] = [];
public async start(context: AppContext): Promise<SqlOpsDataClient> {
try {
const installationStart = Date.now();
const path = await this.download(context); // TodoKusto: Remove this commented line once the Kusto service layer has been published to Github. Until then copy manually for debugging.
// const path = "e:\\repos\\azuredatastudio\\extensions\\kusto\\sqltoolsservice\\Windows\\2.0.0-release.15\\MicrosoftKustoServiceLayer.exe";
const installationComplete = Date.now();
let serverOptions = generateServerOptions(context.extensionContext.logPath, path);
let clientOptions = getClientOptions(context);
this.client = new SqlOpsDataClient(Constants.serviceName, serverOptions, clientOptions); // TodoKusto: Update constant
const processStart = Date.now();
const clientReadyPromise = this.client.onReady().then(() => {
const processEnd = Date.now();
statusView.text = localize('serviceStartedStatusMsg', "{0} Started", Constants.serviceName);
setTimeout(() => {
statusView.hide();
}, 1500);
vscode.commands.registerCommand('kusto.loadCompletionExtension', (params: CompletionExtensionParams) => {
this.client.sendRequest(CompletionExtLoadRequest.type, params);
});
Telemetry.sendTelemetryEvent('startup/LanguageClientStarted', {
installationTime: String(installationComplete - installationStart),
processStartupTime: String(processEnd - processStart),
totalTime: String(processEnd - installationStart),
beginningTimestamp: String(installationStart)
});
});
statusView.show();
statusView.text = localize('startingServiceStatusMsg', "Starting {0}", Constants.serviceName);
this.client.start();
await Promise.all([clientReadyPromise]);
return this.client;
} catch (e) {
Telemetry.sendTelemetryEvent('ServiceInitializingFailed');
vscode.window.showErrorMessage(localize('failedToStartServiceErrorMsg', "Failed to start {0}", Constants.serviceName));
throw e;
}
}
private async download(context: AppContext): Promise<string> {
const rawConfig = await fs.readFile(path.join(context.extensionContext.extensionPath, 'config.json')); // TodoKusto: Update config.json to refer to the right exe
this.config = JSON.parse(rawConfig.toString())!;
this.config.installDirectory = path.join(__dirname, this.config.installDirectory);
this.config.proxy = vscode.workspace.getConfiguration('http').get<string>('proxy')!;
this.config.strictSSL = vscode.workspace.getConfiguration('http').get('proxyStrictSSL') || true;
const serverdownloader = new ServerProvider(this.config);
serverdownloader.eventEmitter.onAny(() => generateHandleServerProviderEvent());
return serverdownloader.getOrDownloadServer();
}
dispose() {
this.disposables.forEach(d => d.dispose());
if (this.client) {
this.client.stop();
}
}
}
function generateServerOptions(logPath: string, executablePath: string): ServerOptions {
const launchArgs = getCommonLaunchArgsAndCleanupOldLogFiles(logPath, 'kustoService.log', executablePath);
return { command: executablePath, args: launchArgs, transport: TransportKind.stdio };
}
function generateHandleServerProviderEvent() {
let dots = 0;
return (e: string, ...args: any[]) => {
switch (e) {
case Events.INSTALL_START:
outputChannel.show(true);
statusView.show();
outputChannel.appendLine(localize('installingServiceChannelMsg', "Installing {0} to {1}", Constants.serviceName, args[0]));
statusView.text = localize('installingServiceStatusMsg', "Installing {0}", Constants.serviceName);
break;
case Events.INSTALL_END:
outputChannel.appendLine(localize('installedServiceChannelMsg', "Installed {0}", Constants.serviceName));
break;
case Events.DOWNLOAD_START:
outputChannel.appendLine(localize('downloadingServiceChannelMsg', "Downloading {0}", args[0]));
outputChannel.append(localize('downloadingServiceSizeChannelMsg', "({0} KB)", Math.ceil(args[1] / 1024).toLocaleString(vscode.env.language)));
statusView.text = localize('downloadingServiceStatusMsg', "Downloading {0}", Constants.serviceName);
break;
case Events.DOWNLOAD_PROGRESS:
let newDots = Math.ceil(args[0] / 5);
if (newDots > dots) {
outputChannel.append('.'.repeat(newDots - dots));
dots = newDots;
}
break;
case Events.DOWNLOAD_END:
outputChannel.appendLine(localize('downloadServiceDoneChannelMsg', "Done installing {0}", Constants.serviceName));
break;
default:
console.error(`Unknown event from Server Provider ${e}`);
break;
}
};
}
function getClientOptions(context: AppContext): ClientOptions {
return {
documentSelector: ['kusto'], // TodoKusto: This should be same as the language id in package.json. See if we can surface that better later.
synchronize: {
configurationSection: Constants.extensionConfigSectionName
},
providerId: Constants.providerId,
errorHandler: new LanguageClientErrorHandler(),
features: [
// we only want to add new features
...SqlOpsDataClient.defaultFeatures,
TelemetryFeature,
SerializationFeature
],
outputChannel: new CustomOutputChannel()
};
}
class CustomOutputChannel implements vscode.OutputChannel {
name!: string;
append(value: string): void {
console.log(value);
}
appendLine(value: string): void {
console.log(value);
}
clear(): void {
}
show(preserveFocus?: boolean): void;
show(column?: vscode.ViewColumn, preserveFocus?: boolean): void;
show(column?: any, preserveFocus?: any) {
}
hide(): void {
}
dispose(): void {
}
}

View File

@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
export const localize = nls.loadMessageBundle();

View File

@@ -0,0 +1,135 @@
/*---------------------------------------------------------------------------------------------
* 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 path from 'path';
import * as Constants from './constants';
import ContextProvider from './contextProvider';
import * as Utils from './utils';
import { AppContext } from './appContext';
import { IExtension } from './kusto';
import { KustoObjectExplorerNodeProvider } from './objectExplorerNodeProvider/objectExplorerNodeProvider';
import { registerSearchServerCommand } from './objectExplorerNodeProvider/command';
import { KustoIconProvider } from './iconProvider';
import { createKustoApi } from './kustoApiFactory';
import { localize } from './localize';
import { KustoServer } from './kustoServer';
import { promises as fs } from 'fs';
export async function activate(context: vscode.ExtensionContext): Promise<IExtension | undefined> {
// lets make sure we support this platform first
let supported = await Utils.verifyPlatform();
if (!supported) {
vscode.window.showErrorMessage(localize('kusto.unsupportedPlatform', 'Unsupported platform'));
return undefined;
}
// ensure our log path exists
if (!(await Utils.exists(context.logPath))) {
await fs.mkdir(context.logPath);
}
let appContext = new AppContext(context);
let nodeProvider = new KustoObjectExplorerNodeProvider(appContext);
azdata.dataprotocol.registerObjectExplorerNodeProvider(nodeProvider);
let iconProvider = new KustoIconProvider();
azdata.dataprotocol.registerIconProvider(iconProvider);
activateNotebookTask();
registerSearchServerCommand();
context.subscriptions.push(new ContextProvider());
registerLogCommand(context);
// initialize client last so we don't have features stuck behind it
const server = new KustoServer();
context.subscriptions.push(server);
await server.start(appContext);
return createKustoApi(appContext);
}
const logFiles = ['resourceprovider.log', 'kustoservice.log', 'credentialstore.log'];
function registerLogCommand(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.commands.registerCommand('kusto.showLogFile', async () => {
const choice = await vscode.window.showQuickPick(logFiles);
if (choice) {
const document = await vscode.workspace.openTextDocument(vscode.Uri.file(path.join(context.logPath, choice)));
if (document) {
vscode.window.showTextDocument(document);
}
}
}));
}
function activateNotebookTask(): void {
azdata.tasks.registerTask(Constants.kustoClusterNewNotebookTask, (profile: azdata.IConnectionProfile) => {
return saveProfileAndCreateNotebook(profile);
});
azdata.tasks.registerTask(Constants.kustoClusterOpenNotebookTask, (profile: azdata.IConnectionProfile) => {
return handleOpenNotebookTask(profile);
});
}
function saveProfileAndCreateNotebook(profile: azdata.IConnectionProfile): Promise<void> {
return handleNewNotebookTask(undefined, profile);
}
function findNextUntitledEditorName(): string {
let nextVal = 0;
// Note: this will go forever if it's coded wrong, or you have inifinite Untitled notebooks!
while (true) {
let title = `Notebook-${nextVal}`;
let hasNotebookDoc = azdata.nb.notebookDocuments.findIndex(doc => doc.isUntitled && doc.fileName === title) > -1;
if (!hasNotebookDoc) {
return title;
}
nextVal++;
}
}
async function handleNewNotebookTask(oeContext?: azdata.ObjectExplorerContext, profile?: azdata.IConnectionProfile): Promise<void> {
// Ensure we get a unique ID for the notebook. For now we're using a different prefix to the built-in untitled files
// to handle this. We should look into improving this in the future
let title = findNextUntitledEditorName();
let untitledUri = vscode.Uri.parse(`untitled:${title}`);
await azdata.nb.showNotebookDocument(untitledUri, {
connectionProfile: profile,
preview: false
});
}
async function handleOpenNotebookTask(profile: azdata.IConnectionProfile): Promise<void> {
let notebookFileTypeName = localize('notebookFileType', "Notebooks");
let filter = {};
filter[notebookFileTypeName] = 'ipynb';
let uris = await vscode.window.showOpenDialog({
filters: filter,
canSelectFiles: true,
canSelectMany: false
});
if (uris && uris.length > 0) {
let fileUri = uris[0];
// Verify this is a .ipynb file since this isn't actually filtered on Mac/Linux
if (path.extname(fileUri.fsPath) !== '.ipynb') {
// in the future might want additional supported types
vscode.window.showErrorMessage(localize('unsupportedFileType', "Only .ipynb Notebooks are supported"));
} else {
await azdata.nb.showNotebookDocument(fileUri, {
connectionProfile: profile,
preview: false
});
}
}
}
// this method is called when your extension is deactivated
export function deactivate(): void {
}

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

View File

@@ -0,0 +1,77 @@
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import { window } from 'vscode';
import PromptFactory from './factory';
import EscapeException from './escapeException';
import { IQuestion, IPrompter, IPromptCallback, Answers } from './question';
// Supports simple pattern for prompting for user input and acting on this
export default class CodeAdapter implements IPrompter {
// TODO define question interface
private fixQuestion(question: IQuestion): any {
if (question.type === 'checkbox' && Array.isArray(question.choices)) {
// For some reason when there's a choice of checkboxes, they aren't formatted properly
// Not sure where the issue is
question.choices = question.choices.map(item => {
if (typeof (item) === 'string') {
return { checked: false, name: item, value: item };
} else {
return item;
}
});
}
}
public async promptSingle<T>(question: IQuestion, ignoreFocusOut?: boolean): Promise<T | undefined> {
let questions: IQuestion[] = [question];
const answers = this.prompt(questions, ignoreFocusOut);
if (answers) {
let response: T = answers[question.name];
return response || undefined;
}
return undefined;
}
public async prompt<T>(questions: IQuestion[], ignoreFocusOut?: boolean): Promise<Answers<T> | undefined> {
// Collapse multiple questions into a set of prompt steps
const promptResult = new Promise<Answers<T>>((resolve) => {
let answers: Answers<T> = {};
questions.forEach(async (question: IQuestion) => {
this.fixQuestion(question);
const prompt = await PromptFactory.createPrompt(question, ignoreFocusOut);
if (!question.shouldPrompt || question.shouldPrompt(answers) === true) {
const result = await prompt.render();
answers[question.name] = result;
if (question.onAnswered) {
question.onAnswered(result);
}
return;
}
});
resolve(answers);
});
try {
return await promptResult;
} catch (err) {
if (err instanceof EscapeException || err instanceof TypeError) {
window.showErrorMessage(err.message);
}
return undefined;
}
}
// Helper to make it possible to prompt using callback pattern. Generally Promise is a preferred flow
public promptCallback(questions: IQuestion[], callback: IPromptCallback): void {
// Collapse multiple questions into a set of prompt steps
this.prompt(questions).then(answers => {
if (callback && answers) {
callback(answers);
}
});
}
}

View File

@@ -0,0 +1,52 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import { window } from 'vscode';
import Prompt from './prompt';
import EscapeException from './escapeException';
const figures = require('figures');
export default class CheckboxPrompt extends Prompt {
constructor(question: any, ignoreFocusOut?: boolean) {
super(question, ignoreFocusOut);
}
public render(): any {
let choices = this._question.choices.reduce((result, choice) => {
let choiceName = choice.name || choice;
result[`${choice.checked === true ? figures.radioOn : figures.radioOff} ${choiceName}`] = choice;
return result;
}, {});
let options = this.defaultQuickPickOptions;
options.placeHolder = this._question.message;
let quickPickOptions = Object.keys(choices);
quickPickOptions.push(figures.tick);
return window.showQuickPick(quickPickOptions, options)
.then(result => {
if (result === undefined) {
throw new EscapeException();
}
if (result !== figures.tick) {
choices[result].checked = !choices[result].checked;
return this.render();
}
return this._question.choices.reduce((result2, choice) => {
if (choice.checked === true) {
result2.push(choice.value);
}
return result2;
}, []);
});
}
}

View File

@@ -0,0 +1,36 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { window } from 'vscode';
import Prompt from './prompt';
import EscapeException from './escapeException';
export default class ConfirmPrompt extends Prompt {
constructor(question: any, ignoreFocusOut?: boolean) {
super(question, ignoreFocusOut);
}
public render(): any {
let choices: { [id: string]: boolean } = {};
choices[localize('msgYes', 'Yes')] = true;
choices[localize('msgNo', 'No')] = false;
let options = this.defaultQuickPickOptions;
options.placeHolder = this._question.message;
return window.showQuickPick(Object.keys(choices), options)
.then(result => {
if (result === undefined) {
throw new EscapeException();
}
return choices[result] || false;
});
}
}

View File

@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export default class EscapeException extends Error {
}

View File

@@ -0,0 +1,78 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import vscode = require('vscode');
import Prompt from './prompt';
import EscapeException from './escapeException';
import { INameValueChoice } from './question';
const figures = require('figures');
export default class ExpandPrompt extends Prompt {
constructor(question: any, ignoreFocusOut?: boolean) {
super(question, ignoreFocusOut);
}
public render(): any {
// label indicates this is a quickpick item. Otherwise it's a name-value pair
if (this._question.choices[0].label) {
return this.renderQuickPick(this._question.choices);
} else {
return this.renderNameValueChoice(this._question.choices);
}
}
private renderQuickPick(choices: vscode.QuickPickItem[]): any {
let options = this.defaultQuickPickOptions;
options.placeHolder = this._question.message;
return vscode.window.showQuickPick(choices, options)
.then(result => {
if (result === undefined) {
throw new EscapeException();
}
return this.validateAndReturn(result || false);
});
}
private renderNameValueChoice(choices: INameValueChoice[]): any {
const choiceMap = this._question.choices.reduce((result, choice) => {
result[choice.name] = choice.value;
return result;
}, {});
let options = this.defaultQuickPickOptions;
options.placeHolder = this._question.message;
return vscode.window.showQuickPick(Object.keys(choiceMap), options)
.then(result => {
if (result === undefined) {
throw new EscapeException();
}
// Note: cannot be used with 0 or false responses
let returnVal = choiceMap[result] || false;
return this.validateAndReturn(returnVal);
});
}
private validateAndReturn(value: any): any {
if (!this.validate(value)) {
return this.render();
}
return value;
}
private validate(value: any): boolean {
const validationError = this._question.validate ? this._question.validate(value || '') : undefined;
if (validationError) {
this._question.message = `${figures.warning} ${validationError}`;
return false;
}
return true;
}
}

View File

@@ -0,0 +1,35 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import Prompt from './prompt';
import InputPrompt from './input';
import PasswordPrompt from './password';
import ListPrompt from './list';
import ConfirmPrompt from './confirm';
import CheckboxPrompt from './checkbox';
import ExpandPrompt from './expand';
export default class PromptFactory {
public static createPrompt(question: any, ignoreFocusOut?: boolean): Prompt {
switch (question.type || 'input') {
case 'string':
case 'input':
return new InputPrompt(question, ignoreFocusOut);
case 'password':
return new PasswordPrompt(question, ignoreFocusOut);
case 'list':
return new ListPrompt(question, ignoreFocusOut);
case 'confirm':
return new ConfirmPrompt(question, ignoreFocusOut);
case 'checkbox':
return new CheckboxPrompt(question, ignoreFocusOut);
case 'expand':
return new ExpandPrompt(question, ignoreFocusOut);
default:
throw new Error(`Could not find a prompt for question type ${question.type}`);
}
}
}

View File

@@ -0,0 +1,59 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import { window, InputBoxOptions } from 'vscode';
import Prompt from './prompt';
import EscapeException from './escapeException';
const figures = require('figures');
export default class InputPrompt extends Prompt {
protected _options: InputBoxOptions;
constructor(question: any, ignoreFocusOut?: boolean) {
super(question, ignoreFocusOut);
this._options = this.defaultInputBoxOptions;
this._options.prompt = this._question.message;
}
// Helper for callers to know the right type to get from the type factory
public static get promptType(): string { return 'input'; }
public render(): any {
// Prefer default over the placeHolder, if specified
let placeHolder = this._question.default ? this._question.default : this._question.placeHolder;
if (this._question.default instanceof Error) {
placeHolder = this._question.default.message;
this._question.default = undefined;
}
this._options.placeHolder = placeHolder;
return window.showInputBox(this._options)
.then(result => {
if (result === undefined) {
throw new EscapeException();
}
if (result === '') {
// Use the default value, if defined
result = this._question.default || '';
}
const validationError = this._question.validate ? this._question.validate(result || '') : undefined;
if (validationError) {
this._question.default = new Error(`${figures.warning} ${validationError}`);
return this.render();
}
return result;
});
}
}

View File

@@ -0,0 +1,33 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import { window } from 'vscode';
import Prompt from './prompt';
import EscapeException from './escapeException';
export default class ListPrompt extends Prompt {
constructor(question: any, ignoreFocusOut?: boolean) {
super(question, ignoreFocusOut);
}
public render(): any {
const choices = this._question.choices.reduce((result, choice) => {
result[choice.name] = choice.value;
return result;
}, {});
let options = this.defaultQuickPickOptions;
options.placeHolder = this._question.message;
return window.showQuickPick(Object.keys(choices), options)
.then(result => {
if (result === undefined) {
throw new EscapeException();
}
return choices[result];
});
}
}

View File

@@ -0,0 +1,15 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import InputPrompt from './input';
export default class PasswordPrompt extends InputPrompt {
constructor(question: any, ignoreFocusOut?: boolean) {
super(question, ignoreFocusOut);
this._options.password = true;
}
}

View File

@@ -0,0 +1,70 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import { window, StatusBarItem, StatusBarAlignment } from 'vscode';
export default class ProgressIndicator {
private _statusBarItem: StatusBarItem;
constructor() {
this._statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left);
}
private _tasks: string[] = [];
public beginTask(task: string): void {
this._tasks.push(task);
this.displayProgressIndicator();
}
public endTask(task: string): void {
if (this._tasks.length > 0) {
this._tasks.pop();
}
this.setMessage();
}
private setMessage(): void {
if (this._tasks.length === 0) {
this._statusBarItem.text = '';
this.hideProgressIndicator();
return;
}
this._statusBarItem.text = this._tasks[this._tasks.length - 1];
this._statusBarItem.show();
}
private _interval: any;
private displayProgressIndicator(): void {
this.setMessage();
this.hideProgressIndicator();
this._interval = setInterval(() => this.onDisplayProgressIndicator(), 100);
}
private hideProgressIndicator(): void {
if (this._interval) {
clearInterval(this._interval);
this._interval = undefined;
}
this.ProgressCounter = 0;
}
private ProgressText = ['|', '/', '-', '\\', '|', '/', '-', '\\'];
private ProgressCounter = 0;
private onDisplayProgressIndicator(): void {
if (this._tasks.length === 0) {
return;
}
let txt = this.ProgressText[this.ProgressCounter];
this._statusBarItem.text = this._tasks[this._tasks.length - 1] + ' ' + txt;
this.ProgressCounter++;
if (this.ProgressCounter >= this.ProgressText.length - 1) {
this.ProgressCounter = 0;
}
}
}

View File

@@ -0,0 +1,33 @@
'use strict';
// This code is originally from https://github.com/DonJayamanne/bowerVSCode
// License: https://github.com/DonJayamanne/bowerVSCode/blob/master/LICENSE
import { InputBoxOptions, QuickPickOptions } from 'vscode';
abstract class Prompt {
protected _question: any;
protected _ignoreFocusOut?: boolean;
constructor(question: any, ignoreFocusOut?: boolean) {
this._question = question;
this._ignoreFocusOut = ignoreFocusOut ? ignoreFocusOut : false;
}
public abstract render(): any;
protected get defaultQuickPickOptions(): QuickPickOptions {
return {
ignoreFocusOut: this._ignoreFocusOut
};
}
protected get defaultInputBoxOptions(): InputBoxOptions {
return {
ignoreFocusOut: this._ignoreFocusOut
};
}
}
export default Prompt;

View File

@@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// TODO: Common file with mssql
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 type Answers<T> = { [key: string]: T };
export interface IPrompter {
promptSingle<T>(question: IQuestion, ignoreFocusOut?: boolean): Promise<T | undefined>;
/**
* Prompts for multiple questions
*
* @returns Map of question IDs to results, or undefined if
* the user canceled the question session
*/
prompt<T>(questions: IQuestion[], ignoreFocusOut?: boolean): Promise<Answers<T> | undefined>;
promptCallback(questions: IQuestion[], callback: IPromptCallback): void;
}
export interface IPromptCallback {
(answers: { [id: string]: any }): void;
}

View File

@@ -0,0 +1,165 @@
/*---------------------------------------------------------------------------------------------
* 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 TelemetryReporter from 'vscode-extension-telemetry';
import { ErrorAction, ErrorHandler, Message, CloseAction } from 'vscode-languageclient';
import * as Utils from './utils';
import * as Constants from './constants';
import { localize } from './localize';
const packageJson = require('../package.json');
const viewKnownIssuesAction = localize('viewKnownIssuesText', "View Known Issues");
export interface ITelemetryEventProperties {
[key: string]: string;
}
export interface ITelemetryEventMeasures {
[key: string]: number;
}
/**
* Filters error paths to only include source files. Exported to support testing
*/
export function FilterErrorPath(line: string): string | undefined {
if (line) {
let values: string[] = line.split('/out/');
if (values.length <= 1) {
// Didn't match expected format
return line;
} else {
return values[1];
}
}
return undefined;
}
export class Telemetry {
private static reporter: TelemetryReporter;
private static disabled: boolean;
/**
* Disable telemetry reporting
*/
public static disable(): void {
this.disabled = true;
}
/**
* Initialize the telemetry reporter for use.
*/
public static initialize(): void {
if (typeof this.reporter === 'undefined') {
// Check if the user has opted out of telemetry
if (!vscode.workspace.getConfiguration('telemetry').get<boolean>('enableTelemetry', true)) {
this.disable();
return;
}
let packageInfo = Utils.getPackageInfo(packageJson)!;
this.reporter = new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
}
}
/**
* Send a telemetry event for an exception
*/
public static sendTelemetryEventForException(
err: any, methodName: string, extensionConfigName: string): void {
let stackArray: string[];
let firstLine: string = '';
if (err !== undefined && err.stack !== undefined) {
stackArray = err.stack.split('\n');
if (stackArray !== undefined && stackArray.length >= 2) {
firstLine = stackArray[1]; // The fist line is the error message and we don't want to send that telemetry event
firstLine = FilterErrorPath(firstLine)!;
}
}
// Only adding the method name and the fist line of the stack trace. We don't add the error message because it might have PII
this.sendTelemetryEvent('Exception', { methodName: methodName, errorLine: firstLine });
}
/**
* Send a telemetry event using application insights
*/
public static sendTelemetryEvent(
eventName: string,
properties?: ITelemetryEventProperties,
measures?: ITelemetryEventMeasures): void {
if (typeof this.disabled === 'undefined') {
this.disabled = false;
}
if (this.disabled || typeof (this.reporter) === 'undefined') {
// Don't do anything if telemetry is disabled
return;
}
if (!properties || typeof properties === 'undefined') {
properties = {};
}
try {
this.reporter.sendTelemetryEvent(eventName, properties, measures);
} catch (telemetryErr) {
// If sending telemetry event fails ignore it so it won't break the extension
console.error('Failed to send telemetry event. error: ' + telemetryErr);
}
}
}
/**
* Handle Language Service client errors
*/
export class LanguageClientErrorHandler implements ErrorHandler {
/**
* Show an error message prompt with a link to known issues wiki page
* @memberOf LanguageClientErrorHandler
*/
showOnErrorPrompt(): void {
Telemetry.sendTelemetryEvent(Constants.serviceName + 'Crash');
vscode.window.showErrorMessage(
localize('serviceCrashMessage', "{0} component exited unexpectedly. Please restart Azure Data Studio.", Constants.serviceName),
viewKnownIssuesAction).then(action => {
if (action && action === viewKnownIssuesAction) {
vscode.env.openExternal(vscode.Uri.parse(Constants.serviceCrashLink));
}
});
}
/**
* Callback for language service client error
*
* @memberOf LanguageClientErrorHandler
*/
error(error: Error, message: Message, count: number): ErrorAction {
this.showOnErrorPrompt();
// we don't retry running the service since crashes leave the extension
// in a bad, unrecovered state
return ErrorAction.Shutdown;
}
/**
* Callback for language service client closed
*
* @memberOf LanguageClientErrorHandler
*/
closed(): CloseAction {
this.showOnErrorPrompt();
// we don't retry running the service since crashes leave the extension
// in a bad, unrecovered state
return CloseAction.DoNotRestart;
}
}
Telemetry.initialize();

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
/**
* @returns whether the provided parameter is undefined or null.
*/
export function isUndefinedOrNull(obj: any): boolean {
// Intentional ==
// eslint-disable-next-line eqeqeq
return obj == undefined;
}

View File

@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/sql/azdata.d.ts'/>
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
/// <reference path='../../../../src/vs/vscode.d.ts'/>

View File

@@ -0,0 +1,141 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//TODO: This is the same file from mssql. Move this into a common place.
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as path from 'path';
import * as os from 'os';
import * as findRemoveSync from 'find-remove';
import { promises as fs } from 'fs';
const configTracingLevel = 'tracingLevel';
const configLogRetentionMinutes = 'logRetentionMinutes';
const configLogFilesRemovalLimit = 'logFilesRemovalLimit';
const extensionConfigSectionName = 'kusto';
export function removeOldLogFiles(logPath: string, prefix: string): JSON {
return findRemoveSync(logPath, { age: { seconds: getConfigLogRetentionSeconds() }, limit: getConfigLogFilesRemovalLimit() });
}
export function getConfiguration(config: string = extensionConfigSectionName): vscode.WorkspaceConfiguration {
return vscode.workspace.getConfiguration(extensionConfigSectionName);
}
export function getConfigLogFilesRemovalLimit(): number | undefined {
let config = getConfiguration();
if (config && config[configLogFilesRemovalLimit]) {
return Number((config[configLogFilesRemovalLimit]).toFixed(0));
}
else {
return undefined;
}
}
export function getConfigLogRetentionSeconds(): number | undefined {
let config = getConfiguration();
if (config && config[configLogRetentionMinutes]) {
return Number((config[configLogRetentionMinutes] * 60).toFixed(0));
}
else {
return undefined;
}
}
export function getConfigTracingLevel(): string | undefined {
let config = getConfiguration();
if (config) {
return config[configTracingLevel];
}
else {
return undefined;
}
}
export function getLogFileName(prefix: string, pid: number): string {
return `${prefix}_${pid}.log`;
}
export function getCommonLaunchArgsAndCleanupOldLogFiles(logPath: string, fileName: string, executablePath: string): string[] {
let launchArgs: string[] = [];
launchArgs.push('--log-file');
let logFile = path.join(logPath, fileName);
launchArgs.push(logFile);
console.log(`logFile for ${path.basename(executablePath)} is ${logFile}`);
console.log(`This process (ui Extenstion Host) is pid: ${process.pid}`);
// Delete old log files
let deletedLogFiles = removeOldLogFiles(logPath, fileName);
console.log(`Old log files deletion report: ${JSON.stringify(deletedLogFiles)}`);
let config = getConfigTracingLevel();
if (config) {
launchArgs.push('--tracing-level');
launchArgs.push(config);
}
return launchArgs;
}
export function ensure(target: object, key: string): any {
if (target[key] === void 0) {
target[key] = {} as any;
}
return target[key];
}
export interface IPackageInfo {
name: string;
version: string;
aiKey: string;
}
export function getPackageInfo(packageJson: IPackageInfo): IPackageInfo | undefined {
if (packageJson) {
return {
name: packageJson.name,
version: packageJson.version,
aiKey: packageJson.aiKey
};
}
return undefined;
}
export function verifyPlatform(): Thenable<boolean> {
if (os.platform() === 'darwin' && parseFloat(os.release()) < 16) {
return Promise.resolve(false);
} else {
return Promise.resolve(true);
}
}
export function getErrorMessage(error: Error | any, removeHeader: boolean = false): string {
let errorMessage: string = (error instanceof Error) ? error.message : error.toString();
if (removeHeader) {
errorMessage = removeErrorHeader(errorMessage);
}
return errorMessage;
}
export function removeErrorHeader(errorMessage: string): string {
if (errorMessage && errorMessage !== '') {
let header: string = 'Error:';
if (errorMessage.startsWith(header)) {
errorMessage = errorMessage.substring(header.length);
}
}
return errorMessage;
}
export function isObjectExplorerContext(object: any): object is azdata.ObjectExplorerContext {
return 'connectionProfile' in object && 'isConnectionNode' in object;
}
export async function exists(path: string): Promise<boolean> {
try {
await fs.access(path);
return true;
} catch (e) {
return false;
}
}