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-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",
|
||||
|
@ -14,6 +14,7 @@ export * from './valueFormats';
|
||||
export * from './field';
|
||||
export * from './events';
|
||||
export * from './themes';
|
||||
export * from './monaco';
|
||||
export {
|
||||
ValueMatcherOptions,
|
||||
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 { 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<Props> {
|
||||
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<Props> {
|
||||
|
||||
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<Props> {
|
||||
});
|
||||
}
|
||||
|
||||
const languagePromise = this.loadCustomLanguage();
|
||||
|
||||
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
|
||||
*
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
onEditorDidMount?: (editor: monacoType.editor.IStandaloneCodeEditor) => void;
|
||||
onEditorDidMount?: (editor: MonacoEditor, monaco: Monaco) => void;
|
||||
|
||||
/** Handler to be performed when editor is blurred */
|
||||
onBlur?: CodeEditorChangeHandler;
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
|
@ -1,10 +1,12 @@
|
||||
export class Deferred {
|
||||
resolve: any;
|
||||
reject: any;
|
||||
promise: Promise<any>;
|
||||
export class Deferred<T = any> {
|
||||
resolve?: (reason?: T | PromiseLike<T>) => void;
|
||||
reject?: (reason?: any) => void;
|
||||
promise: Promise<T>;
|
||||
|
||||
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;
|
||||
|
@ -30,6 +30,10 @@ export default function createMockDatasource() {
|
||||
supportedTimeGrains: [],
|
||||
dimensions: [],
|
||||
}),
|
||||
|
||||
azureLogAnalyticsDatasource: {
|
||||
getKustoSchema: () => Promise.resolve(),
|
||||
},
|
||||
};
|
||||
|
||||
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 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');
|
||||
it('should return a schema to use with monaco-kusto', async () => {
|
||||
const result = await ctx.ds.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
|
||||
|
||||
expect(Object.keys(result.Databases.Default.Functions).length).toBe(1);
|
||||
expect(result.Databases.Default.Functions.Func1.Name).toBe('Func1');
|
||||
});
|
||||
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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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<AzureLogAnalyticsMetadata>(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<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 {
|
||||
if (useCache && this.cache.has(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 { 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(<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 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<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(
|
||||
(newQuery: string) => {
|
||||
onQueryChange({
|
||||
@ -19,12 +72,13 @@ const QueryField: React.FC<AzureQueryEditorFieldProps> = ({ query, onQueryChange
|
||||
return (
|
||||
<CodeEditor
|
||||
value={query.azureLogAnalytics.query}
|
||||
language="kql"
|
||||
language="kusto"
|
||||
height={200}
|
||||
width="100%"
|
||||
showMiniMap={false}
|
||||
onBlur={onChange}
|
||||
onSave={onChange}
|
||||
onEditorDidMount={handleEditorMount}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DataQuery, DataSourceJsonData, DataSourceSettings, TableData } from '@grafana/data';
|
||||
import Datasource from './datasource';
|
||||
import Datasource from '../datasource';
|
||||
|
||||
export type AzureDataSourceSettings = DataSourceSettings<AzureDataSourceJsonData, AzureDataSourceSecureJsonData>;
|
||||
|
||||
@ -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;
|
@ -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/',
|
||||
// to: 'monaco/min/vs/language/kusto/',
|
||||
// },
|
||||
{
|
||||
from: './node_modules/@kusto/monaco-kusto/release/min/',
|
||||
to: '../lib/monaco/min/vs/language/kusto/',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
18
yarn.lock
18
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"
|
||||
|
Loading…
Reference in New Issue
Block a user