actions.test.tsx 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781
  1. import React from 'react';
  2. import { expect } from 'test/lib/common';
  3. import { DataSourceRef, getDefaultTimeRange, LoadingState } from '@grafana/data';
  4. import { setDataSourceSrv } from '@grafana/runtime';
  5. import { reduxTester } from '../../../../test/core/redux/reduxTester';
  6. import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
  7. import { notifyApp } from '../../../core/reducers/appNotification';
  8. import { getTimeSrv, setTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv';
  9. import { variableAdapters } from '../adapters';
  10. import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
  11. import { LegacyVariableQueryEditor } from '../editor/LegacyVariableQueryEditor';
  12. import {
  13. addVariableEditorError,
  14. changeVariableEditorExtended,
  15. initialVariableEditorState,
  16. removeVariableEditorError,
  17. setIdInEditor,
  18. } from '../editor/reducer';
  19. import { updateOptions } from '../state/actions';
  20. import { getPreloadedState, getRootReducer, RootReducerType } from '../state/helpers';
  21. import { toKeyedAction } from '../state/keyedVariablesReducer';
  22. import {
  23. addVariable,
  24. changeVariableProp,
  25. setCurrentVariableValue,
  26. variableStateCompleted,
  27. variableStateFailed,
  28. variableStateFetching,
  29. } from '../state/sharedReducer';
  30. import { variablesInitTransaction } from '../state/transactionReducer';
  31. import { QueryVariableModel, VariableHide, VariableQueryEditorProps, VariableRefresh, VariableSort } from '../types';
  32. import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
  33. import { setVariableQueryRunner, VariableQueryRunner } from './VariableQueryRunner';
  34. import {
  35. changeQueryVariableDataSource,
  36. changeQueryVariableQuery,
  37. flattenQuery,
  38. hasSelfReferencingQuery,
  39. initQueryVariableEditor,
  40. updateQueryVariableOptions,
  41. } from './actions';
  42. import { createQueryVariableAdapter } from './adapter';
  43. import { updateVariableOptions } from './reducer';
  44. const mocks: Record<string, any> = {
  45. datasource: {
  46. metricFindQuery: jest.fn().mockResolvedValue([]),
  47. },
  48. dataSourceSrv: {
  49. get: (ref: DataSourceRef) => Promise.resolve(mocks[ref.uid!]),
  50. getList: jest.fn().mockReturnValue([]),
  51. },
  52. pluginLoader: {
  53. importDataSourcePlugin: jest.fn().mockResolvedValue({ components: {} }),
  54. },
  55. VariableQueryEditor(props: VariableQueryEditorProps) {
  56. return <div>this is a variable query editor</div>;
  57. },
  58. };
  59. setDataSourceSrv(mocks.dataSourceSrv as any);
  60. jest.mock('../../plugins/plugin_loader', () => ({
  61. importDataSourcePlugin: () => mocks.pluginLoader.importDataSourcePlugin(),
  62. }));
  63. jest.mock('../../templating/template_srv', () => ({
  64. replace: jest.fn().mockReturnValue(''),
  65. }));
  66. describe('query actions', () => {
  67. let originalTimeSrv: TimeSrv;
  68. beforeEach(() => {
  69. originalTimeSrv = getTimeSrv();
  70. setTimeSrv({
  71. timeRange: jest.fn().mockReturnValue(getDefaultTimeRange()),
  72. } as unknown as TimeSrv);
  73. setVariableQueryRunner(new VariableQueryRunner());
  74. });
  75. afterEach(() => {
  76. setTimeSrv(originalTimeSrv);
  77. });
  78. variableAdapters.setInit(() => [createQueryVariableAdapter()]);
  79. describe('when updateQueryVariableOptions is dispatched but there is no ongoing transaction', () => {
  80. it('then correct actions are dispatched', async () => {
  81. const variable = createVariable({ includeAll: false });
  82. const optionsMetrics = [createMetric('A'), createMetric('B')];
  83. mockDatasourceMetrics(variable, optionsMetrics);
  84. const tester = await reduxTester<RootReducerType>()
  85. .givenRootReducer(getRootReducer())
  86. .whenActionIsDispatched(
  87. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  88. )
  89. .whenAsyncActionIsDispatched(updateQueryVariableOptions(toKeyedVariableIdentifier(variable)), true);
  90. tester.thenNoActionsWhereDispatched();
  91. });
  92. });
  93. describe('when updateQueryVariableOptions is dispatched for variable without both tags and includeAll', () => {
  94. it('then correct actions are dispatched', async () => {
  95. const variable = createVariable({ includeAll: false });
  96. const optionsMetrics = [createMetric('A'), createMetric('B')];
  97. mockDatasourceMetrics(variable, optionsMetrics);
  98. const tester = await reduxTester<RootReducerType>()
  99. .givenRootReducer(getRootReducer())
  100. .whenActionIsDispatched(
  101. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  102. )
  103. .whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
  104. .whenAsyncActionIsDispatched(updateQueryVariableOptions(toKeyedVariableIdentifier(variable)), true);
  105. const option = createOption('A');
  106. const update = { results: optionsMetrics, templatedRegex: '' };
  107. tester.thenDispatchedActionsShouldEqual(
  108. toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
  109. toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
  110. );
  111. });
  112. });
  113. describe('when updateQueryVariableOptions is dispatched for variable with includeAll but without tags', () => {
  114. it('then correct actions are dispatched', async () => {
  115. const variable = createVariable({ includeAll: true });
  116. const optionsMetrics = [createMetric('A'), createMetric('B')];
  117. mockDatasourceMetrics(variable, optionsMetrics);
  118. const tester = await reduxTester<RootReducerType>()
  119. .givenRootReducer(getRootReducer())
  120. .whenActionIsDispatched(
  121. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  122. )
  123. .whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
  124. .whenAsyncActionIsDispatched(updateQueryVariableOptions(toKeyedVariableIdentifier(variable)), true);
  125. const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
  126. const update = { results: optionsMetrics, templatedRegex: '' };
  127. tester.thenDispatchedActionsShouldEqual(
  128. toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
  129. toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
  130. );
  131. });
  132. });
  133. describe('when updateQueryVariableOptions is dispatched for variable open in editor', () => {
  134. it('then correct actions are dispatched', async () => {
  135. const variable = createVariable({ includeAll: true });
  136. const optionsMetrics = [createMetric('A'), createMetric('B')];
  137. mockDatasourceMetrics(variable, optionsMetrics);
  138. const tester = await reduxTester<RootReducerType>()
  139. .givenRootReducer(getRootReducer())
  140. .whenActionIsDispatched(
  141. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  142. )
  143. .whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
  144. .whenActionIsDispatched(toKeyedAction('key', setIdInEditor({ id: variable.id })))
  145. .whenAsyncActionIsDispatched(updateQueryVariableOptions(toKeyedVariableIdentifier(variable)), true);
  146. const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
  147. const update = { results: optionsMetrics, templatedRegex: '' };
  148. tester.thenDispatchedActionsShouldEqual(
  149. toKeyedAction('key', removeVariableEditorError({ errorProp: 'update' })),
  150. toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
  151. toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
  152. );
  153. });
  154. });
  155. describe('when updateQueryVariableOptions is dispatched for variable with searchFilter', () => {
  156. it('then correct actions are dispatched', async () => {
  157. const variable = createVariable({ includeAll: true });
  158. const optionsMetrics = [createMetric('A'), createMetric('B')];
  159. mockDatasourceMetrics(variable, optionsMetrics);
  160. const tester = await reduxTester<RootReducerType>()
  161. .givenRootReducer(getRootReducer())
  162. .whenActionIsDispatched(
  163. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  164. )
  165. .whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
  166. .whenActionIsDispatched(toKeyedAction('key', setIdInEditor({ id: variable.id })))
  167. .whenAsyncActionIsDispatched(updateQueryVariableOptions(toKeyedVariableIdentifier(variable), 'search'), true);
  168. const update = { results: optionsMetrics, templatedRegex: '' };
  169. tester.thenDispatchedActionsShouldEqual(
  170. toKeyedAction('key', removeVariableEditorError({ errorProp: 'update' })),
  171. toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update)))
  172. );
  173. });
  174. });
  175. describe('when updateQueryVariableOptions is dispatched and fails for variable open in editor', () => {
  176. silenceConsoleOutput();
  177. it('then correct actions are dispatched', async () => {
  178. const variable = createVariable({ includeAll: true });
  179. const error = { message: 'failed to fetch metrics' };
  180. mocks[variable.datasource!.uid!].metricFindQuery = jest.fn(() => Promise.reject(error));
  181. const tester = await reduxTester<RootReducerType>()
  182. .givenRootReducer(getRootReducer())
  183. .whenActionIsDispatched(
  184. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  185. )
  186. .whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
  187. .whenActionIsDispatched(toKeyedAction('key', setIdInEditor({ id: variable.id })))
  188. .whenAsyncActionIsDispatched(updateOptions(toKeyedVariableIdentifier(variable)), true);
  189. tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
  190. const expectedNumberOfActions = 5;
  191. expect(dispatchedActions[0]).toEqual(toKeyedAction('key', variableStateFetching(toVariablePayload(variable))));
  192. expect(dispatchedActions[1]).toEqual(toKeyedAction('key', removeVariableEditorError({ errorProp: 'update' })));
  193. expect(dispatchedActions[2]).toEqual(
  194. toKeyedAction('key', addVariableEditorError({ errorProp: 'update', errorText: error.message }))
  195. );
  196. expect(dispatchedActions[3]).toEqual(
  197. toKeyedAction(
  198. 'key',
  199. variableStateFailed(toVariablePayload(variable, { error: { message: 'failed to fetch metrics' } }))
  200. )
  201. );
  202. expect(dispatchedActions[4].type).toEqual(notifyApp.type);
  203. expect(dispatchedActions[4].payload.title).toEqual('Templating [0]');
  204. expect(dispatchedActions[4].payload.text).toEqual('Error updating options: failed to fetch metrics');
  205. expect(dispatchedActions[4].payload.severity).toEqual('error');
  206. return dispatchedActions.length === expectedNumberOfActions;
  207. });
  208. });
  209. });
  210. describe('when initQueryVariableEditor is dispatched', () => {
  211. it('then correct actions are dispatched', async () => {
  212. const variable = createVariable({ includeAll: true });
  213. const testMetricSource = { name: 'test', value: 'test', meta: {} };
  214. const editor = mocks.VariableQueryEditor;
  215. mocks.dataSourceSrv.getList = jest.fn().mockReturnValue([testMetricSource]);
  216. mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
  217. components: { VariableQueryEditor: editor },
  218. });
  219. const tester = await reduxTester<RootReducerType>()
  220. .givenRootReducer(getRootReducer())
  221. .whenActionIsDispatched(
  222. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  223. )
  224. .whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
  225. .whenAsyncActionIsDispatched(initQueryVariableEditor(toKeyedVariableIdentifier(variable)), true);
  226. tester.thenDispatchedActionsShouldEqual(
  227. toKeyedAction(
  228. 'key',
  229. changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
  230. )
  231. );
  232. });
  233. });
  234. describe('when initQueryVariableEditor is dispatched and metricsource without value is available', () => {
  235. it('then correct actions are dispatched', async () => {
  236. const variable = createVariable({ includeAll: true });
  237. const testMetricSource = { name: 'test', value: null as unknown as string, meta: {} };
  238. const editor = mocks.VariableQueryEditor;
  239. mocks.dataSourceSrv.getList = jest.fn().mockReturnValue([testMetricSource]);
  240. mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
  241. components: { VariableQueryEditor: editor },
  242. });
  243. const tester = await reduxTester<RootReducerType>()
  244. .givenRootReducer(getRootReducer())
  245. .whenActionIsDispatched(
  246. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  247. )
  248. .whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
  249. .whenAsyncActionIsDispatched(initQueryVariableEditor(toKeyedVariableIdentifier(variable)), true);
  250. tester.thenDispatchedActionsShouldEqual(
  251. toKeyedAction(
  252. 'key',
  253. changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
  254. )
  255. );
  256. });
  257. });
  258. describe('when initQueryVariableEditor is dispatched and no metric sources was found', () => {
  259. it('then correct actions are dispatched', async () => {
  260. const variable = createVariable({ includeAll: true });
  261. const editor = mocks.VariableQueryEditor;
  262. mocks.dataSourceSrv.getList = jest.fn().mockReturnValue([]);
  263. mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
  264. components: { VariableQueryEditor: editor },
  265. });
  266. const tester = await reduxTester<RootReducerType>()
  267. .givenRootReducer(getRootReducer())
  268. .whenActionIsDispatched(
  269. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  270. )
  271. .whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
  272. .whenAsyncActionIsDispatched(initQueryVariableEditor(toKeyedVariableIdentifier(variable)), true);
  273. tester.thenDispatchedActionsShouldEqual(
  274. toKeyedAction(
  275. 'key',
  276. changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
  277. )
  278. );
  279. });
  280. });
  281. describe('when changeQueryVariableDataSource is dispatched', () => {
  282. it('then correct actions are dispatched', async () => {
  283. const variable = createVariable({ datasource: { uid: 'other' } });
  284. const editor = mocks.VariableQueryEditor;
  285. mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
  286. components: { VariableQueryEditor: editor },
  287. });
  288. const tester = await reduxTester<RootReducerType>()
  289. .givenRootReducer(getRootReducer())
  290. .whenActionIsDispatched(
  291. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  292. )
  293. .whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
  294. .whenAsyncActionIsDispatched(
  295. changeQueryVariableDataSource(toKeyedVariableIdentifier(variable), { uid: 'datasource' }),
  296. true
  297. );
  298. tester.thenDispatchedActionsShouldEqual(
  299. toKeyedAction(
  300. 'key',
  301. changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
  302. )
  303. );
  304. });
  305. describe('and data source type changed', () => {
  306. it('then correct actions are dispatched', async () => {
  307. const variable = createVariable({ datasource: { uid: 'other' } });
  308. const editor = mocks.VariableQueryEditor;
  309. const previousDataSource: any = { type: 'previous' };
  310. const templatingState = {
  311. editor: {
  312. ...initialVariableEditorState,
  313. extended: { dataSource: previousDataSource, VariableQueryEditor: editor },
  314. },
  315. };
  316. const preloadedState = getPreloadedState('key', templatingState);
  317. mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
  318. components: { VariableQueryEditor: editor },
  319. });
  320. const tester = await reduxTester<RootReducerType>({ preloadedState })
  321. .givenRootReducer(getRootReducer())
  322. .whenActionIsDispatched(
  323. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  324. )
  325. .whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
  326. .whenAsyncActionIsDispatched(
  327. changeQueryVariableDataSource(toKeyedVariableIdentifier(variable), { uid: 'datasource' }),
  328. true
  329. );
  330. tester.thenDispatchedActionsShouldEqual(
  331. toKeyedAction('key', changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: '' }))),
  332. toKeyedAction(
  333. 'key',
  334. changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
  335. )
  336. );
  337. });
  338. });
  339. });
  340. describe('when changeQueryVariableDataSource is dispatched and editor is not configured', () => {
  341. it('then correct actions are dispatched', async () => {
  342. const variable = createVariable({ datasource: { uid: 'other' } });
  343. const editor = LegacyVariableQueryEditor;
  344. mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
  345. components: {},
  346. });
  347. const tester = await reduxTester<RootReducerType>()
  348. .givenRootReducer(getRootReducer())
  349. .whenActionIsDispatched(
  350. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  351. )
  352. .whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
  353. .whenAsyncActionIsDispatched(
  354. changeQueryVariableDataSource(toKeyedVariableIdentifier(variable), { uid: 'datasource' }),
  355. true
  356. );
  357. tester.thenDispatchedActionsShouldEqual(
  358. toKeyedAction(
  359. 'key',
  360. changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
  361. )
  362. );
  363. });
  364. });
  365. describe('when changeQueryVariableQuery is dispatched', () => {
  366. it('then correct actions are dispatched', async () => {
  367. const optionsMetrics = [createMetric('A'), createMetric('B')];
  368. const variable = createVariable({ datasource: { uid: 'datasource' }, includeAll: true });
  369. const query = '$datasource';
  370. const definition = 'depends on datasource variable';
  371. mockDatasourceMetrics({ ...variable, query }, optionsMetrics);
  372. const tester = await reduxTester<RootReducerType>()
  373. .givenRootReducer(getRootReducer())
  374. .whenActionIsDispatched(
  375. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  376. )
  377. .whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
  378. .whenAsyncActionIsDispatched(
  379. changeQueryVariableQuery(toKeyedVariableIdentifier(variable), query, definition),
  380. true
  381. );
  382. const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
  383. const update = { results: optionsMetrics, templatedRegex: '' };
  384. tester.thenDispatchedActionsShouldEqual(
  385. toKeyedAction('key', removeVariableEditorError({ errorProp: 'query' })),
  386. toKeyedAction('key', changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query }))),
  387. toKeyedAction(
  388. 'key',
  389. changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition }))
  390. ),
  391. toKeyedAction('key', variableStateFetching(toVariablePayload(variable))),
  392. toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
  393. toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
  394. toKeyedAction('key', variableStateCompleted(toVariablePayload(variable)))
  395. );
  396. });
  397. });
  398. describe('when changeQueryVariableQuery is dispatched for variable without tags', () => {
  399. it('then correct actions are dispatched', async () => {
  400. const optionsMetrics = [createMetric('A'), createMetric('B')];
  401. const variable = createVariable({ datasource: { uid: 'datasource' }, includeAll: true });
  402. const query = '$datasource';
  403. const definition = 'depends on datasource variable';
  404. mockDatasourceMetrics({ ...variable, query }, optionsMetrics);
  405. const tester = await reduxTester<RootReducerType>()
  406. .givenRootReducer(getRootReducer())
  407. .whenActionIsDispatched(
  408. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  409. )
  410. .whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
  411. .whenAsyncActionIsDispatched(
  412. changeQueryVariableQuery(toKeyedVariableIdentifier(variable), query, definition),
  413. true
  414. );
  415. const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
  416. const update = { results: optionsMetrics, templatedRegex: '' };
  417. tester.thenDispatchedActionsShouldEqual(
  418. toKeyedAction('key', removeVariableEditorError({ errorProp: 'query' })),
  419. toKeyedAction('key', changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query }))),
  420. toKeyedAction(
  421. 'key',
  422. changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition }))
  423. ),
  424. toKeyedAction('key', variableStateFetching(toVariablePayload(variable))),
  425. toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
  426. toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
  427. toKeyedAction('key', variableStateCompleted(toVariablePayload(variable)))
  428. );
  429. });
  430. });
  431. describe('when changeQueryVariableQuery is dispatched for variable without tags and all', () => {
  432. it('then correct actions are dispatched', async () => {
  433. const optionsMetrics = [createMetric('A'), createMetric('B')];
  434. const variable = createVariable({ datasource: { uid: 'datasource' }, includeAll: false });
  435. const query = '$datasource';
  436. const definition = 'depends on datasource variable';
  437. mockDatasourceMetrics({ ...variable, query }, optionsMetrics);
  438. const tester = await reduxTester<RootReducerType>()
  439. .givenRootReducer(getRootReducer())
  440. .whenActionIsDispatched(
  441. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  442. )
  443. .whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
  444. .whenAsyncActionIsDispatched(
  445. changeQueryVariableQuery(toKeyedVariableIdentifier(variable), query, definition),
  446. true
  447. );
  448. const option = createOption('A');
  449. const update = { results: optionsMetrics, templatedRegex: '' };
  450. tester.thenDispatchedActionsShouldEqual(
  451. toKeyedAction('key', removeVariableEditorError({ errorProp: 'query' })),
  452. toKeyedAction('key', changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query }))),
  453. toKeyedAction(
  454. 'key',
  455. changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition }))
  456. ),
  457. toKeyedAction('key', variableStateFetching(toVariablePayload(variable))),
  458. toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
  459. toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
  460. toKeyedAction('key', variableStateCompleted(toVariablePayload(variable)))
  461. );
  462. });
  463. });
  464. describe('when changeQueryVariableQuery is dispatched with invalid query', () => {
  465. it('then correct actions are dispatched', async () => {
  466. const variable = createVariable({ datasource: { uid: 'datasource' }, includeAll: false });
  467. const query = `$${variable.name}`;
  468. const definition = 'depends on datasource variable';
  469. const tester = await reduxTester<RootReducerType>()
  470. .givenRootReducer(getRootReducer())
  471. .whenActionIsDispatched(
  472. toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
  473. )
  474. .whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
  475. .whenAsyncActionIsDispatched(
  476. changeQueryVariableQuery(toKeyedVariableIdentifier(variable), query, definition),
  477. true
  478. );
  479. const errorText = 'Query cannot contain a reference to itself. Variable: $' + variable.name;
  480. tester.thenDispatchedActionsShouldEqual(
  481. toKeyedAction('key', addVariableEditorError({ errorProp: 'query', errorText }))
  482. );
  483. });
  484. });
  485. describe('hasSelfReferencingQuery', () => {
  486. it('when called with a string', () => {
  487. const query = '$query';
  488. const name = 'query';
  489. expect(hasSelfReferencingQuery(name, query)).toBe(true);
  490. });
  491. it('when called with an array', () => {
  492. const query = ['$query'];
  493. const name = 'query';
  494. expect(hasSelfReferencingQuery(name, query)).toBe(true);
  495. });
  496. it('when called with a simple object', () => {
  497. const query = { a: '$query' };
  498. const name = 'query';
  499. expect(hasSelfReferencingQuery(name, query)).toBe(true);
  500. });
  501. it('when called with a complex object', () => {
  502. const query = {
  503. level2: {
  504. level3: {
  505. query: 'query3',
  506. refId: 'C',
  507. num: 2,
  508. bool: true,
  509. arr: [
  510. { query: 'query4', refId: 'D', num: 4, bool: true },
  511. {
  512. query: 'query5',
  513. refId: 'E',
  514. num: 5,
  515. bool: true,
  516. arr: [{ query: '$query', refId: 'F', num: 6, bool: true }],
  517. },
  518. ],
  519. },
  520. query: 'query2',
  521. refId: 'B',
  522. num: 1,
  523. bool: false,
  524. },
  525. query: 'query1',
  526. refId: 'A',
  527. num: 0,
  528. bool: true,
  529. arr: [
  530. { query: 'query7', refId: 'G', num: 7, bool: true },
  531. {
  532. query: 'query8',
  533. refId: 'H',
  534. num: 8,
  535. bool: true,
  536. arr: [{ query: 'query9', refId: 'I', num: 9, bool: true }],
  537. },
  538. ],
  539. };
  540. const name = 'query';
  541. expect(hasSelfReferencingQuery(name, query)).toBe(true);
  542. });
  543. it('when called with a number', () => {
  544. const query = 1;
  545. const name = 'query';
  546. expect(hasSelfReferencingQuery(name, query)).toBe(false);
  547. });
  548. });
  549. describe('flattenQuery', () => {
  550. it('when called with a complex object', () => {
  551. const query = {
  552. level2: {
  553. level3: {
  554. query: '${query3}',
  555. refId: 'C',
  556. num: 2,
  557. bool: true,
  558. arr: [
  559. { query: '${query4}', refId: 'D', num: 4, bool: true },
  560. {
  561. query: '${query5}',
  562. refId: 'E',
  563. num: 5,
  564. bool: true,
  565. arr: [{ query: '${query6}', refId: 'F', num: 6, bool: true }],
  566. },
  567. ],
  568. },
  569. query: '${query2}',
  570. refId: 'B',
  571. num: 1,
  572. bool: false,
  573. },
  574. query: '${query1}',
  575. refId: 'A',
  576. num: 0,
  577. bool: true,
  578. arr: [
  579. { query: '${query7}', refId: 'G', num: 7, bool: true },
  580. {
  581. query: '${query8}',
  582. refId: 'H',
  583. num: 8,
  584. bool: true,
  585. arr: [{ query: '${query9}', refId: 'I', num: 9, bool: true }],
  586. },
  587. ],
  588. };
  589. expect(flattenQuery(query)).toEqual({
  590. query: '${query1}',
  591. refId: 'A',
  592. num: 0,
  593. bool: true,
  594. level2_query: '${query2}',
  595. level2_refId: 'B',
  596. level2_num: 1,
  597. level2_bool: false,
  598. level2_level3_query: '${query3}',
  599. level2_level3_refId: 'C',
  600. level2_level3_num: 2,
  601. level2_level3_bool: true,
  602. level2_level3_arr_0_query: '${query4}',
  603. level2_level3_arr_0_refId: 'D',
  604. level2_level3_arr_0_num: 4,
  605. level2_level3_arr_0_bool: true,
  606. level2_level3_arr_1_query: '${query5}',
  607. level2_level3_arr_1_refId: 'E',
  608. level2_level3_arr_1_num: 5,
  609. level2_level3_arr_1_bool: true,
  610. level2_level3_arr_1_arr_0_query: '${query6}',
  611. level2_level3_arr_1_arr_0_refId: 'F',
  612. level2_level3_arr_1_arr_0_num: 6,
  613. level2_level3_arr_1_arr_0_bool: true,
  614. arr_0_query: '${query7}',
  615. arr_0_refId: 'G',
  616. arr_0_num: 7,
  617. arr_0_bool: true,
  618. arr_1_query: '${query8}',
  619. arr_1_refId: 'H',
  620. arr_1_num: 8,
  621. arr_1_bool: true,
  622. arr_1_arr_0_query: '${query9}',
  623. arr_1_arr_0_refId: 'I',
  624. arr_1_arr_0_num: 9,
  625. arr_1_arr_0_bool: true,
  626. });
  627. });
  628. });
  629. });
  630. function mockDatasourceMetrics(variable: QueryVariableModel, optionsMetrics: any[]) {
  631. const metrics: Record<string, any[]> = {
  632. [variable.query]: optionsMetrics,
  633. };
  634. const { metricFindQuery } = mocks[variable.datasource?.uid!];
  635. metricFindQuery.mockReset();
  636. metricFindQuery.mockImplementation((query: string) => Promise.resolve(metrics[query] ?? []));
  637. }
  638. function createVariable(extend?: Partial<QueryVariableModel>): QueryVariableModel {
  639. return {
  640. type: 'query',
  641. id: '0',
  642. rootStateKey: 'key',
  643. global: false,
  644. current: createOption(''),
  645. options: [],
  646. query: 'options-query',
  647. name: 'Constant',
  648. label: '',
  649. hide: VariableHide.dontHide,
  650. skipUrlSync: false,
  651. index: 0,
  652. datasource: { uid: 'datasource' },
  653. definition: '',
  654. sort: VariableSort.alphabeticalAsc,
  655. refresh: VariableRefresh.onDashboardLoad,
  656. regex: '',
  657. multi: true,
  658. includeAll: true,
  659. state: LoadingState.NotStarted,
  660. error: null,
  661. description: null,
  662. ...(extend ?? {}),
  663. };
  664. }
  665. function createOption(text: string, value?: string) {
  666. const metric = createMetric(text);
  667. return {
  668. ...metric,
  669. value: value ?? metric.text,
  670. selected: false,
  671. };
  672. }
  673. function createMetric(value: string) {
  674. return {
  675. text: value,
  676. };
  677. }