language_provider.test.ts 15 KB

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