mirror of
https://github.com/grafana/grafana.git
synced 2026-08-12 06:05:02 -05:00
Azure Monitor: replace monaco by slate with initial Kusto syntax
This commit is contained in:
@@ -11,7 +11,6 @@
|
|||||||
"url": "http://github.com/grafana/grafana.git"
|
"url": "http://github.com/grafana/grafana.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@alexanderzobnin/monaco-kusto": "^0.2.3-rc.1",
|
|
||||||
"@babel/core": "^7.1.2",
|
"@babel/core": "^7.1.2",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
||||||
"@babel/preset-env": "^7.1.0",
|
"@babel/preset-env": "^7.1.0",
|
||||||
@@ -99,7 +98,6 @@
|
|||||||
"tslint-react": "^3.6.0",
|
"tslint-react": "^3.6.0",
|
||||||
"typescript": "^3.0.3",
|
"typescript": "^3.0.3",
|
||||||
"uglifyjs-webpack-plugin": "^1.2.7",
|
"uglifyjs-webpack-plugin": "^1.2.7",
|
||||||
"vscode-languageserver-types": "^3.14.0",
|
|
||||||
"webpack": "4.19.1",
|
"webpack": "4.19.1",
|
||||||
"webpack-bundle-analyzer": "^2.9.0",
|
"webpack-bundle-analyzer": "^2.9.0",
|
||||||
"webpack-cleanup-plugin": "^0.5.1",
|
"webpack-cleanup-plugin": "^0.5.1",
|
||||||
|
|||||||
+348
@@ -0,0 +1,348 @@
|
|||||||
|
import Plain from 'slate-plain-serializer';
|
||||||
|
|
||||||
|
import QueryField from './query_field';
|
||||||
|
// import debounce from './utils/debounce';
|
||||||
|
// import {getNextCharacter} from './utils/dom';
|
||||||
|
import debounce from 'app/features/explore/utils/debounce';
|
||||||
|
import { getNextCharacter } from 'app/features/explore/utils/dom';
|
||||||
|
|
||||||
|
import { FUNCTIONS, KEYWORDS } from './kusto';
|
||||||
|
// import '../sass/editor.base.scss';
|
||||||
|
|
||||||
|
|
||||||
|
const TYPEAHEAD_DELAY = 500;
|
||||||
|
|
||||||
|
interface Suggestion {
|
||||||
|
text: string;
|
||||||
|
deleteBackwards?: number;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SuggestionGroup {
|
||||||
|
label: string;
|
||||||
|
items: Suggestion[];
|
||||||
|
prefixMatch?: boolean;
|
||||||
|
skipFilter?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
|
||||||
|
const wrapText = text => ({ text });
|
||||||
|
|
||||||
|
export default class KustoQueryField extends QueryField {
|
||||||
|
fields: any;
|
||||||
|
events: any;
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.onTypeahead = debounce(this.onTypeahead, TYPEAHEAD_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.updateMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
onTypeahead = () => {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
if (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;
|
||||||
|
|
||||||
|
if (wrapperClasses.contains('function-context')) {
|
||||||
|
typeaheadContext = 'context-function';
|
||||||
|
if (this.fields) {
|
||||||
|
suggestionGroups = this._getFieldsSuggestions();
|
||||||
|
} else {
|
||||||
|
this._fetchFields();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (modelPrefix.match(/(facet\s$)/i)) {
|
||||||
|
typeaheadContext = 'context-facet';
|
||||||
|
if (this.fields) {
|
||||||
|
suggestionGroups = this._getFieldsSuggestions();
|
||||||
|
} else {
|
||||||
|
this._fetchFields();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (modelPrefix.match(/(,\s*$)/)) {
|
||||||
|
typeaheadContext = 'context-multiple-fields';
|
||||||
|
if (this.fields) {
|
||||||
|
suggestionGroups = this._getFieldsSuggestions();
|
||||||
|
} else {
|
||||||
|
this._fetchFields();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (modelPrefix.match(/(from\s$)/i)) {
|
||||||
|
typeaheadContext = 'context-from';
|
||||||
|
if (this.events) {
|
||||||
|
suggestionGroups = this._getAfterFromSuggestions();
|
||||||
|
} else {
|
||||||
|
this._fetchEvents();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (modelPrefix.match(/(^select\s\w*$)/i)) {
|
||||||
|
typeaheadContext = 'context-select';
|
||||||
|
if (this.fields) {
|
||||||
|
suggestionGroups = this._getAfterSelectSuggestions();
|
||||||
|
} else {
|
||||||
|
this._fetchFields();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
modelPrefix.match(/\)\s$/) ||
|
||||||
|
modelPrefix.match(/SELECT ((?:\$?\w+\(?\w*\)?\,?\s*)+)([\)]\s|\b)/gi)
|
||||||
|
) {
|
||||||
|
typeaheadContext = 'context-after-function';
|
||||||
|
suggestionGroups = this._getAfterFunctionSuggestions();
|
||||||
|
} else if (modelPrefix.match(/from\s\S+\s\w*$/i)) {
|
||||||
|
prefix = '';
|
||||||
|
typeaheadContext = 'context-since';
|
||||||
|
suggestionGroups = this._getAfterEventSuggestions();
|
||||||
|
// } else if (modelPrefix.match(/\d+\s\w*$/)) {
|
||||||
|
// typeaheadContext = 'context-number';
|
||||||
|
// suggestionGroups = this._getAfterNumberSuggestions();
|
||||||
|
} else if (modelPrefix.match(/ago\b/i) || modelPrefix.match(/facet\b/i) || modelPrefix.match(/\$__timefilter\b/i)) {
|
||||||
|
typeaheadContext = 'context-timeseries';
|
||||||
|
suggestionGroups = this._getAfterAgoSuggestions();
|
||||||
|
} else if (prefix && !wrapperClasses.contains('argument')) {
|
||||||
|
typeaheadContext = 'context-builtin';
|
||||||
|
suggestionGroups = this._getKeywordSuggestions();
|
||||||
|
} else if (Plain.serialize(this.state.value) === '') {
|
||||||
|
typeaheadContext = 'context-new';
|
||||||
|
suggestionGroups = this._getInitialSuggestions();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
typeaheadPrefix: prefix,
|
||||||
|
typeaheadContext,
|
||||||
|
typeaheadText: text,
|
||||||
|
suggestions: results > 0 ? filteredSuggestions : [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTypeahead(change, suggestion) {
|
||||||
|
const { typeaheadPrefix, typeaheadContext, typeaheadText } = this.state;
|
||||||
|
let suggestionText = suggestion.text || suggestion;
|
||||||
|
const move = 0;
|
||||||
|
|
||||||
|
// Modify suggestion based on context
|
||||||
|
|
||||||
|
const nextChar = getNextCharacter();
|
||||||
|
if (suggestion.type === 'function') {
|
||||||
|
if (!nextChar || nextChar !== '(') {
|
||||||
|
suggestionText += '(';
|
||||||
|
}
|
||||||
|
} else if (typeaheadContext === 'context-function') {
|
||||||
|
if (!nextChar || nextChar !== ')') {
|
||||||
|
suggestionText += ')';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!nextChar || nextChar !== ' ') {
|
||||||
|
suggestionText += ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resetTypeahead();
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
return change
|
||||||
|
.deleteBackward(backward)
|
||||||
|
.deleteForward(forward)
|
||||||
|
.insertText(suggestionText)
|
||||||
|
.move(move)
|
||||||
|
.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _getAfterFunctionSuggestions(): SuggestionGroup[] {
|
||||||
|
return [{
|
||||||
|
prefixMatch: true,
|
||||||
|
label: 'Keywords',
|
||||||
|
items: ['FROM'].map(wrapText)
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getAfterEventSuggestions(): SuggestionGroup[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
skipFilter: true,
|
||||||
|
label: 'Keywords',
|
||||||
|
items: ['SINCE'].map(wrapText)
|
||||||
|
.map((suggestion: any) => {
|
||||||
|
suggestion.deleteBackwards = 0;
|
||||||
|
return suggestion;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
skipFilter: true,
|
||||||
|
label: 'Macros',
|
||||||
|
items: ['$__timeFilter'].map(wrapText)
|
||||||
|
.map((suggestion: any) => {
|
||||||
|
suggestion.deleteBackwards = 0;
|
||||||
|
return suggestion;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// private _getAfterNumberSuggestions(): SuggestionGroup[] {
|
||||||
|
// return [{
|
||||||
|
// prefixMatch: true,
|
||||||
|
// label: 'Duration',
|
||||||
|
// items: DURATION
|
||||||
|
// .map(d => `${d} AGO`)
|
||||||
|
// .map(wrapText)
|
||||||
|
// }];
|
||||||
|
// }
|
||||||
|
|
||||||
|
private _getAfterAgoSuggestions(): SuggestionGroup[] {
|
||||||
|
return [{
|
||||||
|
prefixMatch: true,
|
||||||
|
label: 'Keywords',
|
||||||
|
items: ['TIMESERIES', 'COMPARE WITH', 'FACET'].map(wrapText)
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getKeywordSuggestions(): SuggestionGroup[] {
|
||||||
|
return [{
|
||||||
|
prefixMatch: true,
|
||||||
|
label: 'Keywords',
|
||||||
|
items: KEYWORDS.map(wrapText)
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getInitialSuggestions(): SuggestionGroup[] {
|
||||||
|
// TODO: return datbase tables as an initial suggestion
|
||||||
|
return [{
|
||||||
|
prefixMatch: true,
|
||||||
|
label: 'Keywords',
|
||||||
|
items: KEYWORDS.map(wrapText)
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchEvents() {
|
||||||
|
const query = 'events';
|
||||||
|
const result = await this.request(query);
|
||||||
|
|
||||||
|
if (result === undefined) {
|
||||||
|
this.events = [];
|
||||||
|
} else {
|
||||||
|
this.events = result;
|
||||||
|
}
|
||||||
|
setTimeout(this.onTypeahead, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchFields() {
|
||||||
|
const query = 'fields';
|
||||||
|
const result = await this.request(query);
|
||||||
|
|
||||||
|
this.fields = result || [];
|
||||||
|
|
||||||
|
setTimeout(this.onTypeahead, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
import KustoQueryField from './KustoQueryField';
|
||||||
|
import Kusto from './kusto';
|
||||||
|
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Editor extends Component<any, any> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
edited: false,
|
||||||
|
query: props.query || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeQuery = value => {
|
||||||
|
const { index, change } = this.props;
|
||||||
|
const { query } = this.state;
|
||||||
|
const edited = query !== value;
|
||||||
|
this.setState({ edited, query: value });
|
||||||
|
if (change) {
|
||||||
|
change(value, index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onPressEnter = () => {
|
||||||
|
const { execute } = this.props;
|
||||||
|
if (execute) {
|
||||||
|
execute();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { request, variables } = this.props;
|
||||||
|
const { edited, query } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gf-form-input" style={{height: 'auto'}}>
|
||||||
|
<KustoQueryField
|
||||||
|
initialQuery={edited ? null : query}
|
||||||
|
onPressEnter={this.onPressEnter}
|
||||||
|
onQueryChange={this.onChangeQuery}
|
||||||
|
prismLanguage="kusto"
|
||||||
|
prismDefinition={Kusto}
|
||||||
|
placeholder="Enter a query"
|
||||||
|
request={request}
|
||||||
|
templateVariables={variables}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coreModule.directive('kustoEditor', [
|
||||||
|
'reactDirective',
|
||||||
|
reactDirective => {
|
||||||
|
return reactDirective(Editor, ['change', 'database', 'execute', 'query', 'request', 'variables']);
|
||||||
|
},
|
||||||
|
]);
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
export const FUNCTIONS = [
|
||||||
|
{ text: 'countof', display: 'countof()', hint: '' },
|
||||||
|
{ text: 'bin', display: 'bin()', hint: '' },
|
||||||
|
{ text: 'extentid', display: 'extentid()', hint: '' },
|
||||||
|
{ text: 'extract', display: 'extract()', hint: '' },
|
||||||
|
{ text: 'extractjson', display: 'extractjson()', hint: '' },
|
||||||
|
{ text: 'floor', display: 'floor()', hint: '' },
|
||||||
|
{ text: 'iif', display: 'iif()', hint: '' },
|
||||||
|
{ text: 'isnull', display: 'isnull()', hint: '' },
|
||||||
|
{ text: 'isnotnull', display: 'isnotnull()', hint: '' },
|
||||||
|
{ text: 'notnull', display: 'notnull()', hint: '' },
|
||||||
|
{ text: 'isempty', display: 'isempty()', hint: '' },
|
||||||
|
{ text: 'isnotempty', display: 'isnotempty()', hint: '' },
|
||||||
|
{ text: 'notempty', display: 'notempty()', hint: '' },
|
||||||
|
{ text: 'now', display: 'now()', hint: '' },
|
||||||
|
{ text: 're2', display: 're2()', hint: '' },
|
||||||
|
{ text: 'strcat', display: 'strcat()', hint: '' },
|
||||||
|
{ text: 'strlen', display: 'strlen()', hint: '' },
|
||||||
|
{ text: 'toupper', display: 'toupper()', hint: '' },
|
||||||
|
{ text: 'tostring', display: 'tostring()', hint: '' },
|
||||||
|
{ text: 'count', display: 'count()', hint: '' },
|
||||||
|
{ text: 'cnt', display: 'cnt()', hint: '' },
|
||||||
|
{ text: 'sum', display: 'sum()', hint: '' },
|
||||||
|
{ text: 'min', display: 'min()', hint: '' },
|
||||||
|
{ text: 'max', display: 'max()', hint: '' },
|
||||||
|
{ text: 'avg', display: 'avg()', hint: '' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const KEYWORDS = [
|
||||||
|
'by', 'on', 'contains', 'notcontains', 'containscs', 'notcontainscs', 'startswith', 'has', 'matches', 'regex', 'true',
|
||||||
|
'false', 'and', 'or', 'typeof', 'int', 'string', 'date', 'datetime', 'time', 'long', 'real', 'boolean', 'bool',
|
||||||
|
// add some more keywords
|
||||||
|
'where', 'order'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Kusto operators
|
||||||
|
// export const OPERATORS = ['+', '-', '*', '/', '>', '<', '==', '<>', '<=', '>=', '~', '!~'];
|
||||||
|
|
||||||
|
export const DURATION = [
|
||||||
|
'SECONDS',
|
||||||
|
'MINUTES',
|
||||||
|
'HOURS',
|
||||||
|
'DAYS',
|
||||||
|
'WEEKS',
|
||||||
|
'MONTHS',
|
||||||
|
'YEARS'
|
||||||
|
];
|
||||||
|
|
||||||
|
const tokenizer = {
|
||||||
|
comment: {
|
||||||
|
pattern: /(^|[^\\:])\/\/.*/,
|
||||||
|
lookbehind: true,
|
||||||
|
greedy: true,
|
||||||
|
},
|
||||||
|
'function-context': {
|
||||||
|
pattern: /[a-z0-9_]+\([^)]*\)?/i,
|
||||||
|
inside: {},
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
pattern: new RegExp(`${DURATION.join('?|')}?`, 'i'),
|
||||||
|
alias: 'number',
|
||||||
|
},
|
||||||
|
builtin: new RegExp(`\\b(?:${FUNCTIONS.map(f => f.text).join('|')})(?=\\s*\\()`, 'i'),
|
||||||
|
string: {
|
||||||
|
pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,
|
||||||
|
greedy: true,
|
||||||
|
},
|
||||||
|
keyword: new RegExp(`\\b(?:${KEYWORDS.join('|')}|\\*)\\b`, 'i'),
|
||||||
|
boolean: /\b(?:true|false)\b/,
|
||||||
|
number: /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,
|
||||||
|
operator: /-|\+|\*|\/|>|<|==|<=?|>=?|<>|!~|~|=|\|/,
|
||||||
|
punctuation: /[{};(),.:]/,
|
||||||
|
variable: /(\[\[(.+?)\]\])|(\$(.+?))\b/
|
||||||
|
};
|
||||||
|
|
||||||
|
tokenizer['function-context'].inside = {
|
||||||
|
argument: {
|
||||||
|
pattern: /[a-z0-9_]+(?=:)/i,
|
||||||
|
alias: 'symbol',
|
||||||
|
},
|
||||||
|
duration: tokenizer.duration,
|
||||||
|
number: tokenizer.number,
|
||||||
|
builtin: tokenizer.builtin,
|
||||||
|
string: tokenizer.string,
|
||||||
|
variable: tokenizer.variable,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default tokenizer;
|
||||||
+333
@@ -0,0 +1,333 @@
|
|||||||
|
import PluginPrism from './slate-plugins/prism';
|
||||||
|
// import PluginPrism from 'slate-prism';
|
||||||
|
// import Prism from 'prismjs';
|
||||||
|
|
||||||
|
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
|
||||||
|
import ClearPlugin from 'app/features/explore/slate-plugins/clear';
|
||||||
|
// Custom plugins (new line on Enter and run on Shift+Enter)
|
||||||
|
import NewlinePlugin from './slate-plugins/newline';
|
||||||
|
import RunnerPlugin from './slate-plugins/runner';
|
||||||
|
|
||||||
|
import Typeahead from './typeahead';
|
||||||
|
|
||||||
|
import { Block, Document, Text, Value } from 'slate';
|
||||||
|
import { Editor } from 'slate-react';
|
||||||
|
import Plain from 'slate-plain-serializer';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
|
||||||
|
function flattenSuggestions(s) {
|
||||||
|
return s ? s.reduce((acc, g) => acc.concat(g.items), []) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeFragment = text => {
|
||||||
|
const lines = text.split('\n').map(line =>
|
||||||
|
Block.create({
|
||||||
|
type: 'paragraph',
|
||||||
|
nodes: [Text.create(line)],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const fragment = Document.create({
|
||||||
|
nodes: lines,
|
||||||
|
});
|
||||||
|
return fragment;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getInitialValue = query => Value.create({ document: makeFragment(query) });
|
||||||
|
|
||||||
|
class Portal extends React.Component<any, any> {
|
||||||
|
node: any;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const { index = 0, prefix = 'query' } = props;
|
||||||
|
this.node = document.createElement('div');
|
||||||
|
this.node.classList.add(`slate-typeahead`, `slate-typeahead-${prefix}-${index}`);
|
||||||
|
document.body.appendChild(this.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.body.removeChild(this.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return ReactDOM.createPortal(this.props.children, this.node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QueryField extends React.Component<any, any> {
|
||||||
|
menuEl: any;
|
||||||
|
plugins: any;
|
||||||
|
resetTimer: any;
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
const { prismDefinition = {}, prismLanguage = 'kusto' } = props;
|
||||||
|
|
||||||
|
this.plugins = [
|
||||||
|
BracesPlugin(),
|
||||||
|
ClearPlugin(),
|
||||||
|
RunnerPlugin({ handler: props.onPressEnter }),
|
||||||
|
NewlinePlugin(),
|
||||||
|
PluginPrism({ definition: prismDefinition, language: prismLanguage }),
|
||||||
|
];
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
labelKeys: {},
|
||||||
|
labelValues: {},
|
||||||
|
suggestions: [],
|
||||||
|
typeaheadIndex: 0,
|
||||||
|
typeaheadPrefix: '',
|
||||||
|
value: getInitialValue(props.initialQuery || ''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.updateMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
clearTimeout(this.resetTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.updateMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = ({ value }) => {
|
||||||
|
const changed = value.document !== this.state.value.document;
|
||||||
|
this.setState({ value }, () => {
|
||||||
|
if (changed) {
|
||||||
|
this.onChangeQuery();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.requestAnimationFrame(this.onTypeahead);
|
||||||
|
}
|
||||||
|
|
||||||
|
request = (url?) => {
|
||||||
|
if (this.props.request) {
|
||||||
|
return this.props.request(url);
|
||||||
|
}
|
||||||
|
return fetch(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeQuery = () => {
|
||||||
|
// Send text change to parent
|
||||||
|
const { onQueryChange } = this.props;
|
||||||
|
if (onQueryChange) {
|
||||||
|
onQueryChange(Plain.serialize(this.state.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown = (event, change) => {
|
||||||
|
const { typeaheadIndex, suggestions } = this.state;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Escape': {
|
||||||
|
if (this.menuEl) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.resetTypeahead();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ' ': {
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.onTypeahead();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Tab': {
|
||||||
|
if (this.menuEl) {
|
||||||
|
// Dont blur input
|
||||||
|
event.preventDefault();
|
||||||
|
if (!suggestions || suggestions.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the currently selected suggestion
|
||||||
|
const flattenedSuggestions = flattenSuggestions(suggestions);
|
||||||
|
const selected = Math.abs(typeaheadIndex);
|
||||||
|
const selectedIndex = selected % flattenedSuggestions.length || 0;
|
||||||
|
const suggestion = flattenedSuggestions[selectedIndex];
|
||||||
|
|
||||||
|
this.applyTypeahead(change, suggestion);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ArrowDown': {
|
||||||
|
if (this.menuEl) {
|
||||||
|
// Select next suggestion
|
||||||
|
event.preventDefault();
|
||||||
|
this.setState({ typeaheadIndex: typeaheadIndex + 1 });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ArrowUp': {
|
||||||
|
if (this.menuEl) {
|
||||||
|
// Select previous suggestion
|
||||||
|
event.preventDefault();
|
||||||
|
this.setState({ typeaheadIndex: Math.max(0, typeaheadIndex - 1) });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
// console.log('default key', event.key, event.which, event.charCode, event.locale, data.key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
onTypeahead = (change?, item?) => {
|
||||||
|
return change || this.state.value.change();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTypeahead(change?, suggestion?): { value: object } { return { value: {} }; }
|
||||||
|
|
||||||
|
resetTypeahead = () => {
|
||||||
|
this.setState({
|
||||||
|
suggestions: [],
|
||||||
|
typeaheadIndex: 0,
|
||||||
|
typeaheadPrefix: '',
|
||||||
|
typeaheadContext: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBlur = () => {
|
||||||
|
const { onBlur } = this.props;
|
||||||
|
// If we dont wait here, menu clicks wont work because the menu
|
||||||
|
// will be gone.
|
||||||
|
this.resetTimer = setTimeout(this.resetTypeahead, 100);
|
||||||
|
if (onBlur) {
|
||||||
|
onBlur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFocus = () => {
|
||||||
|
const { onFocus } = this.props;
|
||||||
|
if (onFocus) {
|
||||||
|
onFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickItem = item => {
|
||||||
|
const { suggestions } = this.state;
|
||||||
|
if (!suggestions || suggestions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the currently selected suggestion
|
||||||
|
const flattenedSuggestions = flattenSuggestions(suggestions);
|
||||||
|
const suggestion = _.find(
|
||||||
|
flattenedSuggestions,
|
||||||
|
suggestion => suggestion.display === item || suggestion.text === item
|
||||||
|
);
|
||||||
|
|
||||||
|
// Manually triggering change
|
||||||
|
const change = this.applyTypeahead(this.state.value.change(), suggestion);
|
||||||
|
this.onChange(change);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMenu = () => {
|
||||||
|
const { suggestions } = this.state;
|
||||||
|
const menu = this.menuEl;
|
||||||
|
const selection = window.getSelection();
|
||||||
|
const node = selection.anchorNode;
|
||||||
|
|
||||||
|
// No menu, nothing to do
|
||||||
|
if (!menu) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No suggestions or blur, remove menu
|
||||||
|
const hasSuggesstions = suggestions && suggestions.length > 0;
|
||||||
|
if (!hasSuggesstions) {
|
||||||
|
menu.removeAttribute('style');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Align menu overlay to editor node
|
||||||
|
if (node && node.parentElement) {
|
||||||
|
// Read from DOM
|
||||||
|
const rect = node.parentElement.getBoundingClientRect();
|
||||||
|
const scrollX = window.scrollX;
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
|
||||||
|
// Write DOM
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
menu.style.opacity = 1;
|
||||||
|
menu.style.top = `${rect.top + scrollY + rect.height + 4}px`;
|
||||||
|
menu.style.left = `${rect.left + scrollX - 2}px`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
menuRef = el => {
|
||||||
|
this.menuEl = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMenu = () => {
|
||||||
|
const { portalPrefix } = this.props;
|
||||||
|
const { suggestions } = this.state;
|
||||||
|
const hasSuggesstions = suggestions && suggestions.length > 0;
|
||||||
|
if (!hasSuggesstions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard selectedIndex to be within the length of the suggestions
|
||||||
|
let selectedIndex = Math.max(this.state.typeaheadIndex, 0);
|
||||||
|
const flattenedSuggestions = flattenSuggestions(suggestions);
|
||||||
|
selectedIndex = selectedIndex % flattenedSuggestions.length || 0;
|
||||||
|
const selectedKeys = (flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : []).map(
|
||||||
|
i => (typeof i === 'object' ? i.text : i)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create typeahead in DOM root so we can later position it absolutely
|
||||||
|
return (
|
||||||
|
<Portal prefix={portalPrefix}>
|
||||||
|
<Typeahead
|
||||||
|
menuRef={this.menuRef}
|
||||||
|
selectedItems={selectedKeys}
|
||||||
|
onClickItem={this.onClickItem}
|
||||||
|
groupedItems={suggestions}
|
||||||
|
/>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="slate-query-field">
|
||||||
|
{this.renderMenu()}
|
||||||
|
<Editor
|
||||||
|
autoCorrect={false}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
onChange={this.onChange}
|
||||||
|
onFocus={this.handleFocus}
|
||||||
|
placeholder={this.props.placeholder}
|
||||||
|
plugins={this.plugins}
|
||||||
|
spellCheck={false}
|
||||||
|
value={this.state.value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueryField;
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
function getIndent(text) {
|
||||||
|
let offset = text.length - text.trimLeft().length;
|
||||||
|
if (offset) {
|
||||||
|
let indent = text[0];
|
||||||
|
while (--offset) {
|
||||||
|
indent += text[0];
|
||||||
|
}
|
||||||
|
return indent;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NewlinePlugin() {
|
||||||
|
return {
|
||||||
|
onKeyDown(event, change) {
|
||||||
|
const { value } = change;
|
||||||
|
if (!value.isCollapsed) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const { startBlock } = value;
|
||||||
|
const currentLineText = startBlock.text;
|
||||||
|
const indent = getIndent(currentLineText);
|
||||||
|
|
||||||
|
return change
|
||||||
|
.splitBlock()
|
||||||
|
.insertText(indent)
|
||||||
|
.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Prism from 'prismjs';
|
||||||
|
|
||||||
|
const TOKEN_MARK = 'prism-token';
|
||||||
|
|
||||||
|
export function setPrismTokens(language, field, values, alias = 'variable') {
|
||||||
|
Prism.languages[language][field] = {
|
||||||
|
alias,
|
||||||
|
pattern: new RegExp(`(?:^|\\s)(${values.join('|')})(?:$|\\s)`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code-highlighting plugin based on Prism and
|
||||||
|
* https://github.com/ianstormtaylor/slate/blob/master/examples/code-highlighting/index.js
|
||||||
|
*
|
||||||
|
* (Adapted to handle nested grammar definitions.)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function PrismPlugin({ definition, language }) {
|
||||||
|
if (definition) {
|
||||||
|
// Don't override exising modified definitions
|
||||||
|
Prism.languages[language] = Prism.languages[language] || definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Render a Slate mark with appropiate CSS class names
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @return {Element}
|
||||||
|
*/
|
||||||
|
|
||||||
|
renderMark(props) {
|
||||||
|
const { children, mark } = props;
|
||||||
|
// Only apply spans to marks identified by this plugin
|
||||||
|
if (mark.type !== TOKEN_MARK) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const className = `token ${mark.data.get('types')}`;
|
||||||
|
return <span className={className}>{children}</span>;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decorate code blocks with Prism.js highlighting.
|
||||||
|
*
|
||||||
|
* @param {Node} node
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
|
|
||||||
|
decorateNode(node) {
|
||||||
|
if (node.type !== 'paragraph') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const texts = node.getTexts().toArray();
|
||||||
|
const tstring = texts.map(t => t.text).join('\n');
|
||||||
|
const grammar = Prism.languages[language];
|
||||||
|
const tokens = Prism.tokenize(tstring, grammar);
|
||||||
|
const decorations: any[] = [];
|
||||||
|
let startText = texts.shift();
|
||||||
|
let endText = startText;
|
||||||
|
let startOffset = 0;
|
||||||
|
let endOffset = 0;
|
||||||
|
let start = 0;
|
||||||
|
|
||||||
|
function processToken(token, acc?) {
|
||||||
|
// Accumulate token types down the tree
|
||||||
|
const types = `${acc || ''} ${token.type || ''} ${token.alias || ''}`;
|
||||||
|
|
||||||
|
// Add mark for token node
|
||||||
|
if (typeof token === 'string' || typeof token.content === 'string') {
|
||||||
|
startText = endText;
|
||||||
|
startOffset = endOffset;
|
||||||
|
|
||||||
|
const content = typeof token === 'string' ? token : token.content;
|
||||||
|
const newlines = content.split('\n').length - 1;
|
||||||
|
const length = content.length - newlines;
|
||||||
|
const end = start + length;
|
||||||
|
|
||||||
|
let available = startText.text.length - startOffset;
|
||||||
|
let remaining = length;
|
||||||
|
|
||||||
|
endOffset = startOffset + remaining;
|
||||||
|
|
||||||
|
while (available < remaining) {
|
||||||
|
endText = texts.shift();
|
||||||
|
remaining = length - available;
|
||||||
|
available = endText.text.length;
|
||||||
|
endOffset = remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject marks from up the tree (acc) as well
|
||||||
|
if (typeof token !== 'string' || acc) {
|
||||||
|
const range = {
|
||||||
|
anchorKey: startText.key,
|
||||||
|
anchorOffset: startOffset,
|
||||||
|
focusKey: endText.key,
|
||||||
|
focusOffset: endOffset,
|
||||||
|
marks: [{ type: TOKEN_MARK, data: { types } }],
|
||||||
|
};
|
||||||
|
|
||||||
|
decorations.push(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
start = end;
|
||||||
|
} else if (token.content && token.content.length) {
|
||||||
|
// Tokens can be nested
|
||||||
|
for (const subToken of token.content) {
|
||||||
|
processToken(subToken, types);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process top-level tokens
|
||||||
|
for (const token of tokens) {
|
||||||
|
processToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorations;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
export default function RunnerPlugin({ handler }) {
|
||||||
|
return {
|
||||||
|
onKeyDown(event) {
|
||||||
|
// Handle enter
|
||||||
|
if (handler && event.key === 'Enter' && event.shiftKey) {
|
||||||
|
// Submit on Enter
|
||||||
|
event.preventDefault();
|
||||||
|
handler(event);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function scrollIntoView(el) {
|
||||||
|
if (!el || !el.offsetParent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const container = el.offsetParent;
|
||||||
|
if (el.offsetTop > container.scrollTop + container.offsetHeight || el.offsetTop < container.scrollTop) {
|
||||||
|
container.scrollTop = el.offsetTop - container.offsetTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TypeaheadItem extends React.PureComponent<any, any> {
|
||||||
|
el: any;
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props.isSelected && !prevProps.isSelected) {
|
||||||
|
scrollIntoView(this.el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRef = el => {
|
||||||
|
this.el = el;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { hint, isSelected, label, onClickItem } = this.props;
|
||||||
|
const className = isSelected ? 'typeahead-item typeahead-item__selected' : 'typeahead-item';
|
||||||
|
const onClick = () => onClickItem(label);
|
||||||
|
return (
|
||||||
|
<li ref={this.getRef} className={className} onClick={onClick}>
|
||||||
|
{label}
|
||||||
|
{hint && isSelected ? <div className="typeahead-item-hint">{hint}</div> : null}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TypeaheadGroup extends React.PureComponent<any, any> {
|
||||||
|
render() {
|
||||||
|
const { items, label, selected, onClickItem } = this.props;
|
||||||
|
return (
|
||||||
|
<li className="typeahead-group">
|
||||||
|
<div className="typeahead-group__title">{label}</div>
|
||||||
|
<ul className="typeahead-group__list">
|
||||||
|
{items.map(item => {
|
||||||
|
const text = typeof item === 'object' ? item.text : item;
|
||||||
|
const label = typeof item === 'object' ? item.display || item.text : item;
|
||||||
|
return (
|
||||||
|
<TypeaheadItem
|
||||||
|
key={text}
|
||||||
|
onClickItem={onClickItem}
|
||||||
|
isSelected={selected.indexOf(text) > -1}
|
||||||
|
hint={item.hint}
|
||||||
|
label={label}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Typeahead extends React.PureComponent<any, any> {
|
||||||
|
render() {
|
||||||
|
const { groupedItems, menuRef, selectedItems, onClickItem } = this.props;
|
||||||
|
return (
|
||||||
|
<ul className="typeahead" ref={menuRef}>
|
||||||
|
{groupedItems.map(g => (
|
||||||
|
<TypeaheadGroup key={g.label} onClickItem={onClickItem} selected={selectedItems} {...g} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Typeahead;
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
Object.assign({});
|
|
||||||
-219
@@ -1,219 +0,0 @@
|
|||||||
// tslint:disable-next-line:no-reference
|
|
||||||
///<reference path="../../../../../../node_modules/monaco-editor/monaco.d.ts" />
|
|
||||||
|
|
||||||
import KustoCodeEditor from './kusto_code_editor';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
describe('KustoCodeEditor', () => {
|
|
||||||
let editor;
|
|
||||||
|
|
||||||
describe('getCompletionItems', () => {
|
|
||||||
let completionItems;
|
|
||||||
let lineContent;
|
|
||||||
let model;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
(global as any).monaco = {
|
|
||||||
languages: {
|
|
||||||
CompletionItemKind: {
|
|
||||||
Keyword: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
model = {
|
|
||||||
getLineCount: () => 3,
|
|
||||||
getValueInRange: () => 'atable/n' + lineContent,
|
|
||||||
getLineContent: () => lineContent,
|
|
||||||
};
|
|
||||||
|
|
||||||
const StandaloneMock = jest.fn<monaco.editor.ICodeEditor>();
|
|
||||||
editor = new KustoCodeEditor(null, 'TimeGenerated', () => {}, {});
|
|
||||||
editor.codeEditor = new StandaloneMock();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when no where clause and no | in model text', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
lineContent = ' ';
|
|
||||||
const position = { lineNumber: 2, column: 2 };
|
|
||||||
completionItems = editor.getCompletionItems(model, position);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not return any grafana macros', () => {
|
|
||||||
expect(completionItems.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when no where clause in model text', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
lineContent = '| ';
|
|
||||||
const position = { lineNumber: 2, column: 3 };
|
|
||||||
completionItems = editor.getCompletionItems(model, position);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return grafana macros for where and timefilter', () => {
|
|
||||||
expect(completionItems.length).toBe(1);
|
|
||||||
|
|
||||||
expect(completionItems[0].label).toBe('where $__timeFilter(timeColumn)');
|
|
||||||
expect(completionItems[0].insertText.value).toBe('where \\$__timeFilter(${0:TimeGenerated})');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when on line with where clause', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
lineContent = '| where Test == 2 and ';
|
|
||||||
const position = { lineNumber: 2, column: 23 };
|
|
||||||
completionItems = editor.getCompletionItems(model, position);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return grafana macros and variables', () => {
|
|
||||||
expect(completionItems.length).toBe(4);
|
|
||||||
|
|
||||||
expect(completionItems[0].label).toBe('$__timeFilter(timeColumn)');
|
|
||||||
expect(completionItems[0].insertText.value).toBe('\\$__timeFilter(${0:TimeGenerated})');
|
|
||||||
|
|
||||||
expect(completionItems[1].label).toBe('$__from');
|
|
||||||
expect(completionItems[1].insertText.value).toBe('\\$__from');
|
|
||||||
|
|
||||||
expect(completionItems[2].label).toBe('$__to');
|
|
||||||
expect(completionItems[2].insertText.value).toBe('\\$__to');
|
|
||||||
|
|
||||||
expect(completionItems[3].label).toBe('$__interval');
|
|
||||||
expect(completionItems[3].insertText.value).toBe('\\$__interval');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('onDidChangeCursorSelection', () => {
|
|
||||||
const keyboardEvent = {
|
|
||||||
selection: {
|
|
||||||
startLineNumber: 4,
|
|
||||||
startColumn: 26,
|
|
||||||
endLineNumber: 4,
|
|
||||||
endColumn: 31,
|
|
||||||
selectionStartLineNumber: 4,
|
|
||||||
selectionStartColumn: 26,
|
|
||||||
positionLineNumber: 4,
|
|
||||||
positionColumn: 31,
|
|
||||||
},
|
|
||||||
secondarySelections: [],
|
|
||||||
source: 'keyboard',
|
|
||||||
reason: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
const modelChangedEvent = {
|
|
||||||
selection: {
|
|
||||||
startLineNumber: 2,
|
|
||||||
startColumn: 1,
|
|
||||||
endLineNumber: 3,
|
|
||||||
endColumn: 3,
|
|
||||||
selectionStartLineNumber: 2,
|
|
||||||
selectionStartColumn: 1,
|
|
||||||
positionLineNumber: 3,
|
|
||||||
positionColumn: 3,
|
|
||||||
},
|
|
||||||
secondarySelections: [],
|
|
||||||
source: 'modelChange',
|
|
||||||
reason: 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('suggestion trigger', () => {
|
|
||||||
let suggestionTriggered;
|
|
||||||
let lineContent = '';
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
(global as any).monaco = {
|
|
||||||
languages: {
|
|
||||||
CompletionItemKind: {
|
|
||||||
Keyword: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
editor: {
|
|
||||||
CursorChangeReason: {
|
|
||||||
NotSet: 0,
|
|
||||||
ContentFlush: 1,
|
|
||||||
RecoverFromMarkers: 2,
|
|
||||||
Explicit: 3,
|
|
||||||
Paste: 4,
|
|
||||||
Undo: 5,
|
|
||||||
Redo: 6,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const StandaloneMock = jest.fn<monaco.editor.ICodeEditor>(() => ({
|
|
||||||
getModel: () => {
|
|
||||||
return {
|
|
||||||
getLineCount: () => 3,
|
|
||||||
getLineContent: () => lineContent,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
editor = new KustoCodeEditor(null, 'TimeGenerated', () => {}, {});
|
|
||||||
editor.codeEditor = new StandaloneMock();
|
|
||||||
editor.triggerSuggestions = () => {
|
|
||||||
suggestionTriggered = true;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when model change event, reason is RecoverFromMarkers and there is a space after', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
suggestionTriggered = false;
|
|
||||||
lineContent = '| ';
|
|
||||||
editor.onDidChangeCursorSelection(modelChangedEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should trigger suggestion', () => {
|
|
||||||
expect(suggestionTriggered).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when not model change event', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
suggestionTriggered = false;
|
|
||||||
editor.onDidChangeCursorSelection(keyboardEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not trigger suggestion', () => {
|
|
||||||
expect(suggestionTriggered).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when model change event but with incorrect reason', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
suggestionTriggered = false;
|
|
||||||
const modelChangedWithInvalidReason = _.cloneDeep(modelChangedEvent);
|
|
||||||
modelChangedWithInvalidReason.reason = 5;
|
|
||||||
editor.onDidChangeCursorSelection(modelChangedWithInvalidReason);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not trigger suggestion', () => {
|
|
||||||
expect(suggestionTriggered).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when model change event but with no space after', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
suggestionTriggered = false;
|
|
||||||
lineContent = '|';
|
|
||||||
editor.onDidChangeCursorSelection(modelChangedEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not trigger suggestion', () => {
|
|
||||||
expect(suggestionTriggered).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when model change event but with no space after', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
suggestionTriggered = false;
|
|
||||||
lineContent = '|';
|
|
||||||
editor.onDidChangeCursorSelection(modelChangedEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not trigger suggestion', () => {
|
|
||||||
expect(suggestionTriggered).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
-332
@@ -1,332 +0,0 @@
|
|||||||
// tslint:disable-next-line:no-reference
|
|
||||||
///<reference path="../../../../../../node_modules/monaco-editor/monaco.d.ts" />
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
export interface SuggestionController {
|
|
||||||
_model: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class KustoCodeEditor {
|
|
||||||
codeEditor: monaco.editor.IStandaloneCodeEditor;
|
|
||||||
completionItemProvider: monaco.IDisposable;
|
|
||||||
signatureHelpProvider: monaco.IDisposable;
|
|
||||||
|
|
||||||
splitWithNewLineRegex = /[^\n]+\n?|\n/g;
|
|
||||||
newLineRegex = /\r?\n/;
|
|
||||||
startsWithKustoPipeRegex = /^\|\s*/g;
|
|
||||||
kustoPipeRegexStrict = /^\|\s*$/g;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private containerDiv: any,
|
|
||||||
private defaultTimeField: string,
|
|
||||||
private getSchema: () => any,
|
|
||||||
private config: any
|
|
||||||
) {}
|
|
||||||
|
|
||||||
initMonaco(scope) {
|
|
||||||
const themeName = this.config.bootData.user.lightTheme ? 'grafana-light' : 'vs-dark';
|
|
||||||
|
|
||||||
monaco.editor.defineTheme('grafana-light', {
|
|
||||||
base: 'vs',
|
|
||||||
inherit: true,
|
|
||||||
rules: [
|
|
||||||
{ token: 'comment', foreground: '008000' },
|
|
||||||
{ token: 'variable.predefined', foreground: '800080' },
|
|
||||||
{ token: 'function', foreground: '0000FF' },
|
|
||||||
{ token: 'operator.sql', foreground: 'FF4500' },
|
|
||||||
{ token: 'string', foreground: 'B22222' },
|
|
||||||
{ token: 'operator.scss', foreground: '0000FF' },
|
|
||||||
{ token: 'variable', foreground: 'C71585' },
|
|
||||||
{ token: 'variable.parameter', foreground: '9932CC' },
|
|
||||||
{ token: '', foreground: '000000' },
|
|
||||||
{ token: 'type', foreground: '0000FF' },
|
|
||||||
{ token: 'tag', foreground: '0000FF' },
|
|
||||||
{ token: 'annotation', foreground: '2B91AF' },
|
|
||||||
{ token: 'keyword', foreground: '0000FF' },
|
|
||||||
{ token: 'number', foreground: '191970' },
|
|
||||||
{ token: 'annotation', foreground: '9400D3' },
|
|
||||||
{ token: 'invalid', background: 'cd3131' },
|
|
||||||
],
|
|
||||||
colors: {
|
|
||||||
'textCodeBlock.background': '#FFFFFF',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
monaco.languages['kusto'].kustoDefaults.setLanguageSettings({
|
|
||||||
includeControlCommands: true,
|
|
||||||
newlineAfterPipe: true,
|
|
||||||
useIntellisenseV2: false,
|
|
||||||
useSemanticColorization: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.codeEditor = monaco.editor.create(this.containerDiv, {
|
|
||||||
value: scope.content || 'Write your query here',
|
|
||||||
language: 'kusto',
|
|
||||||
// language: 'go',
|
|
||||||
selectionHighlight: false,
|
|
||||||
theme: themeName,
|
|
||||||
folding: true,
|
|
||||||
lineNumbers: 'off',
|
|
||||||
lineHeight: 16,
|
|
||||||
suggestFontSize: 13,
|
|
||||||
dragAndDrop: false,
|
|
||||||
occurrencesHighlight: false,
|
|
||||||
minimap: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
renderIndentGuides: false,
|
|
||||||
wordWrap: 'on',
|
|
||||||
});
|
|
||||||
this.codeEditor.layout();
|
|
||||||
|
|
||||||
if (monaco.editor.getModels().length === 1) {
|
|
||||||
this.completionItemProvider = monaco.languages.registerCompletionItemProvider('kusto', {
|
|
||||||
triggerCharacters: ['.', ' '],
|
|
||||||
provideCompletionItems: this.getCompletionItems.bind(this),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.signatureHelpProvider = monaco.languages.registerSignatureHelpProvider('kusto', {
|
|
||||||
signatureHelpTriggerCharacters: ['(', ')'],
|
|
||||||
provideSignatureHelp: this.getSignatureHelp.bind(this),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.codeEditor.createContextKey('readyToExecute', true);
|
|
||||||
|
|
||||||
this.codeEditor.onDidChangeCursorSelection(event => {
|
|
||||||
this.onDidChangeCursorSelection(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.getSchema().then(schema => {
|
|
||||||
if (!schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
monaco.languages['kusto'].getKustoWorker().then(workerAccessor => {
|
|
||||||
const model = this.codeEditor.getModel();
|
|
||||||
if (!model) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
workerAccessor(model.uri).then(worker => {
|
|
||||||
const dbName = Object.keys(schema.Databases).length > 0 ? Object.keys(schema.Databases)[0] : '';
|
|
||||||
worker.setSchemaFromShowSchema(schema, 'https://help.kusto.windows.net', dbName);
|
|
||||||
this.codeEditor.layout();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setOnDidChangeModelContent(listener) {
|
|
||||||
this.codeEditor.onDidChangeModelContent(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
disposeMonaco() {
|
|
||||||
if (this.completionItemProvider) {
|
|
||||||
try {
|
|
||||||
this.completionItemProvider.dispose();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to dispose the completion item provider.', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.signatureHelpProvider) {
|
|
||||||
try {
|
|
||||||
this.signatureHelpProvider.dispose();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to dispose the signature help provider.', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.codeEditor) {
|
|
||||||
try {
|
|
||||||
this.codeEditor.dispose();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to dispose the editor component.', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addCommand(keybinding: number, commandFunc: monaco.editor.ICommandHandler) {
|
|
||||||
this.codeEditor.addCommand(keybinding, commandFunc, 'readyToExecute');
|
|
||||||
}
|
|
||||||
|
|
||||||
getValue() {
|
|
||||||
return this.codeEditor.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
toSuggestionController(srv: monaco.editor.IEditorContribution): SuggestionController {
|
|
||||||
return srv as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEditorContent(value) {
|
|
||||||
this.codeEditor.setValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCompletionItems(model: monaco.editor.IReadOnlyModel, position: monaco.Position) {
|
|
||||||
const timeFilterDocs =
|
|
||||||
'##### Macro that uses the selected timerange in Grafana to filter the query.\n\n' +
|
|
||||||
'- `$__timeFilter()` -> Uses the ' +
|
|
||||||
this.defaultTimeField +
|
|
||||||
' column\n\n' +
|
|
||||||
'- `$__timeFilter(datetimeColumn)` -> Uses the specified datetime column to build the query.';
|
|
||||||
|
|
||||||
const textUntilPosition = model.getValueInRange({
|
|
||||||
startLineNumber: 1,
|
|
||||||
startColumn: 1,
|
|
||||||
endLineNumber: position.lineNumber,
|
|
||||||
endColumn: position.column,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!_.includes(textUntilPosition, '|')) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.includes(textUntilPosition.toLowerCase(), 'where')) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: 'where $__timeFilter(timeColumn)',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
|
||||||
insertText: {
|
|
||||||
value: 'where \\$__timeFilter(${0:' + this.defaultTimeField + '})',
|
|
||||||
},
|
|
||||||
documentation: {
|
|
||||||
value: timeFilterDocs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_.includes(model.getLineContent(position.lineNumber).toLowerCase(), 'where')) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: '$__timeFilter(timeColumn)',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
|
||||||
insertText: {
|
|
||||||
value: '\\$__timeFilter(${0:' + this.defaultTimeField + '})',
|
|
||||||
},
|
|
||||||
documentation: {
|
|
||||||
value: timeFilterDocs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '$__from',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
|
||||||
insertText: {
|
|
||||||
value: `\\$__from`,
|
|
||||||
},
|
|
||||||
documentation: {
|
|
||||||
value:
|
|
||||||
'Built-in variable that returns the from value of the selected timerange in Grafana.\n\n' +
|
|
||||||
'Example: `where ' +
|
|
||||||
this.defaultTimeField +
|
|
||||||
' > $__from` ',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '$__to',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
|
||||||
insertText: {
|
|
||||||
value: `\\$__to`,
|
|
||||||
},
|
|
||||||
documentation: {
|
|
||||||
value:
|
|
||||||
'Built-in variable that returns the to value of the selected timerange in Grafana.\n\n' +
|
|
||||||
'Example: `where ' +
|
|
||||||
this.defaultTimeField +
|
|
||||||
' < $__to` ',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '$__interval',
|
|
||||||
kind: monaco.languages.CompletionItemKind.Keyword,
|
|
||||||
insertText: {
|
|
||||||
value: `\\$__interval`,
|
|
||||||
},
|
|
||||||
documentation: {
|
|
||||||
value:
|
|
||||||
'##### Built-in variable that returns an automatic time grain suitable for the current timerange.\n\n' +
|
|
||||||
'Used with the bin() function - `bin(' +
|
|
||||||
this.defaultTimeField +
|
|
||||||
', $__interval)` \n\n' +
|
|
||||||
'[Grafana docs](http://docs.grafana.org/reference/templating/#the-interval-variable)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
getSignatureHelp(model: monaco.editor.IReadOnlyModel, position: monaco.Position, token: monaco.CancellationToken) {
|
|
||||||
const textUntilPosition = model.getValueInRange({
|
|
||||||
startLineNumber: position.lineNumber,
|
|
||||||
startColumn: position.column - 14,
|
|
||||||
endLineNumber: position.lineNumber,
|
|
||||||
endColumn: position.column,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (textUntilPosition !== '$__timeFilter(') {
|
|
||||||
return {} as monaco.languages.SignatureHelp;
|
|
||||||
}
|
|
||||||
|
|
||||||
const signature: monaco.languages.SignatureHelp = {
|
|
||||||
activeParameter: 0,
|
|
||||||
activeSignature: 0,
|
|
||||||
signatures: [
|
|
||||||
{
|
|
||||||
label: '$__timeFilter(timeColumn)',
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
label: 'timeColumn',
|
|
||||||
documentation:
|
|
||||||
'Default is ' +
|
|
||||||
this.defaultTimeField +
|
|
||||||
' column. Datetime column to filter data using the selected date range. ',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
return signature;
|
|
||||||
}
|
|
||||||
|
|
||||||
onDidChangeCursorSelection(event) {
|
|
||||||
if (event.source !== 'modelChange' || event.reason !== monaco.editor.CursorChangeReason.RecoverFromMarkers) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const lastChar = this.getCharAt(event.selection.positionLineNumber, event.selection.positionColumn - 1);
|
|
||||||
|
|
||||||
if (lastChar !== ' ') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.triggerSuggestions();
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerSuggestions() {
|
|
||||||
const suggestController = this.codeEditor.getContribution('editor.contrib.suggestController');
|
|
||||||
if (!suggestController) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const convertedController = this.toSuggestionController(suggestController);
|
|
||||||
|
|
||||||
convertedController._model.cancel();
|
|
||||||
setTimeout(() => {
|
|
||||||
convertedController._model.trigger(true);
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCharAt(lineNumber: number, column: number) {
|
|
||||||
const model = this.codeEditor.getModel();
|
|
||||||
if (model.getLineCount() === 0 || model.getLineCount() < lineNumber) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const line = model.getLineContent(lineNumber);
|
|
||||||
if (line.length < column || column < 1) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return line[column - 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-105
@@ -1,105 +0,0 @@
|
|||||||
// tslint:disable-next-line:no-reference
|
|
||||||
// ///<reference path="../../../../../../node_modules/monaco-editor/monaco.d.ts" />
|
|
||||||
|
|
||||||
import angular from 'angular';
|
|
||||||
import KustoCodeEditor from './kusto_code_editor';
|
|
||||||
import config from 'app/core/config';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load monaco code editor and its' dependencies as a separate webpack chunk.
|
|
||||||
*/
|
|
||||||
function importMonaco() {
|
|
||||||
return import(
|
|
||||||
/* webpackChunkName: "monaco" */
|
|
||||||
'./monaco-loader'
|
|
||||||
).then(monaco => {
|
|
||||||
return monaco;
|
|
||||||
}).catch(error => {
|
|
||||||
console.error('An error occurred while loading monaco-kusto:\n', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const editorTemplate = `<div id="content" tabindex="0" style="width: 100%; height: 120px"></div>`;
|
|
||||||
|
|
||||||
function link(scope, elem, attrs) {
|
|
||||||
const containerDiv = elem.find('#content')[0];
|
|
||||||
|
|
||||||
if (!(global as any).monaco) {
|
|
||||||
// (global as any).System.import(`./${scope.pluginBaseUrl}/lib/monaco.min.js`).then(() => {
|
|
||||||
importMonaco().then(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
initMonaco(containerDiv, scope);
|
|
||||||
}, 1);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setTimeout(() => {
|
|
||||||
initMonaco(containerDiv, scope);
|
|
||||||
}, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
containerDiv.onblur = () => {
|
|
||||||
scope.onChange();
|
|
||||||
};
|
|
||||||
|
|
||||||
containerDiv.onkeydown = evt => {
|
|
||||||
if (evt.key === 'Escape') {
|
|
||||||
evt.stopPropagation();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
function initMonaco(containerDiv, scope) {
|
|
||||||
const kustoCodeEditor = new KustoCodeEditor(containerDiv, scope.defaultTimeField, scope.getSchema, config);
|
|
||||||
|
|
||||||
kustoCodeEditor.initMonaco(scope);
|
|
||||||
|
|
||||||
/* tslint:disable:no-bitwise */
|
|
||||||
kustoCodeEditor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => {
|
|
||||||
const newValue = kustoCodeEditor.getValue();
|
|
||||||
scope.content = newValue;
|
|
||||||
scope.onChange();
|
|
||||||
});
|
|
||||||
/* tslint:enable:no-bitwise */
|
|
||||||
|
|
||||||
// Sync with outer scope - update editor content if model has been changed from outside of directive.
|
|
||||||
scope.$watch('content', (newValue, oldValue) => {
|
|
||||||
const editorValue = kustoCodeEditor.getValue();
|
|
||||||
if (newValue !== editorValue && newValue !== oldValue) {
|
|
||||||
scope.$$postDigest(() => {
|
|
||||||
kustoCodeEditor.setEditorContent(newValue);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
kustoCodeEditor.setOnDidChangeModelContent(() => {
|
|
||||||
scope.$apply(() => {
|
|
||||||
const newValue = kustoCodeEditor.getValue();
|
|
||||||
scope.content = newValue;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
scope.$on('$destroy', () => {
|
|
||||||
kustoCodeEditor.disposeMonaco();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
export function kustoMonacoEditorDirective() {
|
|
||||||
return {
|
|
||||||
restrict: 'E',
|
|
||||||
template: editorTemplate,
|
|
||||||
scope: {
|
|
||||||
content: '=',
|
|
||||||
onChange: '&',
|
|
||||||
getSchema: '&',
|
|
||||||
defaultTimeField: '@',
|
|
||||||
pluginBaseUrl: '@',
|
|
||||||
},
|
|
||||||
link: link,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
angular.module('grafana.controllers').directive('kustoMonacoEditor', kustoMonacoEditorDirective);
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
// tslint:disable:no-reference
|
|
||||||
// ///<reference path="../../../../../../node_modules/@alexanderzobnin/monaco-kusto/release/min/monaco.d.ts" />
|
|
||||||
|
|
||||||
// (1) Desired editor features:
|
|
||||||
import "monaco-editor/esm/vs/editor/browser/controller/coreCommands.js";
|
|
||||||
import 'monaco-editor/esm/vs/editor/browser/widget/codeEditorWidget.js';
|
|
||||||
import 'monaco-editor/esm/vs/editor/contrib/contextmenu/contextmenu.js';
|
|
||||||
import "monaco-editor/esm/vs/editor/contrib/find/findController.js";
|
|
||||||
import 'monaco-editor/esm/vs/editor/contrib/folding/folding.js';
|
|
||||||
import 'monaco-editor/esm/vs/editor/contrib/format/formatActions.js';
|
|
||||||
import 'monaco-editor/esm/vs/editor/contrib/multicursor/multicursor.js';
|
|
||||||
import 'monaco-editor/esm/vs/editor/contrib/suggest/suggestController.js';
|
|
||||||
import 'monaco-editor/esm/vs/editor/contrib/wordHighlighter/wordHighlighter.js';
|
|
||||||
import 'monaco-editor/esm/vs/editor/standalone/browser/iPadShowKeyboard/iPadShowKeyboard.js';
|
|
||||||
import "monaco-editor/esm/vs/editor/editor.api.js";
|
|
||||||
|
|
||||||
// (2) Desired languages:
|
|
||||||
import '@alexanderzobnin/monaco-kusto/release/webpack/bridge.min.js';
|
|
||||||
import '@alexanderzobnin/monaco-kusto/release/webpack/Kusto.JavaScript.Client.min.js';
|
|
||||||
import '@alexanderzobnin/monaco-kusto/release/webpack/Kusto.Language.Bridge.min.js';
|
|
||||||
import '@alexanderzobnin/monaco-kusto/release/webpack/monaco.contribution.min.js';
|
|
||||||
+14
-2
@@ -118,8 +118,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<kusto-monaco-editor content="ctrl.target.azureLogAnalytics.query" on-change="ctrl.refresh()" get-schema="ctrl.getAzureLogAnalyticsSchema()"
|
<!-- <kusto-monaco-editor content="ctrl.target.azureLogAnalytics.query" on-change="ctrl.refresh()" get-schema="ctrl.getAzureLogAnalyticsSchema()"
|
||||||
default-time-field="TimeGenerated" plugin-base-url={{ctrl.datasource.meta.baseUrl}}></kusto-monaco-editor>
|
default-time-field="TimeGenerated" plugin-base-url={{ctrl.datasource.meta.baseUrl}}></kusto-monaco-editor> -->
|
||||||
|
|
||||||
|
<div class="gf-form gf-form--grow">
|
||||||
|
<kusto-editor
|
||||||
|
class="gf-form gf-form--grow"
|
||||||
|
request="ctrl.requestMetadata"
|
||||||
|
style="border: none"
|
||||||
|
query="ctrl.target.azureLogAnalytics.query"
|
||||||
|
change="ctrl.onLogAnalyticsQueryChange"
|
||||||
|
execute="ctrl.onLogAnalyticsQueryExecute"
|
||||||
|
variables="ctrl.templateVariables"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import _ from 'lodash';
|
|||||||
import { QueryCtrl } from 'app/plugins/sdk';
|
import { QueryCtrl } from 'app/plugins/sdk';
|
||||||
// import './css/query_editor.css';
|
// import './css/query_editor.css';
|
||||||
import TimegrainConverter from './time_grain_converter';
|
import TimegrainConverter from './time_grain_converter';
|
||||||
import './monaco/kusto_monaco_editor';
|
// import './monaco/kusto_monaco_editor';
|
||||||
|
import './editor/editor_component';
|
||||||
|
|
||||||
export interface ResultFormat {
|
export interface ResultFormat {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -323,6 +324,18 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
|
|||||||
.catch(this.handleQueryCtrlError.bind(this));
|
.catch(this.handleQueryCtrlError.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLogAnalyticsQueryChange = (nextQuery: string) => {
|
||||||
|
this.target.azureLogAnalytics.query = nextQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
onLogAnalyticsQueryExecute = () => {
|
||||||
|
this.panelCtrl.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
get templateVariables() {
|
||||||
|
return this.templateSrv.variables.map(t => '$' + t.name);
|
||||||
|
}
|
||||||
|
|
||||||
/* Application Insights Section */
|
/* Application Insights Section */
|
||||||
|
|
||||||
getAppInsightsAutoInterval() {
|
getAppInsightsAutoInterval() {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
const merge = require('webpack-merge');
|
const merge = require('webpack-merge');
|
||||||
const common = require('./webpack.common.js');
|
const common = require('./webpack.common.js');
|
||||||
const monaco = require('./webpack.monaco.js');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
@@ -10,7 +9,7 @@ const CleanWebpackPlugin = require('clean-webpack-plugin');
|
|||||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||||
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
|
||||||
module.exports = merge(common, monaco, {
|
module.exports = merge(common, {
|
||||||
devtool: "cheap-module-source-map",
|
devtool: "cheap-module-source-map",
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
|
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const webpack = require('webpack');
|
|
||||||
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
// output: {
|
|
||||||
// filename: 'monaco.min.js',
|
|
||||||
// path: path.resolve(__dirname, 'dist'),
|
|
||||||
// libraryTarget: 'umd',
|
|
||||||
// library: 'monaco',
|
|
||||||
// globalObject: 'self'
|
|
||||||
// },
|
|
||||||
entry: {
|
|
||||||
// monaco: './public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/monaco-loader.ts',
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
// filename: 'monaco.min.js',
|
|
||||||
// chunkFilename: '[name].bundle.js',
|
|
||||||
globalObject: 'self',
|
|
||||||
},
|
|
||||||
resolveLoader: {
|
|
||||||
alias: {
|
|
||||||
'blob-url-loader': require.resolve('./loaders/blobUrl'),
|
|
||||||
'compile-loader': require.resolve('./loaders/compile'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: [ 'style-loader', 'css-loader' ]
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// // https://github.com/bridgedotnet/Bridge/issues/3097
|
|
||||||
// test: /bridge\.js$/,
|
|
||||||
// loader: 'regexp-replace-loader',
|
|
||||||
// options: {
|
|
||||||
// match: {
|
|
||||||
// pattern: "globals\\.System\\s=\\s\\{\\};"
|
|
||||||
// },
|
|
||||||
// replaceWith: "$& System = globals.System; "
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// test: /Kusto\.JavaScript\.Client\.js$/,
|
|
||||||
// loader: 'regexp-replace-loader',
|
|
||||||
// options: {
|
|
||||||
// match: {
|
|
||||||
// pattern: '"use strict";'
|
|
||||||
// },
|
|
||||||
// replaceWith: "$& System = globals.System; "
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// test: /Kusto\.Language\.Bridge\.js$/,
|
|
||||||
// loader: 'regexp-replace-loader',
|
|
||||||
// options: {
|
|
||||||
// match: {
|
|
||||||
// pattern: '"use strict";'
|
|
||||||
// },
|
|
||||||
// replaceWith: "$& System = globals.System; "
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// test: /newtonsoft\.json\.js$/,
|
|
||||||
// loader: 'regexp-replace-loader',
|
|
||||||
// options: {
|
|
||||||
// match: {
|
|
||||||
// pattern: '"use strict";'
|
|
||||||
// },
|
|
||||||
// replaceWith: "$& System = globals.System; "
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// test: /monaco\.contribution\.js$/,
|
|
||||||
// loader: 'regexp-replace-loader',
|
|
||||||
// options: {
|
|
||||||
// match: {
|
|
||||||
// pattern: 'vs/language/kusto/kustoMode',
|
|
||||||
// flags: 'g'
|
|
||||||
// },
|
|
||||||
// replaceWith: "./kustoMode"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
optimization: {
|
|
||||||
splitChunks: {
|
|
||||||
// chunks: 'all',
|
|
||||||
cacheGroups: {
|
|
||||||
// monacoContribution: {
|
|
||||||
// test: /(src)|(node_modules(?!\/@kusto))/,
|
|
||||||
// name: 'monaco.contribution',
|
|
||||||
// enforce: false,
|
|
||||||
// // chunks: 'all',
|
|
||||||
// },
|
|
||||||
// bridge: {
|
|
||||||
// test: /bridge/,
|
|
||||||
// name: 'bridge',
|
|
||||||
// chunks: 'all',
|
|
||||||
// },
|
|
||||||
// KustoJavaScriptClient: {
|
|
||||||
// test: /Kusto\.JavaScript\.Client/,
|
|
||||||
// name: 'kusto.javaScript.client',
|
|
||||||
// chunks: 'all',
|
|
||||||
// },
|
|
||||||
// KustoLanguageBridge: {
|
|
||||||
// test: /Kusto\.Language\.Bridge/,
|
|
||||||
// name: 'kusto.language.bridge',
|
|
||||||
// chunks: 'all',
|
|
||||||
// },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.IgnorePlugin(/^((fs)|(path)|(os)|(crypto)|(source-map-support))$/, /vs\/language\/typescript\/lib/),
|
|
||||||
// new webpack.optimize.LimitChunkCountPlugin({
|
|
||||||
// maxChunks: 1,
|
|
||||||
// }),
|
|
||||||
// new UglifyJSPlugin()
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -2,14 +2,6 @@
|
|||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
"@alexanderzobnin/monaco-kusto@^0.2.3-rc.1":
|
|
||||||
version "0.2.3-rc.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@alexanderzobnin/monaco-kusto/-/monaco-kusto-0.2.3-rc.1.tgz#1e96eef584d6173f19670afb3f122947329a840f"
|
|
||||||
integrity sha512-bppmiGfH7iXL4AdaKV2No4WqUoWBvS4bDejSYO0VjaK8zrNPFz9QhmGhnASHvQdmexcoPEqqQScA3udA2bQ68A==
|
|
||||||
dependencies:
|
|
||||||
"@kusto/language-service" "0.0.22-alpha"
|
|
||||||
"@kusto/language-service-next" "0.0.25-alpha1"
|
|
||||||
|
|
||||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35":
|
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35":
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
|
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
|
||||||
@@ -662,16 +654,6 @@
|
|||||||
version "0.8.2"
|
version "0.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc"
|
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc"
|
||||||
|
|
||||||
"@kusto/language-service-next@0.0.25-alpha1":
|
|
||||||
version "0.0.25-alpha1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@kusto/language-service-next/-/language-service-next-0.0.25-alpha1.tgz#73977b0873c7c2a23ae0c2cc1fef95a68c723c09"
|
|
||||||
integrity sha512-xxdY+Ei+e/GuzWZYoyjQqOfuzwVPMfHJwPRcxOdcSq5XMt9oZS+ryVH66l+CBxdZDdxEfQD2evVTXLjOAck5Rg==
|
|
||||||
|
|
||||||
"@kusto/language-service@0.0.22-alpha":
|
|
||||||
version "0.0.22-alpha"
|
|
||||||
resolved "https://registry.yarnpkg.com/@kusto/language-service/-/language-service-0.0.22-alpha.tgz#990bbfb82e8e8991c35a12aab00d890a05fff623"
|
|
||||||
integrity sha512-oYiakH2Lq4j7ghahAtqxC+nuOKybH03H1o3IWyB3p8Ll4WkYQOrV8GWpqEjPtMfsuOt3t5k55OzzwDWFaX2zlw==
|
|
||||||
|
|
||||||
"@mrmlnc/readdir-enhanced@^2.2.1":
|
"@mrmlnc/readdir-enhanced@^2.2.1":
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
|
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
|
||||||
@@ -12979,11 +12961,6 @@ vm-browserify@0.0.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
indexof "0.0.1"
|
indexof "0.0.1"
|
||||||
|
|
||||||
vscode-languageserver-types@^3.14.0:
|
|
||||||
version "3.14.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz#d3b5952246d30e5241592b6dde8280e03942e743"
|
|
||||||
integrity sha512-lTmS6AlAlMHOvPQemVwo3CezxBp0sNB95KNPkqp3Nxd5VFEnuG1ByM0zlRWos0zjO3ZWtkvhal0COgiV1xIA4A==
|
|
||||||
|
|
||||||
w3c-blob@0.0.1:
|
w3c-blob@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/w3c-blob/-/w3c-blob-0.0.1.tgz#b0cd352a1a50f515563420ffd5861f950f1d85b8"
|
resolved "https://registry.yarnpkg.com/w3c-blob/-/w3c-blob-0.0.1.tgz#b0cd352a1a50f515563420ffd5861f950f1d85b8"
|
||||||
|
|||||||
Reference in New Issue
Block a user