mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Variables: replaces homegrown variableAdapters with Registry (#22866)
* Refactor: intial commit * Tests: fixes tests * Refactor: adds stricter typings
This commit is contained in:
parent
277edca3a0
commit
cf5064bfa0
@ -41,6 +41,7 @@ import { PerformanceBackend } from './core/services/echo/backends/PerformanceBac
|
||||
import 'app/routes/GrafanaCtrl';
|
||||
import 'app/features/all';
|
||||
import { getStandardFieldConfigs } from '@grafana/ui';
|
||||
import { getDefaultVariableAdapters, variableAdapters } from './features/variables/adapters';
|
||||
|
||||
// add move to lodash for backward compatabiltiy
|
||||
// @ts-ignore
|
||||
@ -84,6 +85,7 @@ export class GrafanaApp {
|
||||
|
||||
setMarkdownOptions({ sanitize: !config.disableSanitizeHtml });
|
||||
standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
|
||||
variableAdapters.setInit(getDefaultVariableAdapters);
|
||||
|
||||
app.config(
|
||||
(
|
||||
|
@ -188,7 +188,7 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
|
||||
const list =
|
||||
dashboard.variables.list.length > 0
|
||||
? dashboard.variables.list
|
||||
: dashboard.templating.list.filter(v => variableAdapters.contains(v.type));
|
||||
: dashboard.templating.list.filter(v => variableAdapters.getIfExists(v.type));
|
||||
await dispatch(initDashboardTemplating(list));
|
||||
await dispatch(processVariables());
|
||||
}
|
||||
|
@ -10,14 +10,6 @@ import { CustomVariable } from './custom_variable';
|
||||
import { ConstantVariable } from './constant_variable';
|
||||
import { AdhocVariable } from './adhoc_variable';
|
||||
import { TextBoxVariable } from './TextBoxVariable';
|
||||
import { variableAdapters } from '../variables/adapters';
|
||||
import { createQueryVariableAdapter } from '../variables/query/adapter';
|
||||
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 { createAdHocVariableAdapter } from '../variables/adhoc/adapter';
|
||||
import { createIntervalVariableAdapter } from '../variables/interval/adapter';
|
||||
|
||||
coreModule.factory('templateSrv', () => templateSrv);
|
||||
|
||||
@ -31,11 +23,3 @@ export {
|
||||
AdhocVariable,
|
||||
TextBoxVariable,
|
||||
};
|
||||
|
||||
variableAdapters.set('query', createQueryVariableAdapter());
|
||||
variableAdapters.set('custom', createCustomVariableAdapter());
|
||||
variableAdapters.set('textbox', createTextBoxVariableAdapter());
|
||||
variableAdapters.set('constant', createConstantVariableAdapter());
|
||||
variableAdapters.set('datasource', createDataSourceVariableAdapter());
|
||||
variableAdapters.set('adhoc', createAdHocVariableAdapter());
|
||||
variableAdapters.set('interval', createIntervalVariableAdapter());
|
||||
|
@ -2,14 +2,34 @@ import { ComponentType } from 'react';
|
||||
import { Reducer } from 'redux';
|
||||
import { UrlQueryValue } from '@grafana/runtime';
|
||||
|
||||
import { VariableModel, VariableOption, VariableType } from '../templating/variable';
|
||||
import {
|
||||
AdHocVariableModel,
|
||||
ConstantVariableModel,
|
||||
CustomVariableModel,
|
||||
DataSourceVariableModel,
|
||||
IntervalVariableModel,
|
||||
QueryVariableModel,
|
||||
TextBoxVariableModel,
|
||||
VariableModel,
|
||||
VariableOption,
|
||||
VariableType,
|
||||
} from '../templating/variable';
|
||||
import { VariableEditorProps } from './editor/types';
|
||||
import { VariablesState } from './state/variablesReducer';
|
||||
import { VariablePickerProps } from './pickers/types';
|
||||
import { Registry } from '@grafana/data';
|
||||
import { createQueryVariableAdapter } from './query/adapter';
|
||||
import { createCustomVariableAdapter } from './custom/adapter';
|
||||
import { createTextBoxVariableAdapter } from './textbox/adapter';
|
||||
import { createConstantVariableAdapter } from './constant/adapter';
|
||||
import { createDataSourceVariableAdapter } from './datasource/adapter';
|
||||
import { createIntervalVariableAdapter } from './interval/adapter';
|
||||
import { createAdHocVariableAdapter } from './adhoc/adapter';
|
||||
|
||||
export interface VariableAdapter<Model extends VariableModel> {
|
||||
id: VariableType;
|
||||
description: string;
|
||||
label: string;
|
||||
name: string;
|
||||
initialState: Model;
|
||||
dependsOn: (variable: Model, variableToTest: Model) => boolean;
|
||||
setValue: (variable: Model, option: VariableOption, emitChanges?: boolean) => Promise<void>;
|
||||
@ -22,40 +42,24 @@ export interface VariableAdapter<Model extends VariableModel> {
|
||||
reducer: Reducer<VariablesState>;
|
||||
}
|
||||
|
||||
const allVariableAdapters: Record<VariableType, VariableAdapter<any> | null> = {
|
||||
interval: null,
|
||||
query: null,
|
||||
datasource: null,
|
||||
custom: null,
|
||||
constant: null,
|
||||
adhoc: null,
|
||||
textbox: null,
|
||||
};
|
||||
export type VariableModels =
|
||||
| QueryVariableModel
|
||||
| CustomVariableModel
|
||||
| TextBoxVariableModel
|
||||
| ConstantVariableModel
|
||||
| DataSourceVariableModel
|
||||
| IntervalVariableModel
|
||||
| AdHocVariableModel;
|
||||
export type VariableTypeRegistry<Model extends VariableModel = VariableModel> = Registry<VariableAdapter<Model>>;
|
||||
|
||||
export interface VariableAdapters {
|
||||
contains: (type: VariableType) => boolean;
|
||||
get: (type: VariableType) => VariableAdapter<any>;
|
||||
set: (type: VariableType, adapter: VariableAdapter<any>) => void;
|
||||
registeredTypes: () => Array<{ type: VariableType; label: string }>;
|
||||
}
|
||||
export const getDefaultVariableAdapters = () => [
|
||||
createQueryVariableAdapter(),
|
||||
createCustomVariableAdapter(),
|
||||
createTextBoxVariableAdapter(),
|
||||
createConstantVariableAdapter(),
|
||||
createDataSourceVariableAdapter(),
|
||||
createIntervalVariableAdapter(),
|
||||
createAdHocVariableAdapter(),
|
||||
];
|
||||
|
||||
export const variableAdapters: VariableAdapters = {
|
||||
contains: (type: VariableType): boolean => !!allVariableAdapters[type],
|
||||
get: (type: VariableType): VariableAdapter<any> => {
|
||||
if (allVariableAdapters[type] !== null) {
|
||||
// @ts-ignore
|
||||
// Suppressing strict null check in this case we know that this is an instance otherwise we throw
|
||||
// Type 'VariableAdapter<any, any> | null' is not assignable to type 'VariableAdapter<any, any>'.
|
||||
// Type 'null' is not assignable to type 'VariableAdapter<any, any>'.
|
||||
return allVariableAdapters[type];
|
||||
}
|
||||
|
||||
throw new Error(`There is no adapter for type:${type}`);
|
||||
},
|
||||
set: (type, adapter) => (allVariableAdapters[type] = adapter),
|
||||
registeredTypes: (): Array<{ type: VariableType; label: string }> => {
|
||||
return Object.keys(allVariableAdapters)
|
||||
.filter((key: VariableType) => allVariableAdapters[key] !== null)
|
||||
.map((key: VariableType) => ({ type: key, label: allVariableAdapters[key]!.label }));
|
||||
},
|
||||
};
|
||||
export const variableAdapters: VariableTypeRegistry = new Registry<VariableAdapter<VariableModels>>();
|
||||
|
@ -40,9 +40,9 @@ type ReducersUsedInContext = {
|
||||
location: LocationState;
|
||||
};
|
||||
|
||||
describe('adhoc actions', () => {
|
||||
variableAdapters.set('adhoc', createAdHocVariableAdapter());
|
||||
variableAdapters.setInit(() => [createAdHocVariableAdapter()]);
|
||||
|
||||
describe('adhoc actions', () => {
|
||||
describe('when applyFilterFromTable is dispatched and filter already exist', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const options: AdHocTableOptions = {
|
||||
|
@ -12,8 +12,9 @@ const noop = async () => {};
|
||||
|
||||
export const createAdHocVariableAdapter = (): VariableAdapter<AdHocVariableModel> => {
|
||||
return {
|
||||
id: 'adhoc',
|
||||
description: 'Add key/value filters on the fly',
|
||||
label: 'Ad hoc filters',
|
||||
name: 'Ad hoc filters',
|
||||
initialState: initialAdHocVariableModelState,
|
||||
reducer: adHocVariableReducer,
|
||||
picker: AdHocPicker,
|
||||
|
@ -11,7 +11,7 @@ import { setCurrentVariableValue } from '../state/sharedReducer';
|
||||
import { initDashboardTemplating } from '../state/actions';
|
||||
|
||||
describe('constant actions', () => {
|
||||
variableAdapters.set('constant', createConstantVariableAdapter());
|
||||
variableAdapters.setInit(() => [createConstantVariableAdapter()]);
|
||||
|
||||
describe('when updateConstantVariableOptions is dispatched', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
|
@ -11,8 +11,9 @@ import { toVariableIdentifier } from '../state/types';
|
||||
|
||||
export const createConstantVariableAdapter = (): VariableAdapter<ConstantVariableModel> => {
|
||||
return {
|
||||
id: 'constant',
|
||||
description: 'Define a hidden constant variable, useful for metric prefixes in dashboards you want to share',
|
||||
label: 'Constant',
|
||||
name: 'Constant',
|
||||
initialState: initialConstantVariableModelState,
|
||||
reducer: constantVariableReducer,
|
||||
picker: OptionsPicker,
|
||||
|
@ -11,7 +11,7 @@ import { TemplatingState } from '../state/reducers';
|
||||
import { createCustomOptionsFromQuery } from './reducer';
|
||||
|
||||
describe('custom actions', () => {
|
||||
variableAdapters.set('custom', createCustomVariableAdapter());
|
||||
variableAdapters.setInit(() => [createCustomVariableAdapter()]);
|
||||
|
||||
describe('when updateCustomVariableOptions is dispatched', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
|
@ -11,8 +11,9 @@ import { ALL_VARIABLE_TEXT, toVariableIdentifier } from '../state/types';
|
||||
|
||||
export const createCustomVariableAdapter = (): VariableAdapter<CustomVariableModel> => {
|
||||
return {
|
||||
id: 'custom',
|
||||
description: 'Define variable values manually',
|
||||
label: 'Custom',
|
||||
name: 'Custom',
|
||||
initialState: initialCustomVariableModelState,
|
||||
reducer: customVariableReducer,
|
||||
picker: OptionsPicker,
|
||||
|
@ -18,7 +18,7 @@ import { changeVariableEditorExtended } from '../editor/reducer';
|
||||
import { datasourceBuilder } from '../shared/testing/builders';
|
||||
|
||||
describe('data source actions', () => {
|
||||
variableAdapters.set('datasource', createDataSourceVariableAdapter());
|
||||
variableAdapters.setInit(() => [createDataSourceVariableAdapter()]);
|
||||
|
||||
describe('when updateDataSourceVariableOptions is dispatched', () => {
|
||||
describe('and there is no regex', () => {
|
||||
|
@ -11,8 +11,9 @@ import { updateDataSourceVariableOptions } from './actions';
|
||||
|
||||
export const createDataSourceVariableAdapter = (): VariableAdapter<DataSourceVariableModel> => {
|
||||
return {
|
||||
id: 'datasource',
|
||||
description: 'Enabled you to dynamically switch the datasource for multiple panels',
|
||||
label: 'Datasource',
|
||||
name: 'Datasource',
|
||||
initialState: initialDataSourceVariableModelState,
|
||||
reducer: dataSourceVariableReducer,
|
||||
picker: OptionsPicker,
|
||||
|
@ -143,9 +143,9 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
|
||||
onChange={this.onTypeChange}
|
||||
aria-label={e2e.pages.Dashboard.Settings.Variables.Edit.General.selectors.generalTypeSelect}
|
||||
>
|
||||
{variableAdapters.registeredTypes().map(item => (
|
||||
<option key={item.type} label={item.label} value={item.type}>
|
||||
{item.label}
|
||||
{variableAdapters.list().map(({ id, name }) => (
|
||||
<option key={id} label={name} value={id}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
@ -20,7 +20,7 @@ import { TemplateSrv } from '../../templating/template_srv';
|
||||
import { intervalBuilder } from '../shared/testing/builders';
|
||||
|
||||
describe('interval actions', () => {
|
||||
variableAdapters.set('interval', createIntervalVariableAdapter());
|
||||
variableAdapters.setInit(() => [createIntervalVariableAdapter()]);
|
||||
describe('when updateIntervalVariableOptions is dispatched', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const interval = intervalBuilder()
|
||||
|
@ -11,8 +11,9 @@ import { updateAutoValue, updateIntervalVariableOptions } from './actions';
|
||||
|
||||
export const createIntervalVariableAdapter = (): VariableAdapter<IntervalVariableModel> => {
|
||||
return {
|
||||
id: 'interval',
|
||||
description: 'Define a timespan interval (ex 1m, 1h, 1d)',
|
||||
label: 'Interval',
|
||||
name: 'Interval',
|
||||
initialState: initialIntervalVariableModelState,
|
||||
reducer: intervalVariableReducer,
|
||||
picker: OptionsPicker,
|
||||
|
@ -40,7 +40,7 @@ jest.mock('@grafana/runtime', () => {
|
||||
});
|
||||
|
||||
describe('options picker actions', () => {
|
||||
variableAdapters.set('query', createQueryVariableAdapter());
|
||||
variableAdapters.setInit(() => [createQueryVariableAdapter()]);
|
||||
|
||||
describe('when navigateOptions is dispatched with navigation key cancel', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
|
@ -47,7 +47,7 @@ jest.mock('../../plugins/plugin_loader', () => ({
|
||||
}));
|
||||
|
||||
describe('query actions', () => {
|
||||
variableAdapters.set('query', createQueryVariableAdapter());
|
||||
variableAdapters.setInit(() => [createQueryVariableAdapter()]);
|
||||
|
||||
describe('when updateQueryVariableOptions is dispatched for variable with tags and includeAll', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
|
@ -12,8 +12,9 @@ import { ALL_VARIABLE_TEXT, toVariableIdentifier } from '../state/types';
|
||||
|
||||
export const createQueryVariableAdapter = (): VariableAdapter<QueryVariableModel> => {
|
||||
return {
|
||||
id: 'query',
|
||||
description: 'Variable values are fetched from a datasource query',
|
||||
label: 'Query',
|
||||
name: 'Query',
|
||||
initialState: initialQueryVariableModelState,
|
||||
reducer: queryVariableReducer,
|
||||
picker: OptionsPicker,
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { AnyAction } from 'redux';
|
||||
import { UrlQueryMap } from '@grafana/runtime';
|
||||
import { dateTime, TimeRange } from '@grafana/data';
|
||||
|
||||
import { getTemplatingAndLocationRootReducer, getTemplatingRootReducer } from './helpers';
|
||||
import { variableAdapters } from '../adapters';
|
||||
@ -8,49 +7,29 @@ import { createQueryVariableAdapter } from '../query/adapter';
|
||||
import { createCustomVariableAdapter } from '../custom/adapter';
|
||||
import { createTextBoxVariableAdapter } from '../textbox/adapter';
|
||||
import { createConstantVariableAdapter } from '../constant/adapter';
|
||||
import { createIntervalVariableAdapter } from '../interval/adapter';
|
||||
import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||
import { TemplatingState } from 'app/features/variables/state/reducers';
|
||||
import {
|
||||
initDashboardTemplating,
|
||||
onTimeRangeUpdated,
|
||||
OnTimeRangeUpdatedDependencies,
|
||||
processVariables,
|
||||
setOptionFromUrl,
|
||||
validateVariableSelectionState,
|
||||
} from './actions';
|
||||
import {
|
||||
addInitLock,
|
||||
addVariable,
|
||||
removeInitLock,
|
||||
removeVariable,
|
||||
resolveInitLock,
|
||||
setCurrentVariableValue,
|
||||
} from './sharedReducer';
|
||||
import { NEW_VARIABLE_ID, toVariableIdentifier, toVariablePayload } from './types';
|
||||
import { changeVariableName } from '../editor/actions';
|
||||
import { changeVariableNameFailed, changeVariableNameSucceeded, setIdInEditor } from '../editor/reducer';
|
||||
import { TemplateSrv } from '../../templating/template_srv';
|
||||
import { Emitter } from '../../../core/core';
|
||||
import { VariableRefresh } from '../../templating/variable';
|
||||
import { DashboardModel } from '../../dashboard/state';
|
||||
import { DashboardState } from '../../../types';
|
||||
import { initDashboardTemplating, processVariables, setOptionFromUrl, validateVariableSelectionState } from './actions';
|
||||
import { addInitLock, addVariable, removeInitLock, resolveInitLock, setCurrentVariableValue } from './sharedReducer';
|
||||
import { toVariableIdentifier, toVariablePayload } from './types';
|
||||
import {
|
||||
constantBuilder,
|
||||
customBuilder,
|
||||
datasourceBuilder,
|
||||
intervalBuilder,
|
||||
queryBuilder,
|
||||
textboxBuilder,
|
||||
} from '../shared/testing/builders';
|
||||
|
||||
variableAdapters.setInit(() => [
|
||||
createQueryVariableAdapter(),
|
||||
createCustomVariableAdapter(),
|
||||
createTextBoxVariableAdapter(),
|
||||
createConstantVariableAdapter(),
|
||||
]);
|
||||
|
||||
describe('shared actions', () => {
|
||||
describe('when initDashboardTemplating is dispatched', () => {
|
||||
it('then correct actions are dispatched', () => {
|
||||
variableAdapters.set('query', createQueryVariableAdapter());
|
||||
variableAdapters.set('custom', createCustomVariableAdapter());
|
||||
variableAdapters.set('textbox', createTextBoxVariableAdapter());
|
||||
variableAdapters.set('constant', createConstantVariableAdapter());
|
||||
const query = queryBuilder().build();
|
||||
const constant = constantBuilder().build();
|
||||
const datasource = datasourceBuilder().build();
|
||||
@ -98,10 +77,6 @@ describe('shared actions', () => {
|
||||
|
||||
describe('when processVariables is dispatched', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
variableAdapters.set('query', createQueryVariableAdapter());
|
||||
variableAdapters.set('custom', createCustomVariableAdapter());
|
||||
variableAdapters.set('textbox', createTextBoxVariableAdapter());
|
||||
variableAdapters.set('constant', createConstantVariableAdapter());
|
||||
const query = queryBuilder().build();
|
||||
const constant = constantBuilder().build();
|
||||
const datasource = datasourceBuilder().build();
|
||||
@ -161,7 +136,6 @@ describe('shared actions', () => {
|
||||
${null} | ${[null]}
|
||||
${undefined} | ${[undefined]}
|
||||
`('and urlValue is $urlValue then correct actions are dispatched', async ({ urlValue, expected }) => {
|
||||
variableAdapters.set('custom', createCustomVariableAdapter());
|
||||
const custom = customBuilder()
|
||||
.withId('0')
|
||||
.withOptions('A', 'B', 'C')
|
||||
@ -195,7 +169,6 @@ describe('shared actions', () => {
|
||||
${['A', 'B', 'C']} | ${'X'} | ${'C'} | ${'C'}
|
||||
${undefined} | ${'B'} | ${undefined} | ${'A'}
|
||||
`('then correct actions are dispatched', async ({ withOptions, withCurrent, defaultValue, expected }) => {
|
||||
variableAdapters.set('custom', createCustomVariableAdapter());
|
||||
let custom;
|
||||
|
||||
if (!withOptions) {
|
||||
@ -249,7 +222,6 @@ describe('shared actions', () => {
|
||||
`(
|
||||
'then correct actions are dispatched',
|
||||
async ({ withOptions, withCurrent, defaultValue, expectedText, expectedSelected }) => {
|
||||
variableAdapters.set('custom', createCustomVariableAdapter());
|
||||
let custom;
|
||||
|
||||
if (!withOptions) {
|
||||
@ -294,311 +266,4 @@ 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 = intervalBuilder()
|
||||
.withId('interval-0')
|
||||
.withName('interval-0')
|
||||
.withOptions('1m', '10m', '30m', '1h', '6h', '12h', '1d', '7d', '14d', '30d')
|
||||
.withCurrent('1m')
|
||||
.withRefresh(VariableRefresh.onTimeRangeChanged)
|
||||
.build();
|
||||
|
||||
// the constant variable should be filtered out
|
||||
const constant = constantBuilder()
|
||||
.withId('constant-1')
|
||||
.withName('constant-1')
|
||||
.withOptions('a constant')
|
||||
.withCurrent('a constant')
|
||||
.build();
|
||||
const initialState = {
|
||||
templating: { variables: { 'interval-0': { ...initialVariable }, 'constant-1': { ...constant } } },
|
||||
dashboard,
|
||||
};
|
||||
|
||||
// updated variable state
|
||||
const updatedVariable = intervalBuilder()
|
||||
.withId('interval-0')
|
||||
.withName('interval-0')
|
||||
.withOptions('1m')
|
||||
.withCurrent('1m')
|
||||
.withRefresh(VariableRefresh.onTimeRangeChanged)
|
||||
.build();
|
||||
|
||||
const variable = args.update ? { ...updatedVariable } : { ...initialVariable };
|
||||
const state = { templating: { variables: { 'interval-0': variable, 'constant-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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeVariableName is dispatched with the same name', () => {
|
||||
it('then no actions are dispatched', () => {
|
||||
const textbox = textboxBuilder()
|
||||
.withId('textbox')
|
||||
.withName('textbox')
|
||||
.build();
|
||||
const constant = constantBuilder()
|
||||
.withId('constant')
|
||||
.withName('constant')
|
||||
.build();
|
||||
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
|
||||
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), constant.name), true)
|
||||
.thenNoActionsWhereDispatched();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeVariableName is dispatched with an unique name', () => {
|
||||
it('then the correct actions are dispatched', () => {
|
||||
const textbox = textboxBuilder()
|
||||
.withId('textbox')
|
||||
.withName('textbox')
|
||||
.build();
|
||||
const constant = constantBuilder()
|
||||
.withId('constant')
|
||||
.withName('constant')
|
||||
.build();
|
||||
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
|
||||
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), 'constant1'), true)
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
addVariable({
|
||||
type: 'constant',
|
||||
id: 'constant1',
|
||||
data: {
|
||||
global: false,
|
||||
index: 1,
|
||||
model: { ...constant, name: 'constant1', id: 'constant1', global: false, index: 1 },
|
||||
},
|
||||
}),
|
||||
changeVariableNameSucceeded({ type: 'constant', id: 'constant1', data: { newName: 'constant1' } }),
|
||||
setIdInEditor({ id: 'constant1' }),
|
||||
removeVariable({ type: 'constant', id: 'constant', data: { reIndex: false } })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeVariableName is dispatched with an unique name for a new variable', () => {
|
||||
it('then the correct actions are dispatched', () => {
|
||||
const textbox = textboxBuilder()
|
||||
.withId('textbox')
|
||||
.withName('textbox')
|
||||
.build();
|
||||
const constant = constantBuilder()
|
||||
.withId(NEW_VARIABLE_ID)
|
||||
.withName('constant')
|
||||
.build();
|
||||
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
|
||||
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), 'constant1'), true)
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
changeVariableNameSucceeded({ type: 'constant', id: NEW_VARIABLE_ID, data: { newName: 'constant1' } })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeVariableName is dispatched with __newName', () => {
|
||||
it('then the correct actions are dispatched', () => {
|
||||
const textbox = textboxBuilder()
|
||||
.withId('textbox')
|
||||
.withName('textbox')
|
||||
.build();
|
||||
const constant = constantBuilder()
|
||||
.withId('constant')
|
||||
.withName('constant')
|
||||
.build();
|
||||
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
|
||||
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), '__newName'), true)
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
changeVariableNameFailed({
|
||||
newName: '__newName',
|
||||
errorText: "Template names cannot begin with '__', that's reserved for Grafana's global variables",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeVariableName is dispatched with illegal characters', () => {
|
||||
it('then the correct actions are dispatched', () => {
|
||||
const textbox = textboxBuilder()
|
||||
.withId('textbox')
|
||||
.withName('textbox')
|
||||
.build();
|
||||
const constant = constantBuilder()
|
||||
.withId('constant')
|
||||
.withName('constant')
|
||||
.build();
|
||||
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
|
||||
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), '#constant!'), true)
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
changeVariableNameFailed({
|
||||
newName: '#constant!',
|
||||
errorText: 'Only word and digit characters are allowed in variable names',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeVariableName is dispatched with a name that is already used', () => {
|
||||
it('then the correct actions are dispatched', () => {
|
||||
const textbox = textboxBuilder()
|
||||
.withId('textbox')
|
||||
.withName('textbox')
|
||||
.build();
|
||||
const constant = constantBuilder()
|
||||
.withId('constant')
|
||||
.withName('constant')
|
||||
.build();
|
||||
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
|
||||
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), 'textbox'), true)
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
changeVariableNameFailed({
|
||||
newName: 'textbox',
|
||||
errorText: 'Variable with the same name already exists',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -55,7 +55,7 @@ export const initDashboardTemplating = (list: VariableModel[]): ThunkResult<void
|
||||
let orderIndex = 0;
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
const model = list[index];
|
||||
if (!variableAdapters.contains(model.type)) {
|
||||
if (!variableAdapters.getIfExists(model.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ export const processVariableDependencies = async (variable: VariableModel, state
|
||||
continue;
|
||||
}
|
||||
|
||||
if (variableAdapters.contains(variable.type)) {
|
||||
if (variableAdapters.getIfExists(variable.type)) {
|
||||
if (variableAdapters.get(variable.type).dependsOn(variable, otherVariable)) {
|
||||
dependencies.push(otherVariable.initLock!.promise);
|
||||
}
|
||||
|
166
public/app/features/variables/state/onTimeRangeUpdated.test.ts
Normal file
166
public/app/features/variables/state/onTimeRangeUpdated.test.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import { dateTime, TimeRange } from '@grafana/data';
|
||||
import { TemplateSrv } from '../../templating/template_srv';
|
||||
import { Emitter } from '../../../core/utils/emitter';
|
||||
import { onTimeRangeUpdated, OnTimeRangeUpdatedDependencies } from './actions';
|
||||
import { DashboardModel } from '../../dashboard/state';
|
||||
import { DashboardState } from '../../../types';
|
||||
import { createIntervalVariableAdapter } from '../interval/adapter';
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { createConstantVariableAdapter } from '../constant/adapter';
|
||||
import { VariableRefresh } from '../../templating/variable';
|
||||
import { constantBuilder, intervalBuilder } from '../shared/testing/builders';
|
||||
|
||||
variableAdapters.setInit(() => [createIntervalVariableAdapter(), createConstantVariableAdapter()]);
|
||||
|
||||
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 = variableAdapters.get('interval');
|
||||
adapter.updateOptions = args.throw ? jest.fn().mockRejectedValue('Something broke') : jest.fn().mockResolvedValue({});
|
||||
|
||||
// initial variable state
|
||||
const initialVariable = intervalBuilder()
|
||||
.withId('interval-0')
|
||||
.withName('interval-0')
|
||||
.withOptions('1m', '10m', '30m', '1h', '6h', '12h', '1d', '7d', '14d', '30d')
|
||||
.withCurrent('1m')
|
||||
.withRefresh(VariableRefresh.onTimeRangeChanged)
|
||||
.build();
|
||||
|
||||
// the constant variable should be filtered out
|
||||
const constant = constantBuilder()
|
||||
.withId('constant-1')
|
||||
.withName('constant-1')
|
||||
.withOptions('a constant')
|
||||
.withCurrent('a constant')
|
||||
.build();
|
||||
const initialState = {
|
||||
templating: { variables: { '0': { ...initialVariable }, '1': { ...constant } } },
|
||||
dashboard,
|
||||
};
|
||||
|
||||
// updated variable state
|
||||
const updatedVariable = intervalBuilder()
|
||||
.withId('interval-0')
|
||||
.withName('interval-0')
|
||||
.withOptions('1m')
|
||||
.withCurrent('1m')
|
||||
.withRefresh(VariableRefresh.onTimeRangeChanged)
|
||||
.build();
|
||||
|
||||
const variable = args.update ? { ...updatedVariable } : { ...initialVariable };
|
||||
const state = { templating: { variables: { 'interval-0': variable, 'constant-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('when onTimeRangeUpdated is dispatched', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
@ -62,14 +62,14 @@ jest.mock('app/features/plugins/datasource_srv', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
variableAdapters.setInit(() => [createCustomVariableAdapter(), createQueryVariableAdapter()]);
|
||||
|
||||
describe('processVariable', () => {
|
||||
// these following processVariable tests will test the following base setup
|
||||
// custom doesn't depend on any other variable
|
||||
// queryDependsOnCustom depends on custom
|
||||
// queryNoDepends doesn't depend on any other variable
|
||||
const getAndSetupProcessVariableContext = () => {
|
||||
variableAdapters.set('custom', createCustomVariableAdapter());
|
||||
variableAdapters.set('query', createQueryVariableAdapter());
|
||||
const custom = customBuilder()
|
||||
.withId('custom')
|
||||
.withName('custom')
|
||||
|
@ -1,11 +1,29 @@
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { cleanUpDashboard } from 'app/features/dashboard/state/reducers';
|
||||
import { VariableHide, VariableModel } from '../../templating/variable';
|
||||
import { QueryVariableModel, VariableHide, VariableType } from '../../templating/variable';
|
||||
import { VariableAdapter, variableAdapters } from '../adapters';
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { variablesReducer, VariablesState } from './variablesReducer';
|
||||
import { toVariablePayload, VariablePayload } from './types';
|
||||
|
||||
const variableAdapter: VariableAdapter<QueryVariableModel> = {
|
||||
id: ('mock' as unknown) as VariableType,
|
||||
name: 'Mock label',
|
||||
description: 'Mock description',
|
||||
dependsOn: jest.fn(),
|
||||
updateOptions: jest.fn(),
|
||||
initialState: {} as QueryVariableModel,
|
||||
reducer: jest.fn().mockReturnValue({}),
|
||||
getValueForUrl: jest.fn(),
|
||||
getSaveModel: jest.fn(),
|
||||
picker: null as any,
|
||||
editor: null as any,
|
||||
setValue: jest.fn(),
|
||||
setValueFromUrl: jest.fn(),
|
||||
};
|
||||
|
||||
variableAdapters.setInit(() => [{ ...variableAdapter }]);
|
||||
|
||||
describe('variablesReducer', () => {
|
||||
describe('when cleanUpDashboard is dispatched', () => {
|
||||
it('then all variables except global variables should be removed', () => {
|
||||
@ -91,30 +109,16 @@ describe('variablesReducer', () => {
|
||||
skipUrlSync: false,
|
||||
},
|
||||
};
|
||||
const variableAdapter: VariableAdapter<VariableModel> = {
|
||||
label: 'Mock label',
|
||||
description: 'Mock description',
|
||||
dependsOn: jest.fn(),
|
||||
updateOptions: jest.fn(),
|
||||
initialState: {} as VariableModel,
|
||||
reducer: jest.fn().mockReturnValue(initialState),
|
||||
getValueForUrl: jest.fn(),
|
||||
getSaveModel: jest.fn(),
|
||||
picker: null as any,
|
||||
editor: null as any,
|
||||
setValue: jest.fn(),
|
||||
setValueFromUrl: jest.fn(),
|
||||
};
|
||||
variableAdapters.set('query', variableAdapter);
|
||||
variableAdapters.get('mock').reducer = jest.fn().mockReturnValue(initialState);
|
||||
const mockAction = createAction<VariablePayload>('mockAction');
|
||||
reducerTester<VariablesState>()
|
||||
.givenReducer(variablesReducer, initialState)
|
||||
.whenActionIsDispatched(mockAction(toVariablePayload({ type: 'query', id: '0' })))
|
||||
.whenActionIsDispatched(mockAction(toVariablePayload({ type: ('mock' as unknown) as VariableType, id: '0' })))
|
||||
.thenStateShouldEqual(initialState);
|
||||
expect(variableAdapter.reducer).toHaveBeenCalledTimes(1);
|
||||
expect(variableAdapter.reducer).toHaveBeenCalledWith(
|
||||
expect(variableAdapters.get('mock').reducer).toHaveBeenCalledTimes(1);
|
||||
expect(variableAdapters.get('mock').reducer).toHaveBeenCalledWith(
|
||||
initialState,
|
||||
mockAction(toVariablePayload({ type: 'query', id: '0' }))
|
||||
mockAction(toVariablePayload({ type: ('mock' as unknown) as VariableType, id: '0' }))
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -132,27 +136,13 @@ describe('variablesReducer', () => {
|
||||
skipUrlSync: false,
|
||||
},
|
||||
};
|
||||
const variableAdapter: VariableAdapter<VariableModel> = {
|
||||
label: 'Mock label',
|
||||
description: 'Mock description',
|
||||
dependsOn: jest.fn(),
|
||||
updateOptions: jest.fn(),
|
||||
initialState: {} as VariableModel,
|
||||
reducer: jest.fn().mockReturnValue(initialState),
|
||||
getValueForUrl: jest.fn(),
|
||||
getSaveModel: jest.fn(),
|
||||
picker: null as any,
|
||||
editor: null as any,
|
||||
setValue: jest.fn(),
|
||||
setValueFromUrl: jest.fn(),
|
||||
};
|
||||
variableAdapters.set('query', variableAdapter);
|
||||
variableAdapters.get('mock').reducer = jest.fn().mockReturnValue(initialState);
|
||||
const mockAction = createAction<VariablePayload>('mockAction');
|
||||
reducerTester<VariablesState>()
|
||||
.givenReducer(variablesReducer, initialState)
|
||||
.whenActionIsDispatched(mockAction(toVariablePayload({ type: 'adhoc', id: '0' })))
|
||||
.thenStateShouldEqual(initialState);
|
||||
expect(variableAdapter.reducer).toHaveBeenCalledTimes(0);
|
||||
expect(variableAdapters.get('mock').reducer).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -169,27 +159,13 @@ describe('variablesReducer', () => {
|
||||
skipUrlSync: false,
|
||||
},
|
||||
};
|
||||
const variableAdapter: VariableAdapter<VariableModel> = {
|
||||
label: 'Mock label',
|
||||
description: 'Mock description',
|
||||
dependsOn: jest.fn(),
|
||||
updateOptions: jest.fn(),
|
||||
initialState: {} as VariableModel,
|
||||
reducer: jest.fn().mockReturnValue(initialState),
|
||||
getValueForUrl: jest.fn(),
|
||||
getSaveModel: jest.fn(),
|
||||
picker: null as any,
|
||||
editor: null as any,
|
||||
setValue: jest.fn(),
|
||||
setValueFromUrl: jest.fn(),
|
||||
};
|
||||
variableAdapters.set('query', variableAdapter);
|
||||
variableAdapters.get('mock').reducer = jest.fn().mockReturnValue(initialState);
|
||||
const mockAction = createAction<string>('mockAction');
|
||||
reducerTester<VariablesState>()
|
||||
.givenReducer(variablesReducer, initialState)
|
||||
.whenActionIsDispatched(mockAction('mocked'))
|
||||
.thenStateShouldEqual(initialState);
|
||||
expect(variableAdapter.reducer).toHaveBeenCalledTimes(0);
|
||||
expect(variableAdapters.get('mock').reducer).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -24,12 +24,13 @@ import { getVariableState, getVariableTestContext } from './helpers';
|
||||
import { initialVariablesState, VariablesState } from './variablesReducer';
|
||||
import { changeVariableNameSucceeded } from '../editor/reducer';
|
||||
|
||||
variableAdapters.setInit(() => [createQueryVariableAdapter()]);
|
||||
|
||||
describe('sharedReducer', () => {
|
||||
describe('when addVariable is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
const model = ({ name: 'name from model', type: 'type from model' } as unknown) as QueryVariableModel;
|
||||
const payload = toVariablePayload({ id: '0', type: 'query' }, { global: true, index: 0, model });
|
||||
variableAdapters.set('query', createQueryVariableAdapter());
|
||||
reducerTester<VariablesState>()
|
||||
.givenReducer(sharedReducer, { ...initialVariablesState })
|
||||
.whenActionIsDispatched(addVariable(payload))
|
||||
@ -107,7 +108,6 @@ describe('sharedReducer', () => {
|
||||
|
||||
describe('when duplicateVariable is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
variableAdapters.set('query', createQueryVariableAdapter());
|
||||
const initialState: VariablesState = getVariableState(3);
|
||||
const payload = toVariablePayload({ id: '1', type: 'query' }, { newId: '11' });
|
||||
reducerTester<VariablesState>()
|
||||
@ -193,7 +193,6 @@ describe('sharedReducer', () => {
|
||||
|
||||
describe('when storeNewVariable is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
variableAdapters.set('query', createQueryVariableAdapter());
|
||||
const initialState: VariablesState = getVariableState(3, -1, true);
|
||||
const payload = toVariablePayload({ id: '11', type: 'query' });
|
||||
reducerTester<VariablesState>()
|
||||
|
@ -27,7 +27,7 @@ export const variablesReducer = (
|
||||
return variables;
|
||||
}
|
||||
|
||||
if (action?.payload?.type && variableAdapters.contains(action?.payload?.type)) {
|
||||
if (action?.payload?.type && variableAdapters.getIfExists(action?.payload?.type)) {
|
||||
// Now that we know we are dealing with a payload that is addressed for an adapted variable let's reduce state:
|
||||
// Firstly call the sharedTemplatingReducer that handles all shared actions between variable types
|
||||
// Secondly call the specific variable type's reducer
|
||||
|
@ -11,7 +11,7 @@ import { setCurrentVariableValue } from '../state/sharedReducer';
|
||||
import { initDashboardTemplating } from '../state/actions';
|
||||
|
||||
describe('textbox actions', () => {
|
||||
variableAdapters.set('textbox', createTextBoxVariableAdapter());
|
||||
variableAdapters.setInit(() => [createTextBoxVariableAdapter()]);
|
||||
|
||||
describe('when updateTextBoxVariableOptions is dispatched', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
|
@ -12,8 +12,9 @@ import { toVariableIdentifier } from '../state/types';
|
||||
|
||||
export const createTextBoxVariableAdapter = (): VariableAdapter<TextBoxVariableModel> => {
|
||||
return {
|
||||
id: 'textbox',
|
||||
description: 'Define a textbox variable, where users can enter any arbitrary string',
|
||||
label: 'Text box',
|
||||
name: 'Text box',
|
||||
initialState: initialTextBoxVariableModelState,
|
||||
reducer: textBoxVariableReducer,
|
||||
picker: TextBoxVariablePicker,
|
||||
|
Loading…
Reference in New Issue
Block a user