language_provider.test.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. // @ts-ignore
  2. import Plain from 'slate-plain-serializer';
  3. import LanguageProvider from '../language_provider';
  4. describe('Language completion provider', () => {
  5. const datasource = {
  6. metadataRequest: () => ({ data: { data: [] as any[] } }),
  7. getTimeRange: () => ({ start: 0, end: 1 }),
  8. };
  9. describe('empty query suggestions', () => {
  10. it('returns default suggestions on emtpty context', () => {
  11. const instance = new LanguageProvider(datasource);
  12. const value = Plain.deserialize('');
  13. const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
  14. expect(result.context).toBeUndefined();
  15. expect(result.refresher).toBeUndefined();
  16. expect(result.suggestions).toMatchObject([
  17. {
  18. label: 'Functions',
  19. },
  20. ]);
  21. });
  22. it('returns default suggestions with metrics on emtpty context when metrics were provided', () => {
  23. const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
  24. const value = Plain.deserialize('');
  25. const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
  26. expect(result.context).toBeUndefined();
  27. expect(result.refresher).toBeUndefined();
  28. expect(result.suggestions).toMatchObject([
  29. {
  30. label: 'Functions',
  31. },
  32. {
  33. label: 'Metrics',
  34. },
  35. ]);
  36. });
  37. it('returns default suggestions with history on emtpty context when history was provided', () => {
  38. const instance = new LanguageProvider(datasource);
  39. const value = Plain.deserialize('');
  40. const history = [
  41. {
  42. query: { refId: '1', expr: 'metric' },
  43. },
  44. ];
  45. const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }, { history });
  46. expect(result.context).toBeUndefined();
  47. expect(result.refresher).toBeUndefined();
  48. expect(result.suggestions).toMatchObject([
  49. {
  50. label: 'History',
  51. items: [
  52. {
  53. label: 'metric',
  54. },
  55. ],
  56. },
  57. {
  58. label: 'Functions',
  59. },
  60. ]);
  61. });
  62. });
  63. describe('range suggestions', () => {
  64. it('returns range suggestions in range context', () => {
  65. const instance = new LanguageProvider(datasource);
  66. const value = Plain.deserialize('1');
  67. const result = instance.provideCompletionItems({
  68. text: '1',
  69. prefix: '1',
  70. value,
  71. wrapperClasses: ['context-range'],
  72. });
  73. expect(result.context).toBe('context-range');
  74. expect(result.refresher).toBeUndefined();
  75. expect(result.suggestions).toMatchObject([
  76. {
  77. items: [
  78. { label: '$__interval', sortText: '$__interval' }, // TODO: figure out why this row and sortText is needed
  79. { label: '1m', sortText: '00:01:00' },
  80. { label: '5m', sortText: '00:05:00' },
  81. { label: '10m', sortText: '00:10:00' },
  82. { label: '30m', sortText: '00:30:00' },
  83. { label: '1h', sortText: '01:00:00' },
  84. { label: '1d', sortText: '24:00:00' },
  85. ],
  86. label: 'Range vector',
  87. },
  88. ]);
  89. });
  90. });
  91. describe('metric suggestions', () => {
  92. it('returns metrics and function suggestions in an unknown context', () => {
  93. const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
  94. const value = Plain.deserialize('a');
  95. const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', value, wrapperClasses: [] });
  96. expect(result.context).toBeUndefined();
  97. expect(result.refresher).toBeUndefined();
  98. expect(result.suggestions).toMatchObject([
  99. {
  100. label: 'Functions',
  101. },
  102. {
  103. label: 'Metrics',
  104. },
  105. ]);
  106. });
  107. it('returns metrics and function suggestions after a binary operator', () => {
  108. const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
  109. const value = Plain.deserialize('*');
  110. const result = instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] });
  111. expect(result.context).toBeUndefined();
  112. expect(result.refresher).toBeUndefined();
  113. expect(result.suggestions).toMatchObject([
  114. {
  115. label: 'Functions',
  116. },
  117. {
  118. label: 'Metrics',
  119. },
  120. ]);
  121. });
  122. it('returns no suggestions at the beginning of a non-empty function', () => {
  123. const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] });
  124. const value = Plain.deserialize('sum(up)');
  125. const range = value.selection.merge({
  126. anchorOffset: 4,
  127. });
  128. const valueWithSelection = value.change().select(range).value;
  129. const result = instance.provideCompletionItems({
  130. text: '',
  131. prefix: '',
  132. value: valueWithSelection,
  133. wrapperClasses: [],
  134. });
  135. expect(result.context).toBeUndefined();
  136. expect(result.refresher).toBeUndefined();
  137. expect(result.suggestions.length).toEqual(0);
  138. });
  139. });
  140. describe('label suggestions', () => {
  141. it('returns default label suggestions on label context and no metric', () => {
  142. const instance = new LanguageProvider(datasource);
  143. const value = Plain.deserialize('{}');
  144. const range = value.selection.merge({
  145. anchorOffset: 1,
  146. });
  147. const valueWithSelection = value.change().select(range).value;
  148. const result = instance.provideCompletionItems({
  149. text: '',
  150. prefix: '',
  151. wrapperClasses: ['context-labels'],
  152. value: valueWithSelection,
  153. });
  154. expect(result.context).toBe('context-labels');
  155. expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'instance' }], label: 'Labels' }]);
  156. });
  157. it('returns label suggestions on label context and metric', () => {
  158. const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['bar'] } });
  159. const value = Plain.deserialize('metric{}');
  160. const range = value.selection.merge({
  161. anchorOffset: 7,
  162. });
  163. const valueWithSelection = value.change().select(range).value;
  164. const result = instance.provideCompletionItems({
  165. text: '',
  166. prefix: '',
  167. wrapperClasses: ['context-labels'],
  168. value: valueWithSelection,
  169. });
  170. expect(result.context).toBe('context-labels');
  171. expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
  172. });
  173. it('returns label suggestions on label context but leaves out labels that already exist', () => {
  174. const instance = new LanguageProvider(datasource, {
  175. labelKeys: { '{job1="foo",job2!="foo",job3=~"foo"}': ['bar', 'job1', 'job2', 'job3'] },
  176. });
  177. const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",}');
  178. const range = value.selection.merge({
  179. anchorOffset: 36,
  180. });
  181. const valueWithSelection = value.change().select(range).value;
  182. const result = instance.provideCompletionItems({
  183. text: '',
  184. prefix: '',
  185. wrapperClasses: ['context-labels'],
  186. value: valueWithSelection,
  187. });
  188. expect(result.context).toBe('context-labels');
  189. expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
  190. });
  191. it('returns label value suggestions inside a label value context after a negated matching operator', () => {
  192. const instance = new LanguageProvider(datasource, {
  193. labelKeys: { '{}': ['label'] },
  194. labelValues: { '{}': { label: ['a', 'b', 'c'] } },
  195. });
  196. const value = Plain.deserialize('{label!=}');
  197. const range = value.selection.merge({ anchorOffset: 8 });
  198. const valueWithSelection = value.change().select(range).value;
  199. const result = instance.provideCompletionItems({
  200. text: '!=',
  201. prefix: '',
  202. wrapperClasses: ['context-labels'],
  203. labelKey: 'label',
  204. value: valueWithSelection,
  205. });
  206. expect(result.context).toBe('context-label-values');
  207. expect(result.suggestions).toEqual([
  208. {
  209. items: [{ label: 'a' }, { label: 'b' }, { label: 'c' }],
  210. label: 'Label values for "label"',
  211. },
  212. ]);
  213. });
  214. it('returns a refresher on label context and unavailable metric', () => {
  215. const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="foo"}': ['bar'] } });
  216. const value = Plain.deserialize('metric{}');
  217. const range = value.selection.merge({
  218. anchorOffset: 7,
  219. });
  220. const valueWithSelection = value.change().select(range).value;
  221. const result = instance.provideCompletionItems({
  222. text: '',
  223. prefix: '',
  224. wrapperClasses: ['context-labels'],
  225. value: valueWithSelection,
  226. });
  227. expect(result.context).toBeUndefined();
  228. expect(result.refresher).toBeInstanceOf(Promise);
  229. expect(result.suggestions).toEqual([]);
  230. });
  231. it('returns label values on label context when given a metric and a label key', () => {
  232. const instance = new LanguageProvider(datasource, {
  233. labelKeys: { '{__name__="metric"}': ['bar'] },
  234. labelValues: { '{__name__="metric"}': { bar: ['baz'] } },
  235. });
  236. const value = Plain.deserialize('metric{bar=ba}');
  237. const range = value.selection.merge({
  238. anchorOffset: 13,
  239. });
  240. const valueWithSelection = value.change().select(range).value;
  241. const result = instance.provideCompletionItems({
  242. text: '=ba',
  243. prefix: 'ba',
  244. wrapperClasses: ['context-labels'],
  245. labelKey: 'bar',
  246. value: valueWithSelection,
  247. });
  248. expect(result.context).toBe('context-label-values');
  249. expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values for "bar"' }]);
  250. });
  251. it('returns label suggestions on aggregation context and metric w/ selector', () => {
  252. const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric",foo="xx"}': ['bar'] } });
  253. const value = Plain.deserialize('sum(metric{foo="xx"}) by ()');
  254. const range = value.selection.merge({
  255. anchorOffset: 26,
  256. });
  257. const valueWithSelection = value.change().select(range).value;
  258. const result = instance.provideCompletionItems({
  259. text: '',
  260. prefix: '',
  261. wrapperClasses: ['context-aggregation'],
  262. value: valueWithSelection,
  263. });
  264. expect(result.context).toBe('context-aggregation');
  265. expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
  266. });
  267. it('returns label suggestions on aggregation context and metric w/o selector', () => {
  268. const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['bar'] } });
  269. const value = Plain.deserialize('sum(metric) by ()');
  270. const range = value.selection.merge({
  271. anchorOffset: 16,
  272. });
  273. const valueWithSelection = value.change().select(range).value;
  274. const result = instance.provideCompletionItems({
  275. text: '',
  276. prefix: '',
  277. wrapperClasses: ['context-aggregation'],
  278. value: valueWithSelection,
  279. });
  280. expect(result.context).toBe('context-aggregation');
  281. expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
  282. });
  283. it('returns label suggestions inside a multi-line aggregation context', () => {
  284. const instance = new LanguageProvider(datasource, {
  285. labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
  286. });
  287. const value = Plain.deserialize('sum(\nmetric\n)\nby ()');
  288. const aggregationTextBlock = value.document.getBlocksAsArray()[3];
  289. const range = value.selection.moveToStartOf(aggregationTextBlock).merge({ anchorOffset: 4 });
  290. const valueWithSelection = value.change().select(range).value;
  291. const result = instance.provideCompletionItems({
  292. text: '',
  293. prefix: '',
  294. wrapperClasses: ['context-aggregation'],
  295. value: valueWithSelection,
  296. });
  297. expect(result.context).toBe('context-aggregation');
  298. expect(result.suggestions).toEqual([
  299. {
  300. items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
  301. label: 'Labels',
  302. },
  303. ]);
  304. });
  305. it('returns label suggestions inside an aggregation context with a range vector', () => {
  306. const instance = new LanguageProvider(datasource, {
  307. labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
  308. });
  309. const value = Plain.deserialize('sum(rate(metric[1h])) by ()');
  310. const range = value.selection.merge({
  311. anchorOffset: 26,
  312. });
  313. const valueWithSelection = value.change().select(range).value;
  314. const result = instance.provideCompletionItems({
  315. text: '',
  316. prefix: '',
  317. wrapperClasses: ['context-aggregation'],
  318. value: valueWithSelection,
  319. });
  320. expect(result.context).toBe('context-aggregation');
  321. expect(result.suggestions).toEqual([
  322. {
  323. items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
  324. label: 'Labels',
  325. },
  326. ]);
  327. });
  328. it('returns label suggestions inside an aggregation context with a range vector and label', () => {
  329. const instance = new LanguageProvider(datasource, {
  330. labelKeys: { '{__name__="metric",label1="value"}': ['label1', 'label2', 'label3'] },
  331. });
  332. const value = Plain.deserialize('sum(rate(metric{label1="value"}[1h])) by ()');
  333. const range = value.selection.merge({
  334. anchorOffset: 42,
  335. });
  336. const valueWithSelection = value.change().select(range).value;
  337. const result = instance.provideCompletionItems({
  338. text: '',
  339. prefix: '',
  340. wrapperClasses: ['context-aggregation'],
  341. value: valueWithSelection,
  342. });
  343. expect(result.context).toBe('context-aggregation');
  344. expect(result.suggestions).toEqual([
  345. {
  346. items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
  347. label: 'Labels',
  348. },
  349. ]);
  350. });
  351. it('returns no suggestions inside an unclear aggregation context using alternate syntax', () => {
  352. const instance = new LanguageProvider(datasource, {
  353. labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
  354. });
  355. const value = Plain.deserialize('sum by ()');
  356. const range = value.selection.merge({
  357. anchorOffset: 8,
  358. });
  359. const valueWithSelection = value.change().select(range).value;
  360. const result = instance.provideCompletionItems({
  361. text: '',
  362. prefix: '',
  363. wrapperClasses: ['context-aggregation'],
  364. value: valueWithSelection,
  365. });
  366. expect(result.context).toBe('context-aggregation');
  367. expect(result.suggestions).toEqual([]);
  368. });
  369. it('returns label suggestions inside an aggregation context using alternate syntax', () => {
  370. const instance = new LanguageProvider(datasource, {
  371. labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
  372. });
  373. const value = Plain.deserialize('sum by () (metric)');
  374. const range = value.selection.merge({
  375. anchorOffset: 8,
  376. });
  377. const valueWithSelection = value.change().select(range).value;
  378. const result = instance.provideCompletionItems({
  379. text: '',
  380. prefix: '',
  381. wrapperClasses: ['context-aggregation'],
  382. value: valueWithSelection,
  383. });
  384. expect(result.context).toBe('context-aggregation');
  385. expect(result.suggestions).toEqual([
  386. {
  387. items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
  388. label: 'Labels',
  389. },
  390. ]);
  391. });
  392. });
  393. });