mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 08:56:43 -06:00
AzureMonitor: Kusto language support (#33528)
* Add Kusto custom language to Monaco * Load Kusto schema into monaco * cleanup + tests * cleanup + tests * cleanup :) * move monaco languages to a registry
This commit is contained in:
parent
4fabade35c
commit
72c9d806fd
@ -81,6 +81,7 @@
|
|||||||
"@grafana/api-documenter": "7.11.2",
|
"@grafana/api-documenter": "7.11.2",
|
||||||
"@grafana/api-extractor": "7.10.1",
|
"@grafana/api-extractor": "7.10.1",
|
||||||
"@grafana/eslint-config": "2.4.0",
|
"@grafana/eslint-config": "2.4.0",
|
||||||
|
"@kusto/monaco-kusto": "3.2.7",
|
||||||
"@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1",
|
"@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1",
|
||||||
"@testing-library/jest-dom": "5.11.5",
|
"@testing-library/jest-dom": "5.11.5",
|
||||||
"@testing-library/react": "11.1.2",
|
"@testing-library/react": "11.1.2",
|
||||||
|
@ -14,6 +14,7 @@ export * from './valueFormats';
|
|||||||
export * from './field';
|
export * from './field';
|
||||||
export * from './events';
|
export * from './events';
|
||||||
export * from './themes';
|
export * from './themes';
|
||||||
|
export * from './monaco';
|
||||||
export {
|
export {
|
||||||
ValueMatcherOptions,
|
ValueMatcherOptions,
|
||||||
BasicValueMatcherOptions,
|
BasicValueMatcherOptions,
|
||||||
|
1
packages/grafana-data/src/monaco/index.ts
Normal file
1
packages/grafana-data/src/monaco/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './languageRegistry';
|
7
packages/grafana-data/src/monaco/languageRegistry.ts
Normal file
7
packages/grafana-data/src/monaco/languageRegistry.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Registry, RegistryItem } from '../utils/Registry';
|
||||||
|
|
||||||
|
export interface MonacoLanguageRegistryItem extends RegistryItem {
|
||||||
|
init: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const monacoLanguageRegistry = new Registry<MonacoLanguageRegistryItem>();
|
@ -1,15 +1,16 @@
|
|||||||
import React from 'react';
|
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 { withTheme2 } from '../../themes';
|
||||||
import { Themeable2 } from '../../types';
|
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 defineThemes from './theme';
|
||||||
import { css } from '@emotion/css';
|
|
||||||
|
|
||||||
type Props = CodeEditorProps & Themeable2;
|
type Props = CodeEditorProps & Themeable2;
|
||||||
|
|
||||||
@ -58,9 +59,23 @@ class UnthemedCodeEditor extends React.PureComponent<Props> {
|
|||||||
if (getSuggestions) {
|
if (getSuggestions) {
|
||||||
this.completionCancel = registerSuggestions(this.monaco, language, 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
|
// This is replaced with a real function when the actual editor mounts
|
||||||
getEditorValue = () => '';
|
getEditorValue = () => '';
|
||||||
|
|
||||||
@ -83,7 +98,6 @@ class UnthemedCodeEditor extends React.PureComponent<Props> {
|
|||||||
|
|
||||||
handleOnMount = (editor: MonacoEditorType, monaco: Monaco) => {
|
handleOnMount = (editor: MonacoEditorType, monaco: Monaco) => {
|
||||||
const { onSave, onEditorDidMount } = this.props;
|
const { onSave, onEditorDidMount } = this.props;
|
||||||
|
|
||||||
this.getEditorValue = () => editor.getValue();
|
this.getEditorValue = () => editor.getValue();
|
||||||
|
|
||||||
if (onSave) {
|
if (onSave) {
|
||||||
@ -92,8 +106,10 @@ class UnthemedCodeEditor extends React.PureComponent<Props> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const languagePromise = this.loadCustomLanguage();
|
||||||
|
|
||||||
if (onEditorDidMount) {
|
if (onEditorDidMount) {
|
||||||
onEditorDidMount(editor);
|
languagePromise.then(() => onEditorDidMount(editor, monaco));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -21,10 +21,8 @@ export interface CodeEditorProps {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback after the editor has mounted that gives you raw access to monaco
|
* 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 */
|
/** Handler to be performed when editor is blurred */
|
||||||
onBlur?: CodeEditorChangeHandler;
|
onBlur?: CodeEditorChangeHandler;
|
||||||
|
@ -36,7 +36,7 @@ export { QueryField } from './QueryField/QueryField';
|
|||||||
|
|
||||||
// Code editor
|
// Code editor
|
||||||
export { CodeEditor } from './Monaco/CodeEditorLazy';
|
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';
|
export { variableSuggestionToCodeEditorSuggestion } from './Monaco/utils';
|
||||||
|
|
||||||
// TODO: namespace
|
// TODO: namespace
|
||||||
|
@ -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
|
// @ts-ignore ignoring this for now, otherwise we would have to extend _ interface with move
|
||||||
import {
|
import {
|
||||||
locationUtil,
|
locationUtil,
|
||||||
|
monacoLanguageRegistry,
|
||||||
setLocale,
|
setLocale,
|
||||||
setTimeZoneResolver,
|
setTimeZoneResolver,
|
||||||
standardEditorsRegistry,
|
standardEditorsRegistry,
|
||||||
@ -45,6 +46,7 @@ import { getTimeSrv } from './features/dashboard/services/TimeSrv';
|
|||||||
import { getVariablesUrlParams } from './features/variables/getAllVariableValuesForUrl';
|
import { getVariablesUrlParams } from './features/variables/getAllVariableValuesForUrl';
|
||||||
import { SafeDynamicImport } from './core/components/DynamicImports/SafeDynamicImport';
|
import { SafeDynamicImport } from './core/components/DynamicImports/SafeDynamicImport';
|
||||||
import { featureToggledRoutes } from './routes/routes';
|
import { featureToggledRoutes } from './routes/routes';
|
||||||
|
import getDefaultMonacoLanguages from '../lib/monaco-languages';
|
||||||
|
|
||||||
// add move to lodash for backward compatabilty with plugins
|
// add move to lodash for backward compatabilty with plugins
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -96,6 +98,7 @@ export class GrafanaApp {
|
|||||||
standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
|
standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
|
||||||
standardTransformersRegistry.setInit(getStandardTransformers);
|
standardTransformersRegistry.setInit(getStandardTransformers);
|
||||||
variableAdapters.setInit(getDefaultVariableAdapters);
|
variableAdapters.setInit(getDefaultVariableAdapters);
|
||||||
|
monacoLanguageRegistry.setInit(getDefaultMonacoLanguages);
|
||||||
|
|
||||||
setQueryRunnerFactory(() => new QueryRunner());
|
setQueryRunnerFactory(() => new QueryRunner());
|
||||||
setVariableQueryRunner(new VariableQueryRunner());
|
setVariableQueryRunner(new VariableQueryRunner());
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
export class Deferred {
|
export class Deferred<T = any> {
|
||||||
resolve: any;
|
resolve?: (reason?: T | PromiseLike<T>) => void;
|
||||||
reject: any;
|
reject?: (reason?: any) => void;
|
||||||
promise: Promise<any>;
|
promise: Promise<T>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.resolve = null;
|
this.resolve = undefined;
|
||||||
this.reject = null;
|
this.reject = undefined;
|
||||||
|
|
||||||
this.promise = new Promise((resolve, reject) => {
|
this.promise = new Promise((resolve, reject) => {
|
||||||
this.resolve = resolve;
|
this.resolve = resolve;
|
||||||
this.reject = reject;
|
this.reject = reject;
|
||||||
|
@ -30,6 +30,10 @@ export default function createMockDatasource() {
|
|||||||
supportedTimeGrains: [],
|
supportedTimeGrains: [],
|
||||||
dimensions: [],
|
dimensions: [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
azureLogAnalyticsDatasource: {
|
||||||
|
getKustoSchema: () => Promise.resolve(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockDatasource = _mockDatasource as Datasource;
|
const mockDatasource = _mockDatasource as Datasource;
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,7 +1,7 @@
|
|||||||
import AzureMonitorDatasource from '../datasource';
|
import AzureMonitorDatasource from '../datasource';
|
||||||
import FakeSchemaData from './__mocks__/schema';
|
import FakeSchemaData from './__mocks__/schema';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { AzureLogsVariable, KustoSchema } from '../types';
|
import { AzureLogsVariable } from '../types';
|
||||||
import { toUtc } from '@grafana/data';
|
import { toUtc } from '@grafana/data';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
import { backendSrv } from 'app/core/services/backend_srv';
|
||||||
|
|
||||||
@ -125,23 +125,39 @@ describe('AzureLogAnalyticsDatasource', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
datasourceRequestMock.mockImplementation((options: { url: string }) => {
|
||||||
expect(options.url).toContain('metadata');
|
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', () => {
|
it('should return a schema to use with monaco-kusto', async () => {
|
||||||
return ctx.ds.azureLogAnalyticsDatasource.getSchema('myWorkspace').then((result: KustoSchema) => {
|
const result = await ctx.ds.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
|
||||||
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.database.tables).toHaveLength(2);
|
||||||
expect(result.Databases.Default.Functions.Func1.Name).toBe('Func1');
|
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',
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { map } from 'lodash';
|
import { map } from 'lodash';
|
||||||
import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder';
|
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 { AzureMonitorQuery, AzureDataSourceJsonData, AzureLogsVariable, AzureQueryType } from '../types';
|
||||||
import {
|
import {
|
||||||
DataQueryRequest,
|
DataQueryRequest,
|
||||||
@ -9,9 +9,10 @@ import {
|
|||||||
DataSourceInstanceSettings,
|
DataSourceInstanceSettings,
|
||||||
MetricFindValue,
|
MetricFindValue,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getBackendSrv, getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime';
|
import { getBackendSrv, getTemplateSrv, DataSourceWithBackend, FetchResponse } from '@grafana/runtime';
|
||||||
import { Observable, from } from 'rxjs';
|
import { Observable, from } from 'rxjs';
|
||||||
import { mergeMap } from 'rxjs/operators';
|
import { mergeMap } from 'rxjs/operators';
|
||||||
|
import { AzureLogAnalyticsMetadata } from '../types/logAnalyticsMetadata';
|
||||||
|
|
||||||
export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||||
AzureMonitorQuery,
|
AzureMonitorQuery,
|
||||||
@ -107,15 +108,20 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
return this.doRequest(workspaceListUrl, true);
|
return this.doRequest(workspaceListUrl, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSchema(workspace: string) {
|
async getMetadata(workspace: string) {
|
||||||
if (!workspace) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
const url = `${this.baseUrl}/${getTemplateSrv().replace(workspace, {})}/metadata`;
|
const url = `${this.baseUrl}/${getTemplateSrv().replace(workspace, {})}/metadata`;
|
||||||
|
const resp = await this.doRequest<AzureLogAnalyticsMetadata>(url);
|
||||||
|
|
||||||
return this.doRequest(url).then((response: any) => {
|
if (!resp.ok) {
|
||||||
return new ResponseParser(response.data).parseSchemaResult();
|
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<string, any> {
|
applyTemplateVariables(target: AzureMonitorQuery, scopedVars: ScopedVars): Record<string, any> {
|
||||||
@ -348,7 +354,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async doRequest(url: string, useCache = false, maxRetries = 1): Promise<any> {
|
async doRequest<T = any>(url: string, useCache = false, maxRetries = 1): Promise<FetchResponse<T>> {
|
||||||
try {
|
try {
|
||||||
if (useCache && this.cache.has(url)) {
|
if (useCache && this.cache.has(url)) {
|
||||||
return this.cache.get(url);
|
return this.cache.get(url);
|
||||||
|
@ -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({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,14 +1,7 @@
|
|||||||
import { concat, find, flattenDeep, forEach, map } from 'lodash';
|
import { concat, find, flattenDeep, forEach, map } from 'lodash';
|
||||||
import { AnnotationEvent, dateTime, TimeSeries } from '@grafana/data';
|
import { AnnotationEvent, dateTime, TimeSeries } from '@grafana/data';
|
||||||
import {
|
import { AzureLogsTableData, AzureLogsVariable } from '../types';
|
||||||
AzureLogsTableData,
|
import { AzureLogAnalyticsMetadata } from '../types/logAnalyticsMetadata';
|
||||||
AzureLogsVariable,
|
|
||||||
KustoColumn,
|
|
||||||
KustoDatabase,
|
|
||||||
KustoFunction,
|
|
||||||
KustoSchema,
|
|
||||||
KustoTable,
|
|
||||||
} from '../types';
|
|
||||||
|
|
||||||
export default class ResponseParser {
|
export default class ResponseParser {
|
||||||
columns: string[];
|
columns: string[];
|
||||||
@ -141,73 +134,6 @@ export default class ResponseParser {
|
|||||||
return list;
|
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 {
|
static findOrCreateBucket(data: TimeSeries[], target: any): TimeSeries {
|
||||||
let dataTarget: any = find(data, ['target', target]);
|
let dataTarget: any = find(data, ['target', target]);
|
||||||
if (!dataTarget) {
|
if (!dataTarget) {
|
||||||
@ -222,3 +148,64 @@ export default class ResponseParser {
|
|||||||
return dateTime(dateTimeValue).valueOf();
|
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(<T>(v: T): v is Exclude<T, undefined> => !!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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,8 +1,61 @@
|
|||||||
import { CodeEditor } from '@grafana/ui';
|
import { CodeEditor, Monaco, MonacoEditor } from '@grafana/ui';
|
||||||
import React, { useCallback } from 'react';
|
import { Deferred } from 'app/core/utils/deferred';
|
||||||
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import { AzureQueryEditorFieldProps } from '../../types';
|
import { AzureQueryEditorFieldProps } from '../../types';
|
||||||
|
|
||||||
const QueryField: React.FC<AzureQueryEditorFieldProps> = ({ 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<AzureQueryEditorFieldProps> = ({ query, datasource, onQueryChange }) => {
|
||||||
|
const monacoPromiseRef = useRef<Deferred<MonacoPromise>>();
|
||||||
|
function getPromise() {
|
||||||
|
if (!monacoPromiseRef.current) {
|
||||||
|
monacoPromiseRef.current = new Deferred<MonacoPromise>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
const onChange = useCallback(
|
||||||
(newQuery: string) => {
|
(newQuery: string) => {
|
||||||
onQueryChange({
|
onQueryChange({
|
||||||
@ -19,12 +72,13 @@ const QueryField: React.FC<AzureQueryEditorFieldProps> = ({ query, onQueryChange
|
|||||||
return (
|
return (
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={query.azureLogAnalytics.query}
|
value={query.azureLogAnalytics.query}
|
||||||
language="kql"
|
language="kusto"
|
||||||
height={200}
|
height={200}
|
||||||
width="100%"
|
width="100%"
|
||||||
showMiniMap={false}
|
showMiniMap={false}
|
||||||
onBlur={onChange}
|
onBlur={onChange}
|
||||||
onSave={onChange}
|
onSave={onChange}
|
||||||
|
onEditorDidMount={handleEditorMount}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { DataQuery, DataSourceJsonData, DataSourceSettings, TableData } from '@grafana/data';
|
import { DataQuery, DataSourceJsonData, DataSourceSettings, TableData } from '@grafana/data';
|
||||||
import Datasource from './datasource';
|
import Datasource from '../datasource';
|
||||||
|
|
||||||
export type AzureDataSourceSettings = DataSourceSettings<AzureDataSourceJsonData, AzureDataSourceSecureJsonData>;
|
export type AzureDataSourceSettings = DataSourceSettings<AzureDataSourceJsonData, AzureDataSourceSecureJsonData>;
|
||||||
|
|
||||||
@ -139,37 +139,6 @@ export interface AzureMonitorResourceGroupsResponse {
|
|||||||
statusText: string;
|
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 {
|
export interface AzureLogsVariable {
|
||||||
text: string;
|
text: string;
|
||||||
value: string;
|
value: string;
|
@ -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[];
|
||||||
|
}
|
6
public/lib/monaco-languages/index.ts
Normal file
6
public/lib/monaco-languages/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import loadKusto from './kusto';
|
||||||
|
|
||||||
|
export default function getDefaultMonacoLanguages() {
|
||||||
|
const kusto = { id: 'kusto', name: 'kusto', init: loadKusto };
|
||||||
|
return [kusto];
|
||||||
|
}
|
65
public/lib/monaco-languages/kusto.ts
Normal file
65
public/lib/monaco-languages/kusto.ts
Normal file
@ -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<void> {
|
||||||
|
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();
|
||||||
|
}
|
@ -88,10 +88,10 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// from: './node_modules/@kusto/monaco-kusto/release/min/',
|
from: './node_modules/@kusto/monaco-kusto/release/min/',
|
||||||
// to: 'monaco/min/vs/language/kusto/',
|
to: '../lib/monaco/min/vs/language/kusto/',
|
||||||
// },
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
18
yarn.lock
18
yarn.lock
@ -2893,6 +2893,24 @@
|
|||||||
"@types/yargs" "^15.0.0"
|
"@types/yargs" "^15.0.0"
|
||||||
chalk "^4.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":
|
"@lerna/add@3.21.0":
|
||||||
version "3.21.0"
|
version "3.21.0"
|
||||||
resolved "https://registry.yarnpkg.com/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b"
|
resolved "https://registry.yarnpkg.com/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b"
|
||||||
|
Loading…
Reference in New Issue
Block a user