language_provider.test.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. import { Editor as SlateEditor } from 'slate';
  2. import Plain from 'slate-plain-serializer';
  3. import { AbstractLabelOperator, HistoryItem } from '@grafana/data';
  4. import { SearchFunctionType } from '@grafana/ui';
  5. import { PrometheusDatasource } from './datasource';
  6. import LanguageProvider from './language_provider';
  7. import { PromQuery } from './types';
  8. import Mock = jest.Mock;
  9. describe('Language completion provider', () => {
  10. const datasource: PrometheusDatasource = {
  11. metadataRequest: () => ({ data: { data: [] as any[] } }),
  12. getTimeRangeParams: () => ({ start: '0', end: '1' }),
  13. interpolateString: (string: string) => string,
  14. } as any as PrometheusDatasource;
  15. describe('cleanText', () => {
  16. const cleanText = new LanguageProvider(datasource).cleanText;
  17. it('does not remove metric or label keys', () => {
  18. expect(cleanText('foo')).toBe('foo');
  19. expect(cleanText('foo_bar')).toBe('foo_bar');
  20. });
  21. it('keeps trailing space but removes leading', () => {
  22. expect(cleanText('foo ')).toBe('foo ');
  23. expect(cleanText(' foo')).toBe('foo');
  24. });
  25. it('removes label syntax', () => {
  26. expect(cleanText('foo="bar')).toBe('bar');
  27. expect(cleanText('foo!="bar')).toBe('bar');
  28. expect(cleanText('foo=~"bar')).toBe('bar');
  29. expect(cleanText('foo!~"bar')).toBe('bar');
  30. expect(cleanText('{bar')).toBe('bar');
  31. });
  32. it('removes previous operators', () => {
  33. expect(cleanText('foo + bar')).toBe('bar');
  34. expect(cleanText('foo+bar')).toBe('bar');
  35. expect(cleanText('foo - bar')).toBe('bar');
  36. expect(cleanText('foo * bar')).toBe('bar');
  37. expect(cleanText('foo / bar')).toBe('bar');
  38. expect(cleanText('foo % bar')).toBe('bar');
  39. expect(cleanText('foo ^ bar')).toBe('bar');
  40. expect(cleanText('foo and bar')).toBe('bar');
  41. expect(cleanText('foo or bar')).toBe('bar');
  42. expect(cleanText('foo unless bar')).toBe('bar');
  43. expect(cleanText('foo == bar')).toBe('bar');
  44. expect(cleanText('foo != bar')).toBe('bar');
  45. expect(cleanText('foo > bar')).toBe('bar');
  46. expect(cleanText('foo < bar')).toBe('bar');
  47. expect(cleanText('foo >= bar')).toBe('bar');
  48. expect(cleanText('foo <= bar')).toBe('bar');
  49. expect(cleanText('memory')).toBe('memory');
  50. });
  51. it('removes aggregation syntax', () => {
  52. expect(cleanText('(bar')).toBe('bar');
  53. expect(cleanText('(foo,bar')).toBe('bar');
  54. expect(cleanText('(foo, bar')).toBe('bar');
  55. });
  56. it('removes range syntax', () => {
  57. expect(cleanText('[1m')).toBe('1m');
  58. });
  59. });
  60. describe('fetchSeries', () => {
  61. it('should use match[] parameter', () => {
  62. const languageProvider = new LanguageProvider(datasource);
  63. const fetchSeries = languageProvider.fetchSeries;
  64. const requestSpy = jest.spyOn(languageProvider, 'request');
  65. fetchSeries('{job="grafana"}');
  66. expect(requestSpy).toHaveBeenCalled();
  67. expect(requestSpy).toHaveBeenCalledWith(
  68. '/api/v1/series',
  69. {},
  70. { end: '1', 'match[]': '{job="grafana"}', start: '0' }
  71. );
  72. });
  73. });
  74. describe('fetchSeriesLabels', () => {
  75. it('should interpolate variable in series', () => {
  76. const languageProvider = new LanguageProvider({
  77. ...datasource,
  78. interpolateString: (string: string) => string.replace(/\$/, 'interpolated-'),
  79. } as PrometheusDatasource);
  80. const fetchSeriesLabels = languageProvider.fetchSeriesLabels;
  81. const requestSpy = jest.spyOn(languageProvider, 'request');
  82. fetchSeriesLabels('$metric');
  83. expect(requestSpy).toHaveBeenCalled();
  84. expect(requestSpy).toHaveBeenCalledWith('/api/v1/series', [], {
  85. end: '1',
  86. 'match[]': 'interpolated-metric',
  87. start: '0',
  88. });
  89. });
  90. });
  91. describe('fetchLabelValues', () => {
  92. it('should interpolate variable in series', () => {
  93. const languageProvider = new LanguageProvider({
  94. ...datasource,
  95. interpolateString: (string: string) => string.replace(/\$/, 'interpolated-'),
  96. } as PrometheusDatasource);
  97. const fetchLabelValues = languageProvider.fetchLabelValues;
  98. const requestSpy = jest.spyOn(languageProvider, 'request');
  99. fetchLabelValues('$job');
  100. expect(requestSpy).toHaveBeenCalled();
  101. expect(requestSpy).toHaveBeenCalledWith('/api/v1/label/interpolated-job/values', [], {
  102. end: '1',
  103. start: '0',
  104. });
  105. });
  106. });
  107. describe('empty query suggestions', () => {
  108. it('returns no suggestions on empty context', async () => {
  109. const instance = new LanguageProvider(datasource);
  110. const value = Plain.deserialize('');
  111. const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
  112. expect(result.context).toBeUndefined();
  113. expect(result.suggestions).toMatchObject([]);
  114. });
  115. it('returns no suggestions with metrics on empty context even when metrics were provided', async () => {
  116. const instance = new LanguageProvider(datasource);
  117. instance.metrics = ['foo', 'bar'];
  118. const value = Plain.deserialize('');
  119. const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] });
  120. expect(result.context).toBeUndefined();
  121. expect(result.suggestions).toMatchObject([]);
  122. });
  123. it('returns history on empty context when history was provided', async () => {
  124. const instance = new LanguageProvider(datasource);
  125. const value = Plain.deserialize('');
  126. const history: Array<HistoryItem<PromQuery>> = [
  127. {
  128. ts: 0,
  129. query: { refId: '1', expr: 'metric' },
  130. },
  131. ];
  132. const result = await instance.provideCompletionItems(
  133. { text: '', prefix: '', value, wrapperClasses: [] },
  134. { history }
  135. );
  136. expect(result.context).toBeUndefined();
  137. expect(result.suggestions).toMatchObject([
  138. {
  139. label: 'History',
  140. items: [
  141. {
  142. label: 'metric',
  143. },
  144. ],
  145. },
  146. ]);
  147. });
  148. });
  149. describe('range suggestions', () => {
  150. it('returns range suggestions in range context', async () => {
  151. const instance = new LanguageProvider(datasource);
  152. const value = Plain.deserialize('1');
  153. const result = await instance.provideCompletionItems({
  154. text: '1',
  155. prefix: '1',
  156. value,
  157. wrapperClasses: ['context-range'],
  158. });
  159. expect(result.context).toBe('context-range');
  160. expect(result.suggestions).toMatchObject([
  161. {
  162. items: [
  163. { label: '$__interval', sortValue: '$__interval' },
  164. { label: '$__rate_interval', sortValue: '$__rate_interval' },
  165. { label: '$__range', sortValue: '$__range' },
  166. { label: '1m', sortValue: '00:01:00' },
  167. { label: '5m', sortValue: '00:05:00' },
  168. { label: '10m', sortValue: '00:10:00' },
  169. { label: '30m', sortValue: '00:30:00' },
  170. { label: '1h', sortValue: '01:00:00' },
  171. { label: '1d', sortValue: '24:00:00' },
  172. ],
  173. label: 'Range vector',
  174. },
  175. ]);
  176. });
  177. });
  178. describe('metric suggestions', () => {
  179. it('returns history, metrics and function suggestions in an uknown context ', async () => {
  180. const instance = new LanguageProvider(datasource);
  181. instance.metrics = ['foo', 'bar'];
  182. const history: Array<HistoryItem<PromQuery>> = [
  183. {
  184. ts: 0,
  185. query: { refId: '1', expr: 'metric' },
  186. },
  187. ];
  188. let value = Plain.deserialize('m');
  189. value = value.setSelection({ anchor: { offset: 1 }, focus: { offset: 1 } });
  190. // Even though no metric with `m` is present, we still get metric completion items, filtering is done by the consumer
  191. const result = await instance.provideCompletionItems(
  192. { text: 'm', prefix: 'm', value, wrapperClasses: [] },
  193. { history }
  194. );
  195. expect(result.context).toBeUndefined();
  196. expect(result.suggestions).toMatchObject([
  197. {
  198. label: 'History',
  199. items: [
  200. {
  201. label: 'metric',
  202. },
  203. ],
  204. },
  205. {
  206. label: 'Functions',
  207. },
  208. {
  209. label: 'Metrics',
  210. },
  211. ]);
  212. });
  213. it('returns no suggestions directly after a binary operator', async () => {
  214. const instance = new LanguageProvider(datasource);
  215. instance.metrics = ['foo', 'bar'];
  216. const value = Plain.deserialize('*');
  217. const result = await instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] });
  218. expect(result.context).toBeUndefined();
  219. expect(result.suggestions).toMatchObject([]);
  220. });
  221. it('returns metric suggestions with prefix after a binary operator', async () => {
  222. const instance = new LanguageProvider(datasource);
  223. instance.metrics = ['foo', 'bar'];
  224. const value = Plain.deserialize('foo + b');
  225. const ed = new SlateEditor({ value });
  226. const valueWithSelection = ed.moveForward(7).value;
  227. const result = await instance.provideCompletionItems({
  228. text: 'foo + b',
  229. prefix: 'b',
  230. value: valueWithSelection,
  231. wrapperClasses: [],
  232. });
  233. expect(result.context).toBeUndefined();
  234. expect(result.suggestions).toMatchObject([
  235. {
  236. label: 'Functions',
  237. },
  238. {
  239. label: 'Metrics',
  240. },
  241. ]);
  242. });
  243. it('returns no suggestions at the beginning of a non-empty function', async () => {
  244. const instance = new LanguageProvider(datasource);
  245. const value = Plain.deserialize('sum(up)');
  246. const ed = new SlateEditor({ value });
  247. const valueWithSelection = ed.moveForward(4).value;
  248. const result = await instance.provideCompletionItems({
  249. text: '',
  250. prefix: '',
  251. value: valueWithSelection,
  252. wrapperClasses: [],
  253. });
  254. expect(result.context).toBeUndefined();
  255. expect(result.suggestions.length).toEqual(0);
  256. });
  257. });
  258. describe('label suggestions', () => {
  259. it('returns default label suggestions on label context and no metric', async () => {
  260. const instance = new LanguageProvider(datasource);
  261. const value = Plain.deserialize('{}');
  262. const ed = new SlateEditor({ value });
  263. const valueWithSelection = ed.moveForward(1).value;
  264. const result = await instance.provideCompletionItems({
  265. text: '',
  266. prefix: '',
  267. wrapperClasses: ['context-labels'],
  268. value: valueWithSelection,
  269. });
  270. expect(result.context).toBe('context-labels');
  271. expect(result.suggestions).toEqual([
  272. {
  273. items: [{ label: 'job' }, { label: 'instance' }],
  274. label: 'Labels',
  275. searchFunctionType: SearchFunctionType.Fuzzy,
  276. },
  277. ]);
  278. });
  279. it('returns label suggestions on label context and metric', async () => {
  280. const datasources: PrometheusDatasource = {
  281. metadataRequest: () => ({ data: { data: [{ __name__: 'metric', bar: 'bazinga' }] as any[] } }),
  282. getTimeRangeParams: () => ({ start: '0', end: '1' }),
  283. interpolateString: (string: string) => string,
  284. } as any as PrometheusDatasource;
  285. const instance = new LanguageProvider(datasources);
  286. const value = Plain.deserialize('metric{}');
  287. const ed = new SlateEditor({ value });
  288. const valueWithSelection = ed.moveForward(7).value;
  289. const result = await instance.provideCompletionItems({
  290. text: '',
  291. prefix: '',
  292. wrapperClasses: ['context-labels'],
  293. value: valueWithSelection,
  294. });
  295. expect(result.context).toBe('context-labels');
  296. expect(result.suggestions).toEqual([
  297. { items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy },
  298. ]);
  299. });
  300. it('returns label suggestions on label context but leaves out labels that already exist', async () => {
  301. const datasource: PrometheusDatasource = {
  302. metadataRequest: () => ({
  303. data: {
  304. data: [
  305. {
  306. __name__: 'metric',
  307. bar: 'asdasd',
  308. job1: 'dsadsads',
  309. job2: 'fsfsdfds',
  310. job3: 'dsadsad',
  311. },
  312. ],
  313. },
  314. }),
  315. getTimeRangeParams: () => ({ start: '0', end: '1' }),
  316. interpolateString: (string: string) => string,
  317. } as any as PrometheusDatasource;
  318. const instance = new LanguageProvider(datasource);
  319. const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",__name__="metric",}');
  320. const ed = new SlateEditor({ value });
  321. const valueWithSelection = ed.moveForward(54).value;
  322. const result = await instance.provideCompletionItems({
  323. text: '',
  324. prefix: '',
  325. wrapperClasses: ['context-labels'],
  326. value: valueWithSelection,
  327. });
  328. expect(result.context).toBe('context-labels');
  329. expect(result.suggestions).toEqual([
  330. { items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy },
  331. ]);
  332. });
  333. it('returns label value suggestions inside a label value context after a negated matching operator', async () => {
  334. const instance = new LanguageProvider({
  335. ...datasource,
  336. metadataRequest: () => {
  337. return { data: { data: ['value1', 'value2'] } };
  338. },
  339. } as any as PrometheusDatasource);
  340. const value = Plain.deserialize('{job!=}');
  341. const ed = new SlateEditor({ value });
  342. const valueWithSelection = ed.moveForward(6).value;
  343. const result = await instance.provideCompletionItems({
  344. text: '!=',
  345. prefix: '',
  346. wrapperClasses: ['context-labels'],
  347. labelKey: 'job',
  348. value: valueWithSelection,
  349. });
  350. expect(result.context).toBe('context-label-values');
  351. expect(result.suggestions).toEqual([
  352. {
  353. items: [{ label: 'value1' }, { label: 'value2' }],
  354. label: 'Label values for "job"',
  355. searchFunctionType: SearchFunctionType.Fuzzy,
  356. },
  357. ]);
  358. });
  359. it('returns a refresher on label context and unavailable metric', async () => {
  360. jest.spyOn(console, 'warn').mockImplementation(() => {});
  361. const instance = new LanguageProvider(datasource);
  362. const value = Plain.deserialize('metric{}');
  363. const ed = new SlateEditor({ value });
  364. const valueWithSelection = ed.moveForward(7).value;
  365. const result = await instance.provideCompletionItems({
  366. text: '',
  367. prefix: '',
  368. wrapperClasses: ['context-labels'],
  369. value: valueWithSelection,
  370. });
  371. expect(result.context).toBeUndefined();
  372. expect(result.suggestions).toEqual([]);
  373. expect(console.warn).toHaveBeenCalledWith('Server did not return any values for selector = {__name__="metric"}');
  374. });
  375. it('returns label values on label context when given a metric and a label key', async () => {
  376. const instance = new LanguageProvider({
  377. ...datasource,
  378. metadataRequest: () => simpleMetricLabelsResponse,
  379. } as any as PrometheusDatasource);
  380. const value = Plain.deserialize('metric{bar=ba}');
  381. const ed = new SlateEditor({ value });
  382. const valueWithSelection = ed.moveForward(13).value;
  383. const result = await instance.provideCompletionItems({
  384. text: '=ba',
  385. prefix: 'ba',
  386. wrapperClasses: ['context-labels'],
  387. labelKey: 'bar',
  388. value: valueWithSelection,
  389. });
  390. expect(result.context).toBe('context-label-values');
  391. expect(result.suggestions).toEqual([
  392. { items: [{ label: 'baz' }], label: 'Label values for "bar"', searchFunctionType: SearchFunctionType.Fuzzy },
  393. ]);
  394. });
  395. it('returns label suggestions on aggregation context and metric w/ selector', async () => {
  396. const instance = new LanguageProvider({
  397. ...datasource,
  398. metadataRequest: () => simpleMetricLabelsResponse,
  399. } as any as PrometheusDatasource);
  400. const value = Plain.deserialize('sum(metric{foo="xx"}) by ()');
  401. const ed = new SlateEditor({ value });
  402. const valueWithSelection = ed.moveForward(26).value;
  403. const result = await instance.provideCompletionItems({
  404. text: '',
  405. prefix: '',
  406. wrapperClasses: ['context-aggregation'],
  407. value: valueWithSelection,
  408. });
  409. expect(result.context).toBe('context-aggregation');
  410. expect(result.suggestions).toEqual([
  411. { items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy },
  412. ]);
  413. });
  414. it('returns label suggestions on aggregation context and metric w/o selector', async () => {
  415. const instance = new LanguageProvider({
  416. ...datasource,
  417. metadataRequest: () => simpleMetricLabelsResponse,
  418. } as any as PrometheusDatasource);
  419. const value = Plain.deserialize('sum(metric) by ()');
  420. const ed = new SlateEditor({ value });
  421. const valueWithSelection = ed.moveForward(16).value;
  422. const result = await instance.provideCompletionItems({
  423. text: '',
  424. prefix: '',
  425. wrapperClasses: ['context-aggregation'],
  426. value: valueWithSelection,
  427. });
  428. expect(result.context).toBe('context-aggregation');
  429. expect(result.suggestions).toEqual([
  430. { items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy },
  431. ]);
  432. });
  433. it('returns label suggestions inside a multi-line aggregation context', async () => {
  434. const instance = new LanguageProvider({
  435. ...datasource,
  436. metadataRequest: () => simpleMetricLabelsResponse,
  437. } as any as PrometheusDatasource);
  438. const value = Plain.deserialize('sum(\nmetric\n)\nby ()');
  439. const aggregationTextBlock = value.document.getBlocks().get(3);
  440. const ed = new SlateEditor({ value });
  441. ed.moveToStartOfNode(aggregationTextBlock);
  442. const valueWithSelection = ed.moveForward(4).value;
  443. const result = await instance.provideCompletionItems({
  444. text: '',
  445. prefix: '',
  446. wrapperClasses: ['context-aggregation'],
  447. value: valueWithSelection,
  448. });
  449. expect(result.context).toBe('context-aggregation');
  450. expect(result.suggestions).toEqual([
  451. {
  452. items: [{ label: 'bar' }],
  453. label: 'Labels',
  454. searchFunctionType: SearchFunctionType.Fuzzy,
  455. },
  456. ]);
  457. });
  458. it('returns label suggestions inside an aggregation context with a range vector', async () => {
  459. const instance = new LanguageProvider({
  460. ...datasource,
  461. metadataRequest: () => simpleMetricLabelsResponse,
  462. } as any as PrometheusDatasource);
  463. const value = Plain.deserialize('sum(rate(metric[1h])) by ()');
  464. const ed = new SlateEditor({ value });
  465. const valueWithSelection = ed.moveForward(26).value;
  466. const result = await instance.provideCompletionItems({
  467. text: '',
  468. prefix: '',
  469. wrapperClasses: ['context-aggregation'],
  470. value: valueWithSelection,
  471. });
  472. expect(result.context).toBe('context-aggregation');
  473. expect(result.suggestions).toEqual([
  474. {
  475. items: [{ label: 'bar' }],
  476. label: 'Labels',
  477. searchFunctionType: SearchFunctionType.Fuzzy,
  478. },
  479. ]);
  480. });
  481. it('returns label suggestions inside an aggregation context with a range vector and label', async () => {
  482. const instance = new LanguageProvider({
  483. ...datasource,
  484. metadataRequest: () => simpleMetricLabelsResponse,
  485. } as any as PrometheusDatasource);
  486. const value = Plain.deserialize('sum(rate(metric{label1="value"}[1h])) by ()');
  487. const ed = new SlateEditor({ value });
  488. const valueWithSelection = ed.moveForward(42).value;
  489. const result = await instance.provideCompletionItems({
  490. text: '',
  491. prefix: '',
  492. wrapperClasses: ['context-aggregation'],
  493. value: valueWithSelection,
  494. });
  495. expect(result.context).toBe('context-aggregation');
  496. expect(result.suggestions).toEqual([
  497. {
  498. items: [{ label: 'bar' }],
  499. label: 'Labels',
  500. searchFunctionType: SearchFunctionType.Fuzzy,
  501. },
  502. ]);
  503. });
  504. it('returns no suggestions inside an unclear aggregation context using alternate syntax', async () => {
  505. const instance = new LanguageProvider(datasource);
  506. const value = Plain.deserialize('sum by ()');
  507. const ed = new SlateEditor({ value });
  508. const valueWithSelection = ed.moveForward(8).value;
  509. const result = await instance.provideCompletionItems({
  510. text: '',
  511. prefix: '',
  512. wrapperClasses: ['context-aggregation'],
  513. value: valueWithSelection,
  514. });
  515. expect(result.context).toBe('context-aggregation');
  516. expect(result.suggestions).toEqual([]);
  517. });
  518. it('returns label suggestions inside an aggregation context using alternate syntax', async () => {
  519. const instance = new LanguageProvider({
  520. ...datasource,
  521. metadataRequest: () => simpleMetricLabelsResponse,
  522. } as any as PrometheusDatasource);
  523. const value = Plain.deserialize('sum by () (metric)');
  524. const ed = new SlateEditor({ value });
  525. const valueWithSelection = ed.moveForward(8).value;
  526. const result = await instance.provideCompletionItems({
  527. text: '',
  528. prefix: '',
  529. wrapperClasses: ['context-aggregation'],
  530. value: valueWithSelection,
  531. });
  532. expect(result.context).toBe('context-aggregation');
  533. expect(result.suggestions).toEqual([
  534. {
  535. items: [{ label: 'bar' }],
  536. label: 'Labels',
  537. searchFunctionType: SearchFunctionType.Fuzzy,
  538. },
  539. ]);
  540. });
  541. it('does not re-fetch default labels', async () => {
  542. const datasource: PrometheusDatasource = {
  543. metadataRequest: jest.fn(() => ({ data: { data: [] as any[] } })),
  544. getTimeRangeParams: jest.fn(() => ({ start: '0', end: '1' })),
  545. interpolateString: (string: string) => string,
  546. } as any as PrometheusDatasource;
  547. const instance = new LanguageProvider(datasource);
  548. const value = Plain.deserialize('{}');
  549. const ed = new SlateEditor({ value });
  550. const valueWithSelection = ed.moveForward(1).value;
  551. const args = {
  552. text: '',
  553. prefix: '',
  554. wrapperClasses: ['context-labels'],
  555. value: valueWithSelection,
  556. };
  557. const promise1 = instance.provideCompletionItems(args);
  558. // one call for 2 default labels job, instance
  559. expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(2);
  560. const promise2 = instance.provideCompletionItems(args);
  561. expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(2);
  562. await Promise.all([promise1, promise2]);
  563. expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(2);
  564. });
  565. });
  566. describe('disabled metrics lookup', () => {
  567. it('does not issue any metadata requests when lookup is disabled', async () => {
  568. jest.spyOn(console, 'warn').mockImplementation(() => {});
  569. const datasource: PrometheusDatasource = {
  570. metadataRequest: jest.fn(() => ({ data: { data: ['foo', 'bar'] as string[] } })),
  571. getTimeRangeParams: jest.fn(() => ({ start: '0', end: '1' })),
  572. lookupsDisabled: true,
  573. } as any as PrometheusDatasource;
  574. const instance = new LanguageProvider(datasource);
  575. const value = Plain.deserialize('{}');
  576. const ed = new SlateEditor({ value });
  577. const valueWithSelection = ed.moveForward(1).value;
  578. const args = {
  579. text: '',
  580. prefix: '',
  581. wrapperClasses: ['context-labels'],
  582. value: valueWithSelection,
  583. };
  584. expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(0);
  585. await instance.start();
  586. expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(0);
  587. await instance.provideCompletionItems(args);
  588. expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(0);
  589. expect(console.warn).toHaveBeenCalledWith('Server did not return any values for selector = {}');
  590. });
  591. it('issues metadata requests when lookup is not disabled', async () => {
  592. const datasource: PrometheusDatasource = {
  593. metadataRequest: jest.fn(() => ({ data: { data: ['foo', 'bar'] as string[] } })),
  594. getTimeRangeParams: jest.fn(() => ({ start: '0', end: '1' })),
  595. lookupsDisabled: false,
  596. interpolateString: (string: string) => string,
  597. } as any as PrometheusDatasource;
  598. const instance = new LanguageProvider(datasource);
  599. expect((datasource.metadataRequest as Mock).mock.calls.length).toBe(0);
  600. await instance.start();
  601. expect((datasource.metadataRequest as Mock).mock.calls.length).toBeGreaterThan(0);
  602. });
  603. });
  604. describe('Query imports', () => {
  605. it('returns empty queries', async () => {
  606. const instance = new LanguageProvider(datasource);
  607. const result = await instance.importFromAbstractQuery({ refId: 'bar', labelMatchers: [] });
  608. expect(result).toEqual({ refId: 'bar', expr: '', range: true });
  609. });
  610. describe('exporting to abstract query', () => {
  611. it('exports labels with metric name', async () => {
  612. const instance = new LanguageProvider(datasource);
  613. const abstractQuery = instance.exportToAbstractQuery({
  614. refId: 'bar',
  615. expr: 'metric_name{label1="value1", label2!="value2", label3=~"value3", label4!~"value4"}',
  616. instant: true,
  617. range: false,
  618. });
  619. expect(abstractQuery).toMatchObject({
  620. refId: 'bar',
  621. labelMatchers: [
  622. { name: 'label1', operator: AbstractLabelOperator.Equal, value: 'value1' },
  623. { name: 'label2', operator: AbstractLabelOperator.NotEqual, value: 'value2' },
  624. { name: 'label3', operator: AbstractLabelOperator.EqualRegEx, value: 'value3' },
  625. { name: 'label4', operator: AbstractLabelOperator.NotEqualRegEx, value: 'value4' },
  626. { name: '__name__', operator: AbstractLabelOperator.Equal, value: 'metric_name' },
  627. ],
  628. });
  629. });
  630. });
  631. });
  632. });
  633. const simpleMetricLabelsResponse = {
  634. data: {
  635. data: [
  636. {
  637. __name__: 'metric',
  638. bar: 'baz',
  639. },
  640. ],
  641. },
  642. };