language_provider.test.ts 15 KB

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