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