actions.test.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. import { DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data';
  2. import { locationService } from '@grafana/runtime';
  3. import { VariableModel } from 'app/features/variables/types';
  4. import { reduxTester } from '../../../../test/core/redux/reduxTester';
  5. import { variableAdapters } from '../adapters';
  6. import { changeVariableEditorExtended, setIdInEditor } from '../editor/reducer';
  7. import { adHocBuilder } from '../shared/testing/builders';
  8. import { getPreloadedState, getRootReducer, RootReducerType } from '../state/helpers';
  9. import { toKeyedAction } from '../state/keyedVariablesReducer';
  10. import { addVariable, changeVariableProp } from '../state/sharedReducer';
  11. import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
  12. import {
  13. addFilter,
  14. AdHocTableOptions,
  15. applyFilterFromTable,
  16. changeFilter,
  17. changeVariableDatasource,
  18. initAdHocVariableEditor,
  19. removeFilter,
  20. setFiltersFromUrl,
  21. } from './actions';
  22. import { createAdHocVariableAdapter } from './adapter';
  23. import { filterAdded, filterRemoved, filtersRestored, filterUpdated } from './reducer';
  24. const getList = jest.fn().mockReturnValue([]);
  25. const getDatasource = jest.fn().mockResolvedValue({});
  26. locationService.partial = jest.fn();
  27. jest.mock('app/features/plugins/datasource_srv', () => ({
  28. getDatasourceSrv: jest.fn(() => ({
  29. get: getDatasource,
  30. getList,
  31. })),
  32. }));
  33. variableAdapters.setInit(() => [createAdHocVariableAdapter()]);
  34. const datasources = [
  35. { ...createDatasource('default', true, true), value: null },
  36. createDatasource('elasticsearch-v1'),
  37. createDatasource('loki', false),
  38. createDatasource('influx'),
  39. createDatasource('google-sheets', false),
  40. createDatasource('elasticsearch-v7'),
  41. ];
  42. const expectedDatasources = [
  43. { text: '', value: {} },
  44. { text: 'default (default)', value: { uid: 'default', type: 'default' } },
  45. { text: 'elasticsearch-v1', value: { uid: 'elasticsearch-v1', type: 'elasticsearch-v1' } },
  46. { text: 'influx', value: { uid: 'influx', type: 'influx' } },
  47. { text: 'elasticsearch-v7', value: { uid: 'elasticsearch-v7', type: 'elasticsearch-v7' } },
  48. ];
  49. describe('adhoc actions', () => {
  50. describe('when applyFilterFromTable is dispatched and filter already exist', () => {
  51. it('then correct actions are dispatched', async () => {
  52. const key = 'key';
  53. const options: AdHocTableOptions = {
  54. datasource: { uid: 'influxdb' },
  55. key: 'filter-key',
  56. value: 'filter-value',
  57. operator: '=',
  58. };
  59. const existingFilter = {
  60. key: 'filter-key',
  61. value: 'filter-existing',
  62. operator: '!=',
  63. condition: '',
  64. };
  65. const variable = adHocBuilder()
  66. .withId('Filters')
  67. .withRootStateKey(key)
  68. .withName('Filters')
  69. .withFilters([existingFilter])
  70. .withDatasource(options.datasource)
  71. .build();
  72. const tester = await reduxTester<RootReducerType>({ preloadedState: getPreloadedState(key, {}) })
  73. .givenRootReducer(getRootReducer())
  74. .whenActionIsDispatched(createAddVariableAction(variable))
  75. .whenAsyncActionIsDispatched(applyFilterFromTable(options), true);
  76. const expectedQuery = { 'var-Filters': ['filter-key|!=|filter-existing', 'filter-key|=|filter-value'] };
  77. const expectedFilter = { key: 'filter-key', value: 'filter-value', operator: '=', condition: '' };
  78. tester.thenDispatchedActionsShouldEqual(
  79. toKeyedAction(key, filterAdded(toVariablePayload(variable, expectedFilter)))
  80. );
  81. expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
  82. });
  83. });
  84. describe('when applyFilterFromTable is dispatched and previously no variable or filter exists', () => {
  85. it('then correct actions are dispatched', async () => {
  86. const key = 'key';
  87. const options: AdHocTableOptions = {
  88. datasource: { uid: 'influxdb' },
  89. key: 'filter-key',
  90. value: 'filter-value',
  91. operator: '=',
  92. };
  93. const tester = await reduxTester<RootReducerType>({ preloadedState: getPreloadedState(key, {}) })
  94. .givenRootReducer(getRootReducer())
  95. .whenAsyncActionIsDispatched(applyFilterFromTable(options), true);
  96. const variable = adHocBuilder()
  97. .withId('Filters')
  98. .withRootStateKey(key)
  99. .withName('Filters')
  100. .withDatasource(options.datasource)
  101. .build();
  102. const expectedQuery = { 'var-Filters': ['filter-key|=|filter-value'] };
  103. const expectedFilter = { key: 'filter-key', value: 'filter-value', operator: '=', condition: '' };
  104. tester.thenDispatchedActionsShouldEqual(
  105. createAddVariableAction(variable),
  106. toKeyedAction(key, filterAdded(toVariablePayload(variable, expectedFilter)))
  107. );
  108. expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
  109. });
  110. });
  111. describe('when applyFilterFromTable is dispatched and previously no filter exists', () => {
  112. it('then correct actions are dispatched', async () => {
  113. const key = 'key';
  114. const options: AdHocTableOptions = {
  115. datasource: { uid: 'influxdb' },
  116. key: 'filter-key',
  117. value: 'filter-value',
  118. operator: '=',
  119. };
  120. const variable = adHocBuilder()
  121. .withId('Filters')
  122. .withRootStateKey(key)
  123. .withName('Filters')
  124. .withFilters([])
  125. .withDatasource(options.datasource)
  126. .build();
  127. const tester = await reduxTester<RootReducerType>({ preloadedState: getPreloadedState(key, {}) })
  128. .givenRootReducer(getRootReducer())
  129. .whenActionIsDispatched(createAddVariableAction(variable))
  130. .whenAsyncActionIsDispatched(applyFilterFromTable(options), true);
  131. const expectedFilter = { key: 'filter-key', value: 'filter-value', operator: '=', condition: '' };
  132. const expectedQuery = { 'var-Filters': ['filter-key|=|filter-value'] };
  133. tester.thenDispatchedActionsShouldEqual(
  134. toKeyedAction(key, filterAdded(toVariablePayload(variable, expectedFilter)))
  135. );
  136. expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
  137. });
  138. });
  139. describe('when applyFilterFromTable is dispatched and adhoc variable with other datasource exists', () => {
  140. it('then correct actions are dispatched', async () => {
  141. const key = 'key';
  142. const options: AdHocTableOptions = {
  143. datasource: { uid: 'influxdb' },
  144. key: 'filter-key',
  145. value: 'filter-value',
  146. operator: '=',
  147. };
  148. const existing = adHocBuilder()
  149. .withId('elastic-filter')
  150. .withRootStateKey(key)
  151. .withName('elastic-filter')
  152. .withDatasource({ uid: 'elasticsearch' })
  153. .build();
  154. const variable = adHocBuilder()
  155. .withId('Filters')
  156. .withRootStateKey(key)
  157. .withName('Filters')
  158. .withDatasource(options.datasource)
  159. .build();
  160. const tester = await reduxTester<RootReducerType>({ preloadedState: getPreloadedState(key, {}) })
  161. .givenRootReducer(getRootReducer())
  162. .whenActionIsDispatched(createAddVariableAction(existing))
  163. .whenAsyncActionIsDispatched(applyFilterFromTable(options), true);
  164. const expectedFilter = { key: 'filter-key', value: 'filter-value', operator: '=', condition: '' };
  165. const expectedQuery = { 'var-elastic-filter': [] as string[], 'var-Filters': ['filter-key|=|filter-value'] };
  166. tester.thenDispatchedActionsShouldEqual(
  167. createAddVariableAction(variable, 1),
  168. toKeyedAction(key, filterAdded(toVariablePayload(variable, expectedFilter)))
  169. );
  170. expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
  171. });
  172. });
  173. describe('when changeFilter is dispatched', () => {
  174. it('then correct actions are dispatched', async () => {
  175. const key = 'key';
  176. const existing = {
  177. key: 'key',
  178. value: 'value',
  179. operator: '=',
  180. condition: '',
  181. };
  182. const updated = {
  183. ...existing,
  184. operator: '!=',
  185. };
  186. const variable = adHocBuilder()
  187. .withId('elastic-filter')
  188. .withRootStateKey(key)
  189. .withName('elastic-filter')
  190. .withFilters([existing])
  191. .withDatasource({ uid: 'elasticsearch' })
  192. .build();
  193. const update = { index: 0, filter: updated };
  194. const tester = await reduxTester<RootReducerType>()
  195. .givenRootReducer(getRootReducer())
  196. .whenActionIsDispatched(createAddVariableAction(variable))
  197. .whenAsyncActionIsDispatched(changeFilter(toKeyedVariableIdentifier(variable), update), true);
  198. const expectedQuery = { 'var-elastic-filter': ['key|!=|value'] };
  199. const expectedUpdate = { index: 0, filter: updated };
  200. tester.thenDispatchedActionsShouldEqual(
  201. toKeyedAction(key, filterUpdated(toVariablePayload(variable, expectedUpdate)))
  202. );
  203. expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
  204. });
  205. });
  206. describe('when addFilter is dispatched on variable with existing filter', () => {
  207. it('then correct actions are dispatched', async () => {
  208. const key = 'key';
  209. const existing = {
  210. key: 'key',
  211. value: 'value',
  212. operator: '=',
  213. condition: '',
  214. };
  215. const adding = {
  216. ...existing,
  217. operator: '!=',
  218. };
  219. const variable = adHocBuilder()
  220. .withId('elastic-filter')
  221. .withRootStateKey(key)
  222. .withName('elastic-filter')
  223. .withFilters([existing])
  224. .withDatasource({ uid: 'elasticsearch' })
  225. .build();
  226. const tester = await reduxTester<RootReducerType>()
  227. .givenRootReducer(getRootReducer())
  228. .whenActionIsDispatched(createAddVariableAction(variable))
  229. .whenAsyncActionIsDispatched(addFilter(toKeyedVariableIdentifier(variable), adding), true);
  230. const expectedQuery = { 'var-elastic-filter': ['key|=|value', 'key|!=|value'] };
  231. const expectedFilter = { key: 'key', value: 'value', operator: '!=', condition: '' };
  232. tester.thenDispatchedActionsShouldEqual(
  233. toKeyedAction(key, filterAdded(toVariablePayload(variable, expectedFilter)))
  234. );
  235. expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
  236. });
  237. });
  238. describe('when addFilter is dispatched on variable with no existing filter', () => {
  239. it('then correct actions are dispatched', async () => {
  240. const key = 'key';
  241. const adding = {
  242. key: 'key',
  243. value: 'value',
  244. operator: '=',
  245. condition: '',
  246. };
  247. const variable = adHocBuilder()
  248. .withId('elastic-filter')
  249. .withRootStateKey(key)
  250. .withName('elastic-filter')
  251. .withFilters([])
  252. .withDatasource({ uid: 'elasticsearch' })
  253. .build();
  254. const tester = await reduxTester<RootReducerType>()
  255. .givenRootReducer(getRootReducer())
  256. .whenActionIsDispatched(createAddVariableAction(variable))
  257. .whenAsyncActionIsDispatched(addFilter(toKeyedVariableIdentifier(variable), adding), true);
  258. const expectedQuery = { 'var-elastic-filter': ['key|=|value'] };
  259. tester.thenDispatchedActionsShouldEqual(toKeyedAction(key, filterAdded(toVariablePayload(variable, adding))));
  260. expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
  261. });
  262. });
  263. describe('when removeFilter is dispatched on variable with no existing filter', () => {
  264. it('then correct actions are dispatched', async () => {
  265. const key = 'key';
  266. const variable = adHocBuilder()
  267. .withId('elastic-filter')
  268. .withRootStateKey(key)
  269. .withName('elastic-filter')
  270. .withFilters([])
  271. .withDatasource({ uid: 'elasticsearch' })
  272. .build();
  273. const tester = await reduxTester<RootReducerType>()
  274. .givenRootReducer(getRootReducer())
  275. .whenActionIsDispatched(createAddVariableAction(variable))
  276. .whenAsyncActionIsDispatched(removeFilter(toKeyedVariableIdentifier(variable), 0), true);
  277. const expectedQuery = { 'var-elastic-filter': [] as string[] };
  278. tester.thenDispatchedActionsShouldEqual(toKeyedAction(key, filterRemoved(toVariablePayload(variable, 0))));
  279. expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
  280. });
  281. });
  282. describe('when removeFilter is dispatched on variable with existing filter', () => {
  283. it('then correct actions are dispatched', async () => {
  284. const key = 'key';
  285. const filter = {
  286. key: 'key',
  287. value: 'value',
  288. operator: '=',
  289. condition: '',
  290. };
  291. const variable = adHocBuilder()
  292. .withId('elastic-filter')
  293. .withRootStateKey(key)
  294. .withName('elastic-filter')
  295. .withFilters([filter])
  296. .withDatasource({ uid: 'elasticsearch' })
  297. .build();
  298. const tester = await reduxTester<RootReducerType>()
  299. .givenRootReducer(getRootReducer())
  300. .whenActionIsDispatched(createAddVariableAction(variable))
  301. .whenAsyncActionIsDispatched(removeFilter(toKeyedVariableIdentifier(variable), 0), true);
  302. const expectedQuery = { 'var-elastic-filter': [] as string[] };
  303. tester.thenDispatchedActionsShouldEqual(toKeyedAction(key, filterRemoved(toVariablePayload(variable, 0))));
  304. expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
  305. });
  306. });
  307. describe('when setFiltersFromUrl is dispatched', () => {
  308. it('then correct actions are dispatched', async () => {
  309. const key = 'key';
  310. const existing = {
  311. key: 'key',
  312. value: 'value',
  313. operator: '=',
  314. condition: '',
  315. };
  316. const variable = adHocBuilder()
  317. .withId('elastic-filter')
  318. .withRootStateKey(key)
  319. .withName('elastic-filter')
  320. .withFilters([existing])
  321. .withDatasource({ uid: 'elasticsearch' })
  322. .build();
  323. const fromUrl = [
  324. { ...existing, condition: '>' },
  325. { ...existing, name: 'value-2' },
  326. ];
  327. const tester = await reduxTester<RootReducerType>()
  328. .givenRootReducer(getRootReducer())
  329. .whenActionIsDispatched(createAddVariableAction(variable))
  330. .whenAsyncActionIsDispatched(setFiltersFromUrl(toKeyedVariableIdentifier(variable), fromUrl), true);
  331. const expectedQuery = { 'var-elastic-filter': ['key|=|value', 'key|=|value'] };
  332. const expectedFilters = [
  333. { key: 'key', value: 'value', operator: '=', condition: '>' },
  334. { key: 'key', value: 'value', operator: '=', condition: '', name: 'value-2' },
  335. ];
  336. tester.thenDispatchedActionsShouldEqual(
  337. toKeyedAction(key, filtersRestored(toVariablePayload(variable, expectedFilters)))
  338. );
  339. expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
  340. });
  341. });
  342. describe('when initAdHocVariableEditor is dispatched', () => {
  343. it('then correct actions are dispatched', async () => {
  344. const key = 'key';
  345. getList.mockRestore();
  346. getList.mockReturnValue(datasources);
  347. const tester = reduxTester<RootReducerType>()
  348. .givenRootReducer(getRootReducer())
  349. .whenActionIsDispatched(initAdHocVariableEditor(key));
  350. tester.thenDispatchedActionsShouldEqual(
  351. toKeyedAction(key, changeVariableEditorExtended({ dataSources: expectedDatasources }))
  352. );
  353. });
  354. });
  355. describe('when changeVariableDatasource is dispatched with unsupported datasource', () => {
  356. it('then correct actions are dispatched', async () => {
  357. const key = 'key';
  358. const datasource = { uid: 'mysql' };
  359. const variable = adHocBuilder()
  360. .withId('Filters')
  361. .withRootStateKey(key)
  362. .withName('Filters')
  363. .withDatasource({ uid: 'influxdb' })
  364. .build();
  365. getDatasource.mockRestore();
  366. getDatasource.mockResolvedValue(null);
  367. getList.mockRestore();
  368. getList.mockReturnValue(datasources);
  369. const tester = await reduxTester<RootReducerType>()
  370. .givenRootReducer(getRootReducer())
  371. .whenActionIsDispatched(createAddVariableAction(variable))
  372. .whenActionIsDispatched(toKeyedAction(key, setIdInEditor({ id: variable.id })))
  373. .whenActionIsDispatched(initAdHocVariableEditor(key))
  374. .whenAsyncActionIsDispatched(changeVariableDatasource(toKeyedVariableIdentifier(variable), datasource), true);
  375. tester.thenDispatchedActionsShouldEqual(
  376. toKeyedAction(
  377. key,
  378. changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource }))
  379. ),
  380. toKeyedAction(
  381. key,
  382. changeVariableEditorExtended({
  383. infoText: 'This data source does not support ad hoc filters yet.',
  384. dataSources: expectedDatasources,
  385. })
  386. )
  387. );
  388. });
  389. });
  390. describe('when changeVariableDatasource is dispatched with datasource', () => {
  391. it('then correct actions are dispatched', async () => {
  392. const key = 'key';
  393. const datasource = { uid: 'elasticsearch' };
  394. const loadingText = 'Ad hoc filters are applied automatically to all queries that target this data source';
  395. const variable = adHocBuilder()
  396. .withId('Filters')
  397. .withRootStateKey(key)
  398. .withName('Filters')
  399. .withDatasource({ uid: 'influxdb' })
  400. .build();
  401. getDatasource.mockRestore();
  402. getDatasource.mockResolvedValue({
  403. getTagKeys: () => {},
  404. });
  405. getList.mockRestore();
  406. getList.mockReturnValue(datasources);
  407. const tester = await reduxTester<RootReducerType>()
  408. .givenRootReducer(getRootReducer())
  409. .whenActionIsDispatched(createAddVariableAction(variable))
  410. .whenActionIsDispatched(toKeyedAction(key, setIdInEditor({ id: variable.id })))
  411. .whenActionIsDispatched(initAdHocVariableEditor(key))
  412. .whenAsyncActionIsDispatched(changeVariableDatasource(toKeyedVariableIdentifier(variable), datasource), true);
  413. tester.thenDispatchedActionsShouldEqual(
  414. toKeyedAction(
  415. key,
  416. changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource }))
  417. ),
  418. toKeyedAction(key, changeVariableEditorExtended({ infoText: loadingText, dataSources: expectedDatasources }))
  419. );
  420. });
  421. });
  422. });
  423. function createAddVariableAction(variable: VariableModel, index = 0) {
  424. const identifier = toKeyedVariableIdentifier(variable);
  425. const global = false;
  426. const data = { global, index, model: { ...variable, index: -1, global } };
  427. return toKeyedAction(variable.rootStateKey!, addVariable(toVariablePayload(identifier, data)));
  428. }
  429. function createDatasource(name: string, selectable = true, isDefault = false): DataSourceInstanceSettings {
  430. return {
  431. name,
  432. meta: {
  433. mixed: !selectable,
  434. } as DataSourcePluginMeta,
  435. isDefault,
  436. uid: name,
  437. type: name,
  438. } as DataSourceInstanceSettings;
  439. }