Commit d953511e by Hugo Häggmark Committed by GitHub

Variables: adds onTimeRangeUpdated to newVariables (#22821)

* Feature: adds onTimeRangeUpdated to newVariables

* Refactor: removes VariableWithRefresh and unused func

* Refactor: adds console output when something throws as well
parent 7f76e8b6
...@@ -17,6 +17,8 @@ import { VariableModel } from '../../templating/variable'; ...@@ -17,6 +17,8 @@ import { VariableModel } from '../../templating/variable';
import { getConfig } from '../../../core/config'; import { getConfig } from '../../../core/config';
import { getVariableClones, getVariables } from 'app/features/variables/state/selectors'; import { getVariableClones, getVariables } from 'app/features/variables/state/selectors';
import { variableAdapters } from 'app/features/variables/adapters'; import { variableAdapters } from 'app/features/variables/adapters';
import { onTimeRangeUpdated } from 'app/features/variables/state/actions';
import { dispatch } from '../../../store/store';
export interface CloneOptions { export interface CloneOptions {
saveVariables?: boolean; saveVariables?: boolean;
...@@ -276,6 +278,7 @@ export class DashboardModel { ...@@ -276,6 +278,7 @@ export class DashboardModel {
timeRangeUpdated(timeRange: TimeRange) { timeRangeUpdated(timeRange: TimeRange) {
this.events.emit(CoreEvents.timeRangeUpdated, timeRange); this.events.emit(CoreEvents.timeRangeUpdated, timeRange);
dispatch(onTimeRangeUpdated(timeRange));
} }
startRefresh() { startRefresh() {
......
...@@ -107,8 +107,8 @@ export interface IntervalVariableModel extends VariableWithOptions { ...@@ -107,8 +107,8 @@ export interface IntervalVariableModel extends VariableWithOptions {
export interface CustomVariableModel extends VariableWithMultiSupport {} export interface CustomVariableModel extends VariableWithMultiSupport {}
export interface DataSourceVariableModel extends VariableWithMultiSupport { export interface DataSourceVariableModel extends VariableWithMultiSupport {
refresh: VariableRefresh;
regex: string; regex: string;
refresh: VariableRefresh;
} }
export interface QueryVariableModel extends DataSourceVariableModel { export interface QueryVariableModel extends DataSourceVariableModel {
......
import { AnyAction } from 'redux';
import { UrlQueryMap } from '@grafana/runtime'; import { UrlQueryMap } from '@grafana/runtime';
import { getTemplatingAndLocationRootReducer, getTemplatingRootReducer, variableMockBuilder } from './helpers'; import { getTemplatingAndLocationRootReducer, getTemplatingRootReducer, variableMockBuilder } from './helpers';
...@@ -8,10 +9,23 @@ import { createTextBoxVariableAdapter } from '../textbox/adapter'; ...@@ -8,10 +9,23 @@ import { createTextBoxVariableAdapter } from '../textbox/adapter';
import { createConstantVariableAdapter } from '../constant/adapter'; import { createConstantVariableAdapter } from '../constant/adapter';
import { reduxTester } from '../../../../test/core/redux/reduxTester'; import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { TemplatingState } from 'app/features/variables/state/reducers'; import { TemplatingState } from 'app/features/variables/state/reducers';
import { initDashboardTemplating, processVariables, setOptionFromUrl, validateVariableSelectionState } from './actions'; import {
initDashboardTemplating,
onTimeRangeUpdated,
OnTimeRangeUpdatedDependencies,
processVariables,
setOptionFromUrl,
validateVariableSelectionState,
} from './actions';
import { addInitLock, addVariable, removeInitLock, resolveInitLock, setCurrentVariableValue } from './sharedReducer'; import { addInitLock, addVariable, removeInitLock, resolveInitLock, setCurrentVariableValue } from './sharedReducer';
import { toVariableIdentifier, toVariablePayload } from './types'; import { toVariableIdentifier, toVariablePayload } from './types';
import { AnyAction } from 'redux'; import { TemplateSrv } from '../../templating/template_srv';
import { Emitter } from '../../../core/core';
import { createIntervalVariableAdapter } from '../interval/adapter';
import { VariableRefresh } from '../../templating/variable';
import { DashboardModel } from '../../dashboard/state';
import { DashboardState } from '../../../types';
import { dateTime, TimeRange } from '@grafana/data';
describe('shared actions', () => { describe('shared actions', () => {
describe('when initDashboardTemplating is dispatched', () => { describe('when initDashboardTemplating is dispatched', () => {
...@@ -267,4 +281,161 @@ describe('shared actions', () => { ...@@ -267,4 +281,161 @@ describe('shared actions', () => {
); );
}); });
}); });
describe('when onTimeRangeUpdated is dispatched', () => {
const getOnTimeRangeUpdatedContext = (args: { update?: boolean; throw?: boolean }) => {
const range: TimeRange = {
from: dateTime(new Date().getTime()).subtract(1, 'minutes'),
to: dateTime(new Date().getTime()),
raw: {
from: 'now-1m',
to: 'now',
},
};
const updateTimeRangeMock = jest.fn();
const templateSrvMock = ({ updateTimeRange: updateTimeRangeMock } as unknown) as TemplateSrv;
const emitMock = jest.fn();
const appEventsMock = ({ emit: emitMock } as unknown) as Emitter;
const dependencies: OnTimeRangeUpdatedDependencies = { templateSrv: templateSrvMock, appEvents: appEventsMock };
const templateVariableValueUpdatedMock = jest.fn();
const dashboard = ({
getModel: () =>
(({
templateVariableValueUpdated: templateVariableValueUpdatedMock,
startRefresh: startRefreshMock,
} as unknown) as DashboardModel),
} as unknown) as DashboardState;
const startRefreshMock = jest.fn();
const adapter = createIntervalVariableAdapter();
adapter.updateOptions = args.throw
? jest.fn().mockRejectedValue('Something broke')
: jest.fn().mockResolvedValue({});
variableAdapters.set('interval', adapter);
variableAdapters.set('constant', createConstantVariableAdapter());
// initial variable state
const initialVariable = variableMockBuilder('interval')
.withUuid('0')
.withName('interval-0')
.withOptions('1m', '10m', '30m', '1h', '6h', '12h', '1d', '7d', '14d', '30d')
.withCurrent('1m')
.withRefresh(VariableRefresh.onTimeRangeChanged)
.create();
// the constant variable should be filtered out
const constant = variableMockBuilder('constant')
.withUuid('1')
.withName('constant-1')
.withOptions('a constant')
.withCurrent('a constant')
.create();
const initialState = {
templating: { variables: { '0': { ...initialVariable }, '1': { ...constant } } },
dashboard,
};
// updated variable state
const updatedVariable = variableMockBuilder('interval')
.withUuid('0')
.withName('interval-0')
.withOptions('1m')
.withCurrent('1m')
.withRefresh(VariableRefresh.onTimeRangeChanged)
.create();
const variable = args.update ? { ...updatedVariable } : { ...initialVariable };
const state = { templating: { variables: { '0': variable, '1': { ...constant } } }, dashboard };
const getStateMock = jest
.fn()
.mockReturnValueOnce(initialState)
.mockReturnValue(state);
const dispatchMock = jest.fn();
return {
range,
dependencies,
dispatchMock,
getStateMock,
updateTimeRangeMock,
templateVariableValueUpdatedMock,
startRefreshMock,
emitMock,
};
};
describe('and options are changed by update', () => {
it('then correct dependencies are called', async () => {
const {
range,
dependencies,
dispatchMock,
getStateMock,
updateTimeRangeMock,
templateVariableValueUpdatedMock,
startRefreshMock,
emitMock,
} = getOnTimeRangeUpdatedContext({ update: true });
await onTimeRangeUpdated(range, dependencies)(dispatchMock, getStateMock, undefined);
expect(dispatchMock).toHaveBeenCalledTimes(0);
expect(getStateMock).toHaveBeenCalledTimes(4);
expect(updateTimeRangeMock).toHaveBeenCalledTimes(1);
expect(updateTimeRangeMock).toHaveBeenCalledWith(range);
expect(templateVariableValueUpdatedMock).toHaveBeenCalledTimes(1);
expect(startRefreshMock).toHaveBeenCalledTimes(1);
expect(emitMock).toHaveBeenCalledTimes(0);
});
});
describe('and options are not changed by update', () => {
it('then correct dependencies are called', async () => {
const {
range,
dependencies,
dispatchMock,
getStateMock,
updateTimeRangeMock,
templateVariableValueUpdatedMock,
startRefreshMock,
emitMock,
} = getOnTimeRangeUpdatedContext({ update: false });
await onTimeRangeUpdated(range, dependencies)(dispatchMock, getStateMock, undefined);
expect(dispatchMock).toHaveBeenCalledTimes(0);
expect(getStateMock).toHaveBeenCalledTimes(3);
expect(updateTimeRangeMock).toHaveBeenCalledTimes(1);
expect(updateTimeRangeMock).toHaveBeenCalledWith(range);
expect(templateVariableValueUpdatedMock).toHaveBeenCalledTimes(0);
expect(startRefreshMock).toHaveBeenCalledTimes(1);
expect(emitMock).toHaveBeenCalledTimes(0);
});
});
describe('and updateOptions throws', () => {
it('then correct dependencies are called', async () => {
const {
range,
dependencies,
dispatchMock,
getStateMock,
updateTimeRangeMock,
templateVariableValueUpdatedMock,
startRefreshMock,
emitMock,
} = getOnTimeRangeUpdatedContext({ update: false, throw: true });
await onTimeRangeUpdated(range, dependencies)(dispatchMock, getStateMock, undefined);
expect(dispatchMock).toHaveBeenCalledTimes(0);
expect(getStateMock).toHaveBeenCalledTimes(1);
expect(updateTimeRangeMock).toHaveBeenCalledTimes(1);
expect(updateTimeRangeMock).toHaveBeenCalledWith(range);
expect(templateVariableValueUpdatedMock).toHaveBeenCalledTimes(0);
expect(startRefreshMock).toHaveBeenCalledTimes(0);
expect(emitMock).toHaveBeenCalledTimes(1);
});
});
});
}); });
import castArray from 'lodash/castArray'; import castArray from 'lodash/castArray';
import { UrlQueryMap, UrlQueryValue } from '@grafana/runtime'; import { UrlQueryMap, UrlQueryValue } from '@grafana/runtime';
import { AppEvents, TimeRange } from '@grafana/data';
import angular from 'angular';
import { import {
QueryVariableModel, QueryVariableModel,
...@@ -15,6 +17,8 @@ import { Graph } from '../../../core/utils/dag'; ...@@ -15,6 +17,8 @@ import { Graph } from '../../../core/utils/dag';
import { updateLocation } from 'app/core/actions'; import { updateLocation } from 'app/core/actions';
import { addInitLock, addVariable, removeInitLock, resolveInitLock, setCurrentVariableValue } from './sharedReducer'; import { addInitLock, addVariable, removeInitLock, resolveInitLock, setCurrentVariableValue } from './sharedReducer';
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from './types'; import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from './types';
import { appEvents } from '../../../core/core';
import templateSrv from '../../templating/template_srv';
// process flow queryVariable // process flow queryVariable
// thunk => processVariables // thunk => processVariables
...@@ -323,6 +327,45 @@ export const variableUpdated = (identifier: VariableIdentifier, emitChangeEvents ...@@ -323,6 +327,45 @@ export const variableUpdated = (identifier: VariableIdentifier, emitChangeEvents
}; };
}; };
export interface OnTimeRangeUpdatedDependencies {
templateSrv: typeof templateSrv;
appEvents: typeof appEvents;
}
export const onTimeRangeUpdated = (
timeRange: TimeRange,
dependencies: OnTimeRangeUpdatedDependencies = { templateSrv: templateSrv, appEvents: appEvents }
): ThunkResult<void> => async (dispatch, getState) => {
dependencies.templateSrv.updateTimeRange(timeRange);
const variablesThatNeedRefresh = getVariables(getState()).filter(variable => {
if (variable.hasOwnProperty('refresh') && variable.hasOwnProperty('options')) {
const variableWithRefresh = (variable as unknown) as QueryVariableModel;
return variableWithRefresh.refresh === VariableRefresh.onTimeRangeChanged;
}
return false;
});
const promises = variablesThatNeedRefresh.map(async (variable: VariableWithOptions) => {
const previousOptions = variable.options.slice();
await variableAdapters.get(variable.type).updateOptions(variable);
const updatedVariable = getVariable<VariableWithOptions>(variable.uuid!, getState());
if (angular.toJson(previousOptions) !== angular.toJson(updatedVariable.options)) {
const dashboard = getState().dashboard.getModel();
dashboard?.templateVariableValueUpdated();
}
});
try {
await Promise.all(promises);
const dashboard = getState().dashboard.getModel();
dashboard?.startRefresh();
} catch (error) {
console.error(error);
dependencies.appEvents.emit(AppEvents.alertError, ['Template variable service failed', error.message]);
}
};
const getQueryWithVariables = (getState: () => StoreState): UrlQueryMap => { const getQueryWithVariables = (getState: () => StoreState): UrlQueryMap => {
const queryParams = getState().location.query; const queryParams = getState().location.query;
......
...@@ -86,8 +86,8 @@ export const variableMockBuilder = (type: VariableType) => { ...@@ -86,8 +86,8 @@ export const variableMockBuilder = (type: VariableType) => {
return instance; return instance;
}; };
const withCurrent = (text: string | string[]) => { const withCurrent = (text: string | string[], value?: string | string[]) => {
model.current = { text, value: text, selected: true }; model.current = { text, value: value ?? text, selected: true };
return instance; return instance;
}; };
......
...@@ -55,6 +55,7 @@ export const reduxTester = <State>(args?: ReduxTesterArguments<State>): ReduxTes ...@@ -55,6 +55,7 @@ export const reduxTester = <State>(args?: ReduxTesterArguments<State>): ReduxTes
middleware: [logActionsMiddleWare, thunk], middleware: [logActionsMiddleWare, thunk],
preloadedState, preloadedState,
}); });
setStore(store as any); setStore(store as any);
return instance; return instance;
...@@ -67,6 +68,7 @@ export const reduxTester = <State>(args?: ReduxTesterArguments<State>): ReduxTes ...@@ -67,6 +68,7 @@ export const reduxTester = <State>(args?: ReduxTesterArguments<State>): ReduxTes
if (clearPreviousActions) { if (clearPreviousActions) {
dispatchedActions.length = 0; dispatchedActions.length = 0;
} }
store.dispatch(action); store.dispatch(action);
return instance; return instance;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment