PrometheusMetricsBrowser.test.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import { render, screen, waitFor } from '@testing-library/react';
  2. import userEvent from '@testing-library/user-event';
  3. import React from 'react';
  4. import { getTheme } from '@grafana/ui';
  5. import PromQlLanguageProvider from '../language_provider';
  6. import {
  7. buildSelector,
  8. facetLabels,
  9. SelectableLabel,
  10. UnthemedPrometheusMetricsBrowser,
  11. BrowserProps,
  12. } from './PrometheusMetricsBrowser';
  13. describe('buildSelector()', () => {
  14. it('returns an empty selector for no labels', () => {
  15. expect(buildSelector([])).toEqual('{}');
  16. });
  17. it('returns an empty selector for selected labels with no values', () => {
  18. const labels: SelectableLabel[] = [{ name: 'foo', selected: true }];
  19. expect(buildSelector(labels)).toEqual('{}');
  20. });
  21. it('returns an empty selector for one selected label with no selected values', () => {
  22. const labels: SelectableLabel[] = [{ name: 'foo', selected: true, values: [{ name: 'bar' }] }];
  23. expect(buildSelector(labels)).toEqual('{}');
  24. });
  25. it('returns a simple selector from a selected label with a selected value', () => {
  26. const labels: SelectableLabel[] = [{ name: 'foo', selected: true, values: [{ name: 'bar', selected: true }] }];
  27. expect(buildSelector(labels)).toEqual('{foo="bar"}');
  28. });
  29. it('metric selector without labels', () => {
  30. const labels: SelectableLabel[] = [{ name: '__name__', selected: true, values: [{ name: 'foo', selected: true }] }];
  31. expect(buildSelector(labels)).toEqual('foo{}');
  32. });
  33. it('selector with multiple metrics', () => {
  34. const labels: SelectableLabel[] = [
  35. {
  36. name: '__name__',
  37. selected: true,
  38. values: [
  39. { name: 'foo', selected: true },
  40. { name: 'bar', selected: true },
  41. ],
  42. },
  43. ];
  44. expect(buildSelector(labels)).toEqual('{__name__=~"foo|bar"}');
  45. });
  46. it('metric selector with labels', () => {
  47. const labels: SelectableLabel[] = [
  48. { name: '__name__', selected: true, values: [{ name: 'foo', selected: true }] },
  49. { name: 'bar', selected: true, values: [{ name: 'baz', selected: true }] },
  50. ];
  51. expect(buildSelector(labels)).toEqual('foo{bar="baz"}');
  52. });
  53. });
  54. describe('facetLabels()', () => {
  55. const possibleLabels = {
  56. cluster: ['dev'],
  57. namespace: ['alertmanager'],
  58. };
  59. const labels: SelectableLabel[] = [
  60. { name: 'foo', selected: true, values: [{ name: 'bar' }] },
  61. { name: 'cluster', values: [{ name: 'dev' }, { name: 'ops' }, { name: 'prod' }] },
  62. { name: 'namespace', values: [{ name: 'alertmanager' }] },
  63. ];
  64. it('returns no labels given an empty label set', () => {
  65. expect(facetLabels([], {})).toEqual([]);
  66. });
  67. it('marks all labels as hidden when no labels are possible', () => {
  68. const result = facetLabels(labels, {});
  69. expect(result.length).toEqual(labels.length);
  70. expect(result[0].hidden).toBeTruthy();
  71. expect(result[0].values).toBeUndefined();
  72. });
  73. it('keeps values as facetted when they are possible', () => {
  74. const result = facetLabels(labels, possibleLabels);
  75. expect(result.length).toEqual(labels.length);
  76. expect(result[0].hidden).toBeTruthy();
  77. expect(result[0].values).toBeUndefined();
  78. expect(result[1].hidden).toBeFalsy();
  79. expect(result[1].values!.length).toBe(1);
  80. expect(result[1].values![0].name).toBe('dev');
  81. });
  82. it('does not facet out label values that are currently being facetted', () => {
  83. const result = facetLabels(labels, possibleLabels, 'cluster');
  84. expect(result.length).toEqual(labels.length);
  85. expect(result[0].hidden).toBeTruthy();
  86. expect(result[1].hidden).toBeFalsy();
  87. // 'cluster' is being facetted, should show all 3 options even though only 1 is possible
  88. expect(result[1].values!.length).toBe(3);
  89. expect(result[2].values!.length).toBe(1);
  90. });
  91. });
  92. describe('PrometheusMetricsBrowser', () => {
  93. const setupProps = (): BrowserProps => {
  94. const mockLanguageProvider = {
  95. start: () => Promise.resolve(),
  96. getLabelValues: (name: string) => {
  97. switch (name) {
  98. case 'label1':
  99. return ['value1-1', 'value1-2'];
  100. case 'label2':
  101. return ['value2-1', 'value2-2'];
  102. case 'label3':
  103. return ['value3-1', 'value3-2'];
  104. }
  105. return [];
  106. },
  107. fetchSeriesLabels: (selector: string) => {
  108. switch (selector) {
  109. case '{label1="value1-1"}':
  110. return { label1: ['value1-1'], label2: ['value2-1'], label3: ['value3-1'] };
  111. case '{label1=~"value1-1|value1-2"}':
  112. return { label1: ['value1-1', 'value1-2'], label2: ['value2-1'], label3: ['value3-1', 'value3-2'] };
  113. }
  114. // Allow full set by default
  115. return {
  116. label1: ['value1-1', 'value1-2'],
  117. label2: ['value2-1', 'value2-2'],
  118. };
  119. },
  120. getLabelKeys: () => ['label1', 'label2', 'label3'],
  121. };
  122. const defaults: BrowserProps = {
  123. theme: getTheme(),
  124. onChange: () => {},
  125. autoSelect: 0,
  126. languageProvider: mockLanguageProvider as unknown as PromQlLanguageProvider,
  127. lastUsedLabels: [],
  128. storeLastUsedLabels: () => {},
  129. deleteLastUsedLabels: () => {},
  130. };
  131. return defaults;
  132. };
  133. // Clear label selection manually because it's saved in localStorage
  134. afterEach(async () => {
  135. const clearBtn = screen.getByLabelText('Selector clear button');
  136. await userEvent.click(clearBtn);
  137. });
  138. it('renders and loader shows when empty, and then first set of labels', async () => {
  139. const props = setupProps();
  140. render(<UnthemedPrometheusMetricsBrowser {...props} />);
  141. // Loading appears and dissappears
  142. screen.getByText(/Loading labels/);
  143. await waitFor(() => {
  144. expect(screen.queryByText(/Loading labels/)).not.toBeInTheDocument();
  145. });
  146. // Initial set of labels is available and not selected
  147. expect(screen.queryByRole('option', { name: 'label1' })).toBeInTheDocument();
  148. expect(screen.queryByRole('option', { name: 'label1', selected: true })).not.toBeInTheDocument();
  149. expect(screen.queryByRole('option', { name: 'label2' })).toBeInTheDocument();
  150. expect(screen.queryByRole('option', { name: 'label2', selected: true })).not.toBeInTheDocument();
  151. expect(screen.queryByLabelText('selector')).toHaveTextContent('{}');
  152. });
  153. it('allows label and value selection/deselection', async () => {
  154. const props = setupProps();
  155. render(<UnthemedPrometheusMetricsBrowser {...props} />);
  156. // Selecting label2
  157. const label2 = await screen.findByRole('option', { name: 'label2', selected: false });
  158. expect(screen.queryByRole('list', { name: /Values/ })).not.toBeInTheDocument();
  159. await userEvent.click(label2);
  160. expect(screen.queryByRole('option', { name: 'label2', selected: true })).toBeInTheDocument();
  161. // List of values for label2 appears
  162. expect(screen.queryByLabelText(/Values for/)).toHaveTextContent('label2');
  163. expect(screen.queryByRole('option', { name: 'value2-1' })).toBeInTheDocument();
  164. expect(screen.queryByRole('option', { name: 'value2-2' })).toBeInTheDocument();
  165. expect(screen.queryByLabelText('selector')).toHaveTextContent('{}');
  166. // Selecting label1, list for its values appears
  167. const label1 = await screen.findByRole('option', { name: 'label1', selected: false });
  168. await userEvent.click(label1);
  169. expect(screen.queryByRole('option', { name: 'label1', selected: true })).toBeInTheDocument();
  170. await screen.findByLabelText('Values for label1');
  171. expect(await screen.findAllByRole('list', { name: /Values/ })).toHaveLength(2);
  172. // Selecting value2-2 of label2
  173. const value = await screen.findByRole('option', { name: 'value2-2', selected: false });
  174. await userEvent.click(value);
  175. await screen.findByRole('option', { name: 'value2-2', selected: true });
  176. expect(screen.queryByLabelText('selector')).toHaveTextContent('{label2="value2-2"}');
  177. // Selecting value2-1 of label2, both values now selected
  178. const value2 = await screen.findByRole('option', { name: 'value2-1', selected: false });
  179. await userEvent.click(value2);
  180. // await screen.findByRole('option', {name: 'value2-1', selected: true});
  181. await screen.findByText('{label2=~"value2-1|value2-2"}');
  182. // Deselecting value2-2, one value should remain
  183. const selectedValue = await screen.findByRole('option', { name: 'value2-2', selected: true });
  184. await userEvent.click(selectedValue);
  185. await screen.findByRole('option', { name: 'value2-1', selected: true });
  186. await screen.findByRole('option', { name: 'value2-2', selected: false });
  187. expect(screen.queryByLabelText('selector')).toHaveTextContent('{label2="value2-1"}');
  188. });
  189. it('allows label selection from multiple labels', async () => {
  190. const props = setupProps();
  191. render(<UnthemedPrometheusMetricsBrowser {...props} />);
  192. // Selecting label2
  193. const label2 = await screen.findByRole('option', { name: 'label2', selected: false });
  194. await userEvent.click(label2);
  195. // List of values for label2 appears
  196. expect(screen.queryByLabelText(/Values for/)).toHaveTextContent('label2');
  197. expect(screen.queryByRole('option', { name: 'value2-1' })).toBeInTheDocument();
  198. expect(screen.queryByRole('option', { name: 'value2-2' })).toBeInTheDocument();
  199. expect(screen.queryByLabelText('selector')).toHaveTextContent('{}');
  200. // Selecting label1, list for its values appears
  201. const label1 = await screen.findByRole('option', { name: 'label1', selected: false });
  202. await userEvent.click(label1);
  203. await screen.findByLabelText('Values for label1');
  204. expect(await screen.findAllByRole('list', { name: /Values/ })).toHaveLength(2);
  205. // Selecting value2-1 of label2
  206. const value2 = await screen.findByRole('option', { name: 'value2-1', selected: false });
  207. await userEvent.click(value2);
  208. await screen.findByText('{label2="value2-1"}');
  209. // Selecting value from label1 for combined selector
  210. const value1 = await screen.findByRole('option', { name: 'value1-2', selected: false });
  211. await userEvent.click(value1);
  212. await screen.findByRole('option', { name: 'value1-2', selected: true });
  213. await screen.findByText('{label1="value1-2",label2="value2-1"}');
  214. // Deselect label1 should remove label and value
  215. const selectedLabel = (await screen.findAllByRole('option', { name: /label1/, selected: true }))[0];
  216. await userEvent.click(selectedLabel);
  217. await screen.findByRole('option', { name: /label1/, selected: false });
  218. expect(await screen.findAllByRole('list', { name: /Values/ })).toHaveLength(1);
  219. expect(screen.queryByLabelText('selector')).toHaveTextContent('{label2="value2-1"}');
  220. });
  221. it('allows clearing the label selection', async () => {
  222. const props = setupProps();
  223. render(<UnthemedPrometheusMetricsBrowser {...props} />);
  224. // Selecting label2
  225. const label2 = await screen.findByRole('option', { name: 'label2', selected: false });
  226. await userEvent.click(label2);
  227. // List of values for label2 appears
  228. expect(screen.queryByLabelText(/Values for/)).toHaveTextContent('label2');
  229. expect(screen.queryByRole('option', { name: 'value2-1' })).toBeInTheDocument();
  230. expect(screen.queryByRole('option', { name: 'value2-2' })).toBeInTheDocument();
  231. expect(screen.queryByLabelText('selector')).toHaveTextContent('{}');
  232. // Selecting label1, list for its values appears
  233. const label1 = await screen.findByRole('option', { name: 'label1', selected: false });
  234. await userEvent.click(label1);
  235. await screen.findByLabelText('Values for label1');
  236. expect(await screen.findAllByRole('list', { name: /Values/ })).toHaveLength(2);
  237. // Selecting value2-1 of label2
  238. const value2 = await screen.findByRole('option', { name: 'value2-1', selected: false });
  239. await userEvent.click(value2);
  240. await screen.findByText('{label2="value2-1"}');
  241. // Clear selector
  242. const clearBtn = screen.getByLabelText('Selector clear button');
  243. await userEvent.click(clearBtn);
  244. await screen.findByRole('option', { name: 'label2', selected: false });
  245. expect(screen.queryByLabelText('selector')).toHaveTextContent('{}');
  246. });
  247. it('filters values by input text', async () => {
  248. const props = setupProps();
  249. render(<UnthemedPrometheusMetricsBrowser {...props} />);
  250. // Selecting label2 and label1
  251. const label2 = await screen.findByRole('option', { name: /label2/, selected: false });
  252. await userEvent.click(label2);
  253. const label1 = await screen.findByRole('option', { name: /label1/, selected: false });
  254. await userEvent.click(label1);
  255. await screen.findByLabelText('Values for label1');
  256. await screen.findByLabelText('Values for label2');
  257. expect(await screen.findAllByRole('option', { name: /value/ })).toHaveLength(4);
  258. // Typing '1' to filter for values
  259. await userEvent.type(screen.getByLabelText('Filter expression for label values'), '1');
  260. expect(screen.getByLabelText('Filter expression for label values')).toHaveValue('1');
  261. expect(await screen.findAllByRole('option', { name: /value/ })).toHaveLength(3);
  262. expect(screen.queryByRole('option', { name: 'value2-2' })).not.toBeInTheDocument();
  263. });
  264. it('facets labels', async () => {
  265. const props = setupProps();
  266. render(<UnthemedPrometheusMetricsBrowser {...props} />);
  267. // Selecting label2 and label1
  268. const label2 = await screen.findByRole('option', { name: /label2/, selected: false });
  269. await userEvent.click(label2);
  270. const label1 = await screen.findByRole('option', { name: /label1/, selected: false });
  271. await userEvent.click(label1);
  272. await screen.findByLabelText('Values for label1');
  273. await screen.findByLabelText('Values for label2');
  274. expect(await screen.findAllByRole('option', { name: /value/ })).toHaveLength(4);
  275. expect(screen.queryByRole('option', { name: /label3/ })).toHaveTextContent('label3');
  276. // Click value1-1 which triggers facetting for value3-x, and still show all value1-x
  277. const value1 = await screen.findByRole('option', { name: 'value1-1', selected: false });
  278. await userEvent.click(value1);
  279. await waitFor(() => expect(screen.queryByRole('option', { name: 'value2-2' })).not.toBeInTheDocument());
  280. expect(screen.queryByRole('option', { name: 'value1-2' })).toBeInTheDocument();
  281. expect(screen.queryByLabelText('selector')).toHaveTextContent('{label1="value1-1"}');
  282. expect(screen.queryByRole('option', { name: /label3/ })).toHaveTextContent('label3 (1)');
  283. // Click value1-2 for which facetting will allow more values for value3-x
  284. const value12 = await screen.findByRole('option', { name: 'value1-2', selected: false });
  285. await userEvent.click(value12);
  286. await screen.findByRole('option', { name: 'value1-2', selected: true });
  287. await screen.findByRole('option', { name: /label3/, selected: false });
  288. await userEvent.click(screen.getByRole('option', { name: /label3/ }));
  289. await screen.findByLabelText('Values for label3');
  290. expect(screen.queryByRole('option', { name: 'value1-1', selected: true })).toBeInTheDocument();
  291. expect(screen.queryByRole('option', { name: 'value1-2', selected: true })).toBeInTheDocument();
  292. expect(screen.queryByLabelText('selector')).toHaveTextContent('{label1=~"value1-1|value1-2"}');
  293. expect(screen.queryAllByRole('option', { name: /label3/ })[0]).toHaveTextContent('label3 (2)');
  294. });
  295. });