language_provider.test.ts 16 KB

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