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/routes/GrafanaCtrl';
|
||||||
import 'app/features/all';
|
import 'app/features/all';
|
||||||
import { getStandardFieldConfigs } from '@grafana/ui';
|
import { getStandardFieldConfigs } from '@grafana/ui';
|
||||||
|
import { getDefaultVariableAdapters, variableAdapters } from './features/variables/adapters';
|
||||||
|
|
||||||
// add move to lodash for backward compatabiltiy
|
// add move to lodash for backward compatabiltiy
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -84,6 +85,7 @@ export class GrafanaApp {
|
|||||||
|
|
||||||
setMarkdownOptions({ sanitize: !config.disableSanitizeHtml });
|
setMarkdownOptions({ sanitize: !config.disableSanitizeHtml });
|
||||||
standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
|
standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
|
||||||
|
variableAdapters.setInit(getDefaultVariableAdapters);
|
||||||
|
|
||||||
app.config(
|
app.config(
|
||||||
(
|
(
|
||||||
|
@ -188,7 +188,7 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
|
|||||||
const list =
|
const list =
|
||||||
dashboard.variables.list.length > 0
|
dashboard.variables.list.length > 0
|
||||||
? dashboard.variables.list
|
? 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(initDashboardTemplating(list));
|
||||||
await dispatch(processVariables());
|
await dispatch(processVariables());
|
||||||
}
|
}
|
||||||
|
@ -10,14 +10,6 @@ import { CustomVariable } from './custom_variable';
|
|||||||
import { ConstantVariable } from './constant_variable';
|
import { ConstantVariable } from './constant_variable';
|
||||||
import { AdhocVariable } from './adhoc_variable';
|
import { AdhocVariable } from './adhoc_variable';
|
||||||
import { TextBoxVariable } from './TextBoxVariable';
|
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);
|
coreModule.factory('templateSrv', () => templateSrv);
|
||||||
|
|
||||||
@ -31,11 +23,3 @@ export {
|
|||||||
AdhocVariable,
|
AdhocVariable,
|
||||||
TextBoxVariable,
|
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 { Reducer } from 'redux';
|
||||||
import { UrlQueryValue } from '@grafana/runtime';
|
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 { VariableEditorProps } from './editor/types';
|
||||||
import { VariablesState } from './state/variablesReducer';
|
import { VariablesState } from './state/variablesReducer';
|
||||||
import { VariablePickerProps } from './pickers/types';
|
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> {
|
export interface VariableAdapter<Model extends VariableModel> {
|
||||||
|
id: VariableType;
|
||||||
description: string;
|
description: string;
|
||||||
label: string;
|
name: string;
|
||||||
initialState: Model;
|
initialState: Model;
|
||||||
dependsOn: (variable: Model, variableToTest: Model) => boolean;
|
dependsOn: (variable: Model, variableToTest: Model) => boolean;
|
||||||
setValue: (variable: Model, option: VariableOption, emitChanges?: boolean) => Promise<void>;
|
setValue: (variable: Model, option: VariableOption, emitChanges?: boolean) => Promise<void>;
|
||||||
@ -22,40 +42,24 @@ export interface VariableAdapter<Model extends VariableModel> {
|
|||||||
reducer: Reducer<VariablesState>;
|
reducer: Reducer<VariablesState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allVariableAdapters: Record<VariableType, VariableAdapter<any> | null> = {
|
export type VariableModels =
|
||||||
interval: null,
|
| QueryVariableModel
|
||||||
query: null,
|
| CustomVariableModel
|
||||||
datasource: null,
|
| TextBoxVariableModel
|
||||||
custom: null,
|
| ConstantVariableModel
|
||||||
constant: null,
|
| DataSourceVariableModel
|
||||||
adhoc: null,
|
| IntervalVariableModel
|
||||||
textbox: null,
|
| AdHocVariableModel;
|
||||||
};
|
export type VariableTypeRegistry<Model extends VariableModel = VariableModel> = Registry<VariableAdapter<Model>>;
|
||||||
|
|
||||||
export interface VariableAdapters {
|
export const getDefaultVariableAdapters = () => [
|
||||||
contains: (type: VariableType) => boolean;
|
createQueryVariableAdapter(),
|
||||||
get: (type: VariableType) => VariableAdapter<any>;
|
createCustomVariableAdapter(),
|
||||||
set: (type: VariableType, adapter: VariableAdapter<any>) => void;
|
createTextBoxVariableAdapter(),
|
||||||
registeredTypes: () => Array<{ type: VariableType; label: string }>;
|
createConstantVariableAdapter(),
|
||||||
}
|
createDataSourceVariableAdapter(),
|
||||||
|
createIntervalVariableAdapter(),
|
||||||
|
createAdHocVariableAdapter(),
|
||||||
|
];
|
||||||
|
|
||||||
export const variableAdapters: VariableAdapters = {
|
export const variableAdapters: VariableTypeRegistry = new Registry<VariableAdapter<VariableModels>>();
|
||||||
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 }));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
@ -40,9 +40,9 @@ type ReducersUsedInContext = {
|
|||||||
location: LocationState;
|
location: LocationState;
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('adhoc actions', () => {
|
variableAdapters.setInit(() => [createAdHocVariableAdapter()]);
|
||||||
variableAdapters.set('adhoc', createAdHocVariableAdapter());
|
|
||||||
|
|
||||||
|
describe('adhoc actions', () => {
|
||||||
describe('when applyFilterFromTable is dispatched and filter already exist', () => {
|
describe('when applyFilterFromTable is dispatched and filter already exist', () => {
|
||||||
it('then correct actions are dispatched', async () => {
|
it('then correct actions are dispatched', async () => {
|
||||||
const options: AdHocTableOptions = {
|
const options: AdHocTableOptions = {
|
||||||
|
@ -12,8 +12,9 @@ const noop = async () => {};
|
|||||||
|
|
||||||
export const createAdHocVariableAdapter = (): VariableAdapter<AdHocVariableModel> => {
|
export const createAdHocVariableAdapter = (): VariableAdapter<AdHocVariableModel> => {
|
||||||
return {
|
return {
|
||||||
|
id: 'adhoc',
|
||||||
description: 'Add key/value filters on the fly',
|
description: 'Add key/value filters on the fly',
|
||||||
label: 'Ad hoc filters',
|
name: 'Ad hoc filters',
|
||||||
initialState: initialAdHocVariableModelState,
|
initialState: initialAdHocVariableModelState,
|
||||||
reducer: adHocVariableReducer,
|
reducer: adHocVariableReducer,
|
||||||
picker: AdHocPicker,
|
picker: AdHocPicker,
|
||||||
|
@ -11,7 +11,7 @@ import { setCurrentVariableValue } from '../state/sharedReducer';
|
|||||||
import { initDashboardTemplating } from '../state/actions';
|
import { initDashboardTemplating } from '../state/actions';
|
||||||
|
|
||||||
describe('constant actions', () => {
|
describe('constant actions', () => {
|
||||||
variableAdapters.set('constant', createConstantVariableAdapter());
|
variableAdapters.setInit(() => [createConstantVariableAdapter()]);
|
||||||
|
|
||||||
describe('when updateConstantVariableOptions is dispatched', () => {
|
describe('when updateConstantVariableOptions is dispatched', () => {
|
||||||
it('then correct actions are dispatched', async () => {
|
it('then correct actions are dispatched', async () => {
|
||||||
|
@ -11,8 +11,9 @@ import { toVariableIdentifier } from '../state/types';
|
|||||||
|
|
||||||
export const createConstantVariableAdapter = (): VariableAdapter<ConstantVariableModel> => {
|
export const createConstantVariableAdapter = (): VariableAdapter<ConstantVariableModel> => {
|
||||||
return {
|
return {
|
||||||
|
id: 'constant',
|
||||||
description: 'Define a hidden constant variable, useful for metric prefixes in dashboards you want to share',
|
description: 'Define a hidden constant variable, useful for metric prefixes in dashboards you want to share',
|
||||||
label: 'Constant',
|
name: 'Constant',
|
||||||
initialState: initialConstantVariableModelState,
|
initialState: initialConstantVariableModelState,
|
||||||
reducer: constantVariableReducer,
|
reducer: constantVariableReducer,
|
||||||
picker: OptionsPicker,
|
picker: OptionsPicker,
|
||||||
|
@ -11,7 +11,7 @@ import { TemplatingState } from '../state/reducers';
|
|||||||
import { createCustomOptionsFromQuery } from './reducer';
|
import { createCustomOptionsFromQuery } from './reducer';
|
||||||
|
|
||||||
describe('custom actions', () => {
|
describe('custom actions', () => {
|
||||||
variableAdapters.set('custom', createCustomVariableAdapter());
|
variableAdapters.setInit(() => [createCustomVariableAdapter()]);
|
||||||
|
|
||||||
describe('when updateCustomVariableOptions is dispatched', () => {
|
describe('when updateCustomVariableOptions is dispatched', () => {
|
||||||
it('then correct actions are dispatched', async () => {
|
it('then correct actions are dispatched', async () => {
|
||||||
|
@ -11,8 +11,9 @@ import { ALL_VARIABLE_TEXT, toVariableIdentifier } from '../state/types';
|
|||||||
|
|
||||||
export const createCustomVariableAdapter = (): VariableAdapter<CustomVariableModel> => {
|
export const createCustomVariableAdapter = (): VariableAdapter<CustomVariableModel> => {
|
||||||
return {
|
return {
|
||||||
|
id: 'custom',
|
||||||
description: 'Define variable values manually',
|
description: 'Define variable values manually',
|
||||||
label: 'Custom',
|
name: 'Custom',
|
||||||
initialState: initialCustomVariableModelState,
|
initialState: initialCustomVariableModelState,
|
||||||
reducer: customVariableReducer,
|
reducer: customVariableReducer,
|
||||||
picker: OptionsPicker,
|
picker: OptionsPicker,
|
||||||
|
@ -18,7 +18,7 @@ import { changeVariableEditorExtended } from '../editor/reducer';
|
|||||||
import { datasourceBuilder } from '../shared/testing/builders';
|
import { datasourceBuilder } from '../shared/testing/builders';
|
||||||
|
|
||||||
describe('data source actions', () => {
|
describe('data source actions', () => {
|
||||||
variableAdapters.set('datasource', createDataSourceVariableAdapter());
|
variableAdapters.setInit(() => [createDataSourceVariableAdapter()]);
|
||||||
|
|
||||||
describe('when updateDataSourceVariableOptions is dispatched', () => {
|
describe('when updateDataSourceVariableOptions is dispatched', () => {
|
||||||
describe('and there is no regex', () => {
|
describe('and there is no regex', () => {
|
||||||
|
@ -11,8 +11,9 @@ import { updateDataSourceVariableOptions } from './actions';
|
|||||||
|
|
||||||
export const createDataSourceVariableAdapter = (): VariableAdapter<DataSourceVariableModel> => {
|
export const createDataSourceVariableAdapter = (): VariableAdapter<DataSourceVariableModel> => {
|
||||||
return {
|
return {
|
||||||
|
id: 'datasource',
|
||||||
description: 'Enabled you to dynamically switch the datasource for multiple panels',
|
description: 'Enabled you to dynamically switch the datasource for multiple panels',
|
||||||
label: 'Datasource',
|
name: 'Datasource',
|
||||||
initialState: initialDataSourceVariableModelState,
|
initialState: initialDataSourceVariableModelState,
|
||||||
reducer: dataSourceVariableReducer,
|
reducer: dataSourceVariableReducer,
|
||||||
picker: OptionsPicker,
|
picker: OptionsPicker,
|
||||||
|
@ -143,9 +143,9 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
|
|||||||
onChange={this.onTypeChange}
|
onChange={this.onTypeChange}
|
||||||
aria-label={e2e.pages.Dashboard.Settings.Variables.Edit.General.selectors.generalTypeSelect}
|
aria-label={e2e.pages.Dashboard.Settings.Variables.Edit.General.selectors.generalTypeSelect}
|
||||||
>
|
>
|
||||||
{variableAdapters.registeredTypes().map(item => (
|
{variableAdapters.list().map(({ id, name }) => (
|
||||||
<option key={item.type} label={item.label} value={item.type}>
|
<option key={id} label={name} value={id}>
|
||||||
{item.label}
|
{name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
@ -20,7 +20,7 @@ import { TemplateSrv } from '../../templating/template_srv';
|
|||||||
import { intervalBuilder } from '../shared/testing/builders';
|
import { intervalBuilder } from '../shared/testing/builders';
|
||||||
|
|
||||||
describe('interval actions', () => {
|
describe('interval actions', () => {
|
||||||
variableAdapters.set('interval', createIntervalVariableAdapter());
|
variableAdapters.setInit(() => [createIntervalVariableAdapter()]);
|
||||||
describe('when updateIntervalVariableOptions is dispatched', () => {
|
describe('when updateIntervalVariableOptions is dispatched', () => {
|
||||||
it('then correct actions are dispatched', async () => {
|
it('then correct actions are dispatched', async () => {
|
||||||
const interval = intervalBuilder()
|
const interval = intervalBuilder()
|
||||||
|
@ -11,8 +11,9 @@ import { updateAutoValue, updateIntervalVariableOptions } from './actions';
|
|||||||
|
|
||||||
export const createIntervalVariableAdapter = (): VariableAdapter<IntervalVariableModel> => {
|
export const createIntervalVariableAdapter = (): VariableAdapter<IntervalVariableModel> => {
|
||||||
return {
|
return {
|
||||||
|
id: 'interval',
|
||||||
description: 'Define a timespan interval (ex 1m, 1h, 1d)',
|
description: 'Define a timespan interval (ex 1m, 1h, 1d)',
|
||||||
label: 'Interval',
|
name: 'Interval',
|
||||||
initialState: initialIntervalVariableModelState,
|
initialState: initialIntervalVariableModelState,
|
||||||
reducer: intervalVariableReducer,
|
reducer: intervalVariableReducer,
|
||||||
picker: OptionsPicker,
|
picker: OptionsPicker,
|
||||||
|
@ -40,7 +40,7 @@ jest.mock('@grafana/runtime', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('options picker actions', () => {
|
describe('options picker actions', () => {
|
||||||
variableAdapters.set('query', createQueryVariableAdapter());
|
variableAdapters.setInit(() => [createQueryVariableAdapter()]);
|
||||||
|
|
||||||
describe('when navigateOptions is dispatched with navigation key cancel', () => {
|
describe('when navigateOptions is dispatched with navigation key cancel', () => {
|
||||||
it('then correct actions are dispatched', async () => {
|
it('then correct actions are dispatched', async () => {
|
||||||
|
@ -47,7 +47,7 @@ jest.mock('../../plugins/plugin_loader', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('query actions', () => {
|
describe('query actions', () => {
|
||||||
variableAdapters.set('query', createQueryVariableAdapter());
|
variableAdapters.setInit(() => [createQueryVariableAdapter()]);
|
||||||
|
|
||||||
describe('when updateQueryVariableOptions is dispatched for variable with tags and includeAll', () => {
|
describe('when updateQueryVariableOptions is dispatched for variable with tags and includeAll', () => {
|
||||||
it('then correct actions are dispatched', async () => {
|
it('then correct actions are dispatched', async () => {
|
||||||
|
@ -12,8 +12,9 @@ import { ALL_VARIABLE_TEXT, toVariableIdentifier } from '../state/types';
|
|||||||
|
|
||||||
export const createQueryVariableAdapter = (): VariableAdapter<QueryVariableModel> => {
|
export const createQueryVariableAdapter = (): VariableAdapter<QueryVariableModel> => {
|
||||||
return {
|
return {
|
||||||
|
id: 'query',
|
||||||
description: 'Variable values are fetched from a datasource query',
|
description: 'Variable values are fetched from a datasource query',
|
||||||
label: 'Query',
|
name: 'Query',
|
||||||
initialState: initialQueryVariableModelState,
|
initialState: initialQueryVariableModelState,
|
||||||
reducer: queryVariableReducer,
|
reducer: queryVariableReducer,
|
||||||
picker: OptionsPicker,
|
picker: OptionsPicker,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { AnyAction } from 'redux';
|
import { AnyAction } from 'redux';
|
||||||
import { UrlQueryMap } from '@grafana/runtime';
|
import { UrlQueryMap } from '@grafana/runtime';
|
||||||
import { dateTime, TimeRange } from '@grafana/data';
|
|
||||||
|
|
||||||
import { getTemplatingAndLocationRootReducer, getTemplatingRootReducer } from './helpers';
|
import { getTemplatingAndLocationRootReducer, getTemplatingRootReducer } from './helpers';
|
||||||
import { variableAdapters } from '../adapters';
|
import { variableAdapters } from '../adapters';
|
||||||
@ -8,49 +7,29 @@ import { createQueryVariableAdapter } from '../query/adapter';
|
|||||||
import { createCustomVariableAdapter } from '../custom/adapter';
|
import { createCustomVariableAdapter } from '../custom/adapter';
|
||||||
import { createTextBoxVariableAdapter } from '../textbox/adapter';
|
import { createTextBoxVariableAdapter } from '../textbox/adapter';
|
||||||
import { createConstantVariableAdapter } from '../constant/adapter';
|
import { createConstantVariableAdapter } from '../constant/adapter';
|
||||||
import { createIntervalVariableAdapter } from '../interval/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 {
|
import { initDashboardTemplating, processVariables, setOptionFromUrl, validateVariableSelectionState } from './actions';
|
||||||
initDashboardTemplating,
|
import { addInitLock, addVariable, removeInitLock, resolveInitLock, setCurrentVariableValue } from './sharedReducer';
|
||||||
onTimeRangeUpdated,
|
import { toVariableIdentifier, toVariablePayload } from './types';
|
||||||
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 {
|
import {
|
||||||
constantBuilder,
|
constantBuilder,
|
||||||
customBuilder,
|
customBuilder,
|
||||||
datasourceBuilder,
|
datasourceBuilder,
|
||||||
intervalBuilder,
|
|
||||||
queryBuilder,
|
queryBuilder,
|
||||||
textboxBuilder,
|
textboxBuilder,
|
||||||
} from '../shared/testing/builders';
|
} from '../shared/testing/builders';
|
||||||
|
|
||||||
|
variableAdapters.setInit(() => [
|
||||||
|
createQueryVariableAdapter(),
|
||||||
|
createCustomVariableAdapter(),
|
||||||
|
createTextBoxVariableAdapter(),
|
||||||
|
createConstantVariableAdapter(),
|
||||||
|
]);
|
||||||
|
|
||||||
describe('shared actions', () => {
|
describe('shared actions', () => {
|
||||||
describe('when initDashboardTemplating is dispatched', () => {
|
describe('when initDashboardTemplating is dispatched', () => {
|
||||||
it('then correct actions are 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 query = queryBuilder().build();
|
||||||
const constant = constantBuilder().build();
|
const constant = constantBuilder().build();
|
||||||
const datasource = datasourceBuilder().build();
|
const datasource = datasourceBuilder().build();
|
||||||
@ -98,10 +77,6 @@ describe('shared actions', () => {
|
|||||||
|
|
||||||
describe('when processVariables is dispatched', () => {
|
describe('when processVariables is dispatched', () => {
|
||||||
it('then correct actions are dispatched', async () => {
|
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 query = queryBuilder().build();
|
||||||
const constant = constantBuilder().build();
|
const constant = constantBuilder().build();
|
||||||
const datasource = datasourceBuilder().build();
|
const datasource = datasourceBuilder().build();
|
||||||
@ -161,7 +136,6 @@ describe('shared actions', () => {
|
|||||||
${null} | ${[null]}
|
${null} | ${[null]}
|
||||||
${undefined} | ${[undefined]}
|
${undefined} | ${[undefined]}
|
||||||
`('and urlValue is $urlValue then correct actions are dispatched', async ({ urlValue, expected }) => {
|
`('and urlValue is $urlValue then correct actions are dispatched', async ({ urlValue, expected }) => {
|
||||||
variableAdapters.set('custom', createCustomVariableAdapter());
|
|
||||||
const custom = customBuilder()
|
const custom = customBuilder()
|
||||||
.withId('0')
|
.withId('0')
|
||||||
.withOptions('A', 'B', 'C')
|
.withOptions('A', 'B', 'C')
|
||||||
@ -195,7 +169,6 @@ describe('shared actions', () => {
|
|||||||
${['A', 'B', 'C']} | ${'X'} | ${'C'} | ${'C'}
|
${['A', 'B', 'C']} | ${'X'} | ${'C'} | ${'C'}
|
||||||
${undefined} | ${'B'} | ${undefined} | ${'A'}
|
${undefined} | ${'B'} | ${undefined} | ${'A'}
|
||||||
`('then correct actions are dispatched', async ({ withOptions, withCurrent, defaultValue, expected }) => {
|
`('then correct actions are dispatched', async ({ withOptions, withCurrent, defaultValue, expected }) => {
|
||||||
variableAdapters.set('custom', createCustomVariableAdapter());
|
|
||||||
let custom;
|
let custom;
|
||||||
|
|
||||||
if (!withOptions) {
|
if (!withOptions) {
|
||||||
@ -249,7 +222,6 @@ describe('shared actions', () => {
|
|||||||
`(
|
`(
|
||||||
'then correct actions are dispatched',
|
'then correct actions are dispatched',
|
||||||
async ({ withOptions, withCurrent, defaultValue, expectedText, expectedSelected }) => {
|
async ({ withOptions, withCurrent, defaultValue, expectedText, expectedSelected }) => {
|
||||||
variableAdapters.set('custom', createCustomVariableAdapter());
|
|
||||||
let custom;
|
let custom;
|
||||||
|
|
||||||
if (!withOptions) {
|
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;
|
let orderIndex = 0;
|
||||||
for (let index = 0; index < list.length; index++) {
|
for (let index = 0; index < list.length; index++) {
|
||||||
const model = list[index];
|
const model = list[index];
|
||||||
if (!variableAdapters.contains(model.type)) {
|
if (!variableAdapters.getIfExists(model.type)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +76,7 @@ export const processVariableDependencies = async (variable: VariableModel, state
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (variableAdapters.contains(variable.type)) {
|
if (variableAdapters.getIfExists(variable.type)) {
|
||||||
if (variableAdapters.get(variable.type).dependsOn(variable, otherVariable)) {
|
if (variableAdapters.get(variable.type).dependsOn(variable, otherVariable)) {
|
||||||
dependencies.push(otherVariable.initLock!.promise);
|
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', () => {
|
describe('processVariable', () => {
|
||||||
// these following processVariable tests will test the following base setup
|
// these following processVariable tests will test the following base setup
|
||||||
// custom doesn't depend on any other variable
|
// custom doesn't depend on any other variable
|
||||||
// queryDependsOnCustom depends on custom
|
// queryDependsOnCustom depends on custom
|
||||||
// queryNoDepends doesn't depend on any other variable
|
// queryNoDepends doesn't depend on any other variable
|
||||||
const getAndSetupProcessVariableContext = () => {
|
const getAndSetupProcessVariableContext = () => {
|
||||||
variableAdapters.set('custom', createCustomVariableAdapter());
|
|
||||||
variableAdapters.set('query', createQueryVariableAdapter());
|
|
||||||
const custom = customBuilder()
|
const custom = customBuilder()
|
||||||
.withId('custom')
|
.withId('custom')
|
||||||
.withName('custom')
|
.withName('custom')
|
||||||
|
@ -1,11 +1,29 @@
|
|||||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||||
import { cleanUpDashboard } from 'app/features/dashboard/state/reducers';
|
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 { VariableAdapter, variableAdapters } from '../adapters';
|
||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import { variablesReducer, VariablesState } from './variablesReducer';
|
import { variablesReducer, VariablesState } from './variablesReducer';
|
||||||
import { toVariablePayload, VariablePayload } from './types';
|
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('variablesReducer', () => {
|
||||||
describe('when cleanUpDashboard is dispatched', () => {
|
describe('when cleanUpDashboard is dispatched', () => {
|
||||||
it('then all variables except global variables should be removed', () => {
|
it('then all variables except global variables should be removed', () => {
|
||||||
@ -91,30 +109,16 @@ describe('variablesReducer', () => {
|
|||||||
skipUrlSync: false,
|
skipUrlSync: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const variableAdapter: VariableAdapter<VariableModel> = {
|
variableAdapters.get('mock').reducer = jest.fn().mockReturnValue(initialState);
|
||||||
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);
|
|
||||||
const mockAction = createAction<VariablePayload>('mockAction');
|
const mockAction = createAction<VariablePayload>('mockAction');
|
||||||
reducerTester<VariablesState>()
|
reducerTester<VariablesState>()
|
||||||
.givenReducer(variablesReducer, initialState)
|
.givenReducer(variablesReducer, initialState)
|
||||||
.whenActionIsDispatched(mockAction(toVariablePayload({ type: 'query', id: '0' })))
|
.whenActionIsDispatched(mockAction(toVariablePayload({ type: ('mock' as unknown) as VariableType, id: '0' })))
|
||||||
.thenStateShouldEqual(initialState);
|
.thenStateShouldEqual(initialState);
|
||||||
expect(variableAdapter.reducer).toHaveBeenCalledTimes(1);
|
expect(variableAdapters.get('mock').reducer).toHaveBeenCalledTimes(1);
|
||||||
expect(variableAdapter.reducer).toHaveBeenCalledWith(
|
expect(variableAdapters.get('mock').reducer).toHaveBeenCalledWith(
|
||||||
initialState,
|
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,
|
skipUrlSync: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const variableAdapter: VariableAdapter<VariableModel> = {
|
variableAdapters.get('mock').reducer = jest.fn().mockReturnValue(initialState);
|
||||||
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);
|
|
||||||
const mockAction = createAction<VariablePayload>('mockAction');
|
const mockAction = createAction<VariablePayload>('mockAction');
|
||||||
reducerTester<VariablesState>()
|
reducerTester<VariablesState>()
|
||||||
.givenReducer(variablesReducer, initialState)
|
.givenReducer(variablesReducer, initialState)
|
||||||
.whenActionIsDispatched(mockAction(toVariablePayload({ type: 'adhoc', id: '0' })))
|
.whenActionIsDispatched(mockAction(toVariablePayload({ type: 'adhoc', id: '0' })))
|
||||||
.thenStateShouldEqual(initialState);
|
.thenStateShouldEqual(initialState);
|
||||||
expect(variableAdapter.reducer).toHaveBeenCalledTimes(0);
|
expect(variableAdapters.get('mock').reducer).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -169,27 +159,13 @@ describe('variablesReducer', () => {
|
|||||||
skipUrlSync: false,
|
skipUrlSync: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const variableAdapter: VariableAdapter<VariableModel> = {
|
variableAdapters.get('mock').reducer = jest.fn().mockReturnValue(initialState);
|
||||||
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);
|
|
||||||
const mockAction = createAction<string>('mockAction');
|
const mockAction = createAction<string>('mockAction');
|
||||||
reducerTester<VariablesState>()
|
reducerTester<VariablesState>()
|
||||||
.givenReducer(variablesReducer, initialState)
|
.givenReducer(variablesReducer, initialState)
|
||||||
.whenActionIsDispatched(mockAction('mocked'))
|
.whenActionIsDispatched(mockAction('mocked'))
|
||||||
.thenStateShouldEqual(initialState);
|
.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 { initialVariablesState, VariablesState } from './variablesReducer';
|
||||||
import { changeVariableNameSucceeded } from '../editor/reducer';
|
import { changeVariableNameSucceeded } from '../editor/reducer';
|
||||||
|
|
||||||
|
variableAdapters.setInit(() => [createQueryVariableAdapter()]);
|
||||||
|
|
||||||
describe('sharedReducer', () => {
|
describe('sharedReducer', () => {
|
||||||
describe('when addVariable is dispatched', () => {
|
describe('when addVariable is dispatched', () => {
|
||||||
it('then state should be correct', () => {
|
it('then state should be correct', () => {
|
||||||
const model = ({ name: 'name from model', type: 'type from model' } as unknown) as QueryVariableModel;
|
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 });
|
const payload = toVariablePayload({ id: '0', type: 'query' }, { global: true, index: 0, model });
|
||||||
variableAdapters.set('query', createQueryVariableAdapter());
|
|
||||||
reducerTester<VariablesState>()
|
reducerTester<VariablesState>()
|
||||||
.givenReducer(sharedReducer, { ...initialVariablesState })
|
.givenReducer(sharedReducer, { ...initialVariablesState })
|
||||||
.whenActionIsDispatched(addVariable(payload))
|
.whenActionIsDispatched(addVariable(payload))
|
||||||
@ -107,7 +108,6 @@ describe('sharedReducer', () => {
|
|||||||
|
|
||||||
describe('when duplicateVariable is dispatched', () => {
|
describe('when duplicateVariable is dispatched', () => {
|
||||||
it('then state should be correct', () => {
|
it('then state should be correct', () => {
|
||||||
variableAdapters.set('query', createQueryVariableAdapter());
|
|
||||||
const initialState: VariablesState = getVariableState(3);
|
const initialState: VariablesState = getVariableState(3);
|
||||||
const payload = toVariablePayload({ id: '1', type: 'query' }, { newId: '11' });
|
const payload = toVariablePayload({ id: '1', type: 'query' }, { newId: '11' });
|
||||||
reducerTester<VariablesState>()
|
reducerTester<VariablesState>()
|
||||||
@ -193,7 +193,6 @@ describe('sharedReducer', () => {
|
|||||||
|
|
||||||
describe('when storeNewVariable is dispatched', () => {
|
describe('when storeNewVariable is dispatched', () => {
|
||||||
it('then state should be correct', () => {
|
it('then state should be correct', () => {
|
||||||
variableAdapters.set('query', createQueryVariableAdapter());
|
|
||||||
const initialState: VariablesState = getVariableState(3, -1, true);
|
const initialState: VariablesState = getVariableState(3, -1, true);
|
||||||
const payload = toVariablePayload({ id: '11', type: 'query' });
|
const payload = toVariablePayload({ id: '11', type: 'query' });
|
||||||
reducerTester<VariablesState>()
|
reducerTester<VariablesState>()
|
||||||
|
@ -27,7 +27,7 @@ export const variablesReducer = (
|
|||||||
return variables;
|
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:
|
// 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
|
// Firstly call the sharedTemplatingReducer that handles all shared actions between variable types
|
||||||
// Secondly call the specific variable type's reducer
|
// Secondly call the specific variable type's reducer
|
||||||
|
@ -11,7 +11,7 @@ import { setCurrentVariableValue } from '../state/sharedReducer';
|
|||||||
import { initDashboardTemplating } from '../state/actions';
|
import { initDashboardTemplating } from '../state/actions';
|
||||||
|
|
||||||
describe('textbox actions', () => {
|
describe('textbox actions', () => {
|
||||||
variableAdapters.set('textbox', createTextBoxVariableAdapter());
|
variableAdapters.setInit(() => [createTextBoxVariableAdapter()]);
|
||||||
|
|
||||||
describe('when updateTextBoxVariableOptions is dispatched', () => {
|
describe('when updateTextBoxVariableOptions is dispatched', () => {
|
||||||
it('then correct actions are dispatched', async () => {
|
it('then correct actions are dispatched', async () => {
|
||||||
|
@ -12,8 +12,9 @@ import { toVariableIdentifier } from '../state/types';
|
|||||||
|
|
||||||
export const createTextBoxVariableAdapter = (): VariableAdapter<TextBoxVariableModel> => {
|
export const createTextBoxVariableAdapter = (): VariableAdapter<TextBoxVariableModel> => {
|
||||||
return {
|
return {
|
||||||
|
id: 'textbox',
|
||||||
description: 'Define a textbox variable, where users can enter any arbitrary string',
|
description: 'Define a textbox variable, where users can enter any arbitrary string',
|
||||||
label: 'Text box',
|
name: 'Text box',
|
||||||
initialState: initialTextBoxVariableModelState,
|
initialState: initialTextBoxVariableModelState,
|
||||||
reducer: textBoxVariableReducer,
|
reducer: textBoxVariableReducer,
|
||||||
picker: TextBoxVariablePicker,
|
picker: TextBoxVariablePicker,
|
||||||
|
Loading…
Reference in New Issue
Block a user