actions.test.ts 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924
  1. import { AnyAction } from 'redux';
  2. import { LoadingState } from '@grafana/data';
  3. import * as runtime from '@grafana/runtime';
  4. import { reduxTester } from '../../../../test/core/redux/reduxTester';
  5. import { toAsyncOfResult } from '../../query/state/DashboardQueryRunner/testHelpers';
  6. import { variableAdapters } from '../adapters';
  7. import { createConstantVariableAdapter } from '../constant/adapter';
  8. import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE, NEW_VARIABLE_ID } from '../constants';
  9. import { createCustomVariableAdapter } from '../custom/adapter';
  10. import { changeVariableName } from '../editor/actions';
  11. import {
  12. changeVariableNameFailed,
  13. changeVariableNameSucceeded,
  14. cleanEditorState,
  15. setIdInEditor,
  16. } from '../editor/reducer';
  17. import { cleanPickerState } from '../pickers/OptionsPicker/reducer';
  18. import { setVariableQueryRunner, VariableQueryRunner } from '../query/VariableQueryRunner';
  19. import { createQueryVariableAdapter } from '../query/adapter';
  20. import { updateVariableOptions } from '../query/reducer';
  21. import {
  22. constantBuilder,
  23. customBuilder,
  24. datasourceBuilder,
  25. queryBuilder,
  26. textboxBuilder,
  27. } from '../shared/testing/builders';
  28. import { createTextBoxVariableAdapter } from '../textbox/adapter';
  29. import { ConstantVariableModel, VariableRefresh } from '../types';
  30. import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
  31. import {
  32. cancelVariables,
  33. changeVariableMultiValue,
  34. cleanUpVariables,
  35. fixSelectedInconsistency,
  36. initDashboardTemplating,
  37. isVariableUrlValueDifferentFromCurrent,
  38. processVariables,
  39. validateVariableSelectionState,
  40. } from './actions';
  41. import { getPreloadedState, getTemplatingRootReducer, TemplatingReducerType } from './helpers';
  42. import { toKeyedAction } from './keyedVariablesReducer';
  43. import {
  44. addVariable,
  45. changeVariableProp,
  46. removeVariable,
  47. setCurrentVariableValue,
  48. variableStateCompleted,
  49. variableStateFetching,
  50. variableStateNotStarted,
  51. } from './sharedReducer';
  52. import { variablesClearTransaction, variablesInitTransaction } from './transactionReducer';
  53. import { cleanVariables } from './variablesReducer';
  54. variableAdapters.setInit(() => [
  55. createQueryVariableAdapter(),
  56. createCustomVariableAdapter(),
  57. createTextBoxVariableAdapter(),
  58. createConstantVariableAdapter(),
  59. ]);
  60. const metricFindQuery = jest
  61. .fn()
  62. .mockResolvedValueOnce([{ text: 'responses' }, { text: 'timers' }])
  63. .mockResolvedValue([{ text: '200' }, { text: '500' }]);
  64. const getMetricSources = jest.fn().mockReturnValue([]);
  65. const getDatasource = jest.fn().mockResolvedValue({ metricFindQuery });
  66. jest.mock('app/features/dashboard/services/TimeSrv', () => ({
  67. getTimeSrv: () => ({
  68. timeRange: jest.fn().mockReturnValue(undefined),
  69. }),
  70. }));
  71. runtime.setDataSourceSrv({
  72. get: getDatasource,
  73. getList: getMetricSources,
  74. } as any);
  75. describe('shared actions', () => {
  76. describe('when initDashboardTemplating is dispatched', () => {
  77. it('then correct actions are dispatched', () => {
  78. const key = 'key';
  79. const query = queryBuilder().build();
  80. const constant = constantBuilder().build();
  81. const datasource = datasourceBuilder().build();
  82. const custom = customBuilder().build();
  83. const textbox = textboxBuilder().build();
  84. const list = [query, constant, datasource, custom, textbox];
  85. const dashboard: any = { templating: { list } };
  86. reduxTester<TemplatingReducerType>()
  87. .givenRootReducer(getTemplatingRootReducer())
  88. .whenActionIsDispatched(initDashboardTemplating(key, dashboard))
  89. .thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
  90. expect(dispatchedActions.length).toEqual(8);
  91. expect(dispatchedActions[0]).toEqual(
  92. toKeyedAction(key, addVariable(toVariablePayload(query, { global: false, index: 0, model: query })))
  93. );
  94. expect(dispatchedActions[1]).toEqual(
  95. toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
  96. );
  97. expect(dispatchedActions[2]).toEqual(
  98. toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 2, model: custom })))
  99. );
  100. expect(dispatchedActions[3]).toEqual(
  101. toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 3, model: textbox })))
  102. );
  103. // because uuid are dynamic we need to get the uuid from the resulting state
  104. // an alternative would be to add our own uuids in the model above instead
  105. expect(dispatchedActions[4]).toEqual(
  106. toKeyedAction(
  107. key,
  108. variableStateNotStarted(
  109. toVariablePayload({ ...query, id: dispatchedActions[4].payload.action.payload.id })
  110. )
  111. )
  112. );
  113. expect(dispatchedActions[5]).toEqual(
  114. toKeyedAction(
  115. key,
  116. variableStateNotStarted(
  117. toVariablePayload({ ...constant, id: dispatchedActions[5].payload.action.payload.id })
  118. )
  119. )
  120. );
  121. expect(dispatchedActions[6]).toEqual(
  122. toKeyedAction(
  123. key,
  124. variableStateNotStarted(
  125. toVariablePayload({ ...custom, id: dispatchedActions[6].payload.action.payload.id })
  126. )
  127. )
  128. );
  129. expect(dispatchedActions[7]).toEqual(
  130. toKeyedAction(
  131. key,
  132. variableStateNotStarted(
  133. toVariablePayload({ ...textbox, id: dispatchedActions[7].payload.action.payload.id })
  134. )
  135. )
  136. );
  137. return true;
  138. });
  139. });
  140. });
  141. describe('when processVariables is dispatched', () => {
  142. it('then circular dependencies fail gracefully', async () => {
  143. const key = 'key';
  144. const var1 = queryBuilder().withName('var1').withQuery('$var2').build();
  145. const var2 = queryBuilder().withName('var2').withQuery('$var1').build();
  146. const dashboard: any = { templating: { list: [var1, var2] } };
  147. const preloadedState = getPreloadedState(key, {});
  148. await expect(async () => {
  149. await reduxTester<TemplatingReducerType>({ preloadedState })
  150. .givenRootReducer(getTemplatingRootReducer())
  151. .whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
  152. .whenActionIsDispatched(initDashboardTemplating(key, dashboard))
  153. .whenAsyncActionIsDispatched(processVariables(key), true);
  154. }).rejects.toThrow(/circular dependency in dashboard variables detected/i);
  155. });
  156. it('then correct actions are dispatched', async () => {
  157. const key = 'key';
  158. const query = queryBuilder().build();
  159. const constant = constantBuilder().build();
  160. const datasource = datasourceBuilder().build();
  161. const custom = customBuilder().build();
  162. const textbox = textboxBuilder().build();
  163. const list = [query, constant, datasource, custom, textbox];
  164. const dashboard: any = { templating: { list } };
  165. const preloadedState = getPreloadedState(key, {});
  166. const locationService: any = { getSearchObject: () => ({}) };
  167. runtime.setLocationService(locationService);
  168. const variableQueryRunner: any = {
  169. cancelRequest: jest.fn(),
  170. queueRequest: jest.fn(),
  171. getResponse: () => toAsyncOfResult({ state: LoadingState.Done, identifier: toKeyedVariableIdentifier(query) }),
  172. destroy: jest.fn(),
  173. };
  174. setVariableQueryRunner(variableQueryRunner);
  175. const tester = await reduxTester<TemplatingReducerType>({ preloadedState })
  176. .givenRootReducer(getTemplatingRootReducer())
  177. .whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
  178. .whenActionIsDispatched(initDashboardTemplating(key, dashboard))
  179. .whenAsyncActionIsDispatched(processVariables(key), true);
  180. await tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
  181. expect(dispatchedActions.length).toEqual(5);
  182. expect(dispatchedActions[0]).toEqual(
  183. toKeyedAction(
  184. key,
  185. variableStateFetching(toVariablePayload({ ...query, id: dispatchedActions[0].payload.action.payload.id }))
  186. )
  187. );
  188. expect(dispatchedActions[1]).toEqual(
  189. toKeyedAction(
  190. key,
  191. variableStateCompleted(
  192. toVariablePayload({ ...constant, id: dispatchedActions[1].payload.action.payload.id })
  193. )
  194. )
  195. );
  196. expect(dispatchedActions[2]).toEqual(
  197. toKeyedAction(
  198. key,
  199. variableStateCompleted(toVariablePayload({ ...custom, id: dispatchedActions[2].payload.action.payload.id }))
  200. )
  201. );
  202. expect(dispatchedActions[3]).toEqual(
  203. toKeyedAction(
  204. key,
  205. variableStateCompleted(
  206. toVariablePayload({ ...textbox, id: dispatchedActions[3].payload.action.payload.id })
  207. )
  208. )
  209. );
  210. expect(dispatchedActions[4]).toEqual(
  211. toKeyedAction(
  212. key,
  213. variableStateCompleted(toVariablePayload({ ...query, id: dispatchedActions[4].payload.action.payload.id }))
  214. )
  215. );
  216. return true;
  217. });
  218. });
  219. // Fix for https://github.com/grafana/grafana/issues/28791
  220. it('fix for https://github.com/grafana/grafana/issues/28791', async () => {
  221. setVariableQueryRunner(new VariableQueryRunner());
  222. const key = 'key';
  223. const stats = queryBuilder()
  224. .withId('stats')
  225. .withRootStateKey(key)
  226. .withName('stats')
  227. .withQuery('stats.*')
  228. .withRefresh(VariableRefresh.onDashboardLoad)
  229. .withCurrent(['response'], ['response'])
  230. .withMulti()
  231. .withIncludeAll()
  232. .build();
  233. const substats = queryBuilder()
  234. .withId('substats')
  235. .withRootStateKey(key)
  236. .withName('substats')
  237. .withQuery('stats.$stats.*')
  238. .withRefresh(VariableRefresh.onDashboardLoad)
  239. .withCurrent([ALL_VARIABLE_TEXT], [ALL_VARIABLE_VALUE])
  240. .withMulti()
  241. .withIncludeAll()
  242. .build();
  243. const list = [stats, substats];
  244. const dashboard: any = { templating: { list } };
  245. const query = { orgId: '1', 'var-stats': 'response', 'var-substats': ALL_VARIABLE_TEXT };
  246. const locationService: any = { getSearchObject: () => query };
  247. runtime.setLocationService(locationService);
  248. const preloadedState = getPreloadedState(key, {});
  249. const tester = await reduxTester<TemplatingReducerType>({ preloadedState })
  250. .givenRootReducer(getTemplatingRootReducer())
  251. .whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
  252. .whenActionIsDispatched(initDashboardTemplating(key, dashboard))
  253. .whenAsyncActionIsDispatched(processVariables(key), true);
  254. await tester.thenDispatchedActionsShouldEqual(
  255. toKeyedAction(key, variableStateFetching(toVariablePayload(stats))),
  256. toKeyedAction(
  257. key,
  258. updateVariableOptions(
  259. toVariablePayload(stats, { results: [{ text: 'responses' }, { text: 'timers' }], templatedRegex: '' })
  260. )
  261. ),
  262. toKeyedAction(
  263. key,
  264. setCurrentVariableValue(
  265. toVariablePayload(stats, {
  266. option: { text: ALL_VARIABLE_TEXT, value: ALL_VARIABLE_VALUE, selected: false },
  267. })
  268. )
  269. ),
  270. toKeyedAction(key, variableStateCompleted(toVariablePayload(stats))),
  271. toKeyedAction(
  272. key,
  273. setCurrentVariableValue(
  274. toVariablePayload(stats, { option: { text: ['response'], value: ['response'], selected: false } })
  275. )
  276. ),
  277. toKeyedAction(key, variableStateFetching(toVariablePayload(substats))),
  278. toKeyedAction(
  279. key,
  280. updateVariableOptions(
  281. toVariablePayload(substats, { results: [{ text: '200' }, { text: '500' }], templatedRegex: '' })
  282. )
  283. ),
  284. toKeyedAction(
  285. key,
  286. setCurrentVariableValue(
  287. toVariablePayload(substats, {
  288. option: { text: [ALL_VARIABLE_TEXT], value: [ALL_VARIABLE_VALUE], selected: true },
  289. })
  290. )
  291. ),
  292. toKeyedAction(key, variableStateCompleted(toVariablePayload(substats))),
  293. toKeyedAction(
  294. key,
  295. setCurrentVariableValue(
  296. toVariablePayload(substats, {
  297. option: { text: [ALL_VARIABLE_TEXT], value: [ALL_VARIABLE_VALUE], selected: false },
  298. })
  299. )
  300. )
  301. );
  302. });
  303. });
  304. describe('when validateVariableSelectionState is dispatched with a custom variable (no dependencies)', () => {
  305. describe('and not multivalue', () => {
  306. it.each`
  307. withOptions | withCurrent | defaultValue | expected
  308. ${['A', 'B', 'C']} | ${undefined} | ${undefined} | ${'A'}
  309. ${['A', 'B', 'C']} | ${'B'} | ${undefined} | ${'B'}
  310. ${['A', 'B', 'C']} | ${'B'} | ${'C'} | ${'B'}
  311. ${['A', 'B', 'C']} | ${'X'} | ${undefined} | ${'A'}
  312. ${['A', 'B', 'C']} | ${'X'} | ${'C'} | ${'C'}
  313. ${undefined} | ${'B'} | ${undefined} | ${'should not dispatch setCurrentVariableValue'}
  314. `('then correct actions are dispatched', async ({ withOptions, withCurrent, defaultValue, expected }) => {
  315. let custom;
  316. const key = 'key';
  317. if (!withOptions) {
  318. custom = customBuilder().withId('0').withRootStateKey(key).withCurrent(withCurrent).withoutOptions().build();
  319. } else {
  320. custom = customBuilder()
  321. .withId('0')
  322. .withRootStateKey(key)
  323. .withOptions(...withOptions)
  324. .withCurrent(withCurrent)
  325. .build();
  326. }
  327. const tester = await reduxTester<TemplatingReducerType>()
  328. .givenRootReducer(getTemplatingRootReducer())
  329. .whenActionIsDispatched(
  330. toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
  331. )
  332. .whenAsyncActionIsDispatched(
  333. validateVariableSelectionState(toKeyedVariableIdentifier(custom), defaultValue),
  334. true
  335. );
  336. await tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
  337. const expectedActions: AnyAction[] = withOptions
  338. ? [
  339. toKeyedAction(
  340. key,
  341. setCurrentVariableValue(
  342. toVariablePayload(
  343. { type: 'custom', id: '0' },
  344. { option: { text: expected, value: expected, selected: false } }
  345. )
  346. )
  347. ),
  348. ]
  349. : [];
  350. expect(dispatchedActions).toEqual(expectedActions);
  351. return true;
  352. });
  353. });
  354. });
  355. describe('and multivalue', () => {
  356. it.each`
  357. withOptions | withCurrent | defaultValue | expectedText | expectedSelected
  358. ${['A', 'B', 'C']} | ${['B']} | ${undefined} | ${['B']} | ${true}
  359. ${['A', 'B', 'C']} | ${['B']} | ${'C'} | ${['B']} | ${true}
  360. ${['A', 'B', 'C']} | ${['B', 'C']} | ${undefined} | ${['B', 'C']} | ${true}
  361. ${['A', 'B', 'C']} | ${['B', 'C']} | ${'C'} | ${['B', 'C']} | ${true}
  362. ${['A', 'B', 'C']} | ${['X']} | ${undefined} | ${'A'} | ${false}
  363. ${['A', 'B', 'C']} | ${['X']} | ${'C'} | ${'A'} | ${false}
  364. `(
  365. 'then correct actions are dispatched',
  366. async ({ withOptions, withCurrent, defaultValue, expectedText, expectedSelected }) => {
  367. let custom;
  368. const key = 'key';
  369. if (!withOptions) {
  370. custom = customBuilder()
  371. .withId('0')
  372. .withRootStateKey(key)
  373. .withMulti()
  374. .withCurrent(withCurrent)
  375. .withoutOptions()
  376. .build();
  377. } else {
  378. custom = customBuilder()
  379. .withId('0')
  380. .withRootStateKey(key)
  381. .withMulti()
  382. .withOptions(...withOptions)
  383. .withCurrent(withCurrent)
  384. .build();
  385. }
  386. const tester = await reduxTester<TemplatingReducerType>()
  387. .givenRootReducer(getTemplatingRootReducer())
  388. .whenActionIsDispatched(
  389. toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
  390. )
  391. .whenAsyncActionIsDispatched(
  392. validateVariableSelectionState(toKeyedVariableIdentifier(custom), defaultValue),
  393. true
  394. );
  395. await tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
  396. const expectedActions: AnyAction[] = withOptions
  397. ? [
  398. toKeyedAction(
  399. key,
  400. setCurrentVariableValue(
  401. toVariablePayload(
  402. { type: 'custom', id: '0' },
  403. { option: { text: expectedText, value: expectedText, selected: expectedSelected } }
  404. )
  405. )
  406. ),
  407. ]
  408. : [];
  409. expect(dispatchedActions).toEqual(expectedActions);
  410. return true;
  411. });
  412. }
  413. );
  414. });
  415. });
  416. describe('changeVariableName', () => {
  417. describe('when changeVariableName is dispatched with the same name', () => {
  418. it('then the correct actions are dispatched', () => {
  419. const key = 'key';
  420. const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
  421. const constant = constantBuilder().withId('constant').withRootStateKey(key).withName('constant').build();
  422. reduxTester<TemplatingReducerType>()
  423. .givenRootReducer(getTemplatingRootReducer())
  424. .whenActionIsDispatched(
  425. toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
  426. )
  427. .whenActionIsDispatched(
  428. toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
  429. )
  430. .whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), constant.name), true)
  431. .thenDispatchedActionsShouldEqual(
  432. toKeyedAction(
  433. key,
  434. changeVariableNameSucceeded({ type: 'constant', id: 'constant', data: { newName: 'constant' } })
  435. )
  436. );
  437. });
  438. });
  439. describe('when changeVariableName is dispatched with an unique name', () => {
  440. it('then the correct actions are dispatched', () => {
  441. const key = 'key';
  442. const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
  443. const constant = constantBuilder().withId('constant').withRootStateKey(key).withName('constant').build();
  444. reduxTester<TemplatingReducerType>()
  445. .givenRootReducer(getTemplatingRootReducer())
  446. .whenActionIsDispatched(
  447. toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
  448. )
  449. .whenActionIsDispatched(
  450. toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
  451. )
  452. .whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), 'constant1'), true)
  453. .thenDispatchedActionsShouldEqual(
  454. toKeyedAction(
  455. key,
  456. addVariable({
  457. type: 'constant',
  458. id: 'constant1',
  459. data: {
  460. global: false,
  461. index: 1,
  462. model: {
  463. ...constant,
  464. name: 'constant1',
  465. id: 'constant1',
  466. global: false,
  467. index: 1,
  468. current: { selected: true, text: '', value: '' },
  469. options: [{ selected: true, text: '', value: '' }],
  470. } as ConstantVariableModel,
  471. },
  472. })
  473. ),
  474. toKeyedAction(
  475. key,
  476. changeVariableNameSucceeded({ type: 'constant', id: 'constant1', data: { newName: 'constant1' } })
  477. ),
  478. toKeyedAction(key, setIdInEditor({ id: 'constant1' })),
  479. toKeyedAction(key, removeVariable({ type: 'constant', id: 'constant', data: { reIndex: false } }))
  480. );
  481. });
  482. });
  483. describe('when changeVariableName is dispatched with an unique name for a new variable', () => {
  484. it('then the correct actions are dispatched', () => {
  485. const key = 'key';
  486. const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
  487. const constant = constantBuilder().withId(NEW_VARIABLE_ID).withRootStateKey(key).withName('constant').build();
  488. reduxTester<TemplatingReducerType>()
  489. .givenRootReducer(getTemplatingRootReducer())
  490. .whenActionIsDispatched(
  491. toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
  492. )
  493. .whenActionIsDispatched(
  494. toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
  495. )
  496. .whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), 'constant1'), true)
  497. .thenDispatchedActionsShouldEqual(
  498. toKeyedAction(
  499. key,
  500. addVariable({
  501. type: 'constant',
  502. id: 'constant1',
  503. data: {
  504. global: false,
  505. index: 1,
  506. model: {
  507. ...constant,
  508. name: 'constant1',
  509. id: 'constant1',
  510. global: false,
  511. index: 1,
  512. current: { selected: true, text: '', value: '' },
  513. options: [{ selected: true, text: '', value: '' }],
  514. } as ConstantVariableModel,
  515. },
  516. })
  517. ),
  518. toKeyedAction(
  519. key,
  520. changeVariableNameSucceeded({ type: 'constant', id: 'constant1', data: { newName: 'constant1' } })
  521. ),
  522. toKeyedAction(key, setIdInEditor({ id: 'constant1' })),
  523. toKeyedAction(key, removeVariable({ type: 'constant', id: NEW_VARIABLE_ID, data: { reIndex: false } }))
  524. );
  525. });
  526. });
  527. describe('when changeVariableName is dispatched with __newName', () => {
  528. it('then the correct actions are dispatched', () => {
  529. const key = 'key';
  530. const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
  531. const constant = constantBuilder().withId('constant').withRootStateKey(key).withName('constant').build();
  532. reduxTester<TemplatingReducerType>()
  533. .givenRootReducer(getTemplatingRootReducer())
  534. .whenActionIsDispatched(
  535. toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
  536. )
  537. .whenActionIsDispatched(
  538. toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
  539. )
  540. .whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), '__newName'), true)
  541. .thenDispatchedActionsShouldEqual(
  542. toKeyedAction(
  543. key,
  544. changeVariableNameFailed({
  545. newName: '__newName',
  546. errorText: "Template names cannot begin with '__', that's reserved for Grafana's global variables",
  547. })
  548. )
  549. );
  550. });
  551. });
  552. describe('when changeVariableName is dispatched with illegal characters', () => {
  553. it('then the correct actions are dispatched', () => {
  554. const key = 'key';
  555. const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
  556. const constant = constantBuilder().withId('constant').withRootStateKey(key).withName('constant').build();
  557. reduxTester<TemplatingReducerType>()
  558. .givenRootReducer(getTemplatingRootReducer())
  559. .whenActionIsDispatched(
  560. toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
  561. )
  562. .whenActionIsDispatched(
  563. toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
  564. )
  565. .whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), '#constant!'), true)
  566. .thenDispatchedActionsShouldEqual(
  567. toKeyedAction(
  568. key,
  569. changeVariableNameFailed({
  570. newName: '#constant!',
  571. errorText: 'Only word and digit characters are allowed in variable names',
  572. })
  573. )
  574. );
  575. });
  576. });
  577. describe('when changeVariableName is dispatched with a name that is already used', () => {
  578. it('then the correct actions are dispatched', () => {
  579. const key = 'key';
  580. const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
  581. const constant = constantBuilder().withId('constant').withRootStateKey(key).withName('constant').build();
  582. reduxTester<TemplatingReducerType>()
  583. .givenRootReducer(getTemplatingRootReducer())
  584. .whenActionIsDispatched(
  585. toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
  586. )
  587. .whenActionIsDispatched(
  588. toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
  589. )
  590. .whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), 'textbox'), true)
  591. .thenDispatchedActionsShouldEqual(
  592. toKeyedAction(
  593. key,
  594. changeVariableNameFailed({
  595. newName: 'textbox',
  596. errorText: 'Variable with the same name already exists',
  597. })
  598. )
  599. );
  600. });
  601. });
  602. });
  603. describe('changeVariableMultiValue', () => {
  604. describe('when changeVariableMultiValue is dispatched for variable with multi enabled', () => {
  605. it('then correct actions are dispatched', () => {
  606. const key = 'key';
  607. const custom = customBuilder()
  608. .withId('custom')
  609. .withRootStateKey(key)
  610. .withMulti(true)
  611. .withCurrent(['A'], ['A'])
  612. .build();
  613. reduxTester<TemplatingReducerType>()
  614. .givenRootReducer(getTemplatingRootReducer())
  615. .whenActionIsDispatched(
  616. toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
  617. )
  618. .whenActionIsDispatched(changeVariableMultiValue(toKeyedVariableIdentifier(custom), false), true)
  619. .thenDispatchedActionsShouldEqual(
  620. toKeyedAction(
  621. key,
  622. changeVariableProp(
  623. toVariablePayload(custom, {
  624. propName: 'multi',
  625. propValue: false,
  626. })
  627. )
  628. ),
  629. toKeyedAction(
  630. key,
  631. changeVariableProp(
  632. toVariablePayload(custom, {
  633. propName: 'current',
  634. propValue: {
  635. value: 'A',
  636. text: 'A',
  637. selected: true,
  638. },
  639. })
  640. )
  641. )
  642. );
  643. });
  644. });
  645. describe('when changeVariableMultiValue is dispatched for variable with multi disabled', () => {
  646. it('then correct actions are dispatched', () => {
  647. const key = 'key';
  648. const custom = customBuilder()
  649. .withId('custom')
  650. .withRootStateKey(key)
  651. .withMulti(false)
  652. .withCurrent(['A'], ['A'])
  653. .build();
  654. reduxTester<TemplatingReducerType>()
  655. .givenRootReducer(getTemplatingRootReducer())
  656. .whenActionIsDispatched(
  657. toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
  658. )
  659. .whenActionIsDispatched(changeVariableMultiValue(toKeyedVariableIdentifier(custom), true), true)
  660. .thenDispatchedActionsShouldEqual(
  661. toKeyedAction(
  662. key,
  663. changeVariableProp(
  664. toVariablePayload(custom, {
  665. propName: 'multi',
  666. propValue: true,
  667. })
  668. )
  669. ),
  670. toKeyedAction(
  671. key,
  672. changeVariableProp(
  673. toVariablePayload(custom, {
  674. propName: 'current',
  675. propValue: {
  676. value: ['A'],
  677. text: ['A'],
  678. selected: true,
  679. },
  680. })
  681. )
  682. )
  683. );
  684. });
  685. });
  686. });
  687. describe('cleanUpVariables', () => {
  688. describe('when called', () => {
  689. it('then correct actions are dispatched', async () => {
  690. const key = 'key';
  691. reduxTester<TemplatingReducerType>()
  692. .givenRootReducer(getTemplatingRootReducer())
  693. .whenActionIsDispatched(cleanUpVariables(key))
  694. .thenDispatchedActionsShouldEqual(
  695. toKeyedAction(key, cleanVariables()),
  696. toKeyedAction(key, cleanEditorState()),
  697. toKeyedAction(key, cleanPickerState()),
  698. toKeyedAction(key, variablesClearTransaction())
  699. );
  700. });
  701. });
  702. });
  703. describe('cancelVariables', () => {
  704. const cancelAllInFlightRequestsMock = jest.fn();
  705. const backendSrvMock: any = {
  706. cancelAllInFlightRequests: cancelAllInFlightRequestsMock,
  707. };
  708. describe('when called', () => {
  709. it('then cancelAllInFlightRequests should be called and correct actions are dispatched', async () => {
  710. const key = 'key';
  711. reduxTester<TemplatingReducerType>()
  712. .givenRootReducer(getTemplatingRootReducer())
  713. .whenActionIsDispatched(cancelVariables(key, { getBackendSrv: () => backendSrvMock }))
  714. .thenDispatchedActionsShouldEqual(
  715. toKeyedAction(key, cleanVariables()),
  716. toKeyedAction(key, cleanEditorState()),
  717. toKeyedAction(key, cleanPickerState()),
  718. toKeyedAction(key, variablesClearTransaction())
  719. );
  720. expect(cancelAllInFlightRequestsMock).toHaveBeenCalledTimes(1);
  721. });
  722. });
  723. });
  724. describe('fixSelectedInconsistency', () => {
  725. describe('when called for a single value variable', () => {
  726. describe('and there is an inconsistency between current and selected in options', () => {
  727. it('then it should set the correct selected', () => {
  728. const variable = customBuilder().withId('custom').withCurrent('A').withOptions('A', 'B', 'C').build();
  729. variable.options[1].selected = true;
  730. expect(variable.options).toEqual([
  731. { text: 'A', value: 'A', selected: false },
  732. { text: 'B', value: 'B', selected: true },
  733. { text: 'C', value: 'C', selected: false },
  734. ]);
  735. fixSelectedInconsistency(variable);
  736. expect(variable.options).toEqual([
  737. { text: 'A', value: 'A', selected: true },
  738. { text: 'B', value: 'B', selected: false },
  739. { text: 'C', value: 'C', selected: false },
  740. ]);
  741. });
  742. });
  743. describe('and there is no matching option in options', () => {
  744. it('then the first option should be selected', () => {
  745. const variable = customBuilder().withId('custom').withCurrent('A').withOptions('X', 'Y', 'Z').build();
  746. expect(variable.options).toEqual([
  747. { text: 'X', value: 'X', selected: false },
  748. { text: 'Y', value: 'Y', selected: false },
  749. { text: 'Z', value: 'Z', selected: false },
  750. ]);
  751. fixSelectedInconsistency(variable);
  752. expect(variable.options).toEqual([
  753. { text: 'X', value: 'X', selected: true },
  754. { text: 'Y', value: 'Y', selected: false },
  755. { text: 'Z', value: 'Z', selected: false },
  756. ]);
  757. });
  758. });
  759. });
  760. describe('when called for a multi value variable', () => {
  761. describe('and there is an inconsistency between current and selected in options', () => {
  762. it('then it should set the correct selected', () => {
  763. const variable = customBuilder().withId('custom').withCurrent(['A', 'C']).withOptions('A', 'B', 'C').build();
  764. variable.options[1].selected = true;
  765. expect(variable.options).toEqual([
  766. { text: 'A', value: 'A', selected: false },
  767. { text: 'B', value: 'B', selected: true },
  768. { text: 'C', value: 'C', selected: false },
  769. ]);
  770. fixSelectedInconsistency(variable);
  771. expect(variable.options).toEqual([
  772. { text: 'A', value: 'A', selected: true },
  773. { text: 'B', value: 'B', selected: false },
  774. { text: 'C', value: 'C', selected: true },
  775. ]);
  776. });
  777. });
  778. describe('and there is no matching option in options', () => {
  779. it('then the first option should be selected', () => {
  780. const variable = customBuilder().withId('custom').withCurrent(['A', 'C']).withOptions('X', 'Y', 'Z').build();
  781. expect(variable.options).toEqual([
  782. { text: 'X', value: 'X', selected: false },
  783. { text: 'Y', value: 'Y', selected: false },
  784. { text: 'Z', value: 'Z', selected: false },
  785. ]);
  786. fixSelectedInconsistency(variable);
  787. expect(variable.options).toEqual([
  788. { text: 'X', value: 'X', selected: true },
  789. { text: 'Y', value: 'Y', selected: false },
  790. { text: 'Z', value: 'Z', selected: false },
  791. ]);
  792. });
  793. });
  794. });
  795. });
  796. describe('isVariableUrlValueDifferentFromCurrent', () => {
  797. describe('when called with a single valued variable', () => {
  798. describe('and values are equal', () => {
  799. it('then it should return false', () => {
  800. const variable = queryBuilder().withMulti(false).withCurrent('A', 'A').build();
  801. const urlValue = 'A';
  802. expect(isVariableUrlValueDifferentFromCurrent(variable, urlValue)).toBe(false);
  803. });
  804. });
  805. describe('and values are different', () => {
  806. it('then it should return true', () => {
  807. const variable = queryBuilder().withMulti(false).withCurrent('A', 'A').build();
  808. const urlValue = 'B';
  809. expect(isVariableUrlValueDifferentFromCurrent(variable, urlValue)).toBe(true);
  810. });
  811. });
  812. });
  813. describe('when called with a multi valued variable', () => {
  814. describe('and values are equal', () => {
  815. it('then it should return false', () => {
  816. const variable = queryBuilder().withMulti(true).withCurrent(['A'], ['A']).build();
  817. const urlValue = ['A'];
  818. expect(isVariableUrlValueDifferentFromCurrent(variable, urlValue)).toBe(false);
  819. });
  820. describe('but urlValue is not an array', () => {
  821. it('then it should return false', () => {
  822. const variable = queryBuilder().withMulti(true).withCurrent(['A'], ['A']).build();
  823. const urlValue = 'A';
  824. expect(isVariableUrlValueDifferentFromCurrent(variable, urlValue)).toBe(false);
  825. });
  826. });
  827. });
  828. describe('and values are different', () => {
  829. it('then it should return true', () => {
  830. const variable = queryBuilder().withMulti(true).withCurrent(['A'], ['A']).build();
  831. const urlValue = ['C'];
  832. expect(isVariableUrlValueDifferentFromCurrent(variable, urlValue)).toBe(true);
  833. });
  834. describe('but urlValue is not an array', () => {
  835. it('then it should return true', () => {
  836. const variable = queryBuilder().withMulti(true).withCurrent(['A'], ['A']).build();
  837. const urlValue = 'C';
  838. expect(isVariableUrlValueDifferentFromCurrent(variable, urlValue)).toBe(true);
  839. });
  840. });
  841. });
  842. });
  843. });
  844. });