reducer.test.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. import { cloneDeep } from 'lodash';
  2. import { MetricFindValue } from '@grafana/data';
  3. import { reducerTester } from '../../../../test/core/redux/reducerTester';
  4. import { getVariableTestContext } from '../state/helpers';
  5. import { VariablesState } from '../state/types';
  6. import { QueryVariableModel, VariableSort } from '../types';
  7. import { toVariablePayload } from '../utils';
  8. import { createQueryVariableAdapter } from './adapter';
  9. import {
  10. metricNamesToVariableValues,
  11. queryVariableReducer,
  12. sortVariableValues,
  13. updateVariableOptions,
  14. } from './reducer';
  15. describe('queryVariableReducer', () => {
  16. const adapter = createQueryVariableAdapter();
  17. describe('when updateVariableOptions is dispatched and includeAll is true', () => {
  18. it('then state should be correct', () => {
  19. const { initialState } = getVariableTestContext(adapter, { includeAll: true });
  20. const metrics = [createMetric('A'), createMetric('B')];
  21. const update = { results: metrics, templatedRegex: '' };
  22. const payload = toVariablePayload({ id: '0', type: 'query' }, update);
  23. reducerTester<VariablesState>()
  24. .givenReducer(queryVariableReducer, cloneDeep(initialState))
  25. .whenActionIsDispatched(updateVariableOptions(payload))
  26. .thenStateShouldEqual({
  27. ...initialState,
  28. '0': {
  29. ...initialState[0],
  30. options: [
  31. { text: 'All', value: '$__all', selected: false },
  32. { text: 'A', value: 'A', selected: false },
  33. { text: 'B', value: 'B', selected: false },
  34. ],
  35. } as unknown as QueryVariableModel,
  36. });
  37. });
  38. });
  39. describe('when updateVariableOptions is dispatched and includeAll is false', () => {
  40. it('then state should be correct', () => {
  41. const { initialState } = getVariableTestContext(adapter, { includeAll: false });
  42. const metrics = [createMetric('A'), createMetric('B')];
  43. const update = { results: metrics, templatedRegex: '' };
  44. const payload = toVariablePayload({ id: '0', type: 'query' }, update);
  45. reducerTester<VariablesState>()
  46. .givenReducer(queryVariableReducer, cloneDeep(initialState))
  47. .whenActionIsDispatched(updateVariableOptions(payload))
  48. .thenStateShouldEqual({
  49. ...initialState,
  50. '0': {
  51. ...initialState[0],
  52. options: [
  53. { text: 'A', value: 'A', selected: false },
  54. { text: 'B', value: 'B', selected: false },
  55. ],
  56. } as unknown as QueryVariableModel,
  57. });
  58. });
  59. });
  60. describe('when updateVariableOptions is dispatched and includeAll is true and payload is an empty array', () => {
  61. it('then state should be correct', () => {
  62. const { initialState } = getVariableTestContext(adapter, { includeAll: true });
  63. const update = { results: [] as MetricFindValue[], templatedRegex: '' };
  64. const payload = toVariablePayload({ id: '0', type: 'query' }, update);
  65. reducerTester<VariablesState>()
  66. .givenReducer(queryVariableReducer, cloneDeep(initialState))
  67. .whenActionIsDispatched(updateVariableOptions(payload))
  68. .thenStateShouldEqual({
  69. ...initialState,
  70. '0': {
  71. ...initialState[0],
  72. options: [{ text: 'All', value: '$__all', selected: false }],
  73. } as unknown as QueryVariableModel,
  74. });
  75. });
  76. });
  77. describe('when updateVariableOptions is dispatched and includeAll is false and payload is an empty array', () => {
  78. it('then state should be correct', () => {
  79. const { initialState } = getVariableTestContext(adapter, { includeAll: false });
  80. const update = { results: [] as MetricFindValue[], templatedRegex: '' };
  81. const payload = toVariablePayload({ id: '0', type: 'query' }, update);
  82. reducerTester<VariablesState>()
  83. .givenReducer(queryVariableReducer, cloneDeep(initialState))
  84. .whenActionIsDispatched(updateVariableOptions(payload))
  85. .thenStateShouldEqual({
  86. ...initialState,
  87. '0': {
  88. ...initialState[0],
  89. options: [{ text: 'None', value: '', selected: false, isNone: true }],
  90. } as unknown as QueryVariableModel,
  91. });
  92. });
  93. });
  94. describe('when updateVariableOptions is dispatched and includeAll is true and regex is set', () => {
  95. it('then state should be correct', () => {
  96. const regex = '/.*(a).*/i';
  97. const { initialState } = getVariableTestContext(adapter, { includeAll: true, regex });
  98. const metrics = [createMetric('A'), createMetric('B')];
  99. const update = { results: metrics, templatedRegex: regex };
  100. const payload = toVariablePayload({ id: '0', type: 'query' }, update);
  101. reducerTester<VariablesState>()
  102. .givenReducer(queryVariableReducer, cloneDeep(initialState))
  103. .whenActionIsDispatched(updateVariableOptions(payload))
  104. .thenStateShouldEqual({
  105. ...initialState,
  106. '0': {
  107. ...initialState[0],
  108. options: [
  109. { text: 'All', value: '$__all', selected: false },
  110. { text: 'A', value: 'A', selected: false },
  111. ],
  112. } as unknown as QueryVariableModel,
  113. });
  114. });
  115. });
  116. describe('when updateVariableOptions is dispatched and includeAll is false and regex is set', () => {
  117. it('then state should be correct', () => {
  118. const regex = '/.*(a).*/i';
  119. const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex });
  120. const metrics = [createMetric('A'), createMetric('B')];
  121. const update = { results: metrics, templatedRegex: regex };
  122. const payload = toVariablePayload({ id: '0', type: 'query' }, update);
  123. reducerTester<VariablesState>()
  124. .givenReducer(queryVariableReducer, cloneDeep(initialState))
  125. .whenActionIsDispatched(updateVariableOptions(payload))
  126. .thenStateShouldEqual({
  127. ...initialState,
  128. '0': {
  129. ...initialState[0],
  130. options: [{ text: 'A', value: 'A', selected: false }],
  131. } as unknown as QueryVariableModel,
  132. });
  133. });
  134. });
  135. describe('when updateVariableOptions is dispatched and includeAll is false and regex is set and uses capture groups', () => {
  136. it('normal regex should capture in order matches', () => {
  137. const regex = '/somelabel="(?<text>[^"]+).*somevalue="(?<value>[^"]+)/i';
  138. const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex });
  139. const metrics = [createMetric('A{somelabel="atext",somevalue="avalue"}'), createMetric('B')];
  140. const update = { results: metrics, templatedRegex: regex };
  141. const payload = toVariablePayload({ id: '0', type: 'query' }, update);
  142. reducerTester<VariablesState>()
  143. .givenReducer(queryVariableReducer, cloneDeep(initialState))
  144. .whenActionIsDispatched(updateVariableOptions(payload))
  145. .thenStateShouldEqual({
  146. ...initialState,
  147. '0': {
  148. ...initialState[0],
  149. options: [{ text: 'atext', value: 'avalue', selected: false }],
  150. } as unknown as QueryVariableModel,
  151. });
  152. });
  153. it('global regex should capture out of order matches', () => {
  154. const regex = '/somevalue="(?<value>[^"]+)|somelabel="(?<text>[^"]+)/gi';
  155. const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex });
  156. const metrics = [createMetric('A{somelabel="atext",somevalue="avalue"}'), createMetric('B')];
  157. const update = { results: metrics, templatedRegex: regex };
  158. const payload = toVariablePayload({ id: '0', type: 'query' }, update);
  159. reducerTester<VariablesState>()
  160. .givenReducer(queryVariableReducer, cloneDeep(initialState))
  161. .whenActionIsDispatched(updateVariableOptions(payload))
  162. .thenStateShouldEqual({
  163. ...initialState,
  164. '0': {
  165. ...initialState[0],
  166. options: [{ text: 'atext', value: 'avalue', selected: false }],
  167. } as unknown as QueryVariableModel,
  168. });
  169. });
  170. it('unmatched text capture will use value capture', () => {
  171. const regex = '/somevalue="(?<value>[^"]+)|somelabel="(?<text>[^"]+)/gi';
  172. const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex });
  173. const metrics = [createMetric('A{somename="atext",somevalue="avalue"}'), createMetric('B')];
  174. const update = { results: metrics, templatedRegex: regex };
  175. const payload = toVariablePayload({ id: '0', type: 'query' }, update);
  176. reducerTester<VariablesState>()
  177. .givenReducer(queryVariableReducer, cloneDeep(initialState))
  178. .whenActionIsDispatched(updateVariableOptions(payload))
  179. .thenStateShouldEqual({
  180. ...initialState,
  181. '0': {
  182. ...initialState[0],
  183. options: [{ text: 'avalue', value: 'avalue', selected: false }],
  184. } as unknown as QueryVariableModel,
  185. });
  186. });
  187. it('unmatched value capture will use text capture', () => {
  188. const regex = '/somevalue="(?<value>[^"]+)|somelabel="(?<text>[^"]+)/gi';
  189. const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex });
  190. const metrics = [createMetric('A{somelabel="atext",somename="avalue"}'), createMetric('B')];
  191. const update = { results: metrics, templatedRegex: regex };
  192. const payload = toVariablePayload({ id: '0', type: 'query' }, update);
  193. reducerTester<VariablesState>()
  194. .givenReducer(queryVariableReducer, cloneDeep(initialState))
  195. .whenActionIsDispatched(updateVariableOptions(payload))
  196. .thenStateShouldEqual({
  197. ...initialState,
  198. '0': {
  199. ...initialState[0],
  200. options: [{ text: 'atext', value: 'atext', selected: false }],
  201. } as unknown as QueryVariableModel,
  202. });
  203. });
  204. it('unnamed capture group returns any unnamed match', () => {
  205. const regex = '/.*_(\\w+)\\{/gi';
  206. const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex });
  207. const metrics = [createMetric('instance_counter{someother="atext",something="avalue"}'), createMetric('B')];
  208. const update = { results: metrics, templatedRegex: regex };
  209. const payload = toVariablePayload({ id: '0', type: 'query' }, update);
  210. reducerTester<VariablesState>()
  211. .givenReducer(queryVariableReducer, cloneDeep(initialState))
  212. .whenActionIsDispatched(updateVariableOptions(payload))
  213. .thenStateShouldEqual({
  214. ...initialState,
  215. '0': {
  216. ...initialState[0],
  217. options: [{ text: 'counter', value: 'counter', selected: false }],
  218. } as unknown as QueryVariableModel,
  219. });
  220. });
  221. it('unmatched text capture and unmatched value capture returns empty state', () => {
  222. const regex = '/somevalue="(?<value>[^"]+)|somelabel="(?<text>[^"]+)/gi';
  223. const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex });
  224. const metrics = [createMetric('A{someother="atext",something="avalue"}'), createMetric('B')];
  225. const update = { results: metrics, templatedRegex: regex };
  226. const payload = toVariablePayload({ id: '0', type: 'query' }, update);
  227. reducerTester<VariablesState>()
  228. .givenReducer(queryVariableReducer, cloneDeep(initialState))
  229. .whenActionIsDispatched(updateVariableOptions(payload))
  230. .thenStateShouldEqual({
  231. ...initialState,
  232. '0': {
  233. ...initialState[0],
  234. options: [{ text: 'None', value: '', selected: false, isNone: true }],
  235. } as unknown as QueryVariableModel,
  236. });
  237. });
  238. });
  239. });
  240. describe('sortVariableValues', () => {
  241. describe('when using any sortOrder with an option with null as text', () => {
  242. it.each`
  243. options | sortOrder | expected
  244. ${[{ text: '1' }, { text: null }, { text: '2' }]} | ${VariableSort.disabled} | ${[{ text: '1' }, { text: null }, { text: '2' }]}
  245. ${[{ text: 'a' }, { text: null }, { text: 'b' }]} | ${VariableSort.alphabeticalAsc} | ${[{ text: 'a' }, { text: 'b' }, { text: null }]}
  246. ${[{ text: 'a' }, { text: null }, { text: 'b' }]} | ${VariableSort.alphabeticalDesc} | ${[{ text: null }, { text: 'b' }, { text: 'a' }]}
  247. ${[{ text: '1' }, { text: null }, { text: '2' }]} | ${VariableSort.numericalAsc} | ${[{ text: null }, { text: '1' }, { text: '2' }]}
  248. ${[{ text: '1' }, { text: null }, { text: '2' }]} | ${VariableSort.numericalDesc} | ${[{ text: '2' }, { text: '1' }, { text: null }]}
  249. ${[{ text: 'a' }, { text: null }, { text: 'b' }]} | ${VariableSort.alphabeticalCaseInsensitiveAsc} | ${[{ text: null }, { text: 'a' }, { text: 'b' }]}
  250. ${[{ text: 'a' }, { text: null }, { text: 'b' }]} | ${VariableSort.alphabeticalCaseInsensitiveDesc} | ${[{ text: 'b' }, { text: 'a' }, { text: null }]}
  251. `(
  252. 'then it should sort the options correctly without throwing (sortOrder:$sortOrder)',
  253. ({ options, sortOrder, expected }) => {
  254. const result = sortVariableValues(options, sortOrder);
  255. expect(result).toEqual(expected);
  256. }
  257. );
  258. });
  259. });
  260. describe('metricNamesToVariableValues', () => {
  261. const item = (str: string) => ({ text: str, value: str, selected: false });
  262. const metricsNames = [
  263. item('go_info{instance="demo.robustperception.io:9090",job="prometheus",version="go1.15.6"} 1 1613047998000'),
  264. item('go_info{instance="demo.robustperception.io:9091",job="pushgateway",version="go1.15.6"} 1 1613047998000'),
  265. item('go_info{instance="demo.robustperception.io:9093",job="alertmanager",version="go1.14.4"} 1 1613047998000'),
  266. item('go_info{instance="demo.robustperception.io:9100",job="node",version="go1.14.4"} 1 1613047998000'),
  267. ];
  268. const expected1 = [
  269. { value: 'demo.robustperception.io:9090', text: 'demo.robustperception.io:9090', selected: false },
  270. { value: 'demo.robustperception.io:9091', text: 'demo.robustperception.io:9091', selected: false },
  271. { value: 'demo.robustperception.io:9093', text: 'demo.robustperception.io:9093', selected: false },
  272. { value: 'demo.robustperception.io:9100', text: 'demo.robustperception.io:9100', selected: false },
  273. ];
  274. const expected2 = [
  275. { value: 'prometheus', text: 'prometheus', selected: false },
  276. { value: 'pushgateway', text: 'pushgateway', selected: false },
  277. { value: 'alertmanager', text: 'alertmanager', selected: false },
  278. { value: 'node', text: 'node', selected: false },
  279. ];
  280. const expected3 = [
  281. { value: 'demo.robustperception.io:9090', text: 'prometheus', selected: false },
  282. { value: 'demo.robustperception.io:9091', text: 'pushgateway', selected: false },
  283. { value: 'demo.robustperception.io:9093', text: 'alertmanager', selected: false },
  284. { value: 'demo.robustperception.io:9100', text: 'node', selected: false },
  285. ];
  286. const expected4 = [
  287. { value: 'demo.robustperception.io:9090', text: 'demo.robustperception.io:9090', selected: false },
  288. { value: undefined, text: undefined, selected: false },
  289. { value: 'demo.robustperception.io:9091', text: 'demo.robustperception.io:9091', selected: false },
  290. { value: 'demo.robustperception.io:9093', text: 'demo.robustperception.io:9093', selected: false },
  291. { value: 'demo.robustperception.io:9100', text: 'demo.robustperception.io:9100', selected: false },
  292. ];
  293. it.each`
  294. variableRegEx | expected
  295. ${''} | ${metricsNames}
  296. ${'/unknown/'} | ${[]}
  297. ${'/unknown/g'} | ${[]}
  298. ${'/go/'} | ${metricsNames}
  299. ${'/go/g'} | ${metricsNames}
  300. ${'/(go)/'} | ${[{ value: 'go', text: 'go', selected: false }]}
  301. ${'/(go)/g'} | ${[{ value: 'go', text: 'go', selected: false }]}
  302. ${'/(go)?/'} | ${[{ value: 'go', text: 'go', selected: false }]}
  303. ${'/(go)?/g'} | ${[{ value: 'go', text: 'go', selected: false }, { value: undefined, text: undefined, selected: false }]}
  304. ${'/go(\\w+)/'} | ${[{ value: '_info', text: '_info', selected: false }]}
  305. ${'/go(\\w+)/g'} | ${[{ value: '_info', text: '_info', selected: false }, { value: '1', text: '1', selected: false }]}
  306. ${'/.*_(\\w+)\\{/'} | ${[{ value: 'info', text: 'info', selected: false }]}
  307. ${'/.*_(\\w+)\\{/g'} | ${[{ value: 'info', text: 'info', selected: false }]}
  308. ${'/instance="(?<value>[^"]+)/'} | ${expected1}
  309. ${'/instance="(?<value>[^"]+)/g'} | ${expected1}
  310. ${'/instance="(?<grp1>[^"]+)/'} | ${expected1}
  311. ${'/instance="(?<grp1>[^"]+)/g'} | ${expected1}
  312. ${'/instancee="(?<value>[^"]+)/'} | ${[]}
  313. ${'/job="(?<text>[^"]+)/'} | ${expected2}
  314. ${'/job="(?<text>[^"]+)/g'} | ${expected2}
  315. ${'/job="(?<grp2>[^"]+)/'} | ${expected2}
  316. ${'/job="(?<grp2>[^"]+)/g'} | ${expected2}
  317. ${'/jobb="(?<text>[^"]+)/g'} | ${[]}
  318. ${'/instance="(?<value>[^"]+)|job="(?<text>[^"]+)/'} | ${expected1}
  319. ${'/instance="(?<value>[^"]+)|job="(?<text>[^"]+)/g'} | ${expected3}
  320. ${'/instance="(?<grp1>[^"]+)|job="(?<grp2>[^"]+)/'} | ${expected1}
  321. ${'/instance="(?<grp1>[^"]+)|job="(?<grp2>[^"]+)/g'} | ${expected4}
  322. ${'/instance="(?<value>[^"]+).*job="(?<text>[^"]+)/'} | ${expected3}
  323. ${'/instance="(?<value>[^"]+).*job="(?<text>[^"]+)/g'} | ${expected3}
  324. ${'/instance="(?<grp1>[^"]+).*job="(?<grp2>[^"]+)/'} | ${expected1}
  325. ${'/instance="(?<grp1>[^"]+).*job="(?<grp2>[^"]+)/g'} | ${expected1}
  326. `('when called with variableRegEx:$variableRegEx then it return correct options', ({ variableRegEx, expected }) => {
  327. const result = metricNamesToVariableValues(variableRegEx, VariableSort.disabled, metricsNames);
  328. expect(result).toEqual(expected);
  329. });
  330. });
  331. function createMetric(value: string) {
  332. return {
  333. text: value,
  334. };
  335. }