The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/plugins/datasource/grafana-azure-monitor-datas.../editor/KustoQueryField.tsx

452 lines
13 KiB

import _ from 'lodash';
import Plain from 'slate-plain-serializer';
import QueryField from './query_field';
import debounce from 'lodash/debounce';
import { DOMUtil } from '@grafana/ui';
import { Editor as CoreEditor } from 'slate';
import { KEYWORDS, functionTokens, operatorTokens, grafanaMacros } from './kusto/kusto';
// import '../sass/editor.base.scss';
const TYPEAHEAD_DELAY = 100;
interface Suggestion {
text: string;
deleteBackwards?: number;
type?: string;
}
interface SuggestionGroup {
label: string;
items: Suggestion[];
prefixMatch?: boolean;
skipFilter?: boolean;
}
interface KustoSchema {
Databases: {
Default: KustoDBSchema;
};
Plugins?: any[];
}
interface KustoDBSchema {
Name?: string;
Functions?: any;
Tables?: any;
}
const defaultSchema: any = () => ({
Databases: {
Default: {},
},
});
const cleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
const wrapText = (text: string) => ({ text });
export default class KustoQueryField extends QueryField {
fields: any;
events: any;
schema: KustoSchema;
constructor(props: any, context: any) {
super(props, context);
this.schema = defaultSchema();
this.onTypeahead = debounce(this.onTypeahead, TYPEAHEAD_DELAY);
}
componentDidMount() {
super.componentDidMount();
this.fetchSchema();
}
onTypeahead = (force = false) => {
const selection = window.getSelection();
if (selection && selection.anchorNode) {
const wrapperNode = selection.anchorNode.parentElement;
if (wrapperNode === null) {
return;
}
const editorNode = wrapperNode.closest('.slate-query-field');
if (!editorNode || this.state.value.isBlurred) {
// Not inside this editor
return;
}
// DOM ranges
const range = selection.getRangeAt(0);
const text = selection.anchorNode.textContent;
if (text === null) {
return;
}
const offset = range.startOffset;
let prefix = cleanText(text.substr(0, offset));
// Model ranges
const modelOffset = this.state.value.anchorOffset;
const modelPrefix = this.state.value.anchorText.text.slice(0, modelOffset);
// Determine candidates by context
let suggestionGroups: SuggestionGroup[] = [];
const wrapperClasses = wrapperNode.classList;
let typeaheadContext: string | null = null;
// Built-in functions
if (wrapperClasses.contains('function-context')) {
typeaheadContext = 'context-function';
suggestionGroups = this.getColumnSuggestions();
// where
} else if (modelPrefix.match(/(where\s(\w+\b)?$)/i)) {
typeaheadContext = 'context-where';
suggestionGroups = this.getColumnSuggestions();
// summarize by
} else if (modelPrefix.match(/(summarize\s(\w+\b)?$)/i)) {
typeaheadContext = 'context-summarize';
suggestionGroups = this.getFunctionSuggestions();
} else if (modelPrefix.match(/(summarize\s(.+\s)?by\s+([^,\s]+,\s*)*([^,\s]+\b)?$)/i)) {
typeaheadContext = 'context-summarize-by';
suggestionGroups = this.getColumnSuggestions();
// order by, top X by, ... by ...
} else if (modelPrefix.match(/(by\s+([^,\s]+,\s*)*([^,\s]+\b)?$)/i)) {
typeaheadContext = 'context-by';
suggestionGroups = this.getColumnSuggestions();
// join
} else if (modelPrefix.match(/(on\s(.+\b)?$)/i)) {
typeaheadContext = 'context-join-on';
suggestionGroups = this.getColumnSuggestions();
} else if (modelPrefix.match(/(join\s+(\(\s+)?(\w+\b)?$)/i)) {
typeaheadContext = 'context-join';
suggestionGroups = this.getTableSuggestions();
// distinct
} else if (modelPrefix.match(/(distinct\s(.+\b)?$)/i)) {
typeaheadContext = 'context-distinct';
suggestionGroups = this.getColumnSuggestions();
// database()
} else if (modelPrefix.match(/(database\(\"(\w+)\"\)\.(.+\b)?$)/i)) {
typeaheadContext = 'context-database-table';
const db = this.getDBFromDatabaseFunction(modelPrefix);
console.log(db);
suggestionGroups = this.getTableSuggestions(db);
prefix = prefix.replace('.', '');
// new
} else if (normalizeQuery(Plain.serialize(this.state.value)).match(/^\s*\w*$/i)) {
typeaheadContext = 'context-new';
if (this.schema) {
suggestionGroups = this.getInitialSuggestions();
} else {
this.fetchSchema();
setTimeout(this.onTypeahead, 0);
return;
}
// built-in
} else if (prefix && !wrapperClasses.contains('argument') && !force) {
// Use only last typed word as a prefix for searching
if (modelPrefix.match(/\s$/i)) {
prefix = '';
return;
}
prefix = getLastWord(prefix);
typeaheadContext = 'context-builtin';
suggestionGroups = this.getKeywordSuggestions();
} else if (force === true) {
typeaheadContext = 'context-builtin-forced';
if (modelPrefix.match(/\s$/i)) {
prefix = '';
}
suggestionGroups = this.getKeywordSuggestions();
}
let results = 0;
prefix = prefix.toLowerCase();
const filteredSuggestions = suggestionGroups
.map(group => {
if (group.items && prefix && !group.skipFilter) {
group.items = group.items.filter(c => c.text.length >= prefix.length);
if (group.prefixMatch) {
group.items = group.items.filter(c => c.text.toLowerCase().indexOf(prefix) === 0);
} else {
group.items = group.items.filter(c => c.text.toLowerCase().indexOf(prefix) > -1);
}
}
results += group.items.length;
return group;
})
.filter(group => group.items.length > 0);
// console.log('onTypeahead', selection.anchorNode, wrapperClasses, text, offset, prefix, typeaheadContext);
// console.log('onTypeahead', prefix, typeaheadContext, force);
this.setState({
typeaheadPrefix: prefix,
typeaheadContext,
typeaheadText: text,
suggestions: results > 0 ? filteredSuggestions : [],
});
}
};
applyTypeahead = (editor: CoreEditor, suggestion: { text: any; type: string; deleteBackwards: any }): CoreEditor => {
const { typeaheadPrefix, typeaheadContext, typeaheadText } = this.state;
let suggestionText = suggestion.text || suggestion;
const move = 0;
// Modify suggestion based on context
const nextChar = DOMUtil.getNextCharacter();
if (suggestion.type === 'function') {
if (!nextChar || nextChar !== '(') {
suggestionText += '(';
}
} else if (typeaheadContext === 'context-function') {
if (!nextChar || nextChar !== ')') {
suggestionText += ')';
}
} else {
if (!nextChar || nextChar !== ' ') {
suggestionText += ' ';
}
}
// Remove the current, incomplete text and replace it with the selected suggestion
const backward = suggestion.deleteBackwards || typeaheadPrefix.length;
const text = cleanText(typeaheadText);
const suffixLength = text.length - typeaheadPrefix.length;
const offset = typeaheadText.indexOf(typeaheadPrefix);
const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestionText === typeaheadText);
const forward = midWord ? suffixLength + offset : 0;
this.resetTypeahead(() =>
editor
.deleteBackward(backward)
.deleteForward(forward)
.insertText(suggestionText)
.moveForward(move)
.focus()
);
return editor;
};
// private _getFieldsSuggestions(): SuggestionGroup[] {
// return [
// {
// prefixMatch: true,
// label: 'Fields',
// items: this.fields.map(wrapText)
// },
// {
// prefixMatch: true,
// label: 'Variables',
// items: this.props.templateVariables.map(wrapText)
// }
// ];
// }
// private _getAfterFromSuggestions(): SuggestionGroup[] {
// return [
// {
// skipFilter: true,
// label: 'Events',
// items: this.events.map(wrapText)
// },
// {
// prefixMatch: true,
// label: 'Variables',
// items: this.props.templateVariables
// .map(wrapText)
// .map(suggestion => {
// suggestion.deleteBackwards = 0;
// return suggestion;
// })
// }
// ];
// }
// private _getAfterSelectSuggestions(): SuggestionGroup[] {
// return [
// {
// prefixMatch: true,
// label: 'Fields',
// items: this.fields.map(wrapText)
// },
// {
// prefixMatch: true,
// label: 'Functions',
// items: FUNCTIONS.map((s: any) => { s.type = 'function'; return s; })
// },
// {
// prefixMatch: true,
// label: 'Variables',
// items: this.props.templateVariables.map(wrapText)
// }
// ];
// }
private getInitialSuggestions(): SuggestionGroup[] {
return this.getTableSuggestions();
}
private getKeywordSuggestions(): SuggestionGroup[] {
return [
{
prefixMatch: true,
label: 'Keywords',
items: KEYWORDS.map(wrapText),
},
{
prefixMatch: true,
label: 'Operators',
items: operatorTokens,
},
{
prefixMatch: true,
label: 'Functions',
items: functionTokens.map((s: any) => {
s.type = 'function';
return s;
}),
},
{
prefixMatch: true,
label: 'Macros',
items: grafanaMacros.map((s: any) => {
s.type = 'function';
return s;
}),
},
{
prefixMatch: true,
label: 'Tables',
items: _.map(this.schema.Databases.Default.Tables, (t: any) => ({ text: t.Name })),
},
];
}
private getFunctionSuggestions(): SuggestionGroup[] {
return [
{
prefixMatch: true,
label: 'Functions',
items: functionTokens.map((s: any) => {
s.type = 'function';
return s;
}),
},
{
prefixMatch: true,
label: 'Macros',
items: grafanaMacros.map((s: any) => {
s.type = 'function';
return s;
}),
},
];
}
getTableSuggestions(db = 'Default'): SuggestionGroup[] {
// @ts-ignore
if (this.schema.Databases[db]) {
return [
{
prefixMatch: true,
label: 'Tables',
// @ts-ignore
items: _.map(this.schema.Databases[db].Tables, (t: any) => ({ text: t.Name })),
},
];
} else {
return [];
}
}
private getColumnSuggestions(): SuggestionGroup[] {
const table = this.getTableFromContext();
if (table) {
const tableSchema = this.schema.Databases.Default.Tables[table];
if (tableSchema) {
return [
{
prefixMatch: true,
label: 'Fields',
items: _.map(tableSchema.OrderedColumns, (f: any) => ({
text: f.Name,
hint: f.Type,
})),
},
];
}
}
return [];
}
private getTableFromContext() {
const query = Plain.serialize(this.state.value);
const tablePattern = /^\s*(\w+)\s*|/g;
const normalizedQuery = normalizeQuery(query);
const match = tablePattern.exec(normalizedQuery);
if (match && match.length > 1 && match[0] && match[1]) {
return match[1];
} else {
return null;
}
}
private getDBFromDatabaseFunction(prefix: string) {
const databasePattern = /database\(\"(\w+)\"\)/gi;
const match = databasePattern.exec(prefix);
if (match && match.length > 1 && match[0] && match[1]) {
return match[1];
} else {
return undefined;
}
}
private async fetchSchema() {
let schema = await this.props.getSchema();
if (schema) {
if (schema.Type === 'AppInsights') {
schema = castSchema(schema);
}
this.schema = schema;
} else {
this.schema = defaultSchema();
}
}
}
/**
* Cast schema from App Insights to default Kusto schema
*/
function castSchema(schema: any) {
const defaultSchemaTemplate = defaultSchema();
defaultSchemaTemplate.Databases.Default = schema;
return defaultSchemaTemplate;
}
function normalizeQuery(query: string): string {
const commentPattern = /\/\/.*$/gm;
let normalizedQuery = query.replace(commentPattern, '');
normalizedQuery = normalizedQuery.replace('\n', ' ');
return normalizedQuery;
}
function getLastWord(str: string): string {
const lastWordPattern = /(?:.*\s)?([^\s]+\s*)$/gi;
const match = lastWordPattern.exec(str);
if (match && match.length > 1) {
return match[1];
}
return '';
}