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