diff --git a/package.json b/package.json index 14493a155d4..c4efcaeb4dd 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "@grafana/api-documenter": "7.11.2", "@grafana/api-extractor": "7.10.1", "@grafana/eslint-config": "2.4.0", + "@kusto/monaco-kusto": "3.2.7", "@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1", "@testing-library/jest-dom": "5.11.5", "@testing-library/react": "11.1.2", diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index b06748151fe..80694043234 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -14,6 +14,7 @@ export * from './valueFormats'; export * from './field'; export * from './events'; export * from './themes'; +export * from './monaco'; export { ValueMatcherOptions, BasicValueMatcherOptions, diff --git a/packages/grafana-data/src/monaco/index.ts b/packages/grafana-data/src/monaco/index.ts new file mode 100644 index 00000000000..4aed44e4d94 --- /dev/null +++ b/packages/grafana-data/src/monaco/index.ts @@ -0,0 +1 @@ +export * from './languageRegistry'; diff --git a/packages/grafana-data/src/monaco/languageRegistry.ts b/packages/grafana-data/src/monaco/languageRegistry.ts new file mode 100644 index 00000000000..5a8fe8bded7 --- /dev/null +++ b/packages/grafana-data/src/monaco/languageRegistry.ts @@ -0,0 +1,7 @@ +import { Registry, RegistryItem } from '../utils/Registry'; + +export interface MonacoLanguageRegistryItem extends RegistryItem { + init: () => Promise; +} + +export const monacoLanguageRegistry = new Registry(); diff --git a/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx b/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx index e8ee8ccf047..3c2e390f969 100644 --- a/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx +++ b/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx @@ -1,15 +1,16 @@ import React from 'react'; +import { css } from '@emotion/css'; +import MonacoEditor, { loader as monacoEditorLoader } from '@monaco-editor/react'; +import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; +import { selectors } from '@grafana/e2e-selectors'; +import { GrafanaTheme2, monacoLanguageRegistry } from '@grafana/data'; + import { withTheme2 } from '../../themes'; import { Themeable2 } from '../../types'; -import { selectors } from '@grafana/e2e-selectors'; -import { GrafanaTheme2 } from '@grafana/data'; -import { Monaco, MonacoEditor as MonacoEditorType, CodeEditorProps, MonacoOptions } from './types'; -import { registerSuggestions } from './suggestions'; -import MonacoEditor, { loader as monacoEditorLoader } from '@monaco-editor/react'; -import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; +import { CodeEditorProps, Monaco, MonacoEditor as MonacoEditorType, MonacoOptions } from './types'; +import { registerSuggestions } from './suggestions'; import defineThemes from './theme'; -import { css } from '@emotion/css'; type Props = CodeEditorProps & Themeable2; @@ -58,9 +59,23 @@ class UnthemedCodeEditor extends React.PureComponent { if (getSuggestions) { this.completionCancel = registerSuggestions(this.monaco, language, getSuggestions); } + + this.loadCustomLanguage(); } } + loadCustomLanguage = () => { + const { language } = this.props; + + const customLanguage = monacoLanguageRegistry.getIfExists(language); + + if (customLanguage) { + return customLanguage.init(); + } + + return Promise.resolve(); + }; + // This is replaced with a real function when the actual editor mounts getEditorValue = () => ''; @@ -83,7 +98,6 @@ class UnthemedCodeEditor extends React.PureComponent { handleOnMount = (editor: MonacoEditorType, monaco: Monaco) => { const { onSave, onEditorDidMount } = this.props; - this.getEditorValue = () => editor.getValue(); if (onSave) { @@ -92,8 +106,10 @@ class UnthemedCodeEditor extends React.PureComponent { }); } + const languagePromise = this.loadCustomLanguage(); + if (onEditorDidMount) { - onEditorDidMount(editor); + languagePromise.then(() => onEditorDidMount(editor, monaco)); } }; diff --git a/packages/grafana-ui/src/components/Monaco/types.ts b/packages/grafana-ui/src/components/Monaco/types.ts index 0d975639f7d..82cb1f5861b 100644 --- a/packages/grafana-ui/src/components/Monaco/types.ts +++ b/packages/grafana-ui/src/components/Monaco/types.ts @@ -21,10 +21,8 @@ export interface CodeEditorProps { /** * Callback after the editor has mounted that gives you raw access to monaco - * - * @alpha -- experimental */ - onEditorDidMount?: (editor: monacoType.editor.IStandaloneCodeEditor) => void; + onEditorDidMount?: (editor: MonacoEditor, monaco: Monaco) => void; /** Handler to be performed when editor is blurred */ onBlur?: CodeEditorChangeHandler; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index d270b248a10..a9f05b23cd6 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -36,7 +36,7 @@ export { QueryField } from './QueryField/QueryField'; // Code editor export { CodeEditor } from './Monaco/CodeEditorLazy'; -export { MonacoEditor, CodeEditorSuggestionItem, CodeEditorSuggestionItemKind } from './Monaco/types'; +export { Monaco, MonacoEditor, CodeEditorSuggestionItem, CodeEditorSuggestionItemKind } from './Monaco/types'; export { variableSuggestionToCodeEditorSuggestion } from './Monaco/utils'; // TODO: namespace diff --git a/public/app/app.ts b/public/app/app.ts index 00cdf9f84ed..17ebd100305 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -15,6 +15,7 @@ import config from 'app/core/config'; // @ts-ignore ignoring this for now, otherwise we would have to extend _ interface with move import { locationUtil, + monacoLanguageRegistry, setLocale, setTimeZoneResolver, standardEditorsRegistry, @@ -45,6 +46,7 @@ import { getTimeSrv } from './features/dashboard/services/TimeSrv'; import { getVariablesUrlParams } from './features/variables/getAllVariableValuesForUrl'; import { SafeDynamicImport } from './core/components/DynamicImports/SafeDynamicImport'; import { featureToggledRoutes } from './routes/routes'; +import getDefaultMonacoLanguages from '../lib/monaco-languages'; // add move to lodash for backward compatabilty with plugins // @ts-ignore @@ -96,6 +98,7 @@ export class GrafanaApp { standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs); standardTransformersRegistry.setInit(getStandardTransformers); variableAdapters.setInit(getDefaultVariableAdapters); + monacoLanguageRegistry.setInit(getDefaultMonacoLanguages); setQueryRunnerFactory(() => new QueryRunner()); setVariableQueryRunner(new VariableQueryRunner()); diff --git a/public/app/core/utils/deferred.ts b/public/app/core/utils/deferred.ts index f74899171dc..2108df00d68 100644 --- a/public/app/core/utils/deferred.ts +++ b/public/app/core/utils/deferred.ts @@ -1,10 +1,12 @@ -export class Deferred { - resolve: any; - reject: any; - promise: Promise; +export class Deferred { + resolve?: (reason?: T | PromiseLike) => void; + reject?: (reason?: any) => void; + promise: Promise; + constructor() { - this.resolve = null; - this.reject = null; + this.resolve = undefined; + this.reject = undefined; + this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts index 7c688ce5eef..63ccbc3595f 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/datasource.ts @@ -30,6 +30,10 @@ export default function createMockDatasource() { supportedTimeGrains: [], dimensions: [], }), + + azureLogAnalyticsDatasource: { + getKustoSchema: () => Promise.resolve(), + }, }; const mockDatasource = _mockDatasource as Datasource; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/__mocks__/schema.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/__mocks__/schema.ts index e9f233f5209..b60cc97d138 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/__mocks__/schema.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/__mocks__/schema.ts @@ -286,6 +286,22 @@ export default class FakeSchemaData { body: 'AzureActivity\n| where ActivityStatus == "" \n', category: 'test', }, + { + id: '19551c5e-1e3e-4425-a1d7-c846a0bca2a1', + name: '_AzureBackup_GetVaults', + displayName: '_AzureBackup_GetVaults', + description: + 'Returns the list of Recovery Sevices vaults in your Azure environment that are associated with the workspace', + body: + '// Params\r\nlet _RangeStart = iff((isnull(RangeStart)), startofday(ago(1d)), startofday(RangeStart));\r\nlet _RangeEnd = iff((isnull(RangeEnd)), startofday(now()), startofday(RangeEnd) + 1d);\r\nlet _VaultSubscriptionList = split(VaultSubscriptionList, \',\');\r\nlet _VaultLocationList = split(VaultLocationList, \',\');\r\nlet _VaultList = split(VaultList, \',\');\r\nlet _VaultTypeList = split(VaultTypeList, \',\');\r\nlet _ExcludeLegacyEvent = ExcludeLegacyEvent;\r\n// Other Vars\r\nlet AsonDay = _RangeEnd-1d;\r\n// Source Tables\r\nlet VaultUnderAzureDiagnostics = ()\r\n{\r\nAzureDiagnostics\r\n// Take records until previous day\r\n| where TimeGenerated >= _RangeStart and TimeGenerated <= _RangeEnd and TimeGenerated < startofday(now())\r\n| where Category == "AzureBackupReport" and OperationName == "Vault" and columnifexists("SchemaVersion_s", "") == "V2"\r\n| project VaultName = columnifexists("VaultName_s", ""), VaultUniqueId = columnifexists("VaultUniqueId_s", ""), VaultTags = columnifexists("VaultTags_s", ""), AzureDataCenter = columnifexists("AzureDataCenter_s", ""), ResourceGroupName = columnifexists("ResourceGroupName_s", ""), SubscriptionId = toupper(SubscriptionId), StorageReplicationType = columnifexists("StorageReplicationType_s", ""), ResourceId, TimeGenerated \r\n| where SubscriptionId in~ (_VaultSubscriptionList) or \'*\' in (_VaultSubscriptionList)\r\n| where AzureDataCenter in~ (_VaultLocationList) or \'*\' in (_VaultLocationList)\r\n| where VaultName in~ (_VaultList) or \'*\' in (_VaultList)\r\n| summarize arg_max(TimeGenerated, *) by ResourceId\r\n| project StorageReplicationType, VaultUniqueId, VaultName, VaultTags, SubscriptionId, ResourceGroupName, AzureDataCenter, ResourceId, TimeGenerated\r\n};\r\nlet VaultUnderResourceSpecific = ()\r\n{\r\nCoreAzureBackup\r\n// Take records until previous day\r\n| where TimeGenerated >= _RangeStart and TimeGenerated <= _RangeEnd and TimeGenerated < startofday(now())\r\n| where OperationName == "Vault" \r\n| project StorageReplicationType, VaultUniqueId, VaultName, VaultTags, SubscriptionId = toupper(SubscriptionId), ResourceGroupName, AzureDataCenter, ResourceId, TimeGenerated \r\n| where SubscriptionId in~ (_VaultSubscriptionList) or \'*\' in (_VaultSubscriptionList)\r\n| where AzureDataCenter in~ (_VaultLocationList) or \'*\' in (_VaultLocationList)\r\n| where VaultName in~ (_VaultList) or \'*\' in (_VaultList)\r\n| summarize arg_max(TimeGenerated, *) by ResourceId\r\n};\r\nlet VaultHistoryUnderAzureDiagnostics = ()\r\n{\r\nAzureDiagnostics\r\n// Take records until previous day\r\n| where TimeGenerated >= _RangeStart and TimeGenerated <= _RangeEnd and TimeGenerated < startofday(now())\r\n| where Category == "AzureBackupReport" and OperationName == "Vault" and columnifexists("SchemaVersion_s", "") == "V2"\r\n| project VaultName = columnifexists("VaultName_s", ""), VaultUniqueId = columnifexists("VaultUniqueId_s", ""), VaultTags = columnifexists("VaultTags_s", ""), AzureDataCenter = columnifexists("AzureDataCenter_s", ""), ResourceGroupName = columnifexists("ResourceGroupName_s", ""), SubscriptionId = toupper(SubscriptionId), StorageReplicationType = columnifexists("StorageReplicationType_s", ""), ResourceId, TimeGenerated \r\n| where SubscriptionId in~ (_VaultSubscriptionList) or \'*\' in (_VaultSubscriptionList)\r\n| where AzureDataCenter in~ (_VaultLocationList) or \'*\' in (_VaultLocationList)\r\n| where VaultName in~ (_VaultList) or \'*\' in (_VaultList)\r\n| summarize arg_max(TimeGenerated, *) by ResourceId, startofday(TimeGenerated)\r\n| project StorageReplicationType, VaultUniqueId, VaultName, VaultTags, SubscriptionId, ResourceGroupName, AzureDataCenter, ResourceId, TimeGenerated = TimeGenerated1\r\n};\r\nlet VaultHistoryUnderResourceSpecific = ()\r\n{\r\nCoreAzureBackup\r\n// Take records until previous day\r\n| where TimeGenerated >= _RangeStart and TimeGenerated <= _RangeEnd and TimeGenerated < startofday(now())\r\n| where OperationName == "Vault" \r\n| project StorageReplicationType, VaultUniqueId, VaultName, VaultTags, SubscriptionId = toupper(SubscriptionId), ResourceGroupName, AzureDataCenter, ResourceId, TimeGenerated \r\n| where SubscriptionId in~ (_VaultSubscriptionList) or \'*\' in (_VaultSubscriptionList)\r\n| where AzureDataCenter in~ (_VaultLocationList) or \'*\' in (_VaultLocationList)\r\n| where VaultName in~ (_VaultList) or \'*\' in (_VaultList)\r\n| summarize arg_max(TimeGenerated, *) by ResourceId, startofday(TimeGenerated)\r\n| project StorageReplicationType, VaultUniqueId, VaultName, VaultTags, SubscriptionId, ResourceGroupName, AzureDataCenter, ResourceId, TimeGenerated = TimeGenerated1\r\n};\r\nlet Vault_LatestTable = () {union isfuzzy = true \r\n(VaultUnderAzureDiagnostics() | where _ExcludeLegacyEvent == false),\r\n(VaultUnderResourceSpecific())\r\n// To show as per as on \'AsonDay\'\r\n| where startofday(TimeGenerated) == AsonDay\r\n| summarize arg_max(TimeGenerated, *) by VaultUniqueId\r\n| project StorageReplicationType, VaultUniqueId, VaultName, VaultTags, SubscriptionId, ResourceGroupName, AzureDataCenter, ResourceId, TimeGenerated};\r\nlet Vault_HistoryTable = () {union isfuzzy = true \r\n(VaultHistoryUnderAzureDiagnostics() | where _ExcludeLegacyEvent == false),\r\n(VaultHistoryUnderResourceSpecific())\r\n| summarize arg_max(TimeGenerated, *) by VaultUniqueId, startofday(TimeGenerated)\r\n| project StorageReplicationType, VaultUniqueId, VaultName, VaultTags, SubscriptionId, ResourceGroupName, AzureDataCenter, ResourceId, TimeGenerated = TimeGenerated1};\r\n// FinalTable From V1 Vault\r\nlet FinalTable_V1Vault = () {union (Vault_LatestTable | where (_RangeEnd-_RangeStart == 1d)), (Vault_HistoryTable | where (_RangeEnd-_RangeStart > 1d))\r\n| project UniqueId = VaultUniqueId, Name = VaultName, Id = ResourceId, SubscriptionId, Location = AzureDataCenter, VaultStore_StorageReplicationType = StorageReplicationType, Tags = VaultTags, TimeGenerated, Type = "Microsoft.RecoveryServices/vaults"\r\n};\r\n// FinalTable_DPPVault to be added later\r\nFinalTable_V1Vault \r\n| where "Microsoft.RecoveryServices/vaults" in~ (_VaultTypeList) or \'*\' in (_VaultTypeList)', + parameters: + 'RangeStart:datetime = datetime(null), VaultSubscriptionList:string="*", ExcludeLegacyEvent:bool=True', + related: { + solutions: ['LogManagement'], + categories: ['Management'], + tables: ['AzureDiagnostics', 'CoreAzureBackup'], + }, + }, ], applications: [], workspaces: [ diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts index 609f11f113e..9ee7bdfaeeb 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts @@ -1,7 +1,7 @@ import AzureMonitorDatasource from '../datasource'; import FakeSchemaData from './__mocks__/schema'; import { TemplateSrv } from 'app/features/templating/template_srv'; -import { AzureLogsVariable, KustoSchema } from '../types'; +import { AzureLogsVariable } from '../types'; import { toUtc } from '@grafana/data'; import { backendSrv } from 'app/core/services/backend_srv'; @@ -125,23 +125,39 @@ describe('AzureLogAnalyticsDatasource', () => { beforeEach(() => { datasourceRequestMock.mockImplementation((options: { url: string }) => { expect(options.url).toContain('metadata'); - return Promise.resolve({ data: FakeSchemaData.getlogAnalyticsFakeMetadata(), status: 200 }); + return Promise.resolve({ data: FakeSchemaData.getlogAnalyticsFakeMetadata(), status: 200, ok: true }); }); }); - it('should return a schema with a table and rows', () => { - return ctx.ds.azureLogAnalyticsDatasource.getSchema('myWorkspace').then((result: KustoSchema) => { - expect(Object.keys(result.Databases.Default.Tables).length).toBe(2); - expect(result.Databases.Default.Tables.Alert.Name).toBe('Alert'); - expect(result.Databases.Default.Tables.AzureActivity.Name).toBe('AzureActivity'); - expect(result.Databases.Default.Tables.Alert.OrderedColumns.length).toBe(69); - expect(result.Databases.Default.Tables.AzureActivity.OrderedColumns.length).toBe(21); - expect(result.Databases.Default.Tables.Alert.OrderedColumns[0].Name).toBe('TimeGenerated'); - expect(result.Databases.Default.Tables.Alert.OrderedColumns[0].Type).toBe('datetime'); - - expect(Object.keys(result.Databases.Default.Functions).length).toBe(1); - expect(result.Databases.Default.Functions.Func1.Name).toBe('Func1'); - }); + it('should return a schema to use with monaco-kusto', async () => { + const result = await ctx.ds.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace'); + + expect(result.database.tables).toHaveLength(2); + expect(result.database.tables[0].name).toBe('Alert'); + expect(result.database.tables[0].timespanColumn).toBe('TimeGenerated'); + expect(result.database.tables[1].name).toBe('AzureActivity'); + expect(result.database.tables[0].columns).toHaveLength(69); + + expect(result.database.functions[1].inputParameters).toEqual([ + { + name: 'RangeStart', + type: 'datetime', + defaultValue: 'datetime(null)', + cslDefaultValue: 'datetime(null)', + }, + { + name: 'VaultSubscriptionList', + type: 'string', + defaultValue: '"*"', + cslDefaultValue: '"*"', + }, + { + name: 'ExcludeLegacyEvent', + type: 'bool', + defaultValue: 'True', + cslDefaultValue: 'True', + }, + ]); }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts index c1e9d06877f..c13d35cdc7d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts @@ -1,6 +1,6 @@ import { map } from 'lodash'; import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder'; -import ResponseParser from './response_parser'; +import ResponseParser, { transformMetadataToKustoSchema } from './response_parser'; import { AzureMonitorQuery, AzureDataSourceJsonData, AzureLogsVariable, AzureQueryType } from '../types'; import { DataQueryRequest, @@ -9,9 +9,10 @@ import { DataSourceInstanceSettings, MetricFindValue, } from '@grafana/data'; -import { getBackendSrv, getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime'; +import { getBackendSrv, getTemplateSrv, DataSourceWithBackend, FetchResponse } from '@grafana/runtime'; import { Observable, from } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; +import { AzureLogAnalyticsMetadata } from '../types/logAnalyticsMetadata'; export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< AzureMonitorQuery, @@ -107,15 +108,20 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< return this.doRequest(workspaceListUrl, true); } - getSchema(workspace: string) { - if (!workspace) { - return Promise.resolve(); - } + async getMetadata(workspace: string) { const url = `${this.baseUrl}/${getTemplateSrv().replace(workspace, {})}/metadata`; + const resp = await this.doRequest(url); - return this.doRequest(url).then((response: any) => { - return new ResponseParser(response.data).parseSchemaResult(); - }); + if (!resp.ok) { + throw new Error('Unable to get metadata for workspace'); + } + + return resp.data; + } + + async getKustoSchema(workspace: string) { + const metadata = await this.getMetadata(workspace); + return transformMetadataToKustoSchema(metadata, workspace); } applyTemplateVariables(target: AzureMonitorQuery, scopedVars: ScopedVars): Record { @@ -348,7 +354,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< }); } - async doRequest(url: string, useCache = false, maxRetries = 1): Promise { + async doRequest(url: string, useCache = false, maxRetries = 1): Promise> { try { if (useCache && this.cache.has(url)) { return this.cache.get(url); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.test.ts deleted file mode 100644 index 7de47e887cb..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import ResponseParser from './response_parser'; -import { expect } from '../../../../../test/lib/common'; - -describe('createSchemaFunctions', () => { - describe('when called and results have functions', () => { - it('then it should return correct result', () => { - const functions = [ - { name: 'some name', body: 'some body', displayName: 'some displayName', category: 'some category' }, - ]; - const parser = new ResponseParser({ functions }); - - const results = parser.createSchemaFunctions(); - - expect(results).toEqual({ - ['some name']: { - Body: 'some body', - DocString: 'some displayName', - Folder: 'some category', - FunctionKind: 'Unknown', - InputParameters: [], - Name: 'some name', - OutputColumns: [], - }, - }); - }); - }); - - describe('when called and results have no functions', () => { - it('then it should return an empty object', () => { - const parser = new ResponseParser({}); - - const results = parser.createSchemaFunctions(); - - expect(results).toEqual({}); - }); - }); -}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts index d4398a49af7..233fca46591 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts @@ -1,14 +1,7 @@ import { concat, find, flattenDeep, forEach, map } from 'lodash'; import { AnnotationEvent, dateTime, TimeSeries } from '@grafana/data'; -import { - AzureLogsTableData, - AzureLogsVariable, - KustoColumn, - KustoDatabase, - KustoFunction, - KustoSchema, - KustoTable, -} from '../types'; +import { AzureLogsTableData, AzureLogsVariable } from '../types'; +import { AzureLogAnalyticsMetadata } from '../types/logAnalyticsMetadata'; export default class ResponseParser { columns: string[]; @@ -141,73 +134,6 @@ export default class ResponseParser { return list; } - parseSchemaResult(): KustoSchema { - return { - Plugins: [ - { - Name: 'pivot', - }, - ], - Databases: this.createSchemaDatabaseWithTables(), - }; - } - - createSchemaDatabaseWithTables(): { [key: string]: KustoDatabase } { - const databases = { - Default: { - Name: 'Default', - Tables: this.createSchemaTables(), - Functions: this.createSchemaFunctions(), - }, - }; - - return databases; - } - - createSchemaTables(): { [key: string]: KustoTable } { - const tables: { [key: string]: KustoTable } = {}; - - for (const table of this.results.tables) { - tables[table.name] = { - Name: table.name, - OrderedColumns: [], - }; - for (const col of table.columns) { - tables[table.name].OrderedColumns.push(this.convertToKustoColumn(col)); - } - } - - return tables; - } - - convertToKustoColumn(col: any): KustoColumn { - return { - Name: col.name, - Type: col.type, - }; - } - - createSchemaFunctions(): { [key: string]: KustoFunction } { - const functions: { [key: string]: KustoFunction } = {}; - if (!this.results.functions) { - return functions; - } - - for (const func of this.results.functions) { - functions[func.name] = { - Name: func.name, - Body: func.body, - DocString: func.displayName, - Folder: func.category, - FunctionKind: 'Unknown', - InputParameters: [], - OutputColumns: [], - }; - } - - return functions; - } - static findOrCreateBucket(data: TimeSeries[], target: any): TimeSeries { let dataTarget: any = find(data, ['target', target]); if (!dataTarget) { @@ -222,3 +148,64 @@ export default class ResponseParser { return dateTime(dateTimeValue).valueOf(); } } + +// matches (name):(type) = (defaultValue) +// e.g. fromRangeStart:datetime = datetime(null) +// - name: fromRangeStart +// - type: datetime +// - defaultValue: datetime(null) +const METADATA_FUNCTION_PARAMS = /([\w\W]+):([\w]+)(?:\s?=\s?([\w\W]+))?/; + +function transformMetadataFunction(sourceSchema: AzureLogAnalyticsMetadata) { + if (!sourceSchema.functions) { + return []; + } + + return sourceSchema.functions.map((fn) => { + const params = + fn.parameters && + fn.parameters + .split(', ') + .map((arg) => { + const match = arg.match(METADATA_FUNCTION_PARAMS); + if (!match) { + return; + } + + const [, name, type, defaultValue] = match; + + return { + name, + type, + defaultValue, + cslDefaultValue: defaultValue, + }; + }) + .filter((v: T): v is Exclude => !!v); + + return { + name: fn.name, + body: fn.body, + inputParameters: params || [], + }; + }); +} + +export function transformMetadataToKustoSchema(sourceSchema: AzureLogAnalyticsMetadata, nameOrIdOrSomething: string) { + const database = { + name: nameOrIdOrSomething, + tables: sourceSchema.tables, + functions: transformMetadataFunction(sourceSchema), + majorVersion: 0, + minorVersion: 0, + }; + + return { + clusterType: 'Engine', + cluster: { + connectionString: nameOrIdOrSomething, + databases: [database], + }, + database: database, + }; +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx index ef82038d1a9..60b5cce0ff7 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/QueryField.tsx @@ -1,8 +1,61 @@ -import { CodeEditor } from '@grafana/ui'; -import React, { useCallback } from 'react'; +import { CodeEditor, Monaco, MonacoEditor } from '@grafana/ui'; +import { Deferred } from 'app/core/utils/deferred'; +import React, { useCallback, useEffect, useRef } from 'react'; import { AzureQueryEditorFieldProps } from '../../types'; -const QueryField: React.FC = ({ query, onQueryChange }) => { +interface MonacoPromise { + editor: MonacoEditor; + monaco: Monaco; +} + +interface MonacoLanguages { + kusto: { + getKustoWorker: () => Promise< + ( + url: any + ) => Promise<{ + setSchema: (schema: any, clusterUrl: string, name: string) => void; + }> + >; + }; +} + +const QueryField: React.FC = ({ query, datasource, onQueryChange }) => { + const monacoPromiseRef = useRef>(); + function getPromise() { + if (!monacoPromiseRef.current) { + monacoPromiseRef.current = new Deferred(); + } + + return monacoPromiseRef.current.promise; + } + + useEffect(() => { + const promises = [ + datasource.azureLogAnalyticsDatasource.getKustoSchema(query.azureLogAnalytics.workspace), + getPromise(), + ] as const; + + // the kusto schema call might fail, but its okay for that to happen silently + Promise.all(promises).then(([schema, { monaco, editor }]) => { + const languages = (monaco.languages as unknown) as MonacoLanguages; + + languages.kusto.getKustoWorker().then((kusto) => { + const model = editor.getModel(); + if (!model) { + return; + } + kusto(model.uri).then((worker) => { + worker.setSchema(schema, 'https://help.kusto.windows.net', 'Samples'); + }); + }); + }); + }, [datasource.azureLogAnalyticsDatasource, query.azureLogAnalytics.workspace]); + + const handleEditorMount = useCallback((editor: MonacoEditor, monaco: Monaco) => { + monacoPromiseRef.current?.resolve?.({ editor, monaco }); + }, []); + const onChange = useCallback( (newQuery: string) => { onQueryChange({ @@ -19,12 +72,13 @@ const QueryField: React.FC = ({ query, onQueryChange return ( ); }; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/index.ts similarity index 86% rename from public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts rename to public/app/plugins/datasource/grafana-azure-monitor-datasource/types/index.ts index 14e110a6a5e..13fecbf320a 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/index.ts @@ -1,5 +1,5 @@ import { DataQuery, DataSourceJsonData, DataSourceSettings, TableData } from '@grafana/data'; -import Datasource from './datasource'; +import Datasource from '../datasource'; export type AzureDataSourceSettings = DataSourceSettings; @@ -139,37 +139,6 @@ export interface AzureMonitorResourceGroupsResponse { statusText: string; } -// Azure Log Analytics types -export interface KustoSchema { - Databases: { [key: string]: KustoDatabase }; - Plugins: any[]; -} -export interface KustoDatabase { - Name: string; - Tables: { [key: string]: KustoTable }; - Functions: { [key: string]: KustoFunction }; -} - -export interface KustoTable { - Name: string; - OrderedColumns: KustoColumn[]; -} - -export interface KustoColumn { - Name: string; - Type: string; -} - -export interface KustoFunction { - Name: string; - DocString: string; - Body: string; - Folder: string; - FunctionKind: string; - InputParameters: any[]; - OutputColumns: any[]; -} - export interface AzureLogsVariable { text: string; value: string; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/logAnalyticsMetadata.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/logAnalyticsMetadata.ts new file mode 100644 index 00000000000..436d9e768dc --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/logAnalyticsMetadata.ts @@ -0,0 +1,96 @@ +export interface AzureLogAnalyticsMetadata { + functions: AzureLogAnalyticsMetadataFunction[]; + resourceTypes: AzureLogAnalyticsMetadataResourceType[]; + tables: AzureLogAnalyticsMetadataTable[]; + solutions: AzureLogAnalyticsMetadataSolution[]; + workspaces: AzureLogAnalyticsMetadataWorkspace[]; + categories: AzureLogAnalyticsMetadataCategory[]; +} + +export interface AzureLogAnalyticsMetadataCategory { + id: string; + displayName: string; + related: AzureLogAnalyticsMetadataCategoryRelated; +} + +export interface AzureLogAnalyticsMetadataCategoryRelated { + tables: string[]; + functions?: string[]; +} + +export interface AzureLogAnalyticsMetadataFunction { + id: string; + name: string; + displayName?: string; + description: string; + body: string; + parameters?: string; + related: AzureLogAnalyticsMetadataFunctionRelated; +} + +export interface AzureLogAnalyticsMetadataFunctionRelated { + solutions: string[]; + categories?: string[]; + tables: string[]; +} + +export interface AzureLogAnalyticsMetadataResourceType { + id: string; + type: string; + displayName: string; + description: string; + related: AzureLogAnalyticsMetadataResourceTypeRelated; +} + +export interface AzureLogAnalyticsMetadataResourceTypeRelated { + tables: string[]; + workspaces: string[]; +} + +export interface AzureLogAnalyticsMetadataSolution { + id: string; + name: string; + related: AzureLogAnalyticsMetadataSolutionRelated; +} + +export interface AzureLogAnalyticsMetadataSolutionRelated { + tables: string[]; + functions: string[]; + workspaces: string[]; +} + +export interface AzureLogAnalyticsMetadataTable { + id: string; + name: string; + description?: string; + timespanColumn: string; + columns: AzureLogAnalyticsMetadataColumn[]; + related: AzureLogAnalyticsMetadataTableRelated; + isTroubleshootingAllowed?: boolean; + hasData?: boolean; +} + +export interface AzureLogAnalyticsMetadataColumn { + name: string; + type: string; + description?: string; + isPreferredFacet?: boolean; +} + +export interface AzureLogAnalyticsMetadataTableRelated { + categories?: string[]; + solutions: string[]; + functions?: string[]; +} + +export interface AzureLogAnalyticsMetadataWorkspace { + id: string; + resourceId: string; + name: string; + region: string; + related: AzureLogAnalyticsMetadataWorkspaceRelated; +} + +export interface AzureLogAnalyticsMetadataWorkspaceRelated { + solutions: string[]; +} diff --git a/public/lib/monaco-languages/index.ts b/public/lib/monaco-languages/index.ts new file mode 100644 index 00000000000..585bd779c49 --- /dev/null +++ b/public/lib/monaco-languages/index.ts @@ -0,0 +1,6 @@ +import loadKusto from './kusto'; + +export default function getDefaultMonacoLanguages() { + const kusto = { id: 'kusto', name: 'kusto', init: loadKusto }; + return [kusto]; +} diff --git a/public/lib/monaco-languages/kusto.ts b/public/lib/monaco-languages/kusto.ts new file mode 100644 index 00000000000..4c2e40abef6 --- /dev/null +++ b/public/lib/monaco-languages/kusto.ts @@ -0,0 +1,65 @@ +declare global { + interface Window { + __monacoKustoResolvePromise: (value: unknown) => void; + __grafana_public_path__: string; + } +} + +const monacoPath = (window.__grafana_public_path__ ?? 'public/') + 'lib/monaco/min/vs'; + +const scripts = [ + [`${monacoPath}/language/kusto/bridge.min.js`], + [ + `${monacoPath}/language/kusto/kusto.javascript.client.min.js`, + `${monacoPath}/language/kusto/newtonsoft.json.min.js`, + `${monacoPath}/language/kusto/Kusto.Language.Bridge.min.js`, + ], +]; + +function loadScript(script: HTMLScriptElement | string): Promise { + return new Promise((resolve, reject) => { + let scriptEl: HTMLScriptElement; + + if (typeof script === 'string') { + scriptEl = document.createElement('script'); + scriptEl.src = script; + } else { + scriptEl = script; + } + + scriptEl.onload = () => resolve(); + scriptEl.onerror = (err) => reject(err); + document.body.appendChild(scriptEl); + }); +} + +const loadMonacoKusto = () => { + return new Promise((resolve) => { + window.__monacoKustoResolvePromise = resolve; + + const script = document.createElement('script'); + script.innerHTML = `require(['vs/language/kusto/monaco.contribution'], function() { + window.__monacoKustoResolvePromise(); + });`; + + return document.body.appendChild(script); + }); +}; + +export default async function loadKusto() { + let promise = Promise.resolve(); + + for (const parallelScripts of scripts) { + await promise; + + // Load all these scripts in parallel, then wait for them all to finish before continuing + // to the next iteration + const allPromises = parallelScripts + .filter((src) => !document.querySelector(`script[src="${src}"]`)) + .map((src) => loadScript(src)); + + await Promise.all(allPromises); + } + + await loadMonacoKusto(); +} diff --git a/scripts/webpack/webpack.common.js b/scripts/webpack/webpack.common.js index cba5ff6c397..a3e5752cb18 100644 --- a/scripts/webpack/webpack.common.js +++ b/scripts/webpack/webpack.common.js @@ -88,10 +88,10 @@ module.exports = { ], }, }, - // { - // from: './node_modules/@kusto/monaco-kusto/release/min/', - // to: 'monaco/min/vs/language/kusto/', - // }, + { + from: './node_modules/@kusto/monaco-kusto/release/min/', + to: '../lib/monaco/min/vs/language/kusto/', + }, ], }), ], diff --git a/yarn.lock b/yarn.lock index 737e1243c15..0e55f5c19e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2893,6 +2893,24 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@kusto/language-service-next@0.0.42": + version "0.0.42" + resolved "https://registry.yarnpkg.com/@kusto/language-service-next/-/language-service-next-0.0.42.tgz#58d69d0a7b5727f0101e59da39928b5dfb8c2749" + integrity sha512-WI/gZjm4/qeXkA/MpnorXNlhImREafVabGwXOsjnb7VQ8fehOxUTGHbhP9kirJqqSJYx9HG7Pf1CFSYIX9CJJw== + +"@kusto/language-service@2.0.0-beta.0": + version "2.0.0-beta.0" + resolved "https://registry.yarnpkg.com/@kusto/language-service/-/language-service-2.0.0-beta.0.tgz#70ea2f7c5d076d762a7c9a03194479effd870cd3" + integrity sha512-HBMASNCxtUe+BPOONpiXhzlCXuS0UIWl9YRrh521dTbEsoDwBN7Orlq6SUlDqKKdy7i4N4+7KtGFwwRjsgke7A== + +"@kusto/monaco-kusto@3.2.7": + version "3.2.7" + resolved "https://registry.yarnpkg.com/@kusto/monaco-kusto/-/monaco-kusto-3.2.7.tgz#57f31022a9790a1cc1f8a2ecfcf866afe67927ba" + integrity sha512-PcMGb04G1pKsnPYD1HSkURaLsdw9gcaP9yB+qYWvb178HCwJCGrTGyCO/QmV2CbvRACUQjrtTmLo+llZOmLqDA== + dependencies: + "@kusto/language-service" "2.0.0-beta.0" + "@kusto/language-service-next" "0.0.42" + "@lerna/add@3.21.0": version "3.21.0" resolved "https://registry.yarnpkg.com/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b"