KustoQueryField.tsx 9.0 KB

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