KustoQueryField.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. import _ from 'lodash';
  2. import Plain from 'slate-plain-serializer';
  3. import QueryField from './query_field';
  4. // import debounce from './utils/debounce';
  5. // import {getNextCharacter} from './utils/dom';
  6. import debounce from 'app/features/explore/utils/debounce';
  7. import { getNextCharacter } from 'app/features/explore/utils/dom';
  8. import { KEYWORDS, functionTokens, operatorTokens, grafanaMacros } from './kusto/kusto';
  9. // import '../sass/editor.base.scss';
  10. const TYPEAHEAD_DELAY = 500;
  11. interface Suggestion {
  12. text: string;
  13. deleteBackwards?: number;
  14. type?: string;
  15. }
  16. interface SuggestionGroup {
  17. label: string;
  18. items: Suggestion[];
  19. prefixMatch?: boolean;
  20. skipFilter?: boolean;
  21. }
  22. interface KustoSchema {
  23. Databases: {
  24. Default?: KustoDBSchema;
  25. };
  26. Plugins?: any[];
  27. }
  28. interface KustoDBSchema {
  29. Name?: string;
  30. Functions?: any;
  31. Tables?: any;
  32. }
  33. const defaultSchema = () => ({
  34. Databases: {
  35. Default: {}
  36. }
  37. });
  38. const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
  39. const wrapText = text => ({ text });
  40. export default class KustoQueryField extends QueryField {
  41. fields: any;
  42. events: any;
  43. schema: KustoSchema;
  44. constructor(props, context) {
  45. super(props, context);
  46. this.schema = defaultSchema();
  47. this.onTypeahead = debounce(this.onTypeahead, TYPEAHEAD_DELAY);
  48. }
  49. componentDidMount() {
  50. super.componentDidMount();
  51. this.fetchSchema();
  52. }
  53. onTypeahead = () => {
  54. const selection = window.getSelection();
  55. if (selection.anchorNode) {
  56. const wrapperNode = selection.anchorNode.parentElement;
  57. if (wrapperNode === null) {
  58. return;
  59. }
  60. const editorNode = wrapperNode.closest('.slate-query-field');
  61. if (!editorNode || this.state.value.isBlurred) {
  62. // Not inside this editor
  63. return;
  64. }
  65. // DOM ranges
  66. const range = selection.getRangeAt(0);
  67. const text = selection.anchorNode.textContent;
  68. if (text === null) {
  69. return;
  70. }
  71. const offset = range.startOffset;
  72. let prefix = cleanText(text.substr(0, offset));
  73. // Model ranges
  74. const modelOffset = this.state.value.anchorOffset;
  75. const modelPrefix = this.state.value.anchorText.text.slice(0, modelOffset);
  76. // Determine candidates by context
  77. let suggestionGroups: SuggestionGroup[] = [];
  78. const wrapperClasses = wrapperNode.classList;
  79. let typeaheadContext: string | null = null;
  80. if (wrapperClasses.contains('function-context')) {
  81. typeaheadContext = 'context-function';
  82. if (this.fields) {
  83. suggestionGroups = this._getKeywordSuggestions();
  84. } else {
  85. this._fetchFields();
  86. return;
  87. }
  88. } else if (modelPrefix.match(/(facet\s$)/i)) {
  89. typeaheadContext = 'context-facet';
  90. if (this.fields) {
  91. suggestionGroups = this._getKeywordSuggestions();
  92. } else {
  93. this._fetchFields();
  94. return;
  95. }
  96. } else if (modelPrefix.match(/(,\s*$)/)) {
  97. typeaheadContext = 'context-multiple-fields';
  98. if (this.fields) {
  99. suggestionGroups = this._getKeywordSuggestions();
  100. } else {
  101. this._fetchFields();
  102. return;
  103. }
  104. } else if (modelPrefix.match(/(from\s$)/i)) {
  105. typeaheadContext = 'context-from';
  106. if (this.events) {
  107. suggestionGroups = this._getKeywordSuggestions();
  108. } else {
  109. this._fetchEvents();
  110. return;
  111. }
  112. } else if (modelPrefix.match(/(^select\s\w*$)/i)) {
  113. typeaheadContext = 'context-select';
  114. if (this.fields) {
  115. suggestionGroups = this._getKeywordSuggestions();
  116. } else {
  117. this._fetchFields();
  118. return;
  119. }
  120. } else if (modelPrefix.match(/from\s\S+\s\w*$/i)) {
  121. prefix = '';
  122. typeaheadContext = 'context-since';
  123. suggestionGroups = this._getKeywordSuggestions();
  124. // } else if (modelPrefix.match(/\d+\s\w*$/)) {
  125. // typeaheadContext = 'context-number';
  126. // suggestionGroups = this._getAfterNumberSuggestions();
  127. } else if (modelPrefix.match(/ago\b/i) || modelPrefix.match(/facet\b/i) || modelPrefix.match(/\$__timefilter\b/i)) {
  128. typeaheadContext = 'context-timeseries';
  129. suggestionGroups = this._getKeywordSuggestions();
  130. } else if (prefix && !wrapperClasses.contains('argument')) {
  131. typeaheadContext = 'context-builtin';
  132. suggestionGroups = this._getKeywordSuggestions();
  133. } else if (Plain.serialize(this.state.value) === '') {
  134. typeaheadContext = 'context-new';
  135. if (this.schema) {
  136. suggestionGroups = this._getInitialSuggestions();
  137. } else {
  138. this.fetchSchema();
  139. setTimeout(this.onTypeahead, 0);
  140. return;
  141. }
  142. }
  143. let results = 0;
  144. prefix = prefix.toLowerCase();
  145. const filteredSuggestions = suggestionGroups.map(group => {
  146. if (group.items && prefix && !group.skipFilter) {
  147. group.items = group.items.filter(c => c.text.length >= prefix.length);
  148. if (group.prefixMatch) {
  149. group.items = group.items.filter(c => c.text.toLowerCase().indexOf(prefix) === 0);
  150. } else {
  151. group.items = group.items.filter(c => c.text.toLowerCase().indexOf(prefix) > -1);
  152. }
  153. }
  154. results += group.items.length;
  155. return group;
  156. })
  157. .filter(group => group.items.length > 0);
  158. // console.log('onTypeahead', selection.anchorNode, wrapperClasses, text, offset, prefix, typeaheadContext);
  159. this.setState({
  160. typeaheadPrefix: prefix,
  161. typeaheadContext,
  162. typeaheadText: text,
  163. suggestions: results > 0 ? filteredSuggestions : [],
  164. });
  165. }
  166. }
  167. applyTypeahead(change, suggestion) {
  168. const { typeaheadPrefix, typeaheadContext, typeaheadText } = this.state;
  169. let suggestionText = suggestion.text || suggestion;
  170. const move = 0;
  171. // Modify suggestion based on context
  172. const nextChar = getNextCharacter();
  173. if (suggestion.type === 'function') {
  174. if (!nextChar || nextChar !== '(') {
  175. suggestionText += '(';
  176. }
  177. } else if (typeaheadContext === 'context-function') {
  178. if (!nextChar || nextChar !== ')') {
  179. suggestionText += ')';
  180. }
  181. } else {
  182. if (!nextChar || nextChar !== ' ') {
  183. suggestionText += ' ';
  184. }
  185. }
  186. this.resetTypeahead();
  187. // Remove the current, incomplete text and replace it with the selected suggestion
  188. const backward = suggestion.deleteBackwards || typeaheadPrefix.length;
  189. const text = cleanText(typeaheadText);
  190. const suffixLength = text.length - typeaheadPrefix.length;
  191. const offset = typeaheadText.indexOf(typeaheadPrefix);
  192. const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestionText === typeaheadText);
  193. const forward = midWord ? suffixLength + offset : 0;
  194. return change
  195. .deleteBackward(backward)
  196. .deleteForward(forward)
  197. .insertText(suggestionText)
  198. .move(move)
  199. .focus();
  200. }
  201. // private _getFieldsSuggestions(): SuggestionGroup[] {
  202. // return [
  203. // {
  204. // prefixMatch: true,
  205. // label: 'Fields',
  206. // items: this.fields.map(wrapText)
  207. // },
  208. // {
  209. // prefixMatch: true,
  210. // label: 'Variables',
  211. // items: this.props.templateVariables.map(wrapText)
  212. // }
  213. // ];
  214. // }
  215. // private _getAfterFromSuggestions(): SuggestionGroup[] {
  216. // return [
  217. // {
  218. // skipFilter: true,
  219. // label: 'Events',
  220. // items: this.events.map(wrapText)
  221. // },
  222. // {
  223. // prefixMatch: true,
  224. // label: 'Variables',
  225. // items: this.props.templateVariables
  226. // .map(wrapText)
  227. // .map(suggestion => {
  228. // suggestion.deleteBackwards = 0;
  229. // return suggestion;
  230. // })
  231. // }
  232. // ];
  233. // }
  234. // private _getAfterSelectSuggestions(): SuggestionGroup[] {
  235. // return [
  236. // {
  237. // prefixMatch: true,
  238. // label: 'Fields',
  239. // items: this.fields.map(wrapText)
  240. // },
  241. // {
  242. // prefixMatch: true,
  243. // label: 'Functions',
  244. // items: FUNCTIONS.map((s: any) => { s.type = 'function'; return s; })
  245. // },
  246. // {
  247. // prefixMatch: true,
  248. // label: 'Variables',
  249. // items: this.props.templateVariables.map(wrapText)
  250. // }
  251. // ];
  252. // }
  253. private _getKeywordSuggestions(): SuggestionGroup[] {
  254. return [
  255. {
  256. prefixMatch: true,
  257. label: 'Keywords',
  258. items: KEYWORDS.map(wrapText)
  259. },
  260. {
  261. prefixMatch: true,
  262. label: 'Operators',
  263. items: operatorTokens
  264. },
  265. {
  266. prefixMatch: true,
  267. label: 'Functions',
  268. items: functionTokens.map((s: any) => { s.type = 'function'; return s; })
  269. },
  270. {
  271. prefixMatch: true,
  272. label: 'Macros',
  273. items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; })
  274. },
  275. {
  276. prefixMatch: true,
  277. label: 'Tables',
  278. items: _.map(this.schema.Databases.Default.Tables, (t: any) => ({ text: t.Name }))
  279. }
  280. ];
  281. }
  282. private _getInitialSuggestions(): SuggestionGroup[] {
  283. return [
  284. {
  285. prefixMatch: true,
  286. label: 'Tables',
  287. items: _.map(this.schema.Databases.Default.Tables, (t: any) => ({ text: t.Name }))
  288. }
  289. ];
  290. // return [
  291. // {
  292. // prefixMatch: true,
  293. // label: 'Keywords',
  294. // items: KEYWORDS.map(wrapText)
  295. // },
  296. // {
  297. // prefixMatch: true,
  298. // label: 'Operators',
  299. // items: operatorTokens.map((s: any) => { s.type = 'function'; return s; })
  300. // },
  301. // {
  302. // prefixMatch: true,
  303. // label: 'Functions',
  304. // items: functionTokens.map((s: any) => { s.type = 'function'; return s; })
  305. // },
  306. // {
  307. // prefixMatch: true,
  308. // label: 'Macros',
  309. // items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; })
  310. // }
  311. // ];
  312. }
  313. private async _fetchEvents() {
  314. // const query = 'events';
  315. // const result = await this.request(query);
  316. // if (result === undefined) {
  317. // this.events = [];
  318. // } else {
  319. // this.events = result;
  320. // }
  321. // setTimeout(this.onTypeahead, 0);
  322. //Stub
  323. this.events = [];
  324. }
  325. private async _fetchFields() {
  326. // const query = 'fields';
  327. // const result = await this.request(query);
  328. // this.fields = result || [];
  329. // setTimeout(this.onTypeahead, 0);
  330. // Stub
  331. this.fields = [];
  332. }
  333. private async fetchSchema() {
  334. const schema = await this.props.getSchema();
  335. if (schema) {
  336. this.schema = schema;
  337. } else {
  338. this.schema = defaultSchema();
  339. }
  340. }
  341. }