language_provider.test.ts 16 KB

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