From 48b7d96999acbeefee3aaff959d623814bac4d2b Mon Sep 17 00:00:00 2001 From: Justin M <63619224+JustinMDotNet@users.noreply.github.com> Date: Tue, 6 Jul 2021 15:27:19 -0700 Subject: [PATCH] Add Azure Monitor Extension (#15397) * Added Azure Log Analytics resource for generating AAD Token. * Fixed AzureResource * Removed debug code from connectionManagementService * Moved AzureLogAnalytics from AzureResource enum in azdata.d.ts to azdata.proposed.d.ts. Added azureLogAnalyticsResource to all azureSettings in providerSettings.ts * Updated endpoint for generating AAD Token for LogAnalytics for UsGov, UsNat, and China * Initial Commit of Azure Monitor Extension * Added extension name to azuremonitor package strings * Removed azureMonitor resource from germanyCloud in providerSettings * Added logic to exclude menuItems in object explorer for LogAnalytics * Changed exe from AzureMonitor to Kusto * Added if clause for queryName for new queries * Changed queryWindow name from KustoQuery to KQLQuery for Kusto and LogAnalytics. * Added LogAnalytics for setTaskBarContent * Added serialization and telemetry feature classes to AzureMonitor. Added references for azdata and vscode. * Added azure monitor light and dark icons * Added config for Dashboard in package.json * Added workspace information to dashboard * Added language support for LogAnalytics * Added Notebook support * Added Hide flag to package.json for databaseName * Changed providerId from LogAnalytics to LOGANALYTICS * Changed Workspace to Workspace ID in package.nls.json * Added support for Azure Widget browser * Changed fullName to use workspaceId when connecting * Changed providerId from alertsManagement to azureMonitor * Added .gitignore and *.vsix to vscodeignore. * Removed unused devDependencies * Code Review Feedback * Changed tsconfig.json to match Kusto and Sql * Changed package.json to match kusto package. * Changed tsconfig to validate unused params and implictAny. Changed existing code to satisfy build. * Fixed tsconfig to use the correct base class. * Added objectExplorerNodeProvider and all related classes. * Removed unused tmLanguage file * Added logic to to download extension from toolservice * Fixed launchArgs. Removed commented code from extension.ts. Changed config.json to use net5.0 * Added displayName to package.nls.json. Removed hide flag from databaseName. Other code review feedback. * Added readme info to AzureMonitor * Removed unused client-error-handler and ui-references files. Combined outputChannel in azuremonitorServer. Removed TODO from contextProvider. Renamed function in extension.ts. Removed unneeded 'use strict' from cancelableStream.ts. Removed second outputChannel from objectExplorerNodeProvider. * Removed unused files --- build/lib/extensions.ts | 1 + extensions/azurecore/package.json | 5 + .../resources/dark/azure_monitor_dark.svg | 10 + .../resources/light/azure_monitor_light.svg | 10 + .../src/azureResource/azure-resource.d.ts | 3 +- .../azurecore/src/azureResource/constants.ts | 4 +- .../azuremonitor/azuremonitorProvider.ts | 26 + .../azuremonitor/azuremonitorService.ts | 41 ++ .../azuremonitorTreeDataProvider.ts | 77 +++ extensions/azurecore/src/extension.ts | 3 + extensions/azuremonitor/.gitignore | 1 + extensions/azuremonitor/.vscodeignore | 8 + extensions/azuremonitor/README.md | 41 ++ extensions/azuremonitor/config.json | 29 ++ .../azuremonitor/extension.webpack.config.js | 17 + .../azuremonitor/language-configuration.json | 73 +++ extensions/azuremonitor/package.json | 212 +++++++++ extensions/azuremonitor/package.nls.json | 31 ++ .../resources/dark/azure_monitor_dark.svg | 10 + .../resources/light/azure_monitor_light.svg | 10 + extensions/azuremonitor/src/appContext.ts | 34 ++ extensions/azuremonitor/src/azuremonitor.d.ts | 38 ++ .../src/azuremonitorApiFactory.ts | 23 + .../azuremonitor/src/azuremonitorServer.ts | 167 +++++++ extensions/azuremonitor/src/constants.ts | 25 + .../azuremonitor/src/contextProvider.ts | 76 +++ extensions/azuremonitor/src/extension.ts | 137 ++++++ .../src/features/accountFeature.ts | 65 +++ .../azuremonitor/src/features/contracts.ts | 81 ++++ .../src/features/serializationFeature.ts | 68 +++ .../src/features/telemetryFeature.ts | 25 + extensions/azuremonitor/src/iconProvider.ts | 21 + extensions/azuremonitor/src/index.ts | 8 + .../cancelableStream.ts | 25 + .../src/objectExplorerNodeProvider/command.ts | 187 ++++++++ .../objectExplorerNodeProvider/connection.ts | 82 ++++ .../objectExplorerNodeProvider.ts | 219 +++++++++ .../providerBase.ts | 13 + .../objectExplorerNodeProvider/treeNodes.ts | 87 ++++ .../src/objectExplorerNodeProvider/types.d.ts | 17 + .../azuremonitor/src/prompts/adapter.ts | 77 +++ .../azuremonitor/src/prompts/checkbox.ts | 52 ++ .../azuremonitor/src/prompts/confirm.ts | 36 ++ .../src/prompts/escapeException.ts | 8 + extensions/azuremonitor/src/prompts/expand.ts | 78 +++ .../azuremonitor/src/prompts/factory.ts | 35 ++ extensions/azuremonitor/src/prompts/input.ts | 59 +++ extensions/azuremonitor/src/prompts/list.ts | 33 ++ .../azuremonitor/src/prompts/password.ts | 15 + .../src/prompts/progressIndicator.ts | 70 +++ extensions/azuremonitor/src/prompts/prompt.ts | 33 ++ .../azuremonitor/src/prompts/question.ts | 73 +++ extensions/azuremonitor/src/telemetry.ts | 167 +++++++ extensions/azuremonitor/src/types.ts | 14 + .../azuremonitor/src/typings/findRemove.d.ts | 17 + extensions/azuremonitor/src/typings/refs.d.ts | 8 + extensions/azuremonitor/src/utils.ts | 147 ++++++ .../syntaxes/azuremonitor.tmLanguage | 449 ++++++++++++++++++ extensions/azuremonitor/tsconfig.json | 12 + extensions/azuremonitor/yarn.lock | 370 +++++++++++++++ .../browser/services/breadcrumb.service.ts | 4 +- .../contrib/query/browser/queryEditor.ts | 14 + .../query/browser/queryInputFactory.ts | 2 +- .../browser/scripting.contribution.ts | 11 +- .../queryEditor/browser/queryEditorService.ts | 4 +- 65 files changed, 3788 insertions(+), 10 deletions(-) create mode 100644 extensions/azurecore/resources/dark/azure_monitor_dark.svg create mode 100644 extensions/azurecore/resources/light/azure_monitor_light.svg create mode 100644 extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorProvider.ts create mode 100644 extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorService.ts create mode 100644 extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorTreeDataProvider.ts create mode 100644 extensions/azuremonitor/.gitignore create mode 100644 extensions/azuremonitor/.vscodeignore create mode 100644 extensions/azuremonitor/README.md create mode 100644 extensions/azuremonitor/config.json create mode 100644 extensions/azuremonitor/extension.webpack.config.js create mode 100644 extensions/azuremonitor/language-configuration.json create mode 100644 extensions/azuremonitor/package.json create mode 100644 extensions/azuremonitor/package.nls.json create mode 100644 extensions/azuremonitor/resources/dark/azure_monitor_dark.svg create mode 100644 extensions/azuremonitor/resources/light/azure_monitor_light.svg create mode 100644 extensions/azuremonitor/src/appContext.ts create mode 100644 extensions/azuremonitor/src/azuremonitor.d.ts create mode 100644 extensions/azuremonitor/src/azuremonitorApiFactory.ts create mode 100644 extensions/azuremonitor/src/azuremonitorServer.ts create mode 100644 extensions/azuremonitor/src/constants.ts create mode 100644 extensions/azuremonitor/src/contextProvider.ts create mode 100644 extensions/azuremonitor/src/extension.ts create mode 100644 extensions/azuremonitor/src/features/accountFeature.ts create mode 100644 extensions/azuremonitor/src/features/contracts.ts create mode 100644 extensions/azuremonitor/src/features/serializationFeature.ts create mode 100644 extensions/azuremonitor/src/features/telemetryFeature.ts create mode 100644 extensions/azuremonitor/src/iconProvider.ts create mode 100644 extensions/azuremonitor/src/index.ts create mode 100644 extensions/azuremonitor/src/objectExplorerNodeProvider/cancelableStream.ts create mode 100644 extensions/azuremonitor/src/objectExplorerNodeProvider/command.ts create mode 100644 extensions/azuremonitor/src/objectExplorerNodeProvider/connection.ts create mode 100644 extensions/azuremonitor/src/objectExplorerNodeProvider/objectExplorerNodeProvider.ts create mode 100644 extensions/azuremonitor/src/objectExplorerNodeProvider/providerBase.ts create mode 100644 extensions/azuremonitor/src/objectExplorerNodeProvider/treeNodes.ts create mode 100644 extensions/azuremonitor/src/objectExplorerNodeProvider/types.d.ts create mode 100644 extensions/azuremonitor/src/prompts/adapter.ts create mode 100644 extensions/azuremonitor/src/prompts/checkbox.ts create mode 100644 extensions/azuremonitor/src/prompts/confirm.ts create mode 100644 extensions/azuremonitor/src/prompts/escapeException.ts create mode 100644 extensions/azuremonitor/src/prompts/expand.ts create mode 100644 extensions/azuremonitor/src/prompts/factory.ts create mode 100644 extensions/azuremonitor/src/prompts/input.ts create mode 100644 extensions/azuremonitor/src/prompts/list.ts create mode 100644 extensions/azuremonitor/src/prompts/password.ts create mode 100644 extensions/azuremonitor/src/prompts/progressIndicator.ts create mode 100644 extensions/azuremonitor/src/prompts/prompt.ts create mode 100644 extensions/azuremonitor/src/prompts/question.ts create mode 100644 extensions/azuremonitor/src/telemetry.ts create mode 100644 extensions/azuremonitor/src/types.ts create mode 100644 extensions/azuremonitor/src/typings/findRemove.d.ts create mode 100644 extensions/azuremonitor/src/typings/refs.d.ts create mode 100644 extensions/azuremonitor/src/utils.ts create mode 100644 extensions/azuremonitor/syntaxes/azuremonitor.tmLanguage create mode 100644 extensions/azuremonitor/tsconfig.json create mode 100644 extensions/azuremonitor/yarn.lock diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index a8ac5f40e0..cb10797cd3 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -251,6 +251,7 @@ const externalExtensions = [ 'asde-deployment', 'azdata', 'azurehybridtoolkit', + 'azuremonitor', 'cms', 'dacpac', 'import', diff --git a/extensions/azurecore/package.json b/extensions/azurecore/package.json index 2d9024d568..7f5b327b5b 100644 --- a/extensions/azurecore/package.json +++ b/extensions/azurecore/package.json @@ -283,6 +283,11 @@ "when": "viewItem == azure.resource.itemType.azureDataExplorer", "group": "inline" }, + { + "command": "azure.resource.connectsqlserver", + "when": "viewItem == azure.resource.itemType.azureMonitor", + "group": "inline" + }, { "command": "azure.resource.startterminal", "when": "viewItem == azure.resource.itemType.account", diff --git a/extensions/azurecore/resources/dark/azure_monitor_dark.svg b/extensions/azurecore/resources/dark/azure_monitor_dark.svg new file mode 100644 index 0000000000..7b5ef94c5b --- /dev/null +++ b/extensions/azurecore/resources/dark/azure_monitor_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/azurecore/resources/light/azure_monitor_light.svg b/extensions/azurecore/resources/light/azure_monitor_light.svg new file mode 100644 index 0000000000..b390b521cf --- /dev/null +++ b/extensions/azurecore/resources/light/azure_monitor_light.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/azurecore/src/azureResource/azure-resource.d.ts b/extensions/azurecore/src/azureResource/azure-resource.d.ts index 5c4d766a73..89ce017dda 100644 --- a/extensions/azurecore/src/azureResource/azure-resource.d.ts +++ b/extensions/azurecore/src/azureResource/azure-resource.d.ts @@ -24,6 +24,7 @@ declare module 'azureResource' { postgresServer = 'microsoft.dbforpostgresql/servers', azureArcService = 'microsoft.azuredata/datacontrollers', storageAccount = 'microsoft.storage/storageaccounts', + logAnalytics = 'microsoft.operationalinsights/workspaces' } export interface IAzureResourceProvider extends DataProvider { @@ -121,7 +122,7 @@ declare module 'azureResource' { proxyOverride: string, vCores: number, dnsZone: string, - } + } } diff --git a/extensions/azurecore/src/azureResource/constants.ts b/extensions/azurecore/src/azureResource/constants.ts index 17b1070200..660918fc3c 100644 --- a/extensions/azurecore/src/azureResource/constants.ts +++ b/extensions/azurecore/src/azureResource/constants.ts @@ -13,7 +13,9 @@ export enum AzureResourceItemType { azureDataExplorerContainer = 'azure.resource.itemType.azureDataExplorerContainer', azureDataExplorer = 'azure.resource.itemType.azureDataExplorer', sqlInstance = 'azure.resource.itemType.sqlInstance', - message = 'azure.resource.itemType.message' + message = 'azure.resource.itemType.message', + azureMonitor = 'azure.resource.itemType.azureMonitor', + azureMonitorContainer = 'azure.resource.itemType.azureMonitorContainer', } export enum AzureResourceServiceNames { diff --git a/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorProvider.ts b/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorProvider.ts new file mode 100644 index 0000000000..60c1af45d3 --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorProvider.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtensionContext } from 'vscode'; + +import { azureResource } from 'azureResource'; +import { IAzureResourceService } from '../../interfaces'; +import { AzureMonitorTreeDataProvider as AzureMonitorTreeDataProvider } from './azuremonitorTreeDataProvider'; + +export class AzureMonitorProvider implements azureResource.IAzureResourceProvider { + public constructor( + private _service: IAzureResourceService, + private _extensionContext: ExtensionContext + ) { + } + + public getTreeDataProvider(): azureResource.IAzureResourceTreeDataProvider { + return new AzureMonitorTreeDataProvider(this._service, this._extensionContext); + } + + public get providerId(): string { + return 'azure.resource.providers.azureMonitor'; + } +} diff --git a/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorService.ts b/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorService.ts new file mode 100644 index 0000000000..0151933c75 --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorService.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { azureResource } from 'azureResource'; +import { ResourceServiceBase, GraphData } from '../resourceTreeDataProviderBase'; + +export interface AzureMonitorGraphData extends GraphData { + properties: { + fullyQualifiedDomainName: string; + administratorLogin: string; + uri: string; + customerId: string + }; +} + +const instanceQuery = `where type == "${azureResource.AzureResourceType.logAnalytics}"`; + +export class AzureMonitorResourceService extends ResourceServiceBase { + + protected get query(): string { + return instanceQuery; + } + + protected convertResource(resource: AzureMonitorGraphData): azureResource.AzureResourceDatabaseServer { + return { + id: resource.id, + name: resource.name, + fullName: resource.properties.customerId, + loginName: '', + defaultDatabaseName: '', + subscription: { + id: resource.subscriptionId, + name: resource.subscriptionName + }, + tenant: resource.tenantId, + resourceGroup: resource.resourceGroup + }; + } +} diff --git a/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorTreeDataProvider.ts b/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorTreeDataProvider.ts new file mode 100644 index 0000000000..02827a5f67 --- /dev/null +++ b/extensions/azurecore/src/azureResource/providers/azuremonitor/azuremonitorTreeDataProvider.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtensionNodeType, TreeItem, Account } from 'azdata'; +import { TreeItemCollapsibleState, ExtensionContext } from 'vscode'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +import { AzureResourceItemType } from '../../constants'; +import { generateGuid } from '../../utils'; +import { IAzureResourceService } from '../../interfaces'; +import { ResourceTreeDataProviderBase } from '../resourceTreeDataProviderBase'; +import { azureResource } from 'azureResource'; + +export class AzureMonitorTreeDataProvider extends ResourceTreeDataProviderBase { + private static readonly containerId = 'azure.resource.providers.AzureMonitorContainer'; + private static readonly containerLabel = localize('azure.resource.providers.AzureMonitorContainerLabel', "Azure Monitor Workspace"); + + public constructor( + databaseServerService: IAzureResourceService, + private _extensionContext: ExtensionContext + ) { + super(databaseServerService); + } + + + protected getTreeItemForResource(databaseServer: azureResource.AzureResourceDatabaseServer, account: Account): TreeItem { + return { + id: `LogAnalytics_${databaseServer.id ? databaseServer.id : databaseServer.name}`, + label: this.browseConnectionMode ? `${databaseServer.name} (${AzureMonitorTreeDataProvider.containerLabel}, ${databaseServer.subscription.name})` : databaseServer.name, + iconPath: { + dark: this._extensionContext.asAbsolutePath('resources/dark/azure_monitor_dark.svg'), + light: this._extensionContext.asAbsolutePath('resources/light/azure_monitor_light.svg') + }, + collapsibleState: this.browseConnectionMode ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed, + contextValue: AzureResourceItemType.azureMonitor, + payload: { + id: generateGuid(), + connectionName: undefined, + serverName: databaseServer.fullName, + databaseName: databaseServer.defaultDatabaseName, + userName: databaseServer.loginName, + password: '', + authenticationType: 'AzureMFA', + savePassword: true, + groupFullName: '', + groupId: '', + providerName: 'LOGANALYTICS', + saveProfile: false, + options: {}, + azureAccount: account.key.accountId + }, + childProvider: 'LOGANALYTICS', + type: ExtensionNodeType.Server + }; + } + + protected createContainerNode(): azureResource.IAzureResourceNode { + return { + account: undefined, + subscription: undefined, + tenantId: undefined, + treeItem: { + id: AzureMonitorTreeDataProvider.containerId, + label: AzureMonitorTreeDataProvider.containerLabel, + iconPath: { + dark: this._extensionContext.asAbsolutePath('resources/dark/folder_inverse.svg'), + light: this._extensionContext.asAbsolutePath('resources/light/folder.svg') + }, + collapsibleState: TreeItemCollapsibleState.Collapsed, + contextValue: AzureResourceItemType.databaseServerContainer + } + }; + } +} diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts index c03447998b..4799b04b77 100644 --- a/extensions/azurecore/src/extension.ts +++ b/extensions/azurecore/src/extension.ts @@ -29,6 +29,8 @@ import { SqlInstanceResourceService } from './azureResource/providers/sqlinstanc import { SqlInstanceProvider } from './azureResource/providers/sqlinstance/sqlInstanceProvider'; import { KustoResourceService } from './azureResource/providers/kusto/kustoService'; import { KustoProvider } from './azureResource/providers/kusto/kustoProvider'; +import { AzureMonitorResourceService } from './azureResource/providers/azuremonitor/azuremonitorService'; +import { AzureMonitorProvider } from './azureResource/providers/azuremonitor/azuremonitorProvider'; import { PostgresServerProvider } from './azureResource/providers/postgresServer/postgresServerProvider'; import { PostgresServerService } from './azureResource/providers/postgresServer/postgresServerService'; import { AzureTerminalService } from './azureResource/services/terminalService'; @@ -156,6 +158,7 @@ export async function activate(context: vscode.ExtensionContext): Promise=1.31.0" + }, + "main": "./out/extension", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/azuredatastudio.git" + }, + "typings": "./src/azuremonitor", + "contributes": { + "connectionProvider": { + "providerId": "LOGANALYTICS", + "languageMode": "loganalytics", + "displayName": "%azuremonitor.displayName%", + "notebookKernelAlias": "LogAnalytics", + "azureResource": "AzureLogAnalytics", + "iconPath": [ + { + "id": "azuremonitor:cloud", + "path": { + "light": "resources/light/azure_monitor_light.svg", + "dark": "resources/dark/azure_monitor_dark.svg" + }, + "default": true + } + ], + "connectionOptions": [ + { + "specialValueType": "serverName", + "isIdentity": true, + "name": "server", + "displayName": "%azuremonitor.connectionProperties.serverName.displayName%", + "description": "%azuremonitor.connectionProperties.serverName.description%", + "groupName": "Source", + "valueType": "string", + "defaultValue": null, + "objectType": null, + "categoryValues": [], + "isRequired": true, + "isArray": false + }, + { + "specialValueType": "authType", + "isIdentity": true, + "name": "authenticationType", + "displayName": "%azuremonitor.connectionProperties.authType.displayName%", + "description": "%azuremonitor.connectionProperties.authType.description%", + "groupName": "Security", + "valueType": "category", + "defaultValue": "AzureMFA", + "objectType": null, + "categoryValues": [ + { + "displayName": "%azuremonitor.connectionProperties.authType.categoryValues.azureMFA%", + "name": "AzureMFA" + } + ], + "isRequired": true, + "isArray": false + }, + { + "specialValueType": "connectionName", + "isIdentity": true, + "name": "connectionName", + "displayName": "%azuremonitor.connectionProperties.connectionName.displayName%", + "description": "%azuremonitor.connectionProperties.connectionName.description%", + "groupName": "Source", + "valueType": "string", + "defaultValue": null, + "objectType": null, + "categoryValues": null, + "isRequired": false, + "isArray": false + }, + { + "specialValueType": "userName", + "isIdentity": true, + "name": "user", + "displayName": "%azuremonitor.connectionProperties.userName.displayName%", + "description": "%azuremonitor.connectionProperties.userName.description%", + "groupName": "Security", + "valueType": "string", + "defaultValue": null, + "objectType": null, + "categoryValues": null, + "isRequired": true, + "isArray": false + }, + { + "specialValueType": "password", + "isIdentity": true, + "name": "password", + "displayName": "%azuremonitor.connectionProperties.password.displayName%", + "description": "%azuremonitor.connectionProperties.password.description%", + "groupName": "Security", + "valueType": "password", + "defaultValue": null, + "objectType": null, + "categoryValues": null, + "isRequired": true, + "isArray": false + }, + { + "specialValueType": "databaseName", + "isIdentity": true, + "name": "database", + "displayName": "%azuremonitor.connectionOptions.databaseName.displayName%", + "description": "%azuremonitor.connectionOptions.databaseName.description%", + "groupName": "Source", + "valueType": "string", + "defaultValue": null, + "objectType": null, + "categoryValues": null, + "isRequired": false, + "isArray": false + } + ] + }, + "dashboard": { + "provider": "LOGANALYTICS", + "flavors": [ + { + "flavor": "cloud", + "conditions": [ + { + "field": "isCloud", + "operator": "==", + "value": true + } + ], + "databaseProperties": [ + { + "displayName": "%azuremonitor.cloud.workspaceProperties.name%", + "value": "name" + }, + { + "displayName": "%azuremonitor.cloud.workspaceProperties.id%", + "value": "id" + } + ], + "serverProperties": [], + "databasesListProperties": [ + { + "displayName": "%azuremonitor.databasesListProperties.name%", + "value": "name", + "widthWeight": 60 + }, + { + "displayName": "%azuremonitor.databasesListProperties.size%", + "value": "sizeInMB", + "widthWeight": 20 + } + ], + "objectsListProperties": [ + { + "displayName": "%azuremonitor.objectsListProperties.name%", + "value": "name", + "widthWeight": 60 + }, + { + "displayName": "%azuremonitor.objectsListProperties.metadataTypeName%", + "value": "metadataTypeName", + "widthWeight": 20 + } + ] + } + ] + }, + "languages": [ + { + "id": "loganalytics", + "aliases": [ + "LogAnalytics", + "loganalytics" + ], + "extensions": [ + ".loganalytics" + ], + "configuration": "./language-configuration.json" + } + ] + }, + "scripts": { + "compile": "gulp compile-extension:azuremonitor-client", + "update-grammar": "node ../../build/npm/update-grammar.js Microsoft/vscode-azuremonitor ./syntaxes/azuremonitor.tmLanguage" + }, + "dependencies": { + "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.2.2", + "figures": "^2.0.0", + "find-remove": "1.2.1", + "service-downloader": "0.2.1", + "vscode-extension-telemetry": "0.1.0", + "vscode-languageclient": "5.2.1", + "vscode-nls": "^4.0.0" + }, + "devDependencies": {}, + "__metadata": { + "id": "", + "publisherDisplayName": "Microsoft", + "publisherId": "Microsoft" + } +} diff --git a/extensions/azuremonitor/package.nls.json b/extensions/azuremonitor/package.nls.json new file mode 100644 index 0000000000..b1b8c0088b --- /dev/null +++ b/extensions/azuremonitor/package.nls.json @@ -0,0 +1,31 @@ +{ + "azuremonitor.description": "Azure Monitor access for Azure Data Studio", + "azuremonitor.displayName": "Azure Monitor Logs", + "azuremonitor.cloud.workspaceProperties.name": "Workspace Name", + "azuremonitor.cloud.workspaceProperties.id": "Workspace ID", + "azuremonitor.databasesListProperties.name": "Name", + "azuremonitor.databasesListProperties.size": "Size (MB)", + "azuremonitor.objectsListProperties.name": "Name", + "azuremonitor.objectsListProperties.metadataTypeName": "Type", + "azuremonitor.connectionProperties.connectionName.displayName": "Name (optional)", + "azuremonitor.connectionProperties.connectionName.description": "Custom name of the connection", + "azuremonitor.connectionProperties.userName.displayName": "User name", + "azuremonitor.connectionProperties.userName.description": "Indicates the user ID to be used when connecting to the data source", + "azuremonitor.connectionProperties.password.displayName": "Password", + "azuremonitor.connectionProperties.password.description": "Indicates the password to be used when connecting to the data source", + "azuremonitor.connectionProperties.dbaPrivilege.label": "Administrative privileges", + "azuremonitor.connectionProperties.dbaPrivilege.description": "Administrative privileges: SYSDBA or SYSOPER", + "azuremonitor.connectionProperties.metadataPooling.label": "Metadata Pooling", + "azuremonitor.connectionProperties.metadataPooling.description": "This attribute indicates whether the metadata information for executed queries is cached for improved performance.", + "azuremonitor.connectionProperties.proxyUserId.label": "Proxy User Id", + "azuremonitor.connectionProperties.proxyUserId.description": "User name of the proxy user.", + "azuremonitor.connectionProperties.proxyUserPassword.label": "Proxy Password", + "azuremonitor.connectionProperties.proxyUserPassword.description": "Password of the proxy user.", + "azuremonitor.connectionProperties.serverName.displayName": "Workspace ID", + "azuremonitor.connectionProperties.serverName.description": "Log Analytics Workspace ID", + "azuremonitor.connectionOptions.databaseName.displayName": "Database", + "azuremonitor.connectionOptions.databaseName.description": "The name of the initial catalog or database in the data source", + "azuremonitor.connectionProperties.authType.displayName": "Authentication type", + "azuremonitor.connectionProperties.authType.description": "Specifies the method of authenticating with Azure Monitor", + "azuremonitor.connectionProperties.authType.categoryValues.azureMFA": "Azure Active Directory - Universal with MFA support" +} diff --git a/extensions/azuremonitor/resources/dark/azure_monitor_dark.svg b/extensions/azuremonitor/resources/dark/azure_monitor_dark.svg new file mode 100644 index 0000000000..7b5ef94c5b --- /dev/null +++ b/extensions/azuremonitor/resources/dark/azure_monitor_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/azuremonitor/resources/light/azure_monitor_light.svg b/extensions/azuremonitor/resources/light/azure_monitor_light.svg new file mode 100644 index 0000000000..b390b521cf --- /dev/null +++ b/extensions/azuremonitor/resources/light/azure_monitor_light.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/extensions/azuremonitor/src/appContext.ts b/extensions/azuremonitor/src/appContext.ts new file mode 100644 index 0000000000..0fc5eb23d3 --- /dev/null +++ b/extensions/azuremonitor/src/appContext.ts @@ -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 = new Map(); + + constructor(public readonly extensionContext: vscode.ExtensionContext) { } + + public getService(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(serviceName: string, service: T): void { + if (this.serviceMap.has(serviceName)) { + console.warn('Multiple services ', serviceName, ' registered!'); + } else { + this.serviceMap.set(serviceName, service); + } + } +} diff --git a/extensions/azuremonitor/src/azuremonitor.d.ts b/extensions/azuremonitor/src/azuremonitor.d.ts new file mode 100644 index 0000000000..76dee761f4 --- /dev/null +++ b/extensions/azuremonitor/src/azuremonitor.d.ts @@ -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 AzureMonitor extension +*/ +export interface IExtension { + /** + * Gets the object explorer API that supports querying over the connections supported by this extension + * + */ + getAzureMonitorObjectExplorerBrowser(): AzureMonitorObjectExplorerBrowser; +} + +/** + * A browser supporting actions over the object explorer connections provided by this extension. + * Currently this is the + */ +export interface AzureMonitorObjectExplorerBrowser { + /** + * Gets the matching node given a context object, e.g. one from a right-click on a node in Object Explorer + */ + getNode(objectExplorerContext: azdata.ObjectExplorerContext): Thenable; +} + +/** + * A tree node in the object explorer tree + */ +export interface ITreeNode { + getNodeInfo(): azdata.NodeInfo; + getChildren(refreshChildren: boolean): ITreeNode[] | Thenable; +} diff --git a/extensions/azuremonitor/src/azuremonitorApiFactory.ts b/extensions/azuremonitor/src/azuremonitorApiFactory.ts new file mode 100644 index 0000000000..04c6d6abba --- /dev/null +++ b/extensions/azuremonitor/src/azuremonitorApiFactory.ts @@ -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, AzureMonitorObjectExplorerBrowser } from './azuremonitor'; +import * as constants from './constants'; +import { AzureMonitorObjectExplorerNodeProvider } from './objectExplorerNodeProvider/objectExplorerNodeProvider'; +import * as azdata from 'azdata'; + +export function createAzureMonitorApi(context: AppContext): IExtension { + return { + getAzureMonitorObjectExplorerBrowser(): AzureMonitorObjectExplorerBrowser { + return { + getNode: (explorerContext: azdata.ObjectExplorerContext) => { + let oeProvider = context.getService(constants.ObjectExplorerService); + return oeProvider?.findSqlClusterNodeByContext(explorerContext); + } + }; + } + }; +} diff --git a/extensions/azuremonitor/src/azuremonitorServer.ts b/extensions/azuremonitor/src/azuremonitorServer.ts new file mode 100644 index 0000000000..224fa261f3 --- /dev/null +++ b/extensions/azuremonitor/src/azuremonitorServer.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * 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 nls from 'vscode-nls'; +import * as path from 'path'; +import { getCommonLaunchArgsAndCleanupOldLogFiles } from './utils'; +import { Telemetry, LanguageClientErrorHandler } from './telemetry'; +import { SqlOpsDataClient, ClientOptions } from 'dataprotocol-client'; +import { SerializationFeature } from './features/serializationFeature'; +import { TelemetryFeature } from './features/telemetryFeature'; +import { AccountFeature } from './features/accountFeature'; +import { AppContext } from './appContext'; +import { CompletionExtensionParams, CompletionExtLoadRequest } from './features/contracts'; +import { promises as fs } from 'fs'; + +const localize = nls.loadMessageBundle(); + +export const outputChannel = vscode.window.createOutputChannel(Constants.serviceName); +const statusView = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + +export class AzureMonitorServer { + + private client!: SqlOpsDataClient; + private config!: IConfig; + private disposables: vscode.Disposable[] = []; + + public async start(context: AppContext): Promise { + try { + const installationStart = Date.now(); + const path = await this.download(context); + const installationComplete = Date.now(); + let serverOptions = generateServerOptions(context.extensionContext.logPath, path); + let clientOptions = getClientOptions(context); + this.client = new SqlOpsDataClient(Constants.serviceName, serverOptions, clientOptions); + 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('azuremonitor.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 { + const rawConfig = await fs.readFile(path.join(context.extensionContext.extensionPath, 'config.json')); + this.config = JSON.parse(rawConfig.toString())!; + this.config.installDirectory = path.join(__dirname, this.config.installDirectory); + this.config.proxy = vscode.workspace.getConfiguration('http').get('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, 'azuremonitorService.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: ['loganalytics'], + synchronize: { + configurationSection: Constants.extensionConfigSectionName + }, + providerId: Constants.providerId, + errorHandler: new LanguageClientErrorHandler(), + features: [ + // we only want to add new features + ...SqlOpsDataClient.defaultFeatures, + TelemetryFeature, + AccountFeature, + 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 { + } +} diff --git a/extensions/azuremonitor/src/constants.ts b/extensions/azuremonitor/src/constants.ts new file mode 100644 index 0000000000..5aedc95ff8 --- /dev/null +++ b/extensions/azuremonitor/src/constants.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * 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 = 'AzureMonitor Tools Service'; +export const providerId = 'LOGANALYTICS'; +export const serviceCrashLink = 'https://github.com/Microsoft/azuredatastudio/issues'; +export const extensionConfigSectionName = 'azuremonitor'; + +// DATA PROTOCOL VALUES /////////////////////////////////////////////////////////// +export const azureMonitorClusterProviderName = 'azureMonitorCluster'; +export const authenticationTypePropName = 'authenticationType'; +export const integratedAuth = 'integrated'; +export const serverPropName = 'server'; +export const userPropName = 'user'; +export const passwordPropName = 'password'; +export const azuremonitorProviderName = 'LOGANALYTICS'; + +// SERVICE NAMES ////////////////////////////////////////////////////////// +export const ObjectExplorerService = 'objectexplorer'; +export const objectExplorerPrefix: string = 'objectexplorer://'; +export const ViewType = 'view'; + +export const azuremonitorClusterNewNotebookTask = 'azuremonitorCluster.task.newNotebook'; +export const azuremonitorClusterOpenNotebookTask = 'azuremonitorCluster.task.openNotebook'; \ No newline at end of file diff --git a/extensions/azuremonitor/src/contextProvider.ts b/extensions/azuremonitor/src/contextProvider.ts new file mode 100644 index 0000000000..847cb36867 --- /dev/null +++ b/extensions/azuremonitor/src/contextProvider.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * 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 = 'azuremonitor:iscloud', + EDITIONID = 'azuremonitor:engineedition', + ISCLUSTER = 'azuremonitor:iscluster', + SERVERMAJORVERSION = 'azuremonitor:servermajorversion' +} + +const isCloudEditions = [ + azdata.DatabaseEngineEdition.SqlDatabase, + azdata.DatabaseEngineEdition.SqlDataWarehouse, + azdata.DatabaseEngineEdition.SqlOnDemand +]; + +export function setCommandContext(key: ContextKeys | string, value: any) { + return vscode.commands.executeCommand(BuiltInCommands.SetContext, key, value); +} + +export default class ContextProvider { + private _disposables = new Array(); + + 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; + let serverMajorVersion: number | undefined; + if (e.profile.providerName.toLowerCase() === 'loganalytics' && !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()); + } +} diff --git a/extensions/azuremonitor/src/extension.ts b/extensions/azuremonitor/src/extension.ts new file mode 100644 index 0000000000..b42bcd0655 --- /dev/null +++ b/extensions/azuremonitor/src/extension.ts @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * 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 nls from 'vscode-nls'; +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 './azuremonitor'; +import { AzureMonitorObjectExplorerNodeProvider } from './objectExplorerNodeProvider/objectExplorerNodeProvider'; +import { registerSearchServerCommand } from './objectExplorerNodeProvider/command'; +import { AzureMonitorIconProvider } from './iconProvider'; +import { createAzureMonitorApi } from './azuremonitorApiFactory'; +import { AzureMonitorServer } from './azuremonitorServer'; +import { promises as fs } from 'fs'; + +const localize = nls.loadMessageBundle(); + +export async function activate(context: vscode.ExtensionContext): Promise { + // lets make sure we support this platform first + let supported = await Utils.verifyPlatform(); + + if (!supported) { + vscode.window.showErrorMessage(localize('azuremonitor.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 AzureMonitorObjectExplorerNodeProvider(appContext); + azdata.dataprotocol.registerObjectExplorerNodeProvider(nodeProvider); + let iconProvider = new AzureMonitorIconProvider(); + azdata.dataprotocol.registerIconProvider(iconProvider); + + registerTasks(); + + registerSearchServerCommand(); + context.subscriptions.push(new ContextProvider()); + + registerLogCommand(context); + + // initialize client last so we don't have features stuck behind it + const server = new AzureMonitorServer(); + context.subscriptions.push(server); + await server.start(appContext); + + return createAzureMonitorApi(appContext); +} + +const logFiles = ['resourceprovider.log', 'azuremonitorservice.log', 'credentialstore.log']; +function registerLogCommand(context: vscode.ExtensionContext) { + context.subscriptions.push(vscode.commands.registerCommand('azuremonitor.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 registerTasks(): void { + azdata.tasks.registerTask(Constants.azuremonitorClusterNewNotebookTask, (profile: azdata.IConnectionProfile) => { + return saveProfileAndCreateNotebook(profile); + }); + azdata.tasks.registerTask(Constants.azuremonitorClusterOpenNotebookTask, (profile: azdata.IConnectionProfile) => { + return handleOpenNotebookTask(profile); + }); +} + +function saveProfileAndCreateNotebook(profile: azdata.IConnectionProfile): Promise { + 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 { + // 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 { + let notebookFileTypeName = localize('notebookFileType', "Notebooks"); + let filter: any = {}; + 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 { +} diff --git a/extensions/azuremonitor/src/features/accountFeature.ts b/extensions/azuremonitor/src/features/accountFeature.ts new file mode 100644 index 0000000000..71b6e58f81 --- /dev/null +++ b/extensions/azuremonitor/src/features/accountFeature.ts @@ -0,0 +1,65 @@ +import * as nls from 'vscode-nls'; +import { SqlOpsDataClient } from 'dataprotocol-client'; +import { ClientCapabilities, StaticFeature } from 'vscode-languageclient'; +import { window } from 'vscode'; +import * as contracts from './contracts'; +import * as azdata from 'azdata'; + +const localize = nls.loadMessageBundle(); + +export class AccountFeature implements StaticFeature { + + constructor(private _client: SqlOpsDataClient) { } + + fillClientCapabilities(_capabilities: ClientCapabilities): void { } + + initialize(): void { + this._client.onRequest(contracts.SecurityTokenRequest.type, async (request): Promise => { + return this.getToken(request); + }); + } + + protected async getToken(request: contracts.RequestSecurityTokenParams): Promise { + const accountList = await azdata.accounts.getAllAccounts(); + let account: azdata.Account | undefined; + + if (accountList.length < 1) { + // TODO: Prompt user to add account + window.showErrorMessage(localize('kusto.missingLinkedAzureAccount', "Azure Data Studio needs to contact Azure Key Vault to access a column master key for Always Encrypted, but no linked Azure account is available. Please add a linked Azure account and retry the query.")); + return undefined; + } else { + account = accountList.find(a => a.key.accountId === request.accountId); + } + + if (!account) { + window.showErrorMessage(localize('kusto.accountDoesNotExist', "Account does not exist.")); + return undefined; + } + + const unauthorizedMessage = localize('kusto.insufficientlyPrivelagedAzureAccount', "The configured Azure account for {0} does not have sufficient permissions for Azure Key Vault to access a column master key for Always Encrypted.", account.key.accountId); + + let tenantId: string = ''; + if (request.provider !== 'dstsAuth') { + const tenant = account.properties.tenants.find((t: { [key: string]: string }) => request.authority.includes(t.id)); + if (!tenant) { + window.showErrorMessage(unauthorizedMessage); + return undefined; + } + tenantId = tenant.id; + } + + const securityToken = await azdata.accounts.getAccountSecurityToken(account, tenantId, azdata.AzureResource.Sql); + + if (!securityToken?.token) { + window.showErrorMessage(unauthorizedMessage); + return undefined; + } + + let params: contracts.RequestSecurityTokenResponse = { + accountKey: JSON.stringify(account.key), + token: securityToken.token + }; + + return params; + } +} \ No newline at end of file diff --git a/extensions/azuremonitor/src/features/contracts.ts b/extensions/azuremonitor/src/features/contracts.ts new file mode 100644 index 0000000000..1b9912de2d --- /dev/null +++ b/extensions/azuremonitor/src/features/contracts.ts @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * 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('telemetry/sqlevent'); +} + +/** + * Update event parameters + */ +export class TelemetryParams { + public params!: { + eventName: string; + properties: ITelemetryEventProperties; + measures: ITelemetryEventMeasures; + }; +} + +// ------------------------------- ---------------------------------- + +// ------------------------------- < Security Token Request > ------------------------------------------ +export interface RequestSecurityTokenParams { + authority: string; + provider: string; + resource: string; + accountId: string; +} + +export interface RequestSecurityTokenResponse { + accountKey: string; + token: string; +} + +export namespace SecurityTokenRequest { + export const type = new RequestType('account/securityTokenRequest'); +} +// ------------------------------- ------------------------------------------ + +// ------------------------------- ----------------------------- +export namespace SerializeDataStartRequest { + export const type = new RequestType('serialize/start'); +} + +export namespace SerializeDataContinueRequest { + export const type = new RequestType('serialize/continue'); +} +// ------------------------------- ----------------------------- + +// ------------------------------- < Load Completion Extension Request > ------------------------------------ +/** + * Completion extension load parameters + */ +export class CompletionExtensionParams { + /// + /// Absolute path for the assembly containing the completion extension + /// + public assemblyPath?: string; + /// + /// The type name for the completion extension + /// + public typeName?: string; + /// + /// Property bag for initializing the completion extension + /// + public properties?: {}; +} + +export namespace CompletionExtLoadRequest { + export const type = new RequestType('completion/extLoad'); +} diff --git a/extensions/azuremonitor/src/features/serializationFeature.ts b/extensions/azuremonitor/src/features/serializationFeature.ts new file mode 100644 index 0000000000..489c8bb059 --- /dev/null +++ b/extensions/azuremonitor/src/features/serializationFeature.ts @@ -0,0 +1,68 @@ +import { SqlOpsDataClient, SqlOpsFeature } from 'dataprotocol-client'; +import { ClientCapabilities, RPCMessageType, ServerCapabilities } from 'vscode-languageclient'; +import { Disposable } from 'vscode'; +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 SerializationFeature extends SqlOpsFeature { + 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 => { + return client.sendRequest(contracts.SerializeDataStartRequest.type, requestParams).then( + r => { + return r; + }, + e => { + client.logFailedRequest(contracts.SerializeDataStartRequest.type, e); + return Promise.resolve({ + succeeded: false, + messages: Utils.getErrorMessage(e) + }); + } + ); + }; + + let continueSerialization = (requestParams: azdata.SerializeDataContinueRequestParams): Thenable => { + return client.sendRequest(contracts.SerializeDataContinueRequest.type, requestParams).then( + r => { + return r; + }, + e => { + client.logFailedRequest(contracts.SerializeDataContinueRequest.type, e); + return Promise.resolve({ + succeeded: false, + messages: Utils.getErrorMessage(e) + }); + } + ); + }; + + return azdata.dataprotocol.registerSerializationProvider({ + providerId: client.providerId, + startSerialization, + continueSerialization + }); + } +} diff --git a/extensions/azuremonitor/src/features/telemetryFeature.ts b/extensions/azuremonitor/src/features/telemetryFeature.ts new file mode 100644 index 0000000000..7723f8fb64 --- /dev/null +++ b/extensions/azuremonitor/src/features/telemetryFeature.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SqlOpsDataClient } from 'dataprotocol-client'; +import { ClientCapabilities, StaticFeature } from 'vscode-languageclient'; +import { Telemetry } from '../telemetry'; +import * as contracts from './contracts'; +import * as Utils from '../utils'; + +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); + }); + } +} diff --git a/extensions/azuremonitor/src/iconProvider.ts b/extensions/azuremonitor/src/iconProvider.ts new file mode 100644 index 0000000000..c674a54bdf --- /dev/null +++ b/extensions/azuremonitor/src/iconProvider.ts @@ -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 = 'azuremonitor:cloud'; + +export class AzureMonitorIconProvider implements azdata.IconProvider { + public readonly providerId: string = constants.azuremonitorProviderName; + public handle?: number; + getConnectionIconId(connection: azdata.IConnectionProfile, _serverInfo: azdata.ServerInfo): Thenable { + let iconName: string | undefined; + if (connection.providerName === this.providerId) { + iconName = cloudIcon; + } + return Promise.resolve(iconName); + } +} diff --git a/extensions/azuremonitor/src/index.ts b/extensions/azuremonitor/src/index.ts new file mode 100644 index 0000000000..415fc08af0 --- /dev/null +++ b/extensions/azuremonitor/src/index.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* + * This file imports all public APIs for this extension and used to generate index.d.ts. + * Generated typing file can be used by other extensions to access public APIs. + */ +export { providerId } from './constants'; diff --git a/extensions/azuremonitor/src/objectExplorerNodeProvider/cancelableStream.ts b/extensions/azuremonitor/src/objectExplorerNodeProvider/cancelableStream.ts new file mode 100644 index 0000000000..3efb7e8e56 --- /dev/null +++ b/extensions/azuremonitor/src/objectExplorerNodeProvider/cancelableStream.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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 override _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(); + } + } +} diff --git a/extensions/azuremonitor/src/objectExplorerNodeProvider/command.ts b/extensions/azuremonitor/src/objectExplorerNodeProvider/command.ts new file mode 100644 index 0000000000..f878669c70 --- /dev/null +++ b/extensions/azuremonitor/src/objectExplorerNodeProvider/command.ts @@ -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); + } + + override dispose(): void { + if (this.disposable) { + this.disposable.dispose(); + } + } + + protected async preExecute(...args: any[]): Promise { + 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, + label: string, + isCancelable: boolean = false, + onCanceled?: () => void + ): Promise { + 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 { + return (await this.prompter.promptSingle({ + type: QuestionTypes.confirm, + message: localize('cancel', 'Cancel operation?'), + default: true + }))!; + } +} + +export function registerSearchServerCommand(): void { + vscode.commands.registerCommand('azureMonitor.searchServers', () => { + vscode.window.showInputBox({ + placeHolder: localize('azureMonitor.searchServers', 'Search Server Names') + }).then((stringSearch) => { + if (stringSearch) { + vscode.commands.executeCommand('registeredServers.searchServer', (stringSearch)); + } + }); + }); + vscode.commands.registerCommand('azureMonitor.clearSearchServerResult', () => { + vscode.commands.executeCommand('registeredServers.clearSearchServerResult'); + }); +} diff --git a/extensions/azuremonitor/src/objectExplorerNodeProvider/connection.ts b/extensions/azuremonitor/src/objectExplorerNodeProvider/connection.ts new file mode 100644 index 0000000000..1de000065a --- /dev/null +++ b/extensions/azuremonitor/src/objectExplorerNodeProvider/connection.ts @@ -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 AzureMonitorClusterConnection { + 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: AzureMonitorClusterConnection | azdata.ConnectionInfo): boolean { + if (!connection) { return false; } + let options1 = connection instanceof AzureMonitorClusterConnection ? + 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; + } +} diff --git a/extensions/azuremonitor/src/objectExplorerNodeProvider/objectExplorerNodeProvider.ts b/extensions/azuremonitor/src/objectExplorerNodeProvider/objectExplorerNodeProvider.ts new file mode 100644 index 0000000000..c347804dc7 --- /dev/null +++ b/extensions/azuremonitor/src/objectExplorerNodeProvider/objectExplorerNodeProvider.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AzureMonitorClusterConnection } from './connection'; +import { TreeNode } from './treeNodes'; +import { AppContext } from '../appContext'; +import * as constants from '../constants'; +import { ICommandObjectExplorerContext } from './command'; +import { outputChannel } from '../azuremonitorServer'; + +export interface ITreeChangeHandler { + notifyNodeChanged(node: TreeNode): void; +} + +export class AzureMonitorObjectExplorerNodeProvider extends ProviderBase implements azdata.ObjectExplorerNodeProvider, ITreeChangeHandler { + public readonly supportedProviderId: string = constants.providerId; + private expandCompleteEmitter = new vscode.EventEmitter(); + + constructor(private appContext: AppContext) { + super(); + this.appContext.registerService(constants.ObjectExplorerService, this); + } + + handleSessionOpen(session: azdata.ObjectExplorerSession): Thenable { + 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 { + 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 { + 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 { + let response = { + sessionId: nodeInfo.sessionId!, + nodePath: nodeInfo.nodePath!, + errorMessage: undefined, + nodes: [] + }; + + this.expandCompleteEmitter.fire(response); + + return true; + } + + refreshNode(nodeInfo: azdata.ExpandNodeInfo): Thenable { + return this.expandNode(nodeInfo, true); + } + + handleSessionClose(_closeSessionInfo: azdata.ObjectExplorerCloseSessionInfo): void { + } + + findNodes(_findNodesInfo: azdata.FindNodesInfo): Thenable { + 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 { + 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) { + outputChannel.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(context: ICommandObjectExplorerContext | azdata.ObjectExplorerContext): Promise { + 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 = session.rootNode; + } else { + // Find the node under the session + node = 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: AzureMonitorClusterConnection, + private _sqlSession: azdata.ObjectExplorerSession, + private _sqlConnectionProfile: azdata.IConnectionProfile + ) { + this._rootNode = new SqlClusterRootNode(this, + this._sqlSession.rootNode.nodePath!); + } + + public get sqlClusterConnection(): AzureMonitorClusterConnection { 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 { + if (refreshChildren || !this._children) { + return this.refreshChildren(); + } + return this._children; + } + + private async refreshChildren(): Promise { + this._children = []; + return this._children; + } + + getTreeItem(): vscode.TreeItem | Promise { + 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; + } +} diff --git a/extensions/azuremonitor/src/objectExplorerNodeProvider/providerBase.ts b/extensions/azuremonitor/src/objectExplorerNodeProvider/providerBase.ts new file mode 100644 index 0000000000..6157cbd453 --- /dev/null +++ b/extensions/azuremonitor/src/objectExplorerNodeProvider/providerBase.ts @@ -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.azureMonitorClusterProviderName; + public handle?: number; +} diff --git a/extensions/azuremonitor/src/objectExplorerNodeProvider/treeNodes.ts b/extensions/azuremonitor/src/objectExplorerNodeProvider/treeNodes.ts new file mode 100644 index 0000000000..b78036ecc0 --- /dev/null +++ b/extensions/azuremonitor/src/objectExplorerNodeProvider/treeNodes.ts @@ -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 { + 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 { + 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; + abstract getTreeItem(): vscode.TreeItem | Promise; + + abstract getNodeInfo(): azdata.NodeInfo; +} diff --git a/extensions/azuremonitor/src/objectExplorerNodeProvider/types.d.ts b/extensions/azuremonitor/src/objectExplorerNodeProvider/types.d.ts new file mode 100644 index 0000000000..4b82aeaae8 --- /dev/null +++ b/extensions/azuremonitor/src/objectExplorerNodeProvider/types.d.ts @@ -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; +} diff --git a/extensions/azuremonitor/src/prompts/adapter.ts b/extensions/azuremonitor/src/prompts/adapter.ts new file mode 100644 index 0000000000..83a8816c3a --- /dev/null +++ b/extensions/azuremonitor/src/prompts/adapter.ts @@ -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(question: IQuestion, ignoreFocusOut?: boolean): Promise { + let questions: IQuestion[] = [question]; + const answers = await this.prompt(questions, ignoreFocusOut); + if (answers) { + let response: T = answers[question.name]; + return response || undefined; + } + return undefined; + } + + public async prompt(questions: IQuestion[], ignoreFocusOut?: boolean): Promise | undefined> { + // Collapse multiple questions into a set of prompt steps + const promptResult = new Promise>((resolve) => { + let answers: Answers = {}; + 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 | undefined): void { + // Collapse multiple questions into a set of prompt steps + this.prompt(questions).then(answers => { + if (callback && answers) { + callback(answers); + } + }); + } +} diff --git a/extensions/azuremonitor/src/prompts/checkbox.ts b/extensions/azuremonitor/src/prompts/checkbox.ts new file mode 100644 index 0000000000..815f374732 --- /dev/null +++ b/extensions/azuremonitor/src/prompts/checkbox.ts @@ -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: { [x: string]: any; }, choice: { name: any; checked: boolean; }) => { + 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: any[], choice: { checked: boolean; value: any; }) => { + if (choice.checked === true) { + result2.push(choice.value); + } + + return result2; + }, []); + }); + } +} diff --git a/extensions/azuremonitor/src/prompts/confirm.ts b/extensions/azuremonitor/src/prompts/confirm.ts new file mode 100644 index 0000000000..055d95a4e7 --- /dev/null +++ b/extensions/azuremonitor/src/prompts/confirm.ts @@ -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; + }); + } +} diff --git a/extensions/azuremonitor/src/prompts/escapeException.ts b/extensions/azuremonitor/src/prompts/escapeException.ts new file mode 100644 index 0000000000..11bcfc4360 --- /dev/null +++ b/extensions/azuremonitor/src/prompts/escapeException.ts @@ -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 { + +} diff --git a/extensions/azuremonitor/src/prompts/expand.ts b/extensions/azuremonitor/src/prompts/expand.ts new file mode 100644 index 0000000000..ee2caf0284 --- /dev/null +++ b/extensions/azuremonitor/src/prompts/expand.ts @@ -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: { [x: string]: any; }, choice: { name: string | number; value: any; }) => { + 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; + } +} diff --git a/extensions/azuremonitor/src/prompts/factory.ts b/extensions/azuremonitor/src/prompts/factory.ts new file mode 100644 index 0000000000..1b9db3dda5 --- /dev/null +++ b/extensions/azuremonitor/src/prompts/factory.ts @@ -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}`); + } + } +} diff --git a/extensions/azuremonitor/src/prompts/input.ts b/extensions/azuremonitor/src/prompts/input.ts new file mode 100644 index 0000000000..eb57f3faee --- /dev/null +++ b/extensions/azuremonitor/src/prompts/input.ts @@ -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; + }); + } +} diff --git a/extensions/azuremonitor/src/prompts/list.ts b/extensions/azuremonitor/src/prompts/list.ts new file mode 100644 index 0000000000..539c5c5557 --- /dev/null +++ b/extensions/azuremonitor/src/prompts/list.ts @@ -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: { [x: string]: any; }, choice: { name: string | number; value: any; }) => { + 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]; + }); + } +} diff --git a/extensions/azuremonitor/src/prompts/password.ts b/extensions/azuremonitor/src/prompts/password.ts new file mode 100644 index 0000000000..ef6de55886 --- /dev/null +++ b/extensions/azuremonitor/src/prompts/password.ts @@ -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; + } +} diff --git a/extensions/azuremonitor/src/prompts/progressIndicator.ts b/extensions/azuremonitor/src/prompts/progressIndicator.ts new file mode 100644 index 0000000000..c6f20574a0 --- /dev/null +++ b/extensions/azuremonitor/src/prompts/progressIndicator.ts @@ -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; + } + } +} diff --git a/extensions/azuremonitor/src/prompts/prompt.ts b/extensions/azuremonitor/src/prompts/prompt.ts new file mode 100644 index 0000000000..700f577d66 --- /dev/null +++ b/extensions/azuremonitor/src/prompts/prompt.ts @@ -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; diff --git a/extensions/azuremonitor/src/prompts/question.ts b/extensions/azuremonitor/src/prompts/question.ts new file mode 100644 index 0000000000..b0678c668a --- /dev/null +++ b/extensions/azuremonitor/src/prompts/question.ts @@ -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; + // 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 = { [key: string]: T }; + +export interface IPrompter { + promptSingle(question: IQuestion, ignoreFocusOut?: boolean): Promise; + /** + * Prompts for multiple questions + * + * @returns Map of question IDs to results, or undefined if + * the user canceled the question session + */ + prompt(questions: IQuestion[], ignoreFocusOut?: boolean): Promise | undefined>; + promptCallback(questions: IQuestion[], callback: IPromptCallback): void; +} + +export interface IPromptCallback { + (answers: { [id: string]: any }): void; +} diff --git a/extensions/azuremonitor/src/telemetry.ts b/extensions/azuremonitor/src/telemetry.ts new file mode 100644 index 0000000000..433abf95d1 --- /dev/null +++ b/extensions/azuremonitor/src/telemetry.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * 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 nls from 'vscode-nls'; +import TelemetryReporter from 'vscode-extension-telemetry'; +import { ErrorAction, ErrorHandler, Message, CloseAction } from 'vscode-languageclient'; + +import * as Utils from './utils'; +import * as Constants from './constants'; + +const localize = nls.loadMessageBundle(); + +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('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(); diff --git a/extensions/azuremonitor/src/types.ts b/extensions/azuremonitor/src/types.ts new file mode 100644 index 0000000000..b09b7d25f4 --- /dev/null +++ b/extensions/azuremonitor/src/types.ts @@ -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; +} diff --git a/extensions/azuremonitor/src/typings/findRemove.d.ts b/extensions/azuremonitor/src/typings/findRemove.d.ts new file mode 100644 index 0000000000..c49686d1b3 --- /dev/null +++ b/extensions/azuremonitor/src/typings/findRemove.d.ts @@ -0,0 +1,17 @@ +declare module 'find-remove' { + namespace findRemove { + interface FindRemoveApi { + (path: string, options: FindRemoveOptions): JSON; + } + + interface FindRemoveOptions { + age?: { + seconds?: number; + }; + limit?: number; + } + } + + const findRemove: findRemove.FindRemoveApi; + export = findRemove; +} diff --git a/extensions/azuremonitor/src/typings/refs.d.ts b/extensions/azuremonitor/src/typings/refs.d.ts new file mode 100644 index 0000000000..dad0d96412 --- /dev/null +++ b/extensions/azuremonitor/src/typings/refs.d.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +/// +/// +/// diff --git a/extensions/azuremonitor/src/utils.ts b/extensions/azuremonitor/src/utils.ts new file mode 100644 index 0000000000..e850d3b088 --- /dev/null +++ b/extensions/azuremonitor/src/utils.ts @@ -0,0 +1,147 @@ +/*--------------------------------------------------------------------------------------------- + * 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 = 'azuremonitor'; + +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(`--locale`, vscode.env.language); + + 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); + } + + launchArgs.push('--service-name'); + launchArgs.push('AzureMonitor'); + + return launchArgs; +} + +export function ensure(target: any, 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 { + 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 { + try { + await fs.access(path); + return true; + } catch (e) { + return false; + } +} diff --git a/extensions/azuremonitor/syntaxes/azuremonitor.tmLanguage b/extensions/azuremonitor/syntaxes/azuremonitor.tmLanguage new file mode 100644 index 0000000000..15c627641e --- /dev/null +++ b/extensions/azuremonitor/syntaxes/azuremonitor.tmLanguage @@ -0,0 +1,449 @@ + + + + + scopeName + source.kusto + + fileTypes + + kql + kusto + csl + + + name + kusto file + + patterns + + + + + match + \b(let|set|alias|declare|pattern|restrict|access|to|set)\b + name + keyword.control.kusto + + + + match + \b(cluster|database|materialize|table|toscalar)(?=\W*\() + name + support.function + + + + match + \b(and|or|!in|has|has_cs|hasprefix|hasprefix_cs|hassuffix|hassuffix_cs|contains|contains_cs|startswith|startswith_cs|endswith|endswith_cs|matches|regex|in|between)\b + name + keyword.operator.kusto + + + + + match + \b(binary_and|binary_not|binary_or|binary_shift_left|binary_shift_right|binary_xor)(?=\W*\() + name + support.function + + + + match + \b(tobool|todatetime|todecimal|todouble|toguid|tohex|toreal|toint|tolong|tolower|toreal|tostring|totimespan|toupper|to_utf8|translate|treepath|trim|trim_end|trim_start|url_decode|url_encode|weekofyear|welch_test|zip)(?=\W*\() + name + support.function + + + + match + (?<=\.\d|\d|\d\W)(d|h|m|s|ms|microsecond|tick|seconds)\b + name + variable.language + + + + match + \b(ago|datetime_add|datetime_part|datetime_diff|dayofmonth|dayofweek|dayofyear|endofday|endofmonth|endofweek|endofyear|format_datetime|format_timespan|getmonth|getyear|hourofday|make_datetime|make_timespan|monthofyear|now|startofday|startofmonth|startofweek|startofyear|todatetime|totimespan|weekofyear)(?=\W*\() + name + support.function + + + + match + \b(array_concat|array_length|array_slice|array_split|array_strcat|bag_keys|pack|pack_all|pack_array|repeat|treepath|zip)(?=\W*\() + name + support.function + + + + match + \b(next|prev|row_cumsum|row_number)(?=\W*\() + name + support.function + + + + match + \b(toscalar)(?=\W*\() + name + support.function + + + + match + \b(abs|acos|asin|atan|atan2|beta_cdf|beta_inv|beta_pdf|cos|cot|degrees|exp|exp100|exp2|gamma|hash|isfinite|isinf|isnan|log|log10|log2|loggamma|not|pi|pow|radians|rand|range|round|sign|sin|sqrt|tan|welch_test)(?=\W*\() + name + support.function + + + + match + \b(column_ifexists|current_principal|cursor_after|extent_id|extent_tags|ingestion_time)(?=\W*\() + name + support.function + + + + match + \b(bin|bin_at|ceiling|floor)(?=\W*\() + name + support.function + + + + match + \b(case|coalesce|iif|iff|max_of|min_of)(?=\W*\() + name + support.function + + + + match + \b(series_add|series_divide|series_equals|series_greater|series_greater_equals|series_less|series_less_equals|series_multiply|series_not_equals|series_subtract)(?=\W*\() + name + support.function + + + + match + \b(series_decompose|series_decompose_anomalies|series_decompose_forecast|series_fill_backward|series_fill_const|series_fill_forward|series_fill_linear|series_fir|series_fit_2lines|series_fit_2lines_dynamic|series_fit_line|series_fit_line_dynamic|series_iir|series_outliers|series_periods_detect|series_periods_validate|series_seasonal|series_stats|series_stats_dynamic)(?=\W*\() + name + support.function + + + + match + \b(base64_decodestring|base64_encodestring|countof|extract|extract_all|extractjson|indexof|isempty|isnotempty|isnotnull|isnull|parse_ipv4|parse_json|parse_url|parse_urlquery|parse_version|replace|reverse|split|strcat|strcat_delim|strcmp|strlen|strrep|substring|toupper|translate|trim|trim_end|trim_start|url_decode|url_encode)(?=\W*\() + name + support.function + + + + match + \b(gettype)(?=\W*\() + name + support.function + + + + match + \b(dcount_hll|hll_merge|percentile_tdigest|percentrank_tdigest|rank_tdigest|tdigest_merge)(?=\W*\() + name + support.function + + + + match + \b(any|arg_max|arg_min|avg|avgif|buildschema|count|countif|dcount|dcountif|hll|hll_merge|make_bag|make_list|make_set|max|min|percentiles|stdev|stdevif|stdevp|sum|sumif|tdigest|tdigest_merge|variance|varianceif|variancep)(?=\W*\() + name + support.function + + + + match + \b(next|prev|row_cumsum|row_number)(?=\W*\() + name + support.function + + + + match + \b(activity_counts_metrics|sliding_window_counts|activity_metrics|new_activity_metrics|activity_engagement|active_users_count|session_count|funnel_sequence|funnel_sequence_completion)(?=\W*\() + name + support.function + + + + match + \.create-or-alter + name + keyword.control.kusto + + + + match + (?<=let ).+(?=\W*=) + name + entity.function.name.lambda.kusto + + + + match + \b(with|folder|docstring|skipvalidation)\b + name + keyword.operator.kusto + + + match + \b(function)\b + name + variable.language + + + + + match + \b(bool|datetime|decimal|dynamic|guid|int|long|real|string|timespan)\b + name + storage.type + + + + match + \b(datatable)(?=\W*\() + name + support.function + + + + + match + \b(as|consume|count|datatable|distinct|evaluate|extend|externaldata|facet|find|fork|getschema|invoke|join|limit|take|lookup|make-series|mv-expand|order|sort|project-away|project-rename|project|parse|partition|print|range|reduce|render|sample|sample-distinct|search|serialize|sort|summarize|take|top-nested|top|top-hitters|union|where)\b + name + keyword.operator.special.kusto + + + + match + \b(autocluster|bag_unpack|basket|dcount_intersect|diffpatterns|narrow|pivot|preview|rolling_percentile|sql_request)(?=\W*\() + name + support.function + + + + match + \b(on|kind|hint\.remote|hint\.strategy)\b + name + keyword.operator.kusto + + + + match + (\$left|\$right)\b + name + keyword.other.kusto + + + + match + \b(innerunique|inner|leftouter|rightouter|fullouter|leftanti|anti|leftantisemi|rightanti|rightantisemi|leftsemi|rightsemi|shuffle|broadcast)\b + name + keyword.other.kusto + + + + match + \b(series_fir|series_iir|series_fit_line|series_fit_line_dynamic|series_fit_2lines|series_fit_2lines_dynamic|series_outliers|series_periods_detect|series_periods_validate|series_stats_dynamic|series_stats)(?=\W*\() + name + support.function + + + + match + \b(series_fill_backward|series_fill_const|series_fill_forward|series_fill_linear)(?=\W*\() + name + support.function + + + + match + \b(bag|array)\b + name + keyword.operator.kusto + + + + match + \b(asc|desc|nulls first|nulls last)\b + name + keyword.other.kusto + + + + match + \b(regex|simple|relaxed)\b + name + keyword.other.kusto + + + + match + \b(anomalychart|areachart|barchart|columnchart|ladderchart|linechart|piechart|pivotchart|scatterchart|stackedareachart|table|timechart|timepivot)\b + name + support.function + + + + match + \b(by|from|in|of|to|step|with)\b + name + keyword.operator.word + + + + + match + ".*?" + name + string.quoted.double + + + match + \{.*?\} + name + string.variable + + + match + '.*?' + name + string.quoted.single + + + + match + //.* + name + comment.line + + + match + \b((0(x|X)[0-9a-fA-F]*)|(([0-9]+\.?[0-9]*+)|(\.[0-9]+))((e|E)(\+|-)?[0-9]+)?)(L|l|UL|ul|u|U|F|f|ll|LL|ull|ULL)?(?=\b|\w) + name + constant.numeric + + + match + \b(true|false)\b + name + constant.language + + + + match + \b(array_strcat|make_dictionary|makelist|makeset|mvexpand|todynamic)(?=\W*\(|\b) + name + invalid.deprecated + + + + match + + name + invalid.illegal + + + uuid + FF0550E0-3A29-11E3-AA6E-0800200C9B77 + + diff --git a/extensions/azuremonitor/tsconfig.json b/extensions/azuremonitor/tsconfig.json new file mode 100644 index 0000000000..75605f1a69 --- /dev/null +++ b/extensions/azuremonitor/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out", + "strict": true, + "noUnusedParameters": true, + "noImplicitAny": true + }, + "include": [ + "src/**/*" + ] +} diff --git a/extensions/azuremonitor/yarn.lock b/extensions/azuremonitor/yarn.lock new file mode 100644 index 0000000000..965d4fa714 --- /dev/null +++ b/extensions/azuremonitor/yarn.lock @@ -0,0 +1,370 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +agent-base@4, agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + +applicationinsights@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.6.tgz#bc201810de91cea910dab34e8ad35ecde488edeb" + integrity sha512-VQT3kBpJVPw5fCO5n+WUeSx0VHjxFtD7znYbILBlVgOS9/cMDuGFmV2Br3ObzFyZUDGNbEfW36fD1y2/vAiCKw== + dependencies: + diagnostic-channel "0.2.0" + diagnostic-channel-publishers "0.2.1" + zone.js "0.7.6" + +async-retry@^1.2.3: + version "1.3.1" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.1.tgz#139f31f8ddce50c0870b0ba558a6079684aaed55" + integrity sha512-aiieFW/7h3hY0Bq5d+ktDBejxuwR78vRu9hDUdR8rNhSaQ29VzPL4AoIRG7D/c7tdenwOcKvgPM6tIxB3cB6HA== + dependencies: + retry "0.12.0" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.2.2": + version "1.2.2" + resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/3ffb03ba8b892cffe6ad363a2269c1f9439cf350" + dependencies: + vscode-languageclient "5.2.1" + +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +diagnostic-channel-publishers@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3" + integrity sha1-ji1geottef6IC1SLxYzGvrKIxPM= + +diagnostic-channel@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17" + integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc= + dependencies: + semver "^5.3.0" + +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eventemitter2@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-5.0.1.tgz#6197a095d5fb6b57e8942f6fd7eaad63a09c9452" + integrity sha1-YZegldX7a1folC9v1+qtY6CclFI= + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + dependencies: + pend "~1.2.0" + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= + dependencies: + escape-string-regexp "^1.0.5" + +find-remove@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/find-remove/-/find-remove-1.2.1.tgz#afd93400d23890e018ea197591e9d850d3d049a2" + integrity sha512-zcspBi9mWAyM9YTcVJLkI/x6rbjSDqHijjPa0vTwEmVZnYSmvYMtixDkUnSnuv2xAAkc9fblpkCg91paBIJaLw== + dependencies: + fmerge "1.2.0" + rimraf "2.6.2" + +fmerge@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fmerge/-/fmerge-1.2.0.tgz#36e99d2ae255e3ee1af666b4df780553671cf692" + integrity sha1-NumdKuJV4+4a9ma033gFU2cc9pI= + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +glob@^7.0.5: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +http-proxy-agent@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" + integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== + dependencies: + agent-base "4" + debug "3.1.0" + +https-proxy-agent@^2.2.3: + version "2.2.4" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" + integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +minipass@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + +retry@0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + +rimraf@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w== + dependencies: + glob "^7.0.5" + +semver@^5.3.0, semver@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +service-downloader@0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/service-downloader/-/service-downloader-0.2.1.tgz#8bd756bc4bc0cbfdf04fe71d4337f19ce6196203" + integrity sha512-5IEy2nyMJj/f41pI65b8RMeJyCecGNrMmNCpUW8hckZ9cBMyX+VCp8GjYoM6Mz/X0XSaGVz7V5gtCWjfeJI7gA== + dependencies: + async-retry "^1.2.3" + eventemitter2 "^5.0.1" + http-proxy-agent "^2.1.0" + https-proxy-agent "^2.2.3" + mkdirp "^0.5.1" + tar "^6.0.1" + tmp "^0.0.33" + yauzl "^2.10.0" + +tar@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" + integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +vscode-extension-telemetry@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.0.tgz#3cdcb61d03829966bd04b5f11471a1e40d6abaad" + integrity sha512-WVCnP+uLxlqB6UD98yQNV47mR5Rf79LFxpuZhSPhEf0Sb4tPZed3a63n003/dchhOwyCTCBuNN4n8XKJkLEI1Q== + dependencies: + applicationinsights "1.0.6" + +vscode-jsonrpc@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-4.0.0.tgz#a7bf74ef3254d0a0c272fab15c82128e378b3be9" + integrity sha512-perEnXQdQOJMTDFNv+UF3h1Y0z4iSiaN9jIlb0OqIYgosPCZGYh/MCUlkFtV2668PL69lRDO32hmvL2yiidUYg== + +vscode-languageclient@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-5.2.1.tgz#7cfc83a294c409f58cfa2b910a8cfeaad0397193" + integrity sha512-7jrS/9WnV0ruqPamN1nE7qCxn0phkH5LjSgSp9h6qoJGoeAKzwKz/PF6M+iGA/aklx4GLZg1prddhEPQtuXI1Q== + dependencies: + semver "^5.5.0" + vscode-languageserver-protocol "3.14.1" + +vscode-languageserver-protocol@3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.14.1.tgz#b8aab6afae2849c84a8983d39a1cf742417afe2f" + integrity sha512-IL66BLb2g20uIKog5Y2dQ0IiigW0XKrvmWiOvc0yXw80z3tMEzEnHjaGAb3ENuU7MnQqgnYJ1Cl2l9RvNgDi4g== + dependencies: + vscode-jsonrpc "^4.0.0" + vscode-languageserver-types "3.14.0" + +vscode-languageserver-types@3.14.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz#d3b5952246d30e5241592b6dde8280e03942e743" + integrity sha512-lTmS6AlAlMHOvPQemVwo3CezxBp0sNB95KNPkqp3Nxd5VFEnuG1ByM0zlRWos0zjO3ZWtkvhal0COgiV1xIA4A== + +vscode-nls@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167" + integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + +zone.js@0.7.6: + version "0.7.6" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009" + integrity sha1-+7w50+AmHQmG8boGMG6zrrDSIAk= diff --git a/src/sql/workbench/contrib/dashboard/browser/services/breadcrumb.service.ts b/src/sql/workbench/contrib/dashboard/browser/services/breadcrumb.service.ts index bbc32f6d6d..a3335967dc 100644 --- a/src/sql/workbench/contrib/dashboard/browser/services/breadcrumb.service.ts +++ b/src/sql/workbench/contrib/dashboard/browser/services/breadcrumb.service.ts @@ -61,8 +61,10 @@ export class BreadcrumbService implements IBreadcrumbService { } private getDbBreadcrumb(profile: ConnectionProfile): MenuItem { + let defaultDatabaseName = profile.providerName === 'LOGANALYTICS' ? 'workspace-name' : 'database-name'; + return { - label: profile.databaseName ? profile.databaseName : 'database-name', + label: profile.databaseName ? profile.databaseName : defaultDatabaseName, routerLink: ['database-dashboard'] }; } diff --git a/src/sql/workbench/contrib/query/browser/queryEditor.ts b/src/sql/workbench/contrib/query/browser/queryEditor.ts index 8f1a0c4bf1..3815861f1b 100644 --- a/src/sql/workbench/contrib/query/browser/queryEditor.ts +++ b/src/sql/workbench/contrib/query/browser/queryEditor.ts @@ -287,6 +287,20 @@ export class QueryEditor extends EditorPane { { action: this._listDatabasesAction } ]; } + else if (providerId === 'LOGANALYTICS' || this.modeService.getExtensions('LogAnalytics').indexOf(fileExtension) > -1) { + if (this.input instanceof UntitledQueryEditorInput) { + this.input.setMode('loganalytics'); + } + + content = [ + { action: this._runQueryAction }, + { action: this._cancelQueryAction }, + { element: separator }, + { action: this._toggleConnectDatabaseAction }, + { action: this._changeConnectionAction }, + { action: this._listDatabasesAction } + ]; + } else { if (previewFeaturesEnabled) { content = [ diff --git a/src/sql/workbench/contrib/query/browser/queryInputFactory.ts b/src/sql/workbench/contrib/query/browser/queryInputFactory.ts index db80d47c0e..2714ad56fc 100644 --- a/src/sql/workbench/contrib/query/browser/queryInputFactory.ts +++ b/src/sql/workbench/contrib/query/browser/queryInputFactory.ts @@ -28,7 +28,7 @@ const editorInputFactoryRegistry = Registry.as(Edit export class QueryEditorLanguageAssociation implements ILanguageAssociation { static readonly isDefault = true; - static readonly languages = ['sql', 'kusto']; //TODO Add language id here for new languages supported in query editor. Make it easier to contribute new extension's languageID + static readonly languages = ['sql', 'kusto', 'loganalytics']; //TODO Add language id here for new languages supported in query editor. Make it easier to contribute new extension's languageID constructor(@IInstantiationService private readonly instantiationService: IInstantiationService, diff --git a/src/sql/workbench/contrib/scripting/browser/scripting.contribution.ts b/src/sql/workbench/contrib/scripting/browser/scripting.contribution.ts index bbc5483f7e..d416676252 100644 --- a/src/sql/workbench/contrib/scripting/browser/scripting.contribution.ts +++ b/src/sql/workbench/contrib/scripting/browser/scripting.contribution.ts @@ -101,6 +101,7 @@ MenuRegistry.appendMenuItem(MenuId.ObjectExplorerItemContext, { }, when: ContextKeyExpr.and( ConnectionContextKey.Provider.notEqualsTo('KUSTO'), + ConnectionContextKey.Provider.notEqualsTo('LOGANALYTICS'), ContextKeyExpr.or( TreeNodeContextKey.NodeType.isEqualTo('Table'), TreeNodeContextKey.NodeType.isEqualTo('View') @@ -115,10 +116,9 @@ MenuRegistry.appendMenuItem(MenuId.ObjectExplorerItemContext, { id: commands.OE_SCRIPT_AS_SELECT_COMMAND_ID, title: localize('scriptKustoSelect', "Take 10") }, - when: ContextKeyExpr.and( - ConnectionContextKey.Provider.isEqualTo('KUSTO'), - TreeNodeContextKey.NodeType.isEqualTo('Table') - ) + when: ContextKeyExpr.or( + ContextKeyExpr.and(ConnectionContextKey.Provider.isEqualTo('KUSTO'), TreeNodeContextKey.NodeType.isEqualTo('Table')), + ContextKeyExpr.and(ConnectionContextKey.Provider.isEqualTo('LOGANALYTICS'), TreeNodeContextKey.NodeType.isEqualTo('Table'))) }); MenuRegistry.appendMenuItem(MenuId.ObjectExplorerItemContext, { @@ -132,6 +132,7 @@ MenuRegistry.appendMenuItem(MenuId.ObjectExplorerItemContext, { ContextKeyExpr.and( TreeNodeContextKey.NodeType.isEqualTo('Table'), ConnectionContextKey.Provider.notEqualsTo('KUSTO'), + ConnectionContextKey.Provider.notEqualsTo('LOGANALYTICS'), MssqlNodeContext.EngineEdition.notEqualsTo(DatabaseEngineEdition.SqlOnDemand.toString()), MssqlNodeContext.EngineEdition.notEqualsTo(DatabaseEngineEdition.SqlDataWarehouse.toString()) ) @@ -147,6 +148,7 @@ MenuRegistry.appendMenuItem(MenuId.ObjectExplorerItemContext, { when: ContextKeyExpr.and( ConnectionContextKey.Provider.notEqualsTo('KUSTO'), + ConnectionContextKey.Provider.notEqualsTo('LOGANALYTICS'), ContextKeyExpr.or( TreeNodeContextKey.NodeType.isEqualTo('Table'), TreeNodeContextKey.NodeType.isEqualTo('View'), @@ -218,6 +220,7 @@ MenuRegistry.appendMenuItem(MenuId.ObjectExplorerItemContext, { when: ContextKeyExpr.and( ConnectionContextKey.Provider.notEqualsTo('KUSTO'), + ConnectionContextKey.Provider.notEqualsTo('LOGANALYTICS'), ContextKeyExpr.or( TreeNodeContextKey.NodeType.isEqualTo(NodeType.Table), TreeNodeContextKey.NodeType.isEqualTo(NodeType.View), diff --git a/src/sql/workbench/services/queryEditor/browser/queryEditorService.ts b/src/sql/workbench/services/queryEditor/browser/queryEditorService.ts index 08012fd4fd..5f7213ae25 100644 --- a/src/sql/workbench/services/queryEditor/browser/queryEditorService.ts +++ b/src/sql/workbench/services/queryEditor/browser/queryEditorService.ts @@ -101,8 +101,8 @@ export class QueryEditorService implements IQueryEditorService { ////// Private functions private createUntitledSqlFilePath(providerName?: string): Promise { - if (providerName === 'KUSTO') { - return this.createPrefixedSqlFilePath(providerName + 'Query'); + if (providerName === 'LOGANALYTICS' || providerName === 'KUSTO') { + return this.createPrefixedSqlFilePath('KQLQuery'); } return this.createPrefixedSqlFilePath('SQLQuery');