diff --git a/public/app/features/templating/all.ts b/public/app/features/templating/all.ts index 50804fe4b61..9953be38e50 100644 --- a/public/app/features/templating/all.ts +++ b/public/app/features/templating/all.ts @@ -16,6 +16,7 @@ import { createCustomVariableAdapter } from '../variables/custom/adapter'; import { createTextBoxVariableAdapter } from '../variables/textbox/adapter'; import { createConstantVariableAdapter } from '../variables/constant/adapter'; import { createDataSourceVariableAdapter } from '../variables/datasource/adapter'; +import { createIntervalVariableAdapter } from '../variables/interval/adapter'; coreModule.factory('templateSrv', () => templateSrv); @@ -35,3 +36,4 @@ variableAdapters.set('custom', createCustomVariableAdapter()); variableAdapters.set('textbox', createTextBoxVariableAdapter()); variableAdapters.set('constant', createConstantVariableAdapter()); variableAdapters.set('datasource', createDataSourceVariableAdapter()); +variableAdapters.set('interval', createIntervalVariableAdapter()); diff --git a/public/app/features/variables/adapters.ts b/public/app/features/variables/adapters.ts index 926ac37c378..3f25beef17d 100644 --- a/public/app/features/variables/adapters.ts +++ b/public/app/features/variables/adapters.ts @@ -23,13 +23,13 @@ export interface VariableAdapter { } const allVariableAdapters: Record | null> = { + interval: null, query: null, - textbox: null, - constant: null, datasource: null, custom: null, - interval: null, + constant: null, adhoc: null, + textbox: null, }; export interface VariableAdapters { diff --git a/public/app/features/variables/constant/actions.test.ts b/public/app/features/variables/constant/actions.test.ts index 84f2e602d55..97985ea128f 100644 --- a/public/app/features/variables/constant/actions.test.ts +++ b/public/app/features/variables/constant/actions.test.ts @@ -44,7 +44,7 @@ describe('constant actions', () => { .whenActionIsDispatched(initDashboardTemplating([variable])) .whenAsyncActionIsDispatched(updateConstantVariableOptions(toVariablePayload(variable)), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [createAction, setCurrentAction] = actions; const expectedNumberOfActions = 2; diff --git a/public/app/features/variables/custom/actions.test.ts b/public/app/features/variables/custom/actions.test.ts index f182cf8820e..6a362b7fd0e 100644 --- a/public/app/features/variables/custom/actions.test.ts +++ b/public/app/features/variables/custom/actions.test.ts @@ -57,7 +57,7 @@ describe('custom actions', () => { .whenActionIsDispatched(initDashboardTemplating([variable])) .whenAsyncActionIsDispatched(updateCustomVariableOptions(toVariablePayload(variable)), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [createAction, setCurrentAction] = actions; const expectedNumberOfActions = 2; diff --git a/public/app/features/variables/datasource/actions.test.ts b/public/app/features/variables/datasource/actions.test.ts index a2e5ed4e036..ab4b8d0f02d 100644 --- a/public/app/features/variables/datasource/actions.test.ts +++ b/public/app/features/variables/datasource/actions.test.ts @@ -52,7 +52,7 @@ describe('data source actions', () => { true ); - await tester.thenDispatchedActionShouldEqual( + await tester.thenDispatchedActionsShouldEqual( createDataSourceOptions( toVariablePayload({ type: 'datasource', uuid: '0' }, { sources, regex: (undefined as unknown) as RegExp }) ), @@ -103,7 +103,7 @@ describe('data source actions', () => { true ); - await tester.thenDispatchedActionShouldEqual( + await tester.thenDispatchedActionsShouldEqual( createDataSourceOptions( toVariablePayload({ type: 'datasource', uuid: '0' }, { sources, regex: /.*(second-name).*/ }) ), @@ -157,7 +157,7 @@ describe('data source actions', () => { .givenRootReducer(getTemplatingRootReducer()) .whenAsyncActionIsDispatched(initDataSourceVariableEditor(dependencies)); - await tester.thenDispatchedActionShouldEqual( + await tester.thenDispatchedActionsShouldEqual( changeVariableEditorExtended({ propName: 'dataSourceTypes', propValue: [ diff --git a/public/app/features/variables/interval/IntervalVariableEditor.tsx b/public/app/features/variables/interval/IntervalVariableEditor.tsx new file mode 100644 index 00000000000..ad216d61419 --- /dev/null +++ b/public/app/features/variables/interval/IntervalVariableEditor.tsx @@ -0,0 +1,119 @@ +import React, { ChangeEvent, FocusEvent, PureComponent } from 'react'; + +import { IntervalVariableModel } from '../../templating/variable'; +import { VariableEditorProps } from '../editor/types'; +import { FormLabel, Switch } from '@grafana/ui'; + +export interface Props extends VariableEditorProps {} + +export class IntervalVariableEditor extends PureComponent { + onAutoChange = (event: ChangeEvent) => { + this.props.onPropChange({ + propName: 'auto', + propValue: event.target.checked, + updateOptions: true, + }); + }; + + onQueryChanged = (event: ChangeEvent) => { + this.props.onPropChange({ + propName: 'query', + propValue: event.target.value, + }); + }; + + onQueryBlur = (event: FocusEvent) => { + this.props.onPropChange({ + propName: 'query', + propValue: event.target.value, + updateOptions: true, + }); + }; + + onAutoCountChanged = (event: ChangeEvent) => { + this.props.onPropChange({ + propName: 'auto_count', + propValue: event.target.value, + updateOptions: true, + }); + }; + + onAutoMinChanged = (event: ChangeEvent) => { + this.props.onPropChange({ + propName: 'auto_min', + propValue: event.target.value, + updateOptions: true, + }); + }; + + render() { + return ( + <> +
+
Interval Options
+ +
+ Values + +
+ +
+ + + {this.props.variable.auto && ( + <> +
+ + Step count + +
+ +
+
+
+ + Min interval + + +
+ + )} +
+
+ + ); + } +} diff --git a/public/app/features/variables/interval/actions.test.ts b/public/app/features/variables/interval/actions.test.ts new file mode 100644 index 00000000000..b12ce662d3b --- /dev/null +++ b/public/app/features/variables/interval/actions.test.ts @@ -0,0 +1,188 @@ +import { getTemplatingRootReducer, variableMockBuilder } from '../state/helpers'; +import { reduxTester } from '../../../../test/core/redux/reduxTester'; +import { TemplatingState } from '../state/reducers'; +import { initDashboardTemplating } from '../state/actions'; +import { toVariableIdentifier } from '../state/types'; +import { + updateAutoValue, + UpdateAutoValueDependencies, + updateIntervalVariableOptions, + UpdateIntervalVariableOptionsDependencies, +} from './actions'; +import { createIntervalOptions } from './reducer'; +import { setCurrentVariableValue } from '../state/sharedReducer'; +import { variableAdapters } from '../adapters'; +import { createIntervalVariableAdapter } from './adapter'; +import { Emitter } from 'app/core/core'; +import { AppEvents, dateTime } from '@grafana/data'; +import { getTimeSrv, setTimeSrv, TimeSrv } from '../../dashboard/services/TimeSrv'; +import { TemplateSrv } from '../../templating/template_srv'; + +describe('interval actions', () => { + variableAdapters.set('interval', createIntervalVariableAdapter()); + describe('when updateIntervalVariableOptions is dispatched', () => { + it('then correct actions are dispatched', async () => { + const interval = variableMockBuilder('interval') + .withUuid('0') + .withQuery('1s,1m,1h,1d') + .withAuto(false) + .create(); + + const tester = await reduxTester<{ templating: TemplatingState }>() + .givenRootReducer(getTemplatingRootReducer()) + .whenActionIsDispatched(initDashboardTemplating([interval])) + .whenAsyncActionIsDispatched(updateIntervalVariableOptions(toVariableIdentifier(interval)), true); + + tester.thenDispatchedActionsShouldEqual( + createIntervalOptions({ type: 'interval', uuid: '0', data: undefined }), + setCurrentVariableValue({ + type: 'interval', + uuid: '0', + data: { option: { text: '1s', value: '1s', selected: false } }, + }) + ); + }); + }); + + describe('when updateIntervalVariableOptions is dispatched but something throws', () => { + it('then an app event should be emitted', async () => { + const timeSrvMock = ({ + timeRange: jest.fn().mockReturnValue({ + from: dateTime(new Date()) + .subtract(1, 'days') + .toDate(), + to: new Date(), + raw: { + from: 'now-1d', + to: 'now', + }, + }), + } as unknown) as TimeSrv; + const originalTimeSrv = getTimeSrv(); + setTimeSrv(timeSrvMock); + const interval = variableMockBuilder('interval') + .withUuid('0') + .withQuery('1s,1m,1h,1d') + .withAuto(true) + .withAutoMin('1') // illegal interval string + .create(); + const appEventMock = ({ + emit: jest.fn(), + } as unknown) as Emitter; + const dependencies: UpdateIntervalVariableOptionsDependencies = { appEvents: appEventMock }; + + await reduxTester<{ templating: TemplatingState }>() + .givenRootReducer(getTemplatingRootReducer()) + .whenActionIsDispatched(initDashboardTemplating([interval])) + .whenAsyncActionIsDispatched(updateIntervalVariableOptions(toVariableIdentifier(interval), dependencies), true); + + expect(appEventMock.emit).toHaveBeenCalledTimes(1); + expect(appEventMock.emit).toHaveBeenCalledWith(AppEvents.alertError, [ + 'Templating', + 'Invalid interval string, expecting a number followed by one of "Mwdhmsy"', + ]); + setTimeSrv(originalTimeSrv); + }); + }); + + describe('when updateAutoValue is dispatched', () => { + describe('and auto is false', () => { + it('then no dependencies are called', async () => { + const interval = variableMockBuilder('interval') + .withUuid('0') + .withAuto(false) + .create(); + + const dependencies: UpdateAutoValueDependencies = { + kbn: { + calculateInterval: jest.fn(), + }, + getTimeSrv: () => { + return ({ + timeRange: jest.fn().mockReturnValue({ + from: '2001-01-01', + to: '2001-01-02', + raw: { + from: '2001-01-01', + to: '2001-01-02', + }, + }), + } as unknown) as TimeSrv; + }, + templateSrv: ({ + setGrafanaVariable: jest.fn(), + } as unknown) as TemplateSrv, + }; + + await reduxTester<{ templating: TemplatingState }>() + .givenRootReducer(getTemplatingRootReducer()) + .whenActionIsDispatched(initDashboardTemplating([interval])) + .whenAsyncActionIsDispatched(updateAutoValue(toVariableIdentifier(interval), dependencies), true); + + expect(dependencies.kbn.calculateInterval).toHaveBeenCalledTimes(0); + expect(dependencies.getTimeSrv().timeRange).toHaveBeenCalledTimes(0); + expect(dependencies.templateSrv.setGrafanaVariable).toHaveBeenCalledTimes(0); + }); + }); + + describe('and auto is true', () => { + it('then correct dependencies are called', async () => { + const interval = variableMockBuilder('interval') + .withUuid('0') + .withName('intervalName') + .withAuto(true) + .withAutoCount(33) + .withAutoMin('13s') + .create(); + + const timeRangeMock = jest.fn().mockReturnValue({ + from: '2001-01-01', + to: '2001-01-02', + raw: { + from: '2001-01-01', + to: '2001-01-02', + }, + }); + const setGrafanaVariableMock = jest.fn(); + const dependencies: UpdateAutoValueDependencies = { + kbn: { + calculateInterval: jest.fn().mockReturnValue({ interval: '10s' }), + }, + getTimeSrv: () => { + return ({ + timeRange: timeRangeMock, + } as unknown) as TimeSrv; + }, + templateSrv: ({ + setGrafanaVariable: setGrafanaVariableMock, + } as unknown) as TemplateSrv, + }; + + await reduxTester<{ templating: TemplatingState }>() + .givenRootReducer(getTemplatingRootReducer()) + .whenActionIsDispatched(initDashboardTemplating([interval])) + .whenAsyncActionIsDispatched(updateAutoValue(toVariableIdentifier(interval), dependencies), true); + + expect(dependencies.kbn.calculateInterval).toHaveBeenCalledTimes(1); + expect(dependencies.kbn.calculateInterval).toHaveBeenCalledWith( + { + from: '2001-01-01', + to: '2001-01-02', + raw: { + from: '2001-01-01', + to: '2001-01-02', + }, + }, + 33, + '13s' + ); + expect(timeRangeMock).toHaveBeenCalledTimes(1); + expect(setGrafanaVariableMock).toHaveBeenCalledTimes(2); + expect(setGrafanaVariableMock.mock.calls[0][0]).toBe('$__auto_interval_intervalName'); + expect(setGrafanaVariableMock.mock.calls[0][1]).toBe('10s'); + expect(setGrafanaVariableMock.mock.calls[1][0]).toBe('$__auto_interval'); + expect(setGrafanaVariableMock.mock.calls[1][1]).toBe('10s'); + }); + }); + }); +}); diff --git a/public/app/features/variables/interval/actions.ts b/public/app/features/variables/interval/actions.ts new file mode 100644 index 00000000000..334d09afbf4 --- /dev/null +++ b/public/app/features/variables/interval/actions.ts @@ -0,0 +1,56 @@ +import { AppEvents } from '@grafana/data'; + +import { toVariablePayload, VariableIdentifier } from '../state/types'; +import { ThunkResult } from '../../../types'; +import { createIntervalOptions } from './reducer'; +import { validateVariableSelectionState } from '../state/actions'; +import { getVariable } from '../state/selectors'; +import { IntervalVariableModel } from '../../templating/variable'; +import kbn from '../../../core/utils/kbn'; +import { getTimeSrv } from '../../dashboard/services/TimeSrv'; +import templateSrv from '../../templating/template_srv'; +import appEvents from '../../../core/app_events'; + +export interface UpdateIntervalVariableOptionsDependencies { + appEvents: typeof appEvents; +} + +export const updateIntervalVariableOptions = ( + identifier: VariableIdentifier, + dependencies: UpdateIntervalVariableOptionsDependencies = { appEvents: appEvents } +): ThunkResult => async dispatch => { + try { + await dispatch(createIntervalOptions(toVariablePayload(identifier))); + await dispatch(updateAutoValue(identifier)); + await dispatch(validateVariableSelectionState(identifier)); + } catch (error) { + dependencies.appEvents.emit(AppEvents.alertError, ['Templating', error.message]); + } +}; + +export interface UpdateAutoValueDependencies { + kbn: typeof kbn; + getTimeSrv: typeof getTimeSrv; + templateSrv: typeof templateSrv; +} + +export const updateAutoValue = ( + identifier: VariableIdentifier, + dependencies: UpdateAutoValueDependencies = { + kbn: kbn, + getTimeSrv: getTimeSrv, + templateSrv: templateSrv, + } +): ThunkResult => (dispatch, getState) => { + const variableInState = getVariable(identifier.uuid, getState()); + if (variableInState.auto) { + const res = dependencies.kbn.calculateInterval( + dependencies.getTimeSrv().timeRange(), + variableInState.auto_count, + variableInState.auto_min + ); + dependencies.templateSrv.setGrafanaVariable('$__auto_interval_' + variableInState.name, res.interval); + // for backward compatibility, to be removed eventually + dependencies.templateSrv.setGrafanaVariable('$__auto_interval', res.interval); + } +}; diff --git a/public/app/features/variables/interval/adapter.ts b/public/app/features/variables/interval/adapter.ts new file mode 100644 index 00000000000..dca4db2c9b9 --- /dev/null +++ b/public/app/features/variables/interval/adapter.ts @@ -0,0 +1,42 @@ +import cloneDeep from 'lodash/cloneDeep'; +import { IntervalVariableModel } from '../../templating/variable'; +import { dispatch } from '../../../store/store'; +import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions'; +import { VariableAdapter } from '../adapters'; +import { initialIntervalVariableModelState, intervalVariableReducer } from './reducer'; +import { OptionsPicker } from '../pickers'; +import { toVariableIdentifier } from '../state/types'; +import { IntervalVariableEditor } from './IntervalVariableEditor'; +import { updateAutoValue, updateIntervalVariableOptions } from './actions'; + +export const createIntervalVariableAdapter = (): VariableAdapter => { + return { + description: 'Define a timespan interval (ex 1m, 1h, 1d)', + label: 'Interval', + initialState: initialIntervalVariableModelState, + reducer: intervalVariableReducer, + picker: OptionsPicker, + editor: IntervalVariableEditor, + dependsOn: () => { + return false; + }, + setValue: async (variable, option, emitChanges = false) => { + await dispatch(updateAutoValue(toVariableIdentifier(variable))); + await dispatch(setOptionAsCurrent(toVariableIdentifier(variable), option, emitChanges)); + }, + setValueFromUrl: async (variable, urlValue) => { + await dispatch(updateAutoValue(toVariableIdentifier(variable))); + await dispatch(setOptionFromUrl(toVariableIdentifier(variable), urlValue)); + }, + updateOptions: async variable => { + await dispatch(updateIntervalVariableOptions(toVariableIdentifier(variable))); + }, + getSaveModel: variable => { + const { index, uuid, initLock, global, ...rest } = cloneDeep(variable); + return rest; + }, + getValueForUrl: variable => { + return variable.current.value; + }, + }; +}; diff --git a/public/app/features/variables/interval/reducer.test.ts b/public/app/features/variables/interval/reducer.test.ts new file mode 100644 index 00000000000..6d43aab0de3 --- /dev/null +++ b/public/app/features/variables/interval/reducer.test.ts @@ -0,0 +1,123 @@ +import cloneDeep from 'lodash/cloneDeep'; + +import { getVariableTestContext } from '../state/helpers'; +import { toVariablePayload } from '../state/types'; +import { createIntervalVariableAdapter } from './adapter'; +import { IntervalVariableModel } from '../../templating/variable'; +import { reducerTester } from '../../../../test/core/redux/reducerTester'; +import { VariablesState } from '../state/variablesReducer'; +import { createIntervalOptions, intervalVariableReducer } from './reducer'; + +describe('intervalVariableReducer', () => { + const adapter = createIntervalVariableAdapter(); + describe('when createIntervalOptions is dispatched', () => { + describe('and auto is false', () => { + it('then state should be correct', () => { + const uuid = '0'; + const query = '1s,1m,1h,1d'; + const auto = false; + const { initialState } = getVariableTestContext(adapter, { uuid, query, auto }); + const payload = toVariablePayload({ uuid: '0', type: 'interval' }); + + reducerTester() + .givenReducer(intervalVariableReducer, cloneDeep(initialState)) + .whenActionIsDispatched(createIntervalOptions(payload)) + .thenStateShouldEqual({ + '0': { + ...initialState['0'], + uuid: '0', + query: '1s,1m,1h,1d', + auto: false, + options: [ + { text: '1s', value: '1s', selected: false }, + { text: '1m', value: '1m', selected: false }, + { text: '1h', value: '1h', selected: false }, + { text: '1d', value: '1d', selected: false }, + ], + } as IntervalVariableModel, + }); + }); + }); + + describe('and auto is true', () => { + it('then state should be correct', () => { + const uuid = '0'; + const query = '1s,1m,1h,1d'; + const auto = true; + const { initialState } = getVariableTestContext(adapter, { uuid, query, auto }); + const payload = toVariablePayload({ uuid: '0', type: 'interval' }); + + reducerTester() + .givenReducer(intervalVariableReducer, cloneDeep(initialState)) + .whenActionIsDispatched(createIntervalOptions(payload)) + .thenStateShouldEqual({ + '0': { + ...initialState['0'], + uuid: '0', + query: '1s,1m,1h,1d', + auto: true, + options: [ + { text: 'auto', value: '$__auto_interval_0', selected: false }, + { text: '1s', value: '1s', selected: false }, + { text: '1m', value: '1m', selected: false }, + { text: '1h', value: '1h', selected: false }, + { text: '1d', value: '1d', selected: false }, + ], + } as IntervalVariableModel, + }); + }); + }); + + describe('and query contains "', () => { + it('then state should be correct', () => { + const uuid = '0'; + const query = '"kalle, anka","donald, duck"'; + const auto = false; + const { initialState } = getVariableTestContext(adapter, { uuid, query, auto }); + const payload = toVariablePayload({ uuid: '0', type: 'interval' }); + + reducerTester() + .givenReducer(intervalVariableReducer, cloneDeep(initialState)) + .whenActionIsDispatched(createIntervalOptions(payload)) + .thenStateShouldEqual({ + '0': { + ...initialState['0'], + uuid: '0', + query: '"kalle, anka","donald, duck"', + auto: false, + options: [ + { text: 'kalle, anka', value: 'kalle, anka', selected: false }, + { text: 'donald, duck', value: 'donald, duck', selected: false }, + ], + } as IntervalVariableModel, + }); + }); + }); + + describe("and query contains '", () => { + it('then state should be correct', () => { + const uuid = '0'; + const query = "'kalle, anka','donald, duck'"; + const auto = false; + const { initialState } = getVariableTestContext(adapter, { uuid, query, auto }); + const payload = toVariablePayload({ uuid: '0', type: 'interval' }); + + reducerTester() + .givenReducer(intervalVariableReducer, cloneDeep(initialState)) + .whenActionIsDispatched(createIntervalOptions(payload)) + .thenStateShouldEqual({ + '0': { + ...initialState['0'], + uuid: '0', + query: "'kalle, anka','donald, duck'", + auto: false, + options: [ + { text: 'kalle, anka', value: 'kalle, anka', selected: false }, + { text: 'donald, duck', value: 'donald, duck', selected: false }, + ], + } as IntervalVariableModel, + }); + }); + }); + }); +}); diff --git a/public/app/features/variables/interval/reducer.ts b/public/app/features/variables/interval/reducer.ts new file mode 100644 index 00000000000..47830089691 --- /dev/null +++ b/public/app/features/variables/interval/reducer.ts @@ -0,0 +1,55 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { IntervalVariableModel, VariableHide, VariableOption, VariableRefresh } from '../../templating/variable'; +import { EMPTY_UUID, getInstanceState, VariablePayload } from '../state/types'; +import { initialVariablesState, VariablesState } from '../state/variablesReducer'; +import _ from 'lodash'; + +export const initialIntervalVariableModelState: IntervalVariableModel = { + uuid: EMPTY_UUID, + global: false, + type: 'interval', + name: '', + label: '', + hide: VariableHide.dontHide, + skipUrlSync: false, + auto_count: 30, + auto_min: '10s', + options: [], + auto: false, + query: '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d', + refresh: VariableRefresh.onTimeRangeChanged, + current: {} as VariableOption, + index: -1, + initLock: null, +}; + +export const intervalVariableSlice = createSlice({ + name: 'templating/interval', + initialState: initialVariablesState, + reducers: { + createIntervalOptions: (state: VariablesState, action: PayloadAction) => { + const instanceState = getInstanceState(state, action.payload.uuid!); + const options: VariableOption[] = _.map(instanceState.query.match(/(["'])(.*?)\1|\w+/g), text => { + text = text.replace(/["']+/g, ''); + return { text: text.trim(), value: text.trim(), selected: false }; + }); + + if (instanceState.auto) { + // add auto option if missing + if (options.length && options[0].text !== 'auto') { + options.unshift({ + text: 'auto', + value: '$__auto_interval_' + instanceState.name, + selected: false, + }); + } + } + + instanceState.options = options; + }, + }, +}); + +export const intervalVariableReducer = intervalVariableSlice.reducer; + +export const { createIntervalOptions } = intervalVariableSlice.actions; diff --git a/public/app/features/variables/pickers/OptionsPicker/actions.test.ts b/public/app/features/variables/pickers/OptionsPicker/actions.test.ts index ffa8c39567a..023fd4f9e83 100644 --- a/public/app/features/variables/pickers/OptionsPicker/actions.test.ts +++ b/public/app/features/variables/pickers/OptionsPicker/actions.test.ts @@ -62,7 +62,7 @@ describe('options picker actions', () => { tags: [] as any[], }; - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [setCurrentValue, changeQueryValue, updateOption, hideAction] = actions; const expectedNumberOfActions = 4; @@ -93,7 +93,7 @@ describe('options picker actions', () => { .whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, false)) .whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [toggleOptionAction] = actions; const expectedNumberOfActions = 1; @@ -118,7 +118,7 @@ describe('options picker actions', () => { .whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers)) .whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [toggleOptionAction] = actions; const expectedNumberOfActions = 1; @@ -145,7 +145,7 @@ describe('options picker actions', () => { .whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers)) .whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [toggleOptionAction] = actions; const expectedNumberOfActions = 1; @@ -173,7 +173,7 @@ describe('options picker actions', () => { .whenActionIsDispatched(navigateOptions(NavigationKey.moveUp, clearOthers)) .whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [toggleOptionAction] = actions; const expectedNumberOfActions = 1; @@ -208,7 +208,7 @@ describe('options picker actions', () => { tags: [] as any[], }; - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [toggleOptionAction, setCurrentValue, changeQueryValue, updateOption, hideAction] = actions; const expectedNumberOfActions = 5; @@ -237,7 +237,7 @@ describe('options picker actions', () => { .whenActionIsDispatched(showOptions(variable)) .whenAsyncActionIsDispatched(filterOrSearchOptions(filter), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [updateQueryValue, updateAndFilter] = actions; const expectedNumberOfActions = 2; @@ -267,7 +267,7 @@ describe('options picker actions', () => { tags: [] as any[], }; - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [setCurrentValue, changeQueryValue, hideAction] = actions; const expectedNumberOfActions = 3; @@ -303,7 +303,7 @@ describe('options picker actions', () => { tags: [] as any[], }; - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [setCurrentValue, changeQueryValue, updateOption, hideAction] = actions; const expectedNumberOfActions = 4; @@ -334,7 +334,7 @@ describe('options picker actions', () => { const option = createOption('A'); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [toggleOptionAction] = actions; const expectedNumberOfActions = 1; @@ -356,7 +356,7 @@ describe('options picker actions', () => { .whenActionIsDispatched(showOptions(variable)) .whenAsyncActionIsDispatched(toggleAndFetchTag(tag), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [toggleTagAction] = actions; const expectedNumberOfActions = 1; @@ -383,7 +383,7 @@ describe('options picker actions', () => { .whenActionIsDispatched(showOptions(variable)) .whenAsyncActionIsDispatched(toggleAndFetchTag(tag), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [toggleTagAction] = actions; const expectedNumberOfActions = 1; diff --git a/public/app/features/variables/query/actions.test.ts b/public/app/features/variables/query/actions.test.ts index be03b31e955..4b79aaef279 100644 --- a/public/app/features/variables/query/actions.test.ts +++ b/public/app/features/variables/query/actions.test.ts @@ -64,7 +64,7 @@ describe('query actions', () => { const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [updateOptions, updateTags, setCurrentAction] = actions; const expectedNumberOfActions = 3; @@ -91,7 +91,7 @@ describe('query actions', () => { const option = createOption('A'); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [updateOptions, updateTags, setCurrentAction] = actions; const expectedNumberOfActions = 3; @@ -117,7 +117,7 @@ describe('query actions', () => { const option = createOption('A'); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [updateOptions, setCurrentAction] = actions; const expectedNumberOfActions = 2; @@ -142,7 +142,7 @@ describe('query actions', () => { const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [updateOptions, setCurrentAction] = actions; const expectedNumberOfActions = 2; @@ -168,7 +168,7 @@ describe('query actions', () => { const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [clearErrors, updateOptions, setCurrentAction] = actions; const expectedNumberOfActions = 3; @@ -193,7 +193,7 @@ describe('query actions', () => { .whenActionIsDispatched(setIdInEditor({ id: variable.uuid! })) .whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [clearErrors, errorOccurred] = actions; const expectedNumberOfActions = 2; @@ -221,7 +221,7 @@ describe('query actions', () => { .whenActionIsDispatched(initDashboardTemplating([variable])) .whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [updateDatasources, setDatasource, setEditor] = actions; const expectedNumberOfActions = 3; @@ -254,7 +254,7 @@ describe('query actions', () => { .whenActionIsDispatched(initDashboardTemplating([variable])) .whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [updateDatasources, setDatasource, setEditor] = actions; const expectedNumberOfActions = 3; @@ -286,7 +286,7 @@ describe('query actions', () => { .whenActionIsDispatched(initDashboardTemplating([variable])) .whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [updateDatasources, setDatasource, setEditor] = actions; const expectedNumberOfActions = 3; @@ -312,7 +312,7 @@ describe('query actions', () => { .whenActionIsDispatched(initDashboardTemplating([variable])) .whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [updateDatasources] = actions; const expectedNumberOfActions = 1; @@ -336,7 +336,7 @@ describe('query actions', () => { .whenActionIsDispatched(initDashboardTemplating([variable])) .whenAsyncActionIsDispatched(changeQueryVariableDataSource(toVariablePayload(variable), 'datasource'), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [updateDatasource, updateEditor] = actions; const expectedNumberOfActions = 2; @@ -366,7 +366,7 @@ describe('query actions', () => { .whenActionIsDispatched(initDashboardTemplating([variable])) .whenAsyncActionIsDispatched(changeQueryVariableDataSource(toVariablePayload(variable), 'datasource'), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [updateDatasource, updateEditor] = actions; const expectedNumberOfActions = 2; @@ -400,7 +400,7 @@ describe('query actions', () => { const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [clearError, changeQuery, changeDefinition, updateOptions, updateTags, setOption] = actions; const expectedNumberOfActions = 6; @@ -437,7 +437,7 @@ describe('query actions', () => { const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [clearError, changeQuery, changeDefinition, updateOptions, setOption] = actions; const expectedNumberOfActions = 5; @@ -472,7 +472,7 @@ describe('query actions', () => { const option = createOption('A'); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [clearError, changeQuery, changeDefinition, updateOptions, setOption] = actions; const expectedNumberOfActions = 5; @@ -504,7 +504,7 @@ describe('query actions', () => { const errorText = 'Query cannot contain a reference to itself. Variable: $' + variable.name; - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [editorError] = actions; const expectedNumberOfActions = 1; diff --git a/public/app/features/variables/state/actions.test.ts b/public/app/features/variables/state/actions.test.ts index 8903a5bfdd7..feb4cb86879 100644 --- a/public/app/features/variables/state/actions.test.ts +++ b/public/app/features/variables/state/actions.test.ts @@ -30,7 +30,7 @@ describe('shared actions', () => { reduxTester<{ templating: TemplatingState }>() .givenRootReducer(getTemplatingRootReducer()) .whenActionIsDispatched(initDashboardTemplating(list)) - .thenDispatchedActionPredicateShouldEqual(dispatchedActions => { + .thenDispatchedActionsPredicateShouldEqual(dispatchedActions => { expect(dispatchedActions.length).toEqual(8); expect(dispatchedActions[0]).toEqual( addVariable(toVariablePayload(query, { global: false, index: 0, model: query })) @@ -85,7 +85,7 @@ describe('shared actions', () => { .whenActionIsDispatched(initDashboardTemplating(list)) .whenAsyncActionIsDispatched(processVariables(), true); - await tester.thenDispatchedActionPredicateShouldEqual(dispatchedActions => { + await tester.thenDispatchedActionsPredicateShouldEqual(dispatchedActions => { expect(dispatchedActions.length).toEqual(8); expect(dispatchedActions[0]).toEqual( @@ -142,7 +142,7 @@ describe('shared actions', () => { .whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom }))) .whenAsyncActionIsDispatched(setOptionFromUrl(toVariableIdentifier(custom), urlValue), true); - await tester.thenDispatchedActionShouldEqual( + await tester.thenDispatchedActionsShouldEqual( setCurrentVariableValue( toVariablePayload( { type: 'custom', uuid: '0' }, @@ -191,7 +191,7 @@ describe('shared actions', () => { true ); - await tester.thenDispatchedActionPredicateShouldEqual(dispatchedActions => { + await tester.thenDispatchedActionsPredicateShouldEqual(dispatchedActions => { const expectedActions: AnyAction[] = !withOptions ? [] : [ @@ -249,7 +249,7 @@ describe('shared actions', () => { true ); - await tester.thenDispatchedActionPredicateShouldEqual(dispatchedActions => { + await tester.thenDispatchedActionsPredicateShouldEqual(dispatchedActions => { const expectedActions: AnyAction[] = !withOptions ? [] : [ diff --git a/public/app/features/variables/state/helpers.ts b/public/app/features/variables/state/helpers.ts index 188b18668d3..390121cd342 100644 --- a/public/app/features/variables/state/helpers.ts +++ b/public/app/features/variables/state/helpers.ts @@ -111,6 +111,21 @@ export const variableMockBuilder = (type: VariableType) => { return instance; }; + const withAuto = (auto: boolean) => { + model.auto = auto; + return instance; + }; + + const withAutoCount = (autoCount: number) => { + model.auto_count = autoCount; + return instance; + }; + + const withAutoMin = (autoMin: string) => { + model.auto_min = autoMin; + return instance; + }; + const create = () => model; const instance = { @@ -122,6 +137,9 @@ export const variableMockBuilder = (type: VariableType) => { withQuery, withMulti, withRegEx, + withAuto, + withAutoCount, + withAutoMin, create, }; diff --git a/public/app/features/variables/state/processVariable.test.ts b/public/app/features/variables/state/processVariable.test.ts index 766d4595342..829a545b4b1 100644 --- a/public/app/features/variables/state/processVariable.test.ts +++ b/public/app/features/variables/state/processVariable.test.ts @@ -111,7 +111,9 @@ describe('processVariable', () => { .whenActionIsDispatched(initDashboardTemplating(list)) .whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(custom), queryParams), true); - await tester.thenDispatchedActionShouldEqual(resolveInitLock(toVariablePayload({ type: 'custom', uuid: '0' }))); + await tester.thenDispatchedActionsShouldEqual( + resolveInitLock(toVariablePayload({ type: 'custom', uuid: '0' })) + ); }); }); @@ -124,7 +126,7 @@ describe('processVariable', () => { .whenActionIsDispatched(initDashboardTemplating(list)) .whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(custom), queryParams), true); - await tester.thenDispatchedActionShouldEqual( + await tester.thenDispatchedActionsShouldEqual( setCurrentVariableValue( toVariablePayload({ type: 'custom', uuid: '0' }, { option: { text: ['B'], value: ['B'], selected: false } }) ), @@ -149,7 +151,7 @@ describe('processVariable', () => { .whenActionIsDispatched(initDashboardTemplating(list)) .whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(queryNoDepends), queryParams), true); - await tester.thenDispatchedActionShouldEqual( + await tester.thenDispatchedActionsShouldEqual( resolveInitLock(toVariablePayload({ type: 'query', uuid: '2' })) ); }); @@ -165,7 +167,7 @@ describe('processVariable', () => { .whenActionIsDispatched(initDashboardTemplating(list)) .whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(queryNoDepends), queryParams), true); - await tester.thenDispatchedActionShouldEqual( + await tester.thenDispatchedActionsShouldEqual( updateVariableOptions( toVariablePayload({ type: 'query', uuid: '2' }, [ { value: 'A', text: 'A' }, @@ -196,7 +198,7 @@ describe('processVariable', () => { .whenActionIsDispatched(initDashboardTemplating(list)) .whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(queryNoDepends), queryParams), true); - await tester.thenDispatchedActionShouldEqual( + await tester.thenDispatchedActionsShouldEqual( setCurrentVariableValue( toVariablePayload( { type: 'query', uuid: '2' }, @@ -222,7 +224,7 @@ describe('processVariable', () => { .whenActionIsDispatched(initDashboardTemplating(list)) .whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(queryNoDepends), queryParams), true); - await tester.thenDispatchedActionShouldEqual( + await tester.thenDispatchedActionsShouldEqual( updateVariableOptions( toVariablePayload({ type: 'query', uuid: '2' }, [ { value: 'A', text: 'A' }, @@ -267,7 +269,7 @@ describe('processVariable', () => { true ); - await tester.thenDispatchedActionShouldEqual( + await tester.thenDispatchedActionsShouldEqual( resolveInitLock(toVariablePayload({ type: 'query', uuid: '1' })) ); }); @@ -288,7 +290,7 @@ describe('processVariable', () => { true ); - await tester.thenDispatchedActionShouldEqual( + await tester.thenDispatchedActionsShouldEqual( updateVariableOptions( toVariablePayload({ type: 'query', uuid: '1' }, [ { value: 'AA', text: 'AA' }, @@ -327,7 +329,7 @@ describe('processVariable', () => { true ); - await tester.thenDispatchedActionShouldEqual( + await tester.thenDispatchedActionsShouldEqual( setCurrentVariableValue( toVariablePayload( { type: 'query', uuid: '1' }, @@ -358,7 +360,7 @@ describe('processVariable', () => { true ); - await tester.thenDispatchedActionShouldEqual( + await tester.thenDispatchedActionsShouldEqual( updateVariableOptions( toVariablePayload({ type: 'query', uuid: '1' }, [ { value: 'AA', text: 'AA' }, diff --git a/public/app/features/variables/textbox/actions.test.ts b/public/app/features/variables/textbox/actions.test.ts index e8eb7adf162..68139e62b67 100644 --- a/public/app/features/variables/textbox/actions.test.ts +++ b/public/app/features/variables/textbox/actions.test.ts @@ -44,7 +44,7 @@ describe('textbox actions', () => { .whenActionIsDispatched(initDashboardTemplating([variable])) .whenAsyncActionIsDispatched(updateTextBoxVariableOptions(toVariablePayload(variable)), true); - tester.thenDispatchedActionPredicateShouldEqual(actions => { + tester.thenDispatchedActionsPredicateShouldEqual(actions => { const [createAction, setCurrentAction] = actions; const expectedNumberOfActions = 2; diff --git a/public/test/core/redux/reduxTester.ts b/public/test/core/redux/reduxTester.ts index 41976f7a7b4..0a53c6eab9f 100644 --- a/public/test/core/redux/reduxTester.ts +++ b/public/test/core/redux/reduxTester.ts @@ -21,8 +21,8 @@ export interface ReduxTesterWhen { } export interface ReduxTesterThen { - thenDispatchedActionShouldEqual: (...dispatchedAction: AnyAction[]) => ReduxTesterWhen; - thenDispatchedActionPredicateShouldEqual: ( + thenDispatchedActionsShouldEqual: (...dispatchedActions: AnyAction[]) => ReduxTesterWhen; + thenDispatchedActionsPredicateShouldEqual: ( predicate: (dispatchedActions: AnyAction[]) => boolean ) => ReduxTesterWhen; } @@ -85,7 +85,7 @@ export const reduxTester = (args?: ReduxTesterArguments): ReduxTes return instance; }; - const thenDispatchedActionShouldEqual = (...actions: AnyAction[]): ReduxTesterWhen => { + const thenDispatchedActionsShouldEqual = (...actions: AnyAction[]): ReduxTesterWhen => { if (debug) { console.log('Dispatched Actions', JSON.stringify(dispatchedActions, null, 2)); } @@ -98,7 +98,7 @@ export const reduxTester = (args?: ReduxTesterArguments): ReduxTes return instance; }; - const thenDispatchedActionPredicateShouldEqual = ( + const thenDispatchedActionsPredicateShouldEqual = ( predicate: (dispatchedActions: AnyAction[]) => boolean ): ReduxTesterWhen => { if (debug) { @@ -113,8 +113,8 @@ export const reduxTester = (args?: ReduxTesterArguments): ReduxTes givenRootReducer, whenActionIsDispatched, whenAsyncActionIsDispatched, - thenDispatchedActionShouldEqual, - thenDispatchedActionPredicateShouldEqual, + thenDispatchedActionsShouldEqual, + thenDispatchedActionsPredicateShouldEqual, }; return instance;