LokiLabelBrowser.test.tsx 14 KB

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