mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
@@ -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 {
|
|||||||
|
|
||||||
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 {
|
|||||||
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 {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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';
|
|||||||
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', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
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';
|
|||||||
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
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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) => {
|
|||||||
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
|
|||||||
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
|
|||||||
if (clearPreviousActions) {
|
if (clearPreviousActions) {
|
||||||
dispatchedActions.length = 0;
|
dispatchedActions.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
store.dispatch(action);
|
store.dispatch(action);
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
|||||||
Reference in New Issue
Block a user