Variables: move state tree into a keyed state (#44642)

* Variables: move state tree into a keyed state

* Update public/app/features/variables/state/transactionReducer.ts

Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com>

* Chore: fix prettier error

* Chore: renamed slices and lastUid

* Chore: rename toUidAction

* Chore: rename dashboardVariableReducer

* Chore: rename state prop back to templating

* Chore renames variable.dashboardUid

* Chore: rename toDashboardVariableIdentifier

* Chore: rename getDashboardVariable

* Chore: rename getDashboardVariablesState

* Chore: rename getDashboardVariables

* Chore: some more renames

* Chore: small clean up

* Chore: small rename

* Chore: removes unused function

* Chore: rename VariableModel.stateKey

* Chore: rename KeyedVariableIdentifier.stateKey

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

* user essentials mob! 🔱

Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com>
Co-authored-by: kay delaney <kay@grafana.com>
Co-authored-by: Alexandra Vargas <alexa1866@gmail.com>
Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
Hugo Häggmark
2022-02-18 06:06:04 +01:00
committed by GitHub
co-authored by kay delaney kay delaney Alexandra Vargas Ashley Harrison
parent a9de33601c
commit dbec2b02fd
105 changed files with 2894 additions and 1644 deletions
+1 -1
View File
@@ -209,7 +209,7 @@ exports[`no enzyme tests`] = {
"public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.test.tsx:4134073823": [
[1, 17, 13, "RegExp match", "2409514259"]
],
"public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx:1141305288": [
"public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx:1044891955": [
[1, 35, 13, "RegExp match", "2409514259"]
],
"public/app/features/dashboard/dashgrid/DashboardGrid.test.tsx:1798654441": [
+2 -2
View File
@@ -13,11 +13,11 @@ import usersReducers from 'app/features/users/state/reducers';
import userReducers from 'app/features/profile/state/reducers';
import organizationReducers from 'app/features/org/state/reducers';
import ldapReducers from 'app/features/admin/state/reducers';
import templatingReducers from 'app/features/variables/state/reducers';
import importDashboardReducers from 'app/features/manage-dashboards/state/reducers';
import panelEditorReducers from 'app/features/dashboard/components/PanelEditor/state/reducers';
import panelsReducers from 'app/features/panel/state/reducers';
import serviceAccountsReducer from 'app/features/serviceaccounts/state/reducers';
import templatingReducers from 'app/features/variables/state/keyedVariablesReducer';
const rootReducers = {
...sharedReducers,
@@ -33,10 +33,10 @@ const rootReducers = {
...userReducers,
...organizationReducers,
...ldapReducers,
...templatingReducers,
...importDashboardReducers,
...panelEditorReducers,
...panelsReducers,
...templatingReducers,
plugins: pluginsReducer,
};
@@ -70,7 +70,7 @@ export function DashboardSettings({ dashboard, editview }: Props) {
title: 'Variables',
id: 'templating',
icon: 'calculator-alt',
component: <VariableEditorContainer />,
component: <VariableEditorContainer dashboard={dashboard} />,
});
pages.push({
@@ -34,7 +34,6 @@ import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
import { toggleTableView } from './state/reducers';
import { getPanelEditorTabs } from './state/selectors';
import { getVariables } from 'app/features/variables/state/selectors';
import { StoreState } from 'app/types';
import { DisplayMode, displayModes, PanelEditorTab } from './types';
@@ -55,6 +54,7 @@ import { notifyApp } from '../../../../core/actions';
import { PanelEditorTableView } from './PanelEditorTableView';
import { PanelModelWithLibraryPanel } from 'app/features/library-panels/types';
import { getPanelStateForModel } from 'app/features/panel/state/selectors';
import { getVariablesByKey } from '../../../variables/state/selectors';
interface OwnProps {
dashboard: DashboardModel;
@@ -62,7 +62,7 @@ interface OwnProps {
tab?: string;
}
const mapStateToProps = (state: StoreState) => {
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => {
const panel = state.panelEditor.getPanel();
const panelState = getPanelStateForModel(state, panel);
@@ -73,7 +73,7 @@ const mapStateToProps = (state: StoreState) => {
initDone: state.panelEditor.initDone,
uiState: state.panelEditor.ui,
tableViewEnabled: state.panelEditor.tableViewEnabled,
variables: getVariables(state),
variables: getVariablesByKey(ownProps.dashboard.uid, state),
};
};
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import { Select } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { getVariables } from '../../../variables/state/selectors';
import { getLastKey, getVariablesByKey } from '../../../variables/state/selectors';
import { StoreState } from '../../../../types';
export interface Props {
@@ -13,7 +13,9 @@ export interface Props {
}
export const RepeatRowSelect: FC<Props> = ({ repeat, onChange, id }) => {
const variables = useSelector((state: StoreState) => getVariables(state));
const variables = useSelector((state: StoreState) => {
return getVariablesByKey(getLastKey(state), state);
});
const variableOptions = useMemo(() => {
const options = variables.map((item: any) => {
@@ -96,7 +96,7 @@ function shareLinkScenario(description: string, scenarioFn: (ctx: ScenarioContex
}
describe('ShareModal', () => {
let templateSrv = initTemplateSrv([]);
let templateSrv = initTemplateSrv('key', []);
beforeAll(() => {
variableAdapters.register(createQueryVariableAdapter());
@@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import { connect, MapStateToProps } from 'react-redux';
import { StoreState } from '../../../../types';
import { getSubMenuVariables } from '../../../variables/state/selectors';
import { getSubMenuVariables, getVariablesState } from '../../../variables/state/selectors';
import { VariableModel } from '../../../variables/types';
import { DashboardModel } from '../../state';
import { DashboardLinks } from './DashboardLinks';
@@ -64,9 +64,11 @@ class SubMenuUnConnected extends PureComponent<Props> {
}
}
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state) => {
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, ownProps) => {
const { uid } = ownProps.dashboard;
const templatingState = getVariablesState(uid, state);
return {
variables: getSubMenuVariables(state.templating.variables),
variables: getSubMenuVariables(uid, templatingState.variables),
};
};
@@ -168,7 +168,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
const templateVarChanges = findTemplateVarChanges(this.props.queryParams, prevProps.queryParams);
if (templateVarChanges) {
templateVarsChangedInUrl(templateVarChanges);
templateVarsChangedInUrl(dashboard.uid, templateVarChanges);
}
}
@@ -37,7 +37,7 @@ import {
UrlQueryValue,
} from '@grafana/data';
import { CoreEvents, DashboardMeta, KioskMode } from 'app/types';
import { GetVariables, getVariables } from 'app/features/variables/state/selectors';
import { GetVariables, getVariablesByKey } from 'app/features/variables/state/selectors';
import { variableAdapters } from 'app/features/variables/adapters';
import { onTimeRangeUpdated } from 'app/features/variables/state/actions';
import { dispatch } from '../../../store/store';
@@ -139,7 +139,7 @@ export class DashboardModel implements TimeModel {
lastRefresh: true,
};
constructor(data: any, meta?: DashboardMeta, private getVariablesFromState: GetVariables = getVariables) {
constructor(data: any, meta?: DashboardMeta, private getVariablesFromState: GetVariables = getVariablesByKey) {
if (!data) {
data = {};
}
@@ -346,7 +346,7 @@ export class DashboardModel implements TimeModel {
defaults: { saveTimerange: boolean; saveVariables: boolean } & CloneOptions
) {
const originalVariables = this.originalTemplating;
const currentVariables = this.getVariablesFromState();
const currentVariables = this.getVariablesFromState(this.uid);
copy.templating = {
list: currentVariables.map((variable) =>
@@ -374,7 +374,7 @@ export class DashboardModel implements TimeModel {
timeRangeUpdated(timeRange: TimeRange) {
this.events.publish(new TimeRangeUpdatedEvent(timeRange));
dispatch(onTimeRangeUpdated(timeRange));
dispatch(onTimeRangeUpdated(this.uid, timeRange));
}
startRefresh(event: VariablesChangedEvent = { refreshAll: true, panelIds: [] }) {
@@ -1085,11 +1085,11 @@ export class DashboardModel implements TimeModel {
return;
}
this.originalTemplating = this.cloneVariablesFrom(this.getVariablesFromState());
this.originalTemplating = this.cloneVariablesFrom(this.getVariablesFromState(this.uid));
}
hasVariableValuesChanged() {
return this.hasVariablesChanged(this.originalTemplating, this.getVariablesFromState());
return this.hasVariablesChanged(this.originalTemplating, this.getVariablesFromState(this.uid));
}
autoFitPanels(viewHeight: number, kioskMode?: UrlQueryValue) {
@@ -1164,7 +1164,7 @@ export class DashboardModel implements TimeModel {
}
getVariables = () => {
return this.getVariablesFromState();
return this.getVariablesFromState(this.uid);
};
canAddAnnotations() {
@@ -1179,7 +1179,7 @@ export class DashboardModel implements TimeModel {
}
private getPanelRepeatVariable(panel: PanelModel) {
return this.getVariablesFromState().find((variable) => variable.name === panel.repeat);
return this.getVariablesFromState(this.uid).find((variable) => variable.name === panel.repeat);
}
private isSnapshotTruthy() {
@@ -1187,7 +1187,7 @@ export class DashboardModel implements TimeModel {
}
private hasVariables() {
return this.getVariablesFromState().length > 0;
return this.getVariablesFromState(this.uid).length > 0;
}
private hasVariablesChanged(originalVariables: any[], currentVariables: any[]): boolean {
@@ -119,12 +119,12 @@ export const cleanUpDashboardAndVariables = (): ThunkResult<void> => (dispatch,
if (dashboard) {
dashboard.destroy();
dispatch(cancelVariables(dashboard.uid));
}
getTimeSrv().stopAutoRefresh();
dispatch(cleanUpDashboard());
dispatch(cancelVariables());
};
export const updateTimeZoneDashboard =
@@ -11,7 +11,7 @@ import { Echo } from '../../../core/services/echo/Echo';
import { variableAdapters } from 'app/features/variables/adapters';
import { createConstantVariableAdapter } from 'app/features/variables/constant/adapter';
import { constantBuilder } from 'app/features/variables/shared/testing/builders';
import { variablesInitTransaction } from '../../variables/state/transactionReducer';
import { initialTransactionState, variablesInitTransaction } from '../../variables/state/transactionReducer';
import { keybindingSrv } from 'app/core/services/keybindingSrv';
import { getTimeSrv, setTimeSrv } from '../services/TimeSrv';
import { DashboardLoaderSrv, setDashboardLoaderSrv } from '../services/DashboardLoaderSrv';
@@ -22,6 +22,7 @@ import {
} from '../../query/state/DashboardQueryRunner/DashboardQueryRunner';
import { emptyResult } from '../../query/state/DashboardQueryRunner/utils';
import { TransactionStatus } from '../../variables/types';
import { getPreloadedState } from '../../variables/state/helpers';
jest.mock('app/core/services/backend_srv');
jest.mock('app/features/dashboard/services/TimeSrv', () => {
@@ -53,7 +54,7 @@ interface ScenarioContext {
}
type ScenarioFn = (ctx: ScenarioContext) => void;
const DASH_UID = 'DGmvKKxZz';
function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
describe(description, () => {
const loaderSrv = {
@@ -83,6 +84,7 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
templating: {
list: [constantBuilder().build()],
},
uid: DASH_UID,
},
})),
};
@@ -100,7 +102,7 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
const ctx: ScenarioContext = {
args: {
urlUid: 'DGmvKKxZz',
urlUid: DASH_UID,
fixUrl: false,
routeName: DashboardRoutes.Normal,
},
@@ -120,10 +122,10 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
queries: [],
},
},
templating: {
...getPreloadedState(DASH_UID, {
variables: {},
transaction: { uid: 'DGmvKKxZz', status: TransactionStatus.Completed },
},
transaction: { ...initialTransactionState, uid: DASH_UID, status: TransactionStatus.Completed },
}),
},
setup: (fn: () => void) => {
setupFn = fn;
@@ -258,7 +260,7 @@ describeInitScenario('Initializing existing dashboard', (ctx) => {
});
it('Should initialize redux variables if newVariables is enabled', () => {
expect(ctx.actions[2].type).toBe(variablesInitTransaction.type);
expect(ctx.actions[2].payload.action.type).toBe(variablesInitTransaction.type);
});
});
@@ -287,3 +289,14 @@ describeInitScenario('Initializing previously canceled dashboard initialization'
expect(getDashboardQueryRunner().run).toBeCalled();
});
});
describeInitScenario('Initializing snapshot dashboard', (ctx) => {
ctx.setup(() => {
ctx.args.urlUid = undefined;
});
it('Should send action initVariablesTransaction with correct payload', () => {
expect(ctx.actions[2].payload.action.type).toBe(variablesInitTransaction.type);
expect(ctx.actions[2].payload.action.payload.uid).toBe(DASH_UID);
});
});
@@ -23,6 +23,8 @@ import { emitDashboardViewEvent } from './analyticsProcessor';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { config, locationService } from '@grafana/runtime';
import { createDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner';
import { getIfExistsLastKey } from '../../variables/state/selectors';
import { toStateKey } from 'app/features/variables/utils';
export interface InitDashboardArgs {
urlUid?: string;
@@ -155,15 +157,16 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
timeSrv.init(dashboard);
const dashboardUid = toStateKey(args.urlUid ?? dashboard.uid);
// template values service needs to initialize completely before the rest of the dashboard can load
await dispatch(initVariablesTransaction(args.urlUid!, dashboard));
await dispatch(initVariablesTransaction(dashboardUid, dashboard));
// DashboardQueryRunner needs to run after all variables have been resolved so that any annotation query including a variable
// will be correctly resolved
const runner = createDashboardQueryRunner({ dashboard, timeSrv });
runner.run({ dashboard, range: timeSrv.timeRange() });
if (getState().templating.transaction.uid !== args.urlUid) {
if (getIfExistsLastKey(getState()) !== dashboardUid) {
// if a previous dashboard has slow running variable queries the batch uid will be the new one
// but the args.urlUid will be the same as before initVariablesTransaction was called so then we can't continue initializing
// the previous dashboard.
@@ -32,7 +32,7 @@ describe('linkSrv', () => {
_dashboard.refresh = false;
setTimeSrv(timeSrv);
templateSrv = initTemplateSrv([
templateSrv = initTemplateSrv('key', [
{ type: 'query', name: 'home', current: { value: '127.0.0.1' } },
{ type: 'query', name: 'server1', current: { value: '192.168.0.100' } },
]);
@@ -67,7 +67,7 @@ function expectOnResults(args: {
done();
} catch (err) {
subscription.unsubscribe();
done.fail(err);
done(err);
}
},
});
@@ -69,7 +69,7 @@ function expectOnResults(args: {
expectCallback(value);
done();
} catch (err) {
done.fail(err);
done(err);
}
},
});
@@ -9,6 +9,8 @@ import { FormatRegistryID } from './formatRegistry';
import { setDataSourceSrv } from '@grafana/runtime';
import { mockDataSource, MockDataSourceSrv } from '../alerting/unified/mocks';
const key = 'key';
variableAdapters.setInit(() => [
createQueryVariableAdapter() as unknown as VariableAdapter<VariableModel>,
createAdHocVariableAdapter() as unknown as VariableAdapter<VariableModel>,
@@ -20,7 +22,7 @@ describe('templateSrv', () => {
describe('init', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('should initialize template data', () => {
@@ -31,7 +33,7 @@ describe('templateSrv', () => {
describe('replace can pass scoped vars', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('scoped vars should support objects', () => {
@@ -115,7 +117,7 @@ describe('templateSrv', () => {
describe('getAdhocFilters', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([
_templateSrv = initTemplateSrv(key, [
{
type: 'datasource',
name: 'ds',
@@ -152,7 +154,7 @@ describe('templateSrv', () => {
describe('replace can pass multi / all format', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([
_templateSrv = initTemplateSrv(key, [
{
type: 'query',
name: 'test',
@@ -168,7 +170,7 @@ describe('templateSrv', () => {
describe('when the globbed variable only has one value', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([
_templateSrv = initTemplateSrv(key, [
{
type: 'query',
name: 'test',
@@ -216,7 +218,7 @@ describe('templateSrv', () => {
describe('variable with all option', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([
_templateSrv = initTemplateSrv(key, [
{
type: 'query',
name: 'test',
@@ -254,7 +256,7 @@ describe('templateSrv', () => {
describe('variable with all option and custom value', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([
_templateSrv = initTemplateSrv(key, [
{
type: 'query',
name: 'test',
@@ -298,19 +300,19 @@ describe('templateSrv', () => {
describe('lucene format', () => {
it('should properly escape $test with lucene escape sequences', () => {
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
const target = _templateSrv.replace('this:$test', {}, 'lucene');
expect(target).toBe('this:value\\/4');
});
it('should properly escape ${test} with lucene escape sequences', () => {
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
const target = _templateSrv.replace('this:${test}', {}, 'lucene');
expect(target).toBe('this:value\\/4');
});
it('should properly escape ${test:lucene} with lucene escape sequences', () => {
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
const target = _templateSrv.replace('this:${test:lucene}', {});
expect(target).toBe('this:value\\/4');
});
@@ -318,7 +320,7 @@ describe('templateSrv', () => {
describe('html format', () => {
it('should encode values html escape sequences', () => {
_templateSrv = initTemplateSrv([
_templateSrv = initTemplateSrv(key, [
{ type: 'query', name: 'test', current: { value: '<script>alert(asd)</script>' } },
]);
const target = _templateSrv.replace('$test', {}, 'html');
@@ -426,7 +428,7 @@ describe('templateSrv', () => {
describe('can check if variable exists', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('should return true if $test exists', () => {
@@ -467,7 +469,7 @@ describe('templateSrv', () => {
describe('can highlight variables in string', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
});
it('should insert html', () => {
@@ -488,7 +490,7 @@ describe('templateSrv', () => {
describe('updateIndex with simple value', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'muuuu' } }]);
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'muuuu' } }]);
});
it('should set current value and update template data', () => {
@@ -499,7 +501,7 @@ describe('templateSrv', () => {
describe('replaceWithText', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([
_templateSrv = initTemplateSrv(key, [
{
type: 'query',
name: 'server',
@@ -544,7 +546,7 @@ describe('templateSrv', () => {
describe('replaceWithText can pass all / multi value', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([
_templateSrv = initTemplateSrv(key, [
{
type: 'query',
name: 'server',
@@ -595,7 +597,7 @@ describe('templateSrv', () => {
describe('built in interval variables', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([]);
_templateSrv = initTemplateSrv(key, []);
});
it('should replace $__interval_ms with interval milliseconds', () => {
@@ -608,7 +610,7 @@ describe('templateSrv', () => {
describe('date formating', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([], {
_templateSrv = initTemplateSrv(key, [], {
from: dateTime(1594671549254),
to: dateTime(1595237229747),
} as TimeRange);
@@ -642,7 +644,7 @@ describe('templateSrv', () => {
describe('handle objects gracefully', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: { test: 'A' } } }]);
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: { test: 'A' } } }]);
});
it('should not pass object to custom function', () => {
@@ -658,7 +660,7 @@ describe('templateSrv', () => {
describe('handle objects gracefully and call toString if defined', () => {
beforeEach(() => {
const value = { test: 'A', toString: () => 'hello' };
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value } }]);
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value } }]);
});
it('should not pass object to custom function', () => {
@@ -673,7 +675,7 @@ describe('templateSrv', () => {
describe('adhoc variables', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([
_templateSrv = initTemplateSrv(key, [
{
type: 'adhoc',
name: 'adhoc',
@@ -713,7 +715,7 @@ describe('templateSrv', () => {
describe('queryparam', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([
_templateSrv = initTemplateSrv(key, [
{
type: 'query',
name: 'single',
@@ -1,11 +1,11 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { AdHocVariableEditorUnConnected as AdHocVariableEditor } from './AdHocVariableEditor';
import { initialAdHocVariableModelState } from './reducer';
import { selectOptionInTest } from '@grafana/ui';
import { getSelectParent } from '@grafana/ui/src/components/Select/test-utils';
import { AdHocVariableEditorUnConnected as AdHocVariableEditor } from './AdHocVariableEditor';
import { adHocBuilder } from '../shared/testing/builders';
const props = {
extended: {
dataSources: [
@@ -13,7 +13,7 @@ const props = {
{ text: 'Loki', value: { type: 'loki-ds', uid: 'abc' } },
],
},
variable: { ...initialAdHocVariableModelState },
variable: adHocBuilder().withId('adhoc').withRootStateKey('key').withName('adhoc').build(),
onPropChange: jest.fn(),
// connected actions
@@ -37,7 +37,10 @@ describe('AdHocVariableEditor', () => {
render(<AdHocVariableEditor {...props} />);
await selectOptionInTest(screen.getByLabelText('Data source'), 'Loki');
expect(props.changeVariableDatasource).toBeCalledWith({ type: 'loki-ds', uid: 'abc' });
expect(props.changeVariableDatasource).toBeCalledWith(
{ type: 'adhoc', id: 'adhoc', rootStateKey: 'key' },
{ type: 'loki-ds', uid: 'abc' }
);
});
it('renders informational text', () => {
@@ -5,15 +5,31 @@ import { DataSourceRef, SelectableValue } from '@grafana/data';
import { AdHocVariableModel } from '../types';
import { VariableEditorProps } from '../editor/types';
import { initialVariableEditorState } from '../editor/reducer';
import { changeVariableDatasource, initAdHocVariableEditor } from './actions';
import { StoreState } from 'app/types';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableSelectField } from '../editor/VariableSelectField';
import { getAdhocVariableEditorState } from '../editor/selectors';
import { getVariablesState } from '../state/selectors';
import { toKeyedVariableIdentifier } from '../utils';
const mapStateToProps = (state: StoreState) => ({
extended: getAdhocVariableEditorState(state.templating.editor),
});
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => {
const { rootStateKey } = ownProps.variable;
if (!rootStateKey) {
console.error('AdHocVariableEditor: variable has no rootStateKey');
return {
extended: getAdhocVariableEditorState(initialVariableEditorState),
};
}
const { editor } = getVariablesState(rootStateKey, state);
return {
extended: getAdhocVariableEditorState(editor),
};
};
const mapDispatchToProps = {
initAdHocVariableEditor,
@@ -28,11 +44,17 @@ type Props = OwnProps & ConnectedProps<typeof connector>;
export class AdHocVariableEditorUnConnected extends PureComponent<Props> {
componentDidMount() {
this.props.initAdHocVariableEditor();
const { rootStateKey } = this.props.variable;
if (!rootStateKey) {
console.error('AdHocVariableEditor: variable has no rootStateKey');
return;
}
this.props.initAdHocVariableEditor(rootStateKey);
}
onDatasourceChanged = (option: SelectableValue<DataSourceRef>) => {
this.props.changeVariableDatasource(option.value);
this.props.changeVariableDatasource(toKeyedVariableIdentifier(this.props.variable), option.value);
};
render() {
@@ -3,8 +3,7 @@ import { DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data'
import { variableAdapters } from '../adapters';
import { createAdHocVariableAdapter } from './adapter';
import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { getRootReducer, RootReducerType } from '../state/helpers';
import { toVariableIdentifier, toVariablePayload } from '../state/types';
import { getPreloadedState, getRootReducer, RootReducerType } from '../state/helpers';
import {
addFilter,
AdHocTableOptions,
@@ -21,6 +20,8 @@ import { VariableModel } from 'app/features/variables/types';
import { changeVariableEditorExtended, setIdInEditor } from '../editor/reducer';
import { adHocBuilder } from '../shared/testing/builders';
import { locationService } from '@grafana/runtime';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
const getList = jest.fn().mockReturnValue([]);
const getDatasource = jest.fn().mockResolvedValue({});
@@ -55,6 +56,7 @@ const expectedDatasources = [
describe('adhoc actions', () => {
describe('when applyFilterFromTable is dispatched and filter already exist', () => {
it('then correct actions are dispatched', async () => {
const key = 'key';
const options: AdHocTableOptions = {
datasource: { uid: 'influxdb' },
key: 'filter-key',
@@ -71,12 +73,13 @@ describe('adhoc actions', () => {
const variable = adHocBuilder()
.withId('Filters')
.withRootStateKey(key)
.withName('Filters')
.withFilters([existingFilter])
.withDatasource(options.datasource)
.build();
const tester = await reduxTester<RootReducerType>()
const tester = await reduxTester<RootReducerType>({ preloadedState: getPreloadedState(key, {}) })
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenAsyncActionIsDispatched(applyFilterFromTable(options), true);
@@ -84,7 +87,9 @@ describe('adhoc actions', () => {
const expectedQuery = { 'var-Filters': ['filter-key|!=|filter-existing', 'filter-key|=|filter-value'] };
const expectedFilter = { key: 'filter-key', value: 'filter-value', operator: '=', condition: '' };
tester.thenDispatchedActionsShouldEqual(filterAdded(toVariablePayload(variable, expectedFilter)));
tester.thenDispatchedActionsShouldEqual(
toKeyedAction(key, filterAdded(toVariablePayload(variable, expectedFilter)))
);
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
});
@@ -92,6 +97,7 @@ describe('adhoc actions', () => {
describe('when applyFilterFromTable is dispatched and previously no variable or filter exists', () => {
it('then correct actions are dispatched', async () => {
const key = 'key';
const options: AdHocTableOptions = {
datasource: { uid: 'influxdb' },
key: 'filter-key',
@@ -99,18 +105,23 @@ describe('adhoc actions', () => {
operator: '=',
};
const tester = await reduxTester<RootReducerType>()
const tester = await reduxTester<RootReducerType>({ preloadedState: getPreloadedState(key, {}) })
.givenRootReducer(getRootReducer())
.whenAsyncActionIsDispatched(applyFilterFromTable(options), true);
const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource(options.datasource).build();
const variable = adHocBuilder()
.withId('Filters')
.withRootStateKey(key)
.withName('Filters')
.withDatasource(options.datasource)
.build();
const expectedQuery = { 'var-Filters': ['filter-key|=|filter-value'] };
const expectedFilter = { key: 'filter-key', value: 'filter-value', operator: '=', condition: '' };
tester.thenDispatchedActionsShouldEqual(
createAddVariableAction(variable),
filterAdded(toVariablePayload(variable, expectedFilter))
toKeyedAction(key, filterAdded(toVariablePayload(variable, expectedFilter)))
);
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
@@ -119,6 +130,7 @@ describe('adhoc actions', () => {
describe('when applyFilterFromTable is dispatched and previously no filter exists', () => {
it('then correct actions are dispatched', async () => {
const key = 'key';
const options: AdHocTableOptions = {
datasource: { uid: 'influxdb' },
key: 'filter-key',
@@ -128,12 +140,13 @@ describe('adhoc actions', () => {
const variable = adHocBuilder()
.withId('Filters')
.withRootStateKey(key)
.withName('Filters')
.withFilters([])
.withDatasource(options.datasource)
.build();
const tester = await reduxTester<RootReducerType>()
const tester = await reduxTester<RootReducerType>({ preloadedState: getPreloadedState(key, {}) })
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenAsyncActionIsDispatched(applyFilterFromTable(options), true);
@@ -141,13 +154,16 @@ describe('adhoc actions', () => {
const expectedFilter = { key: 'filter-key', value: 'filter-value', operator: '=', condition: '' };
const expectedQuery = { 'var-Filters': ['filter-key|=|filter-value'] };
tester.thenDispatchedActionsShouldEqual(filterAdded(toVariablePayload(variable, expectedFilter)));
tester.thenDispatchedActionsShouldEqual(
toKeyedAction(key, filterAdded(toVariablePayload(variable, expectedFilter)))
);
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
});
});
describe('when applyFilterFromTable is dispatched and adhoc variable with other datasource exists', () => {
it('then correct actions are dispatched', async () => {
const key = 'key';
const options: AdHocTableOptions = {
datasource: { uid: 'influxdb' },
key: 'filter-key',
@@ -157,13 +173,19 @@ describe('adhoc actions', () => {
const existing = adHocBuilder()
.withId('elastic-filter')
.withRootStateKey(key)
.withName('elastic-filter')
.withDatasource({ uid: 'elasticsearch' })
.build();
const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource(options.datasource).build();
const variable = adHocBuilder()
.withId('Filters')
.withRootStateKey(key)
.withName('Filters')
.withDatasource(options.datasource)
.build();
const tester = await reduxTester<RootReducerType>()
const tester = await reduxTester<RootReducerType>({ preloadedState: getPreloadedState(key, {}) })
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(existing))
.whenAsyncActionIsDispatched(applyFilterFromTable(options), true);
@@ -173,7 +195,7 @@ describe('adhoc actions', () => {
tester.thenDispatchedActionsShouldEqual(
createAddVariableAction(variable, 1),
filterAdded(toVariablePayload(variable, expectedFilter))
toKeyedAction(key, filterAdded(toVariablePayload(variable, expectedFilter)))
);
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
@@ -182,6 +204,7 @@ describe('adhoc actions', () => {
describe('when changeFilter is dispatched', () => {
it('then correct actions are dispatched', async () => {
const key = 'key';
const existing = {
key: 'key',
value: 'value',
@@ -196,6 +219,7 @@ describe('adhoc actions', () => {
const variable = adHocBuilder()
.withId('elastic-filter')
.withRootStateKey(key)
.withName('elastic-filter')
.withFilters([existing])
.withDatasource({ uid: 'elasticsearch' })
@@ -206,12 +230,14 @@ describe('adhoc actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenAsyncActionIsDispatched(changeFilter('elastic-filter', update), true);
.whenAsyncActionIsDispatched(changeFilter(toKeyedVariableIdentifier(variable), update), true);
const expectedQuery = { 'var-elastic-filter': ['key|!=|value'] };
const expectedUpdate = { index: 0, filter: updated };
tester.thenDispatchedActionsShouldEqual(filterUpdated(toVariablePayload(variable, expectedUpdate)));
tester.thenDispatchedActionsShouldEqual(
toKeyedAction(key, filterUpdated(toVariablePayload(variable, expectedUpdate)))
);
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
});
@@ -219,6 +245,7 @@ describe('adhoc actions', () => {
describe('when addFilter is dispatched on variable with existing filter', () => {
it('then correct actions are dispatched', async () => {
const key = 'key';
const existing = {
key: 'key',
value: 'value',
@@ -233,6 +260,7 @@ describe('adhoc actions', () => {
const variable = adHocBuilder()
.withId('elastic-filter')
.withRootStateKey(key)
.withName('elastic-filter')
.withFilters([existing])
.withDatasource({ uid: 'elasticsearch' })
@@ -241,18 +269,21 @@ describe('adhoc actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenAsyncActionIsDispatched(addFilter('elastic-filter', adding), true);
.whenAsyncActionIsDispatched(addFilter(toKeyedVariableIdentifier(variable), adding), true);
const expectedQuery = { 'var-elastic-filter': ['key|=|value', 'key|!=|value'] };
const expectedFilter = { key: 'key', value: 'value', operator: '!=', condition: '' };
tester.thenDispatchedActionsShouldEqual(filterAdded(toVariablePayload(variable, expectedFilter)));
tester.thenDispatchedActionsShouldEqual(
toKeyedAction(key, filterAdded(toVariablePayload(variable, expectedFilter)))
);
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
});
});
describe('when addFilter is dispatched on variable with no existing filter', () => {
it('then correct actions are dispatched', async () => {
const key = 'key';
const adding = {
key: 'key',
value: 'value',
@@ -262,6 +293,7 @@ describe('adhoc actions', () => {
const variable = adHocBuilder()
.withId('elastic-filter')
.withRootStateKey(key)
.withName('elastic-filter')
.withFilters([])
.withDatasource({ uid: 'elasticsearch' })
@@ -270,19 +302,21 @@ describe('adhoc actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenAsyncActionIsDispatched(addFilter('elastic-filter', adding), true);
.whenAsyncActionIsDispatched(addFilter(toKeyedVariableIdentifier(variable), adding), true);
const expectedQuery = { 'var-elastic-filter': ['key|=|value'] };
tester.thenDispatchedActionsShouldEqual(filterAdded(toVariablePayload(variable, adding)));
tester.thenDispatchedActionsShouldEqual(toKeyedAction(key, filterAdded(toVariablePayload(variable, adding))));
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
});
});
describe('when removeFilter is dispatched on variable with no existing filter', () => {
it('then correct actions are dispatched', async () => {
const key = 'key';
const variable = adHocBuilder()
.withId('elastic-filter')
.withRootStateKey(key)
.withName('elastic-filter')
.withFilters([])
.withDatasource({ uid: 'elasticsearch' })
@@ -291,17 +325,18 @@ describe('adhoc actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenAsyncActionIsDispatched(removeFilter('elastic-filter', 0), true);
.whenAsyncActionIsDispatched(removeFilter(toKeyedVariableIdentifier(variable), 0), true);
const expectedQuery = { 'var-elastic-filter': [] as string[] };
tester.thenDispatchedActionsShouldEqual(filterRemoved(toVariablePayload(variable, 0)));
tester.thenDispatchedActionsShouldEqual(toKeyedAction(key, filterRemoved(toVariablePayload(variable, 0))));
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
});
});
describe('when removeFilter is dispatched on variable with existing filter', () => {
it('then correct actions are dispatched', async () => {
const key = 'key';
const filter = {
key: 'key',
value: 'value',
@@ -311,6 +346,7 @@ describe('adhoc actions', () => {
const variable = adHocBuilder()
.withId('elastic-filter')
.withRootStateKey(key)
.withName('elastic-filter')
.withFilters([filter])
.withDatasource({ uid: 'elasticsearch' })
@@ -319,17 +355,18 @@ describe('adhoc actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenAsyncActionIsDispatched(removeFilter('elastic-filter', 0), true);
.whenAsyncActionIsDispatched(removeFilter(toKeyedVariableIdentifier(variable), 0), true);
const expectedQuery = { 'var-elastic-filter': [] as string[] };
tester.thenDispatchedActionsShouldEqual(filterRemoved(toVariablePayload(variable, 0)));
tester.thenDispatchedActionsShouldEqual(toKeyedAction(key, filterRemoved(toVariablePayload(variable, 0))));
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
});
});
describe('when setFiltersFromUrl is dispatched', () => {
it('then correct actions are dispatched', async () => {
const key = 'key';
const existing = {
key: 'key',
value: 'value',
@@ -339,6 +376,7 @@ describe('adhoc actions', () => {
const variable = adHocBuilder()
.withId('elastic-filter')
.withRootStateKey(key)
.withName('elastic-filter')
.withFilters([existing])
.withDatasource({ uid: 'elasticsearch' })
@@ -352,7 +390,7 @@ describe('adhoc actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenAsyncActionIsDispatched(setFiltersFromUrl('elastic-filter', fromUrl), true);
.whenAsyncActionIsDispatched(setFiltersFromUrl(toKeyedVariableIdentifier(variable), fromUrl), true);
const expectedQuery = { 'var-elastic-filter': ['key|=|value', 'key|=|value'] };
const expectedFilters = [
@@ -360,28 +398,40 @@ describe('adhoc actions', () => {
{ key: 'key', value: 'value', operator: '=', condition: '', name: 'value-2' },
];
tester.thenDispatchedActionsShouldEqual(filtersRestored(toVariablePayload(variable, expectedFilters)));
tester.thenDispatchedActionsShouldEqual(
toKeyedAction(key, filtersRestored(toVariablePayload(variable, expectedFilters)))
);
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
});
});
describe('when initAdHocVariableEditor is dispatched', () => {
it('then correct actions are dispatched', async () => {
const key = 'key';
getList.mockRestore();
getList.mockReturnValue(datasources);
const tester = reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(initAdHocVariableEditor());
.whenActionIsDispatched(initAdHocVariableEditor(key));
tester.thenDispatchedActionsShouldEqual(changeVariableEditorExtended({ dataSources: expectedDatasources }));
tester.thenDispatchedActionsShouldEqual(
toKeyedAction(key, changeVariableEditorExtended({ dataSources: expectedDatasources }))
);
});
});
describe('when changeVariableDatasource is dispatched with unsupported datasource', () => {
it('then correct actions are dispatched', async () => {
const key = 'key';
const datasource = { uid: 'mysql' };
const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource({ uid: 'influxdb' }).build();
const variable = adHocBuilder()
.withId('Filters')
.withRootStateKey(key)
.withName('Filters')
.withDatasource({ uid: 'influxdb' })
.build();
getDatasource.mockRestore();
getDatasource.mockResolvedValue(null);
@@ -391,25 +441,37 @@ describe('adhoc actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenActionIsDispatched(setIdInEditor({ id: variable.id }))
.whenActionIsDispatched(initAdHocVariableEditor())
.whenAsyncActionIsDispatched(changeVariableDatasource(datasource), true);
.whenActionIsDispatched(toKeyedAction(key, setIdInEditor({ id: variable.id })))
.whenActionIsDispatched(initAdHocVariableEditor(key))
.whenAsyncActionIsDispatched(changeVariableDatasource(toKeyedVariableIdentifier(variable), datasource), true);
tester.thenDispatchedActionsShouldEqual(
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource })),
changeVariableEditorExtended({
infoText: 'This data source does not support ad hoc filters yet.',
dataSources: expectedDatasources,
})
toKeyedAction(
key,
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource }))
),
toKeyedAction(
key,
changeVariableEditorExtended({
infoText: 'This data source does not support ad hoc filters yet.',
dataSources: expectedDatasources,
})
)
);
});
});
describe('when changeVariableDatasource is dispatched with datasource', () => {
it('then correct actions are dispatched', async () => {
const key = 'key';
const datasource = { uid: 'elasticsearch' };
const loadingText = 'Ad hoc filters are applied automatically to all queries that target this data source';
const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource({ uid: 'influxdb' }).build();
const variable = adHocBuilder()
.withId('Filters')
.withRootStateKey(key)
.withName('Filters')
.withDatasource({ uid: 'influxdb' })
.build();
getDatasource.mockRestore();
getDatasource.mockResolvedValue({
@@ -421,23 +483,26 @@ describe('adhoc actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(createAddVariableAction(variable))
.whenActionIsDispatched(setIdInEditor({ id: variable.id }))
.whenActionIsDispatched(initAdHocVariableEditor())
.whenAsyncActionIsDispatched(changeVariableDatasource(datasource), true);
.whenActionIsDispatched(toKeyedAction(key, setIdInEditor({ id: variable.id })))
.whenActionIsDispatched(initAdHocVariableEditor(key))
.whenAsyncActionIsDispatched(changeVariableDatasource(toKeyedVariableIdentifier(variable), datasource), true);
tester.thenDispatchedActionsShouldEqual(
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource })),
changeVariableEditorExtended({ infoText: loadingText, dataSources: expectedDatasources })
toKeyedAction(
key,
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource }))
),
toKeyedAction(key, changeVariableEditorExtended({ infoText: loadingText, dataSources: expectedDatasources }))
);
});
});
});
function createAddVariableAction(variable: VariableModel, index = 0) {
const identifier = toVariableIdentifier(variable);
const identifier = toKeyedVariableIdentifier(variable);
const global = false;
const data = { global, index, model: { ...variable, index: -1, global } };
return addVariable(toVariablePayload(identifier, data));
return toKeyedAction(variable.rootStateKey!, addVariable(toVariablePayload(identifier, data)));
}
function createDatasource(name: string, selectable = true, isDefault = false): DataSourceInstanceSettings {
+87 -55
View File
@@ -3,8 +3,8 @@ import { StoreState, ThunkResult } from 'app/types';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { changeVariableEditorExtended } from '../editor/reducer';
import { addVariable, changeVariableProp } from '../state/sharedReducer';
import { getNewVariableIndex, getVariable } from '../state/selectors';
import { AddVariable, toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
import { getLastKey, getNewVariableIndex, getVariable, getVariablesState } from '../state/selectors';
import { AddVariable, KeyedVariableIdentifier } from '../state/types';
import {
AdHocVariabelFilterUpdate,
filterAdded,
@@ -18,6 +18,8 @@ import { variableUpdated } from '../state/actions';
import { isAdHoc } from '../guard';
import { DataSourceRef, getDataSourceRef } from '@grafana/data';
import { getAdhocVariableEditorState } from '../editor/selectors';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
export interface AdHocTableOptions {
datasource: DataSourceRef;
@@ -35,6 +37,9 @@ export const applyFilterFromTable = (options: AdHocTableOptions): ThunkResult<vo
if (!variable) {
dispatch(createAdHocVariable(options));
variable = getVariableByOptions(options, getState());
if (!variable) {
return;
}
}
const index = variable.filters.findIndex((f) => f.key === options.key && f.value === options.value);
@@ -42,52 +47,66 @@ export const applyFilterFromTable = (options: AdHocTableOptions): ThunkResult<vo
if (index === -1) {
const { value, key, operator } = options;
const filter = { value, key, operator, condition: '' };
return await dispatch(addFilter(variable.id, filter));
return await dispatch(addFilter(toKeyedVariableIdentifier(variable), filter));
}
const filter = { ...variable.filters[index], operator: options.operator };
return await dispatch(changeFilter(variable.id, { index, filter }));
return await dispatch(changeFilter(toKeyedVariableIdentifier(variable), { index, filter }));
};
};
export const changeFilter = (id: string, update: AdHocVariabelFilterUpdate): ThunkResult<void> => {
export const changeFilter = (
identifier: KeyedVariableIdentifier,
update: AdHocVariabelFilterUpdate
): ThunkResult<void> => {
return async (dispatch, getState) => {
const variable = getVariable(id, getState());
dispatch(filterUpdated(toVariablePayload(variable, update)));
await dispatch(variableUpdated(toVariableIdentifier(variable), true));
const variable = getVariable(identifier, getState());
dispatch(toKeyedAction(identifier.rootStateKey, filterUpdated(toVariablePayload(variable, update))));
await dispatch(variableUpdated(toKeyedVariableIdentifier(variable), true));
};
};
export const removeFilter = (id: string, index: number): ThunkResult<void> => {
export const removeFilter = (identifier: KeyedVariableIdentifier, index: number): ThunkResult<void> => {
return async (dispatch, getState) => {
const variable = getVariable(id, getState());
dispatch(filterRemoved(toVariablePayload(variable, index)));
await dispatch(variableUpdated(toVariableIdentifier(variable), true));
const variable = getVariable(identifier, getState());
dispatch(toKeyedAction(identifier.rootStateKey, filterRemoved(toVariablePayload(variable, index))));
await dispatch(variableUpdated(toKeyedVariableIdentifier(variable), true));
};
};
export const addFilter = (id: string, filter: AdHocVariableFilter): ThunkResult<void> => {
export const addFilter = (identifier: KeyedVariableIdentifier, filter: AdHocVariableFilter): ThunkResult<void> => {
return async (dispatch, getState) => {
const variable = getVariable(id, getState());
dispatch(filterAdded(toVariablePayload(variable, filter)));
await dispatch(variableUpdated(toVariableIdentifier(variable), true));
const variable = getVariable(identifier, getState());
dispatch(toKeyedAction(identifier.rootStateKey, filterAdded(toVariablePayload(variable, filter))));
await dispatch(variableUpdated(toKeyedVariableIdentifier(variable), true));
};
};
export const setFiltersFromUrl = (id: string, filters: AdHocVariableFilter[]): ThunkResult<void> => {
export const setFiltersFromUrl = (
identifier: KeyedVariableIdentifier,
filters: AdHocVariableFilter[]
): ThunkResult<void> => {
return async (dispatch, getState) => {
const variable = getVariable(id, getState());
dispatch(filtersRestored(toVariablePayload(variable, filters)));
await dispatch(variableUpdated(toVariableIdentifier(variable), true));
const variable = getVariable(identifier, getState());
dispatch(toKeyedAction(identifier.rootStateKey, filtersRestored(toVariablePayload(variable, filters))));
await dispatch(variableUpdated(toKeyedVariableIdentifier(variable), true));
};
};
export const changeVariableDatasource = (datasource?: DataSourceRef): ThunkResult<void> => {
export const changeVariableDatasource = (
identifier: KeyedVariableIdentifier,
datasource?: DataSourceRef
): ThunkResult<void> => {
return async (dispatch, getState) => {
const { editor } = getState().templating;
const { editor } = getVariablesState(identifier.rootStateKey, getState());
const extended = getAdhocVariableEditorState(editor);
const variable = getVariable(editor.id, getState());
dispatch(changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource })));
const variable = getVariable(identifier, getState());
dispatch(
toKeyedAction(
identifier.rootStateKey,
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource }))
)
);
const ds = await getDatasourceSrv().get(datasource);
@@ -97,57 +116,70 @@ export const changeVariableDatasource = (datasource?: DataSourceRef): ThunkResul
: 'This data source does not support ad hoc filters yet.';
dispatch(
changeVariableEditorExtended({
infoText: message,
dataSources: extended?.dataSources ?? [],
})
toKeyedAction(
identifier.rootStateKey,
changeVariableEditorExtended({
infoText: message,
dataSources: extended?.dataSources ?? [],
})
)
);
};
};
export const initAdHocVariableEditor = (): ThunkResult<void> => (dispatch) => {
const dataSources = getDatasourceSrv().getList({ metrics: true, variables: true });
const selectable = dataSources.reduce(
(all: Array<{ text: string; value: DataSourceRef | null }>, ds) => {
if (ds.meta.mixed) {
export const initAdHocVariableEditor =
(key: string): ThunkResult<void> =>
(dispatch) => {
const dataSources = getDatasourceSrv().getList({ metrics: true, variables: true });
const selectable = dataSources.reduce(
(all: Array<{ text: string; value: DataSourceRef | null }>, ds) => {
if (ds.meta.mixed) {
return all;
}
const text = ds.isDefault ? `${ds.name} (default)` : ds.name;
const value = getDataSourceRef(ds);
all.push({ text, value });
return all;
}
},
[{ text: '', value: {} }]
);
const text = ds.isDefault ? `${ds.name} (default)` : ds.name;
const value = getDataSourceRef(ds);
all.push({ text, value });
return all;
},
[{ text: '', value: {} }]
);
dispatch(
changeVariableEditorExtended({
dataSources: selectable,
})
);
};
dispatch(
toKeyedAction(
key,
changeVariableEditorExtended({
dataSources: selectable,
})
)
);
};
const createAdHocVariable = (options: AdHocTableOptions): ThunkResult<void> => {
return (dispatch, getState) => {
const model = {
const key = getLastKey(getState());
const model: AdHocVariableModel = {
...cloneDeep(initialAdHocVariableModelState),
datasource: options.datasource,
name: filterTableName,
id: filterTableName,
rootStateKey: key,
};
const global = false;
const index = getNewVariableIndex(getState());
const identifier: VariableIdentifier = { type: 'adhoc', id: model.id };
const index = getNewVariableIndex(key, getState());
const identifier: KeyedVariableIdentifier = { type: 'adhoc', id: model.id, rootStateKey: key };
dispatch(addVariable(toVariablePayload<AddVariable>(identifier, { global, model, index })));
dispatch(toKeyedAction(key, addVariable(toVariablePayload<AddVariable>(identifier, { global, model, index }))));
};
};
const getVariableByOptions = (options: AdHocTableOptions, state: StoreState): AdHocVariableModel => {
return Object.values(state.templating.variables).find(
const getVariableByOptions = (options: AdHocTableOptions, state: StoreState): AdHocVariableModel | undefined => {
const key = getLastKey(state);
const templatingState = getVariablesState(key, state);
return Object.values(templatingState.variables).find(
(v) => isAdHoc(v) && v.datasource?.uid === options.datasource.uid
) as AdHocVariableModel;
};
@@ -8,6 +8,7 @@ import { adHocVariableReducer, initialAdHocVariableModelState } from './reducer'
import { AdHocVariableEditor } from './AdHocVariableEditor';
import { setFiltersFromUrl } from './actions';
import * as urlParser from './urlParser';
import { toKeyedVariableIdentifier } from '../utils';
const noop = async () => {};
@@ -24,11 +25,11 @@ export const createAdHocVariableAdapter = (): VariableAdapter<AdHocVariableModel
setValue: noop,
setValueFromUrl: async (variable, urlValue) => {
const filters = urlParser.toFilters(urlValue);
await dispatch(setFiltersFromUrl(variable.id, filters));
await dispatch(setFiltersFromUrl(toKeyedVariableIdentifier(variable), filters));
},
updateOptions: noop,
getSaveModel: (variable) => {
const { index, id, state, global, ...rest } = cloneDeep(variable);
const { index, id, state, global, rootStateKey, ...rest } = cloneDeep(variable);
return rest;
},
getValueForUrl: (variable) => {
@@ -4,6 +4,7 @@ import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/variables/
import { VariablePickerProps } from '../../pickers/types';
import { addFilter, changeFilter, removeFilter } from '../actions';
import { AdHocFilter } from './AdHocFilter';
import { toKeyedVariableIdentifier } from '../../utils';
const mapDispatchToProps = {
addFilter,
@@ -23,15 +24,15 @@ type Props = OwnProps & ConnectedProps<typeof connector>;
*/
export class AdHocPickerUnconnected extends PureComponent<Props> {
addFilter = (filter: AdHocVariableFilter) => {
this.props.addFilter(this.props.variable.id, filter);
this.props.addFilter(toKeyedVariableIdentifier(this.props.variable), filter);
};
removeFilter = (index: number) => {
this.props.removeFilter(this.props.variable.id, index);
this.props.removeFilter(toKeyedVariableIdentifier(this.props.variable), index);
};
changeFilter = (index: number, filter: AdHocVariableFilter) => {
this.props.changeFilter(this.props.variable.id, {
this.props.changeFilter(toKeyedVariableIdentifier(this.props.variable), {
index,
filter,
});
@@ -1,10 +1,11 @@
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { cloneDeep } from 'lodash';
import { getVariableTestContext } from '../state/helpers';
import { toVariablePayload, VariablesState } from '../state/types';
import { VariablesState } from '../state/types';
import { adHocVariableReducer, filterAdded, filterRemoved, filtersRestored, filterUpdated } from './reducer';
import { AdHocVariableFilter, AdHocVariableModel } from '../types';
import { createAdHocVariableAdapter } from './adapter';
import { toVariablePayload } from '../utils';
describe('adHocVariableReducer', () => {
const adapter = createAdHocVariableAdapter();
@@ -1,7 +1,8 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AdHocVariableFilter, AdHocVariableModel, initialVariableModelState } from 'app/features/variables/types';
import { getInstanceState, initialVariablesState, VariablePayload, VariablesState } from '../state/types';
import { initialVariablesState, VariablePayload, VariablesState } from '../state/types';
import { getInstanceState } from '../state/selectors';
export interface AdHocVariabelFilterUpdate {
index: number;
@@ -4,9 +4,10 @@ import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { updateConstantVariableOptions } from './actions';
import { getRootReducer, RootReducerType } from '../state/helpers';
import { ConstantVariableModel, initialVariableModelState, VariableOption } from '../types';
import { toVariablePayload } from '../state/types';
import { createConstantOptionsFromQuery } from './reducer';
import { addVariable, setCurrentVariableValue } from '../state/sharedReducer';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
describe('constant actions', () => {
variableAdapters.setInit(() => [createConstantVariableAdapter()]);
@@ -22,6 +23,7 @@ describe('constant actions', () => {
const variable: ConstantVariableModel = {
...initialVariableModelState,
id: '0',
rootStateKey: 'key',
index: 0,
type: 'constant',
name: 'Constant',
@@ -36,12 +38,14 @@ describe('constant actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenAsyncActionIsDispatched(updateConstantVariableOptions(toVariablePayload(variable)), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenAsyncActionIsDispatched(updateConstantVariableOptions(toKeyedVariableIdentifier(variable)), true);
tester.thenDispatchedActionsShouldEqual(
createConstantOptionsFromQuery(toVariablePayload(variable)),
setCurrentVariableValue(toVariablePayload(variable, { option }))
toKeyedAction('key', createConstantOptionsFromQuery(toVariablePayload(variable))),
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
);
});
});
@@ -1,11 +1,14 @@
import { validateVariableSelectionState } from '../state/actions';
import { ThunkResult } from 'app/types';
import { createConstantOptionsFromQuery } from './reducer';
import { toVariablePayload, VariableIdentifier } from '../state/types';
import { KeyedVariableIdentifier } from '../state/types';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { toVariablePayload } from '../utils';
export const updateConstantVariableOptions = (identifier: VariableIdentifier): ThunkResult<void> => {
export const updateConstantVariableOptions = (identifier: KeyedVariableIdentifier): ThunkResult<void> => {
return async (dispatch) => {
await dispatch(createConstantOptionsFromQuery(toVariablePayload(identifier)));
const { rootStateKey } = identifier;
await dispatch(toKeyedAction(rootStateKey, createConstantOptionsFromQuery(toVariablePayload(identifier))));
await dispatch(validateVariableSelectionState(identifier));
};
};
@@ -6,8 +6,8 @@ import { VariableAdapter } from '../adapters';
import { constantVariableReducer, initialConstantVariableModelState } from './reducer';
import { ConstantVariableEditor } from './ConstantVariableEditor';
import { updateConstantVariableOptions } from './actions';
import { toVariableIdentifier } from '../state/types';
import { optionPickerFactory } from '../pickers';
import { toKeyedVariableIdentifier } from '../utils';
export const createConstantVariableAdapter = (): VariableAdapter<ConstantVariableModel> => {
return {
@@ -22,16 +22,16 @@ export const createConstantVariableAdapter = (): VariableAdapter<ConstantVariabl
return false;
},
setValue: async (variable, option, emitChanges = false) => {
await dispatch(setOptionAsCurrent(toVariableIdentifier(variable), option, emitChanges));
await dispatch(setOptionAsCurrent(toKeyedVariableIdentifier(variable), option, emitChanges));
},
setValueFromUrl: async (variable, urlValue) => {
await dispatch(setOptionFromUrl(toVariableIdentifier(variable), urlValue));
await dispatch(setOptionFromUrl(toKeyedVariableIdentifier(variable), urlValue));
},
updateOptions: async (variable) => {
await dispatch(updateConstantVariableOptions(toVariableIdentifier(variable)));
await dispatch(updateConstantVariableOptions(toKeyedVariableIdentifier(variable)));
},
getSaveModel: (variable) => {
const { index, id, state, global, current, options, ...rest } = cloneDeep(variable);
const { index, id, state, global, current, options, rootStateKey, ...rest } = cloneDeep(variable);
return rest;
},
getValueForUrl: (variable) => {
@@ -1,10 +1,12 @@
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { cloneDeep } from 'lodash';
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { getVariableTestContext } from '../state/helpers';
import { toVariablePayload, VariablesState } from '../state/types';
import { VariablesState } from '../state/types';
import { constantVariableReducer, createConstantOptionsFromQuery } from './reducer';
import { ConstantVariableModel } from '../types';
import { createConstantVariableAdapter } from './adapter';
import { toVariablePayload } from '../utils';
describe('constantVariableReducer', () => {
const adapter = createConstantVariableAdapter();
@@ -1,6 +1,8 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { ConstantVariableModel, initialVariableModelState, VariableHide, VariableOption } from '../types';
import { getInstanceState, VariablePayload, initialVariablesState, VariablesState } from '../state/types';
import { initialVariablesState, VariablePayload, VariablesState } from '../state/types';
import { getInstanceState } from '../state/selectors';
export const initialConstantVariableModelState: ConstantVariableModel = {
...initialVariableModelState,
@@ -4,9 +4,10 @@ import { createCustomVariableAdapter } from './adapter';
import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { getRootReducer, RootReducerType } from '../state/helpers';
import { CustomVariableModel, initialVariableModelState, VariableOption } from '../types';
import { toVariablePayload } from '../state/types';
import { addVariable, setCurrentVariableValue } from '../state/sharedReducer';
import { createCustomOptionsFromQuery } from './reducer';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
describe('custom actions', () => {
variableAdapters.setInit(() => [createCustomVariableAdapter()]);
@@ -22,6 +23,7 @@ describe('custom actions', () => {
const variable: CustomVariableModel = {
...initialVariableModelState,
id: '0',
rootStateKey: 'key',
index: 0,
type: 'custom',
name: 'Custom',
@@ -49,12 +51,14 @@ describe('custom actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenAsyncActionIsDispatched(updateCustomVariableOptions(toVariablePayload(variable)), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenAsyncActionIsDispatched(updateCustomVariableOptions(toKeyedVariableIdentifier(variable)), true);
tester.thenDispatchedActionsShouldEqual(
createCustomOptionsFromQuery(toVariablePayload(variable)),
setCurrentVariableValue(toVariablePayload(variable, { option }))
toKeyedAction('key', createCustomOptionsFromQuery(toVariablePayload(variable))),
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
);
});
});
@@ -1,11 +1,14 @@
import { validateVariableSelectionState } from '../state/actions';
import { ThunkResult } from 'app/types';
import { createCustomOptionsFromQuery } from './reducer';
import { toVariablePayload, VariableIdentifier } from '../state/types';
import { KeyedVariableIdentifier } from '../state/types';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { toVariablePayload } from '../utils';
export const updateCustomVariableOptions = (identifier: VariableIdentifier): ThunkResult<void> => {
export const updateCustomVariableOptions = (identifier: KeyedVariableIdentifier): ThunkResult<void> => {
return async (dispatch) => {
await dispatch(createCustomOptionsFromQuery(toVariablePayload(identifier)));
const { rootStateKey } = identifier;
await dispatch(toKeyedAction(rootStateKey, createCustomOptionsFromQuery(toVariablePayload(identifier))));
await dispatch(validateVariableSelectionState(identifier));
};
};
@@ -6,8 +6,7 @@ import { VariableAdapter } from '../adapters';
import { customVariableReducer, initialCustomVariableModelState } from './reducer';
import { CustomVariableEditor } from './CustomVariableEditor';
import { updateCustomVariableOptions } from './actions';
import { toVariableIdentifier } from '../state/types';
import { isAllVariable } from '../utils';
import { isAllVariable, toKeyedVariableIdentifier } from '../utils';
import { optionPickerFactory } from '../pickers';
import { ALL_VARIABLE_TEXT } from '../constants';
@@ -24,16 +23,16 @@ export const createCustomVariableAdapter = (): VariableAdapter<CustomVariableMod
return false;
},
setValue: async (variable, option, emitChanges = false) => {
await dispatch(setOptionAsCurrent(toVariableIdentifier(variable), option, emitChanges));
await dispatch(setOptionAsCurrent(toKeyedVariableIdentifier(variable), option, emitChanges));
},
setValueFromUrl: async (variable, urlValue) => {
await dispatch(setOptionFromUrl(toVariableIdentifier(variable), urlValue));
await dispatch(setOptionFromUrl(toKeyedVariableIdentifier(variable), urlValue));
},
updateOptions: async (variable) => {
await dispatch(updateCustomVariableOptions(toVariableIdentifier(variable)));
await dispatch(updateCustomVariableOptions(toKeyedVariableIdentifier(variable)));
},
getSaveModel: (variable) => {
const { index, id, state, global, ...rest } = cloneDeep(variable);
const { index, id, state, global, rootStateKey, ...rest } = cloneDeep(variable);
return rest;
},
getValueForUrl: (variable) => {
@@ -1,11 +1,13 @@
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { cloneDeep } from 'lodash';
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { getVariableTestContext } from '../state/helpers';
import { toVariablePayload, VariablesState } from '../state/types';
import { VariablesState } from '../state/types';
import { createCustomOptionsFromQuery, customVariableReducer } from './reducer';
import { createCustomVariableAdapter } from './adapter';
import { CustomVariableModel } from '../types';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
import { toVariablePayload } from '../utils';
describe('customVariableReducer', () => {
const adapter = createCustomVariableAdapter();
@@ -1,8 +1,9 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { CustomVariableModel, initialVariableModelState, VariableOption } from '../types';
import { getInstanceState, VariablePayload, initialVariablesState, VariablesState } from '../state/types';
import { initialVariablesState, VariablePayload, VariablesState } from '../state/types';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
import { getInstanceState } from '../state/selectors';
export const initialCustomVariableModelState: CustomVariableModel = {
...initialVariableModelState,
@@ -1,23 +1,38 @@
import React, { FormEvent, PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { SelectableValue } from '@grafana/data';
import { InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { DataSourceVariableModel, VariableWithMultiSupport } from '../types';
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { initialVariableEditorState } from '../editor/reducer';
import { initDataSourceVariableEditor } from './actions';
import { StoreState } from '../../../types';
import { changeVariableMultiValue } from '../state/actions';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableSelectField } from '../editor/VariableSelectField';
import { SelectableValue } from '@grafana/data';
import { VariableTextField } from '../editor/VariableTextField';
import { getVariablesState } from '../state/selectors';
import { selectors } from '@grafana/e2e-selectors';
import { getDatasourceVariableEditorState } from '../editor/selectors';
const mapStateToProps = (state: StoreState) => ({
extended: getDatasourceVariableEditorState(state.templating.editor),
});
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => {
const {
variable: { rootStateKey },
} = ownProps;
if (!rootStateKey) {
console.error('DataSourceVariableEditor: variable has no rootStateKey');
return {
extended: getDatasourceVariableEditorState(initialVariableEditorState),
};
}
const { editor } = getVariablesState(rootStateKey, state);
return {
extended: getDatasourceVariableEditorState(editor),
};
};
const mapDispatchToProps = {
initDataSourceVariableEditor,
@@ -32,7 +47,13 @@ type Props = OwnProps & ConnectedProps<typeof connector>;
export class DataSourceVariableEditorUnConnected extends PureComponent<Props> {
componentDidMount() {
this.props.initDataSourceVariableEditor();
const { rootStateKey } = this.props.variable;
if (!rootStateKey) {
console.error('DataSourceVariableEditor: variable has no rootStateKey');
return;
}
this.props.initDataSourceVariableEditor(rootStateKey);
}
onRegExChange = (event: FormEvent<HTMLInputElement>) => {
@@ -2,7 +2,6 @@ import { DataSourceInstanceSettings } from '@grafana/data';
import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { getRootReducer, RootReducerType } from '../state/helpers';
import { toVariableIdentifier, toVariablePayload } from '../state/types';
import { variableAdapters } from '../adapters';
import { createDataSourceVariableAdapter } from './adapter';
import {
@@ -16,6 +15,8 @@ import { addVariable, setCurrentVariableValue } from '../state/sharedReducer';
import { changeVariableEditorExtended } from '../editor/reducer';
import { datasourceBuilder } from '../shared/testing/builders';
import { getDataSourceInstanceSetting } from '../shared/testing/helpers';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
interface Args {
sources?: DataSourceInstanceSettings[];
@@ -27,7 +28,7 @@ function getTestContext({ sources = [], query, regex }: Args = {}) {
const getListMock = jest.fn().mockReturnValue(sources);
const getDatasourceSrvMock = jest.fn().mockReturnValue({ getList: getListMock });
const dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrvMock };
const datasource = datasourceBuilder().withId('0').withQuery(query).withRegEx(regex).build();
const datasource = datasourceBuilder().withId('0').withRootStateKey('key').withQuery(query).withRegEx(regex).build();
return { getListMock, getDatasourceSrvMock, dependencies, datasource };
}
@@ -51,27 +52,36 @@ describe('data source actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(
addVariable(toVariablePayload(datasource, { global: false, index: 0, model: datasource }))
toKeyedAction(
'key',
addVariable(toVariablePayload(datasource, { global: false, index: 0, model: datasource }))
)
)
.whenAsyncActionIsDispatched(
updateDataSourceVariableOptions(toVariableIdentifier(datasource), dependencies),
updateDataSourceVariableOptions(toKeyedVariableIdentifier(datasource), dependencies),
true
);
tester.thenDispatchedActionsShouldEqual(
createDataSourceOptions(
toVariablePayload(
{ type: 'datasource', id: '0' },
{
sources,
regex: undefined as unknown as RegExp,
}
toKeyedAction(
'key',
createDataSourceOptions(
toVariablePayload(
{ type: 'datasource', id: '0' },
{
sources,
regex: undefined as unknown as RegExp,
}
)
)
),
setCurrentVariableValue(
toVariablePayload(
{ type: 'datasource', id: '0' },
{ option: { text: 'first-name', value: 'first-name', selected: false } }
toKeyedAction(
'key',
setCurrentVariableValue(
toVariablePayload(
{ type: 'datasource', id: '0' },
{ option: { text: 'first-name', value: 'first-name', selected: false } }
)
)
)
);
@@ -99,27 +109,36 @@ describe('data source actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(
addVariable(toVariablePayload(datasource, { global: false, index: 0, model: datasource }))
toKeyedAction(
'key',
addVariable(toVariablePayload(datasource, { global: false, index: 0, model: datasource }))
)
)
.whenAsyncActionIsDispatched(
updateDataSourceVariableOptions(toVariableIdentifier(datasource), dependencies),
updateDataSourceVariableOptions(toKeyedVariableIdentifier(datasource), dependencies),
true
);
tester.thenDispatchedActionsShouldEqual(
createDataSourceOptions(
toVariablePayload(
{ type: 'datasource', id: '0' },
{
sources,
regex: /.*(second-name).*/,
}
toKeyedAction(
'key',
createDataSourceOptions(
toVariablePayload(
{ type: 'datasource', id: '0' },
{
sources,
regex: /.*(second-name).*/,
}
)
)
),
setCurrentVariableValue(
toVariablePayload(
{ type: 'datasource', id: '0' },
{ option: { text: 'second-name', value: 'second-name', selected: false } }
toKeyedAction(
'key',
setCurrentVariableValue(
toVariablePayload(
{ type: 'datasource', id: '0' },
{ option: { text: 'second-name', value: 'second-name', selected: false } }
)
)
)
);
@@ -143,14 +162,17 @@ describe('data source actions', () => {
reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(initDataSourceVariableEditor(dependencies))
.whenActionIsDispatched(initDataSourceVariableEditor('key', dependencies))
.thenDispatchedActionsShouldEqual(
changeVariableEditorExtended({
dataSourceTypes: [
{ text: '', value: '' },
{ text: 'mock-data-name', value: 'mock-data-id' },
],
})
toKeyedAction(
'key',
changeVariableEditorExtended({
dataSourceTypes: [
{ text: '', value: '' },
{ text: 'mock-data-name', value: 'mock-data-id' },
],
})
)
);
expect(getListMock).toHaveBeenCalledTimes(1);
@@ -2,7 +2,7 @@ import { chain } from 'lodash';
import { getTemplateSrv } from '@grafana/runtime';
import { stringToJsRegex } from '@grafana/data';
import { toVariablePayload, VariableIdentifier } from '../state/types';
import { KeyedVariableIdentifier } from '../state/types';
import { ThunkResult } from '../../../types';
import { createDataSourceOptions } from './reducer';
import { validateVariableSelectionState } from '../state/actions';
@@ -10,6 +10,8 @@ import { getDatasourceSrv } from '../../plugins/datasource_srv';
import { getVariable } from '../state/selectors';
import { DataSourceVariableModel } from '../types';
import { changeVariableEditorExtended } from '../editor/reducer';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { toVariablePayload } from '../utils';
export interface DataSourceVariableActionDependencies {
getDatasourceSrv: typeof getDatasourceSrv;
@@ -17,12 +19,13 @@ export interface DataSourceVariableActionDependencies {
export const updateDataSourceVariableOptions =
(
identifier: VariableIdentifier,
identifier: KeyedVariableIdentifier,
dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrv }
): ThunkResult<void> =>
async (dispatch, getState) => {
const { rootStateKey } = identifier;
const sources = dependencies.getDatasourceSrv().getList({ metrics: true, variables: false });
const variableInState = getVariable<DataSourceVariableModel>(identifier.id, getState());
const variableInState = getVariable<DataSourceVariableModel>(identifier, getState());
let regex;
if (variableInState.regex) {
@@ -30,12 +33,15 @@ export const updateDataSourceVariableOptions =
regex = stringToJsRegex(regex);
}
dispatch(createDataSourceOptions(toVariablePayload(identifier, { sources, regex })));
dispatch(toKeyedAction(rootStateKey, createDataSourceOptions(toVariablePayload(identifier, { sources, regex }))));
await dispatch(validateVariableSelectionState(identifier));
};
export const initDataSourceVariableEditor =
(dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrv }): ThunkResult<void> =>
(
key: string,
dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrv }
): ThunkResult<void> =>
(dispatch) => {
const dataSources = dependencies.getDatasourceSrv().getList({ metrics: true, variables: true });
const dataSourceTypes = chain(dataSources)
@@ -47,5 +53,5 @@ export const initDataSourceVariableEditor =
dataSourceTypes.unshift({ text: '', value: '' });
dispatch(changeVariableEditorExtended({ dataSourceTypes }));
dispatch(toKeyedAction(key, changeVariableEditorExtended({ dataSourceTypes })));
};
@@ -4,10 +4,9 @@ import { dispatch } from '../../../store/store';
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions';
import { VariableAdapter } from '../adapters';
import { dataSourceVariableReducer, initialDataSourceVariableModelState } from './reducer';
import { toVariableIdentifier } from '../state/types';
import { DataSourceVariableEditor } from './DataSourceVariableEditor';
import { updateDataSourceVariableOptions } from './actions';
import { containsVariable, isAllVariable } from '../utils';
import { containsVariable, isAllVariable, toKeyedVariableIdentifier } from '../utils';
import { optionPickerFactory } from '../pickers';
import { ALL_VARIABLE_TEXT } from '../constants';
@@ -27,16 +26,16 @@ export const createDataSourceVariableAdapter = (): VariableAdapter<DataSourceVar
return false;
},
setValue: async (variable, option, emitChanges = false) => {
await dispatch(setOptionAsCurrent(toVariableIdentifier(variable), option, emitChanges));
await dispatch(setOptionAsCurrent(toKeyedVariableIdentifier(variable), option, emitChanges));
},
setValueFromUrl: async (variable, urlValue) => {
await dispatch(setOptionFromUrl(toVariableIdentifier(variable), urlValue));
await dispatch(setOptionFromUrl(toKeyedVariableIdentifier(variable), urlValue));
},
updateOptions: async (variable) => {
await dispatch(updateDataSourceVariableOptions(toVariableIdentifier(variable)));
await dispatch(updateDataSourceVariableOptions(toKeyedVariableIdentifier(variable)));
},
getSaveModel: (variable) => {
const { index, id, state, global, ...rest } = cloneDeep(variable);
const { index, id, state, global, rootStateKey, ...rest } = cloneDeep(variable);
return { ...rest, options: [] };
},
getValueForUrl: (variable) => {
@@ -1,14 +1,15 @@
import { cloneDeep } from 'lodash';
import { DataSourceInstanceSettings } from '@grafana/data';
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { createDataSourceOptions, dataSourceVariableReducer } from './reducer';
import { DataSourceVariableModel } from '../types';
import { getVariableTestContext } from '../state/helpers';
import { cloneDeep } from 'lodash';
import { createDataSourceVariableAdapter } from './adapter';
import { toVariablePayload, VariablesState } from '../state/types';
import { VariablesState } from '../state/types';
import { getMockPlugins } from '../../plugins/__mocks__/pluginMocks';
import { getDataSourceInstanceSetting } from '../shared/testing/helpers';
import { toVariablePayload } from '../utils';
describe('dataSourceVariableReducer', () => {
const adapter = createDataSourceVariableAdapter();
@@ -2,8 +2,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { DataSourceInstanceSettings } from '@grafana/data';
import { DataSourceVariableModel, initialVariableModelState, VariableOption, VariableRefresh } from '../types';
import { getInstanceState, initialVariablesState, VariablePayload, VariablesState } from '../state/types';
import { initialVariablesState, VariablePayload, VariablesState } from '../state/types';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
import { getInstanceState } from '../state/selectors';
export const initialDataSourceVariableModelState: DataSourceVariableModel = {
...initialVariableModelState,
@@ -4,14 +4,15 @@ import { selectors } from '@grafana/e2e-selectors';
import { VariableWithMultiSupport } from '../types';
import { VariableEditorProps } from './types';
import { toVariableIdentifier, VariableIdentifier } from '../state/types';
import { KeyedVariableIdentifier } from '../state/types';
import { VariableSectionHeader } from './VariableSectionHeader';
import { VariableSwitchField } from './VariableSwitchField';
import { VariableTextField } from './VariableTextField';
import { toKeyedVariableIdentifier } from '../utils';
export interface SelectionOptionsEditorProps<Model extends VariableWithMultiSupport = VariableWithMultiSupport>
extends VariableEditorProps<Model> {
onMultiChanged: (identifier: VariableIdentifier, value: boolean) => void;
onMultiChanged: (identifier: KeyedVariableIdentifier, value: boolean) => void;
}
export const SelectionOptionsEditor: FunctionComponent<SelectionOptionsEditorProps> = ({
@@ -21,7 +22,7 @@ export const SelectionOptionsEditor: FunctionComponent<SelectionOptionsEditorPro
}) => {
const onMultiChanged = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
onMultiChangedProps(toVariableIdentifier(variable), event.target.checked);
onMultiChangedProps(toKeyedVariableIdentifier(variable), event.target.checked);
},
[onMultiChangedProps, variable]
);
@@ -1,36 +1,61 @@
import React, { MouseEvent, PureComponent } from 'react';
import { bindActionCreators } from 'redux';
import { connect, ConnectedProps } from 'react-redux';
import { Icon, LinkButton } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
import { StoreState } from '../../../types';
import { KeyedVariableIdentifier } from '../state/types';
import { StoreState, ThunkDispatch } from '../../../types';
import { VariableEditorEditor } from './VariableEditorEditor';
import { connect, ConnectedProps } from 'react-redux';
import { getEditorVariables } from '../state/selectors';
import { getEditorVariables, getVariablesState } from '../state/selectors';
import { switchToEditMode, switchToListMode, switchToNewMode } from './actions';
import { changeVariableOrder, duplicateVariable, removeVariable } from '../state/sharedReducer';
import { VariableEditorList } from './VariableEditorList';
import { VariablesUnknownTable } from '../inspect/VariablesUnknownTable';
import { VariablesDependenciesButton } from '../inspect/VariablesDependenciesButton';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
const mapStateToProps = (state: StoreState) => ({
variables: getEditorVariables(state),
idInEditor: state.templating.editor.id,
dashboard: state.dashboard.getModel(),
usagesNetwork: state.templating.inspect.usagesNetwork,
usages: state.templating.inspect.usages,
});
const mapDispatchToProps = {
changeVariableOrder,
duplicateVariable,
removeVariable,
switchToNewMode,
switchToEditMode,
switchToListMode,
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => {
const { uid } = ownProps.dashboard;
const templatingState = getVariablesState(uid, state);
return {
variables: getEditorVariables(uid, state),
idInEditor: templatingState.editor.id,
usagesNetwork: templatingState.inspect.usagesNetwork,
usages: templatingState.inspect.usages,
};
};
interface OwnProps {}
const mapDispatchToProps = (dispatch: ThunkDispatch) => {
return {
...bindActionCreators({ switchToNewMode, switchToEditMode, switchToListMode }, dispatch),
changeVariableOrder: (identifier: KeyedVariableIdentifier, fromIndex: number, toIndex: number) =>
dispatch(
toKeyedAction(
identifier.rootStateKey,
changeVariableOrder(toVariablePayload(identifier, { fromIndex, toIndex }))
)
),
duplicateVariable: (identifier: KeyedVariableIdentifier) =>
dispatch(
toKeyedAction(
identifier.rootStateKey,
duplicateVariable(toVariablePayload(identifier, { newId: undefined as unknown as string }))
)
),
removeVariable: (identifier: KeyedVariableIdentifier) => {
dispatch(
toKeyedAction(identifier.rootStateKey, removeVariable(toVariablePayload(identifier, { reIndex: true })))
);
},
};
};
interface OwnProps {
dashboard: DashboardModel;
}
const connector = connect(mapStateToProps, mapDispatchToProps);
@@ -38,32 +63,32 @@ type Props = OwnProps & ConnectedProps<typeof connector>;
class VariableEditorContainerUnconnected extends PureComponent<Props> {
componentDidMount(): void {
this.props.switchToListMode();
this.props.switchToListMode(this.props.dashboard.uid);
}
onChangeToListMode = (event: MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.props.switchToListMode();
this.props.switchToListMode(this.props.dashboard.uid);
};
onEditVariable = (identifier: VariableIdentifier) => {
onEditVariable = (identifier: KeyedVariableIdentifier) => {
this.props.switchToEditMode(identifier);
};
onNewVariable = () => {
this.props.switchToNewMode();
this.props.switchToNewMode(this.props.dashboard.uid);
};
onChangeVariableOrder = (identifier: VariableIdentifier, fromIndex: number, toIndex: number) => {
this.props.changeVariableOrder(toVariablePayload(identifier, { fromIndex, toIndex }));
onChangeVariableOrder = (identifier: KeyedVariableIdentifier, fromIndex: number, toIndex: number) => {
this.props.changeVariableOrder(identifier, fromIndex, toIndex);
};
onDuplicateVariable = (identifier: VariableIdentifier) => {
this.props.duplicateVariable(toVariablePayload(identifier, { newId: undefined as unknown as string }));
onDuplicateVariable = (identifier: KeyedVariableIdentifier) => {
this.props.duplicateVariable(identifier);
};
onRemoveVariable = (identifier: VariableIdentifier) => {
this.props.removeVariable(toVariablePayload(identifier, { reIndex: true }));
onRemoveVariable = (identifier: KeyedVariableIdentifier) => {
this.props.removeVariable(identifier);
};
render() {
@@ -117,7 +142,7 @@ class VariableEditorContainerUnconnected extends PureComponent<Props> {
{!variableToEdit && this.props.variables.length > 0 && (
<VariablesUnknownTable variables={this.props.variables} dashboard={this.props.dashboard} />
)}
{variableToEdit && <VariableEditorEditor identifier={toVariableIdentifier(variableToEdit)} />}
{variableToEdit && <VariableEditorEditor identifier={toKeyedVariableIdentifier(variableToEdit)} />}
</div>
);
}
@@ -1,20 +1,17 @@
import React, { FormEvent, PureComponent } from 'react';
import { bindActionCreators } from 'redux';
import { connect, ConnectedProps } from 'react-redux';
import { isEqual } from 'lodash';
import { AppEvents, LoadingState, SelectableValue, VariableType } from '@grafana/data';
import { Button, Icon, InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { variableAdapters } from '../adapters';
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
import { VariableHide, VariableModel } from '../types';
import { KeyedVariableIdentifier } from '../state/types';
import { VariableHide } from '../types';
import { appEvents } from '../../../core/core';
import { VariableValuesPreview } from './VariableValuesPreview';
import { changeVariableName, onEditorUpdate, variableEditorMount, variableEditorUnMount } from './actions';
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { StoreState } from '../../../types';
import { VariableEditorState } from './reducer';
import { getVariable } from '../state/selectors';
import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
import { OnPropChangeArguments } from './types';
import { changeVariableProp, changeVariableType } from '../state/sharedReducer';
import { updateOptions } from '../state/actions';
@@ -23,27 +20,41 @@ import { VariableSectionHeader } from './VariableSectionHeader';
import { hasOptions } from '../guard';
import { VariableTypeSelect } from './VariableTypeSelect';
import { VariableHideSelect } from './VariableHideSelect';
import { getVariable, getVariablesState } from '../state/selectors';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { StoreState, ThunkDispatch } from '../../../types';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => ({
editor: getVariablesState(ownProps.identifier.rootStateKey, state).editor,
variable: getVariable(ownProps.identifier, state, false), // we could be renaming a variable and we don't want this to throw
});
const mapDispatchToProps = (dispatch: ThunkDispatch) => {
return {
...bindActionCreators(
{ variableEditorMount, variableEditorUnMount, changeVariableName, onEditorUpdate, updateOptions },
dispatch
),
changeVariableProp: (identifier: KeyedVariableIdentifier, propName: string, propValue: any) =>
dispatch(
toKeyedAction(
identifier.rootStateKey,
changeVariableProp(toVariablePayload(identifier, { propName, propValue }))
)
),
changeVariableType: (identifier: KeyedVariableIdentifier, newType: VariableType) =>
dispatch(toKeyedAction(identifier.rootStateKey, changeVariableType(toVariablePayload(identifier, { newType })))),
};
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export interface OwnProps {
identifier: VariableIdentifier;
identifier: KeyedVariableIdentifier;
}
interface ConnectedProps {
editor: VariableEditorState;
variable: VariableModel;
}
interface DispatchProps {
variableEditorMount: typeof variableEditorMount;
variableEditorUnMount: typeof variableEditorUnMount;
changeVariableName: typeof changeVariableName;
changeVariableProp: typeof changeVariableProp;
onEditorUpdate: typeof onEditorUpdate;
changeVariableType: typeof changeVariableType;
updateOptions: typeof updateOptions;
}
type Props = OwnProps & ConnectedProps & DispatchProps;
type Props = OwnProps & ConnectedProps<typeof connector>;
export class VariableEditorEditorUnConnected extends PureComponent<Props> {
componentDidMount(): void {
@@ -71,35 +82,26 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
if (!option.value) {
return;
}
this.props.changeVariableType(toVariablePayload(this.props.identifier, { newType: option.value }));
this.props.changeVariableType(this.props.identifier, option.value);
};
onLabelChange = (event: FormEvent<HTMLInputElement>) => {
event.preventDefault();
this.props.changeVariableProp(
toVariablePayload(this.props.identifier, { propName: 'label', propValue: event.currentTarget.value })
);
this.props.changeVariableProp(this.props.identifier, 'label', event.currentTarget.value);
};
onDescriptionChange = (event: FormEvent<HTMLInputElement>) => {
this.props.changeVariableProp(
toVariablePayload(this.props.identifier, { propName: 'description', propValue: event.currentTarget.value })
);
this.props.changeVariableProp(this.props.identifier, 'description', event.currentTarget.value);
};
onHideChange = (option: SelectableValue<VariableHide>) => {
this.props.changeVariableProp(
toVariablePayload(this.props.identifier, {
propName: 'hide',
propValue: option.value,
})
);
this.props.changeVariableProp(this.props.identifier, 'hide', option.value);
};
onPropChanged = async ({ propName, propValue, updateOptions = false }: OnPropChangeArguments) => {
this.props.changeVariableProp(toVariablePayload(this.props.identifier, { propName, propValue }));
this.props.changeVariableProp(this.props.identifier, propName, propValue);
if (updateOptions) {
await this.props.updateOptions(toVariableIdentifier(this.props.variable));
await this.props.updateOptions(toKeyedVariableIdentifier(this.props.variable));
}
};
@@ -191,23 +193,4 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
}
}
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, ownProps) => ({
editor: state.templating.editor,
variable: getVariable(ownProps.identifier.id, state, false), // we could be renaming a variable and we don't want this to throw
});
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
variableEditorMount,
variableEditorUnMount,
changeVariableName,
changeVariableProp,
onEditorUpdate,
changeVariableType,
updateOptions,
};
export const VariableEditorEditor = connectWithStore(
VariableEditorEditorUnConnected,
mapStateToProps,
mapDispatchToProps
);
export const VariableEditorEditor = connector(VariableEditorEditorUnConnected);
@@ -4,7 +4,7 @@ import { selectors } from '@grafana/e2e-selectors';
import { reportInteraction } from '@grafana/runtime';
import { VariableModel } from '../types';
import { VariableIdentifier } from '../state/types';
import { KeyedVariableIdentifier } from '../state/types';
import { UsagesToNetwork, VariableUsageTree } from '../inspect/utils';
import { VariableEditorListRow } from './VariableEditorListRow';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
@@ -14,10 +14,10 @@ export interface Props {
usages: VariableUsageTree[];
usagesNetwork: UsagesToNetwork[];
onAdd: () => void;
onEdit: (identifier: VariableIdentifier) => void;
onChangeOrder: (identifier: VariableIdentifier, fromIndex: number, toIndex: number) => void;
onDuplicate: (identifier: VariableIdentifier) => void;
onDelete: (identifier: VariableIdentifier) => void;
onEdit: (identifier: KeyedVariableIdentifier) => void;
onChangeOrder: (identifier: KeyedVariableIdentifier, fromIndex: number, toIndex: number) => void;
onDuplicate: (identifier: KeyedVariableIdentifier) => void;
onDelete: (identifier: KeyedVariableIdentifier) => void;
}
export function VariableEditorList({
@@ -8,18 +8,19 @@ import { reportInteraction } from '@grafana/runtime';
import { getVariableUsages, UsagesToNetwork, VariableUsageTree } from '../inspect/utils';
import { hasOptions, isAdHoc, isQuery } from '../guard';
import { toVariableIdentifier, VariableIdentifier } from '../state/types';
import { KeyedVariableIdentifier } from '../state/types';
import { VariableUsagesButton } from '../inspect/VariableUsagesButton';
import { VariableModel } from '../types';
import { toKeyedVariableIdentifier } from '../utils';
export interface VariableEditorListRowProps {
index: number;
variable: VariableModel;
usageTree: VariableUsageTree[];
usagesNetwork: UsagesToNetwork[];
onEdit: (identifier: VariableIdentifier) => void;
onDuplicate: (identifier: VariableIdentifier) => void;
onDelete: (identifier: VariableIdentifier) => void;
onEdit: (identifier: KeyedVariableIdentifier) => void;
onDuplicate: (identifier: KeyedVariableIdentifier) => void;
onDelete: (identifier: KeyedVariableIdentifier) => void;
}
export function VariableEditorListRow({
@@ -36,7 +37,7 @@ export function VariableEditorListRow({
const definition = getDefinition(variable);
const usages = getVariableUsages(variable.id, usageTree);
const passed = usages > 0 || isAdHoc(variable);
const identifier = toVariableIdentifier(variable);
const identifier = toKeyedVariableIdentifier(variable);
return (
<Draggable draggableId={JSON.stringify(identifier)} index={index}>
+49 -37
View File
@@ -1,5 +1,5 @@
import { ThunkResult } from '../../../types';
import { getEditorVariables, getNewVariableIndex, getVariable, getVariables } from '../state/selectors';
import { getEditorVariables, getNewVariableIndex, getVariable, getVariablesByKey } from '../state/selectors';
import {
changeVariableNameFailed,
changeVariableNameSucceeded,
@@ -9,7 +9,7 @@ import {
variableEditorUnMounted,
} from './reducer';
import { variableAdapters } from '../adapters';
import { AddVariable, toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
import { AddVariable, KeyedVariableIdentifier, VariableIdentifier } from '../state/types';
import { cloneDeep } from 'lodash';
import { VariableType } from '@grafana/data';
import { addVariable, removeVariable } from '../state/sharedReducer';
@@ -17,28 +17,33 @@ import { updateOptions } from '../state/actions';
import { VariableModel } from '../types';
import { initInspect } from '../inspect/reducer';
import { createUsagesNetwork, transformUsagesToNetwork } from '../inspect/utils';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
export const variableEditorMount = (identifier: VariableIdentifier): ThunkResult<void> => {
export const variableEditorMount = (identifier: KeyedVariableIdentifier): ThunkResult<void> => {
return async (dispatch) => {
dispatch(variableEditorMounted({ name: getVariable(identifier.id).name }));
const { rootStateKey } = identifier;
dispatch(toKeyedAction(rootStateKey, variableEditorMounted({ name: getVariable(identifier).name })));
};
};
export const variableEditorUnMount = (identifier: VariableIdentifier): ThunkResult<void> => {
export const variableEditorUnMount = (identifier: KeyedVariableIdentifier): ThunkResult<void> => {
return async (dispatch, getState) => {
dispatch(variableEditorUnMounted(toVariablePayload(identifier)));
const { rootStateKey } = identifier;
dispatch(toKeyedAction(rootStateKey, variableEditorUnMounted(toVariablePayload(identifier))));
};
};
export const onEditorUpdate = (identifier: VariableIdentifier): ThunkResult<void> => {
export const onEditorUpdate = (identifier: KeyedVariableIdentifier): ThunkResult<void> => {
return async (dispatch) => {
await dispatch(updateOptions(identifier));
dispatch(switchToListMode());
dispatch(switchToListMode(identifier.rootStateKey));
};
};
export const changeVariableName = (identifier: VariableIdentifier, newName: string): ThunkResult<void> => {
export const changeVariableName = (identifier: KeyedVariableIdentifier, newName: string): ThunkResult<void> => {
return (dispatch, getState) => {
const { id, rootStateKey: uid } = identifier;
let errorText = null;
if (!newName.match(/^(?!__).*$/)) {
errorText = "Template names cannot begin with '__', that's reserved for Grafana's global variables";
@@ -48,15 +53,15 @@ export const changeVariableName = (identifier: VariableIdentifier, newName: stri
errorText = 'Only word and digit characters are allowed in variable names';
}
const variables = getVariables(getState());
const foundVariables = variables.filter((v) => v.name === newName && v.id !== identifier.id);
const variables = getVariablesByKey(uid, getState());
const foundVariables = variables.filter((v) => v.name === newName && v.id !== id);
if (foundVariables.length) {
errorText = 'Variable with the same name already exists';
}
if (errorText) {
dispatch(changeVariableNameFailed({ newName, errorText }));
dispatch(toKeyedAction(uid, changeVariableNameFailed({ newName, errorText })));
return;
}
@@ -65,54 +70,61 @@ export const changeVariableName = (identifier: VariableIdentifier, newName: stri
};
export const completeChangeVariableName =
(identifier: VariableIdentifier, newName: string): ThunkResult<void> =>
(identifier: KeyedVariableIdentifier, newName: string): ThunkResult<void> =>
(dispatch, getState) => {
const originalVariable = getVariable(identifier.id, getState());
const { rootStateKey } = identifier;
const originalVariable = getVariable(identifier, getState());
if (originalVariable.name === newName) {
dispatch(changeVariableNameSucceeded(toVariablePayload(identifier, { newName })));
dispatch(toKeyedAction(rootStateKey, changeVariableNameSucceeded(toVariablePayload(identifier, { newName }))));
return;
}
const model = { ...cloneDeep(originalVariable), name: newName, id: newName };
const global = originalVariable.global;
const index = originalVariable.index;
const renamedIdentifier = toVariableIdentifier(model);
const renamedIdentifier = toKeyedVariableIdentifier(model);
dispatch(addVariable(toVariablePayload(renamedIdentifier, { global, index, model })));
dispatch(changeVariableNameSucceeded(toVariablePayload(renamedIdentifier, { newName })));
dispatch(toKeyedAction(rootStateKey, addVariable(toVariablePayload(renamedIdentifier, { global, index, model }))));
dispatch(
toKeyedAction(rootStateKey, changeVariableNameSucceeded(toVariablePayload(renamedIdentifier, { newName })))
);
dispatch(switchToEditMode(renamedIdentifier));
dispatch(removeVariable(toVariablePayload(identifier, { reIndex: false })));
dispatch(toKeyedAction(rootStateKey, removeVariable(toVariablePayload(identifier, { reIndex: false }))));
};
export const switchToNewMode =
(type: VariableType = 'query'): ThunkResult<void> =>
(key: string, type: VariableType = 'query'): ThunkResult<void> =>
(dispatch, getState) => {
const id = getNextAvailableId(type, getVariables(getState()));
const identifier = { type, id };
const id = getNextAvailableId(type, getVariablesByKey(key, getState()));
const identifier: VariableIdentifier = { type, id };
const global = false;
const index = getNewVariableIndex(getState());
const model = cloneDeep(variableAdapters.get(type).initialState);
const index = getNewVariableIndex(key, getState());
const model: VariableModel = cloneDeep(variableAdapters.get(type).initialState);
model.id = id;
model.name = id;
dispatch(addVariable(toVariablePayload<AddVariable>(identifier, { global, model, index })));
dispatch(setIdInEditor({ id: identifier.id }));
model.rootStateKey = key;
dispatch(toKeyedAction(key, addVariable(toVariablePayload<AddVariable>(identifier, { global, model, index }))));
dispatch(toKeyedAction(key, setIdInEditor({ id: identifier.id })));
};
export const switchToEditMode =
(identifier: VariableIdentifier): ThunkResult<void> =>
(identifier: KeyedVariableIdentifier): ThunkResult<void> =>
(dispatch) => {
dispatch(setIdInEditor({ id: identifier.id }));
const { rootStateKey } = identifier;
dispatch(toKeyedAction(rootStateKey, setIdInEditor({ id: identifier.id })));
};
export const switchToListMode = (): ThunkResult<void> => (dispatch, getState) => {
dispatch(clearIdInEditor());
const state = getState();
const variables = getEditorVariables(state);
const dashboard = state.dashboard.getModel();
const { usages } = createUsagesNetwork(variables, dashboard);
const usagesNetwork = transformUsagesToNetwork(usages);
export const switchToListMode =
(key: string): ThunkResult<void> =>
(dispatch, getState) => {
dispatch(toKeyedAction(key, clearIdInEditor()));
const state = getState();
const variables = getEditorVariables(key, state);
const dashboard = state.dashboard.getModel();
const { usages } = createUsagesNetwork(variables, dashboard);
const usagesNetwork = transformUsagesToNetwork(usages);
dispatch(initInspect({ usages, usagesNetwork }));
};
dispatch(toKeyedAction(key, initInspect({ usages, usagesNetwork })));
};
export function getNextAvailableId(type: VariableType, variables: VariableModel[]): string {
let counter = 0;
@@ -14,7 +14,7 @@ import {
VariableEditorState,
variableEditorUnMounted,
} from './reducer';
import { toVariablePayload } from '../state/types';
import { toVariablePayload } from '../utils';
describe('variableEditorReducer', () => {
describe('when setIdInEditor is dispatched', () => {
@@ -4,6 +4,8 @@ import { createQueryVariableAdapter } from './query/adapter';
import { getVariablesUrlParams } from './getAllVariableValuesForUrl';
import { initTemplateSrv } from '../../../test/helpers/initTemplateSrv';
const key = 'key';
describe('getAllVariableValuesForUrl', () => {
beforeAll(() => {
variableAdapters.register(createQueryVariableAdapter());
@@ -12,10 +14,11 @@ describe('getAllVariableValuesForUrl', () => {
describe('with multi value', () => {
beforeEach(() => {
setTemplateSrv(
initTemplateSrv([
initTemplateSrv(key, [
{
type: 'query',
name: 'test',
rootStateKey: key,
current: { value: ['val1', 'val2'] },
getValueForUrl: function () {
return this.current.value;
@@ -34,9 +37,10 @@ describe('getAllVariableValuesForUrl', () => {
describe('skip url sync', () => {
beforeEach(() => {
setTemplateSrv(
initTemplateSrv([
initTemplateSrv(key, [
{
name: 'test',
rootStateKey: key,
skipUrlSync: true,
current: { value: 'value' },
getValueForUrl: function () {
@@ -56,10 +60,11 @@ describe('getAllVariableValuesForUrl', () => {
describe('with multi value with skip url sync', () => {
beforeEach(() => {
setTemplateSrv(
initTemplateSrv([
initTemplateSrv(key, [
{
type: 'query',
name: 'test',
rootStateKey: key,
skipUrlSync: true,
current: { value: ['val1', 'val2'] },
getValueForUrl: function () {
@@ -78,7 +83,9 @@ describe('getAllVariableValuesForUrl', () => {
describe('fillVariableValuesForUrl with multi value and scopedVars', () => {
beforeEach(() => {
setTemplateSrv(initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]));
setTemplateSrv(
initTemplateSrv(key, [{ type: 'query', name: 'test', rootStateKey: key, current: { value: ['val1', 'val2'] } }])
);
});
it('should set scoped value as url params', () => {
@@ -91,7 +98,9 @@ describe('getAllVariableValuesForUrl', () => {
describe('fillVariableValuesForUrl with multi value, scopedVars and skip url sync', () => {
beforeEach(() => {
setTemplateSrv(initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]));
setTemplateSrv(
initTemplateSrv(key, [{ type: 'query', name: 'test', rootStateKey: key, current: { value: ['val1', 'val2'] } }])
);
});
it('should not set scoped value as url params', () => {
@@ -6,7 +6,6 @@ export function getVariablesUrlParams(scopedVars?: ScopedVars): UrlQueryMap {
const params: UrlQueryMap = {};
const variables = getTemplateSrv().getVariables();
// console.log(variables)
for (let i = 0; i < variables.length; i++) {
const variable = variables[i];
if (scopedVars && scopedVars[variable.name] !== void 0) {
@@ -1,6 +1,5 @@
import { getRootReducer, RootReducerType } from '../state/helpers';
import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { toVariableIdentifier, toVariablePayload } from '../state/types';
import { updateAutoValue, UpdateAutoValueDependencies, updateIntervalVariableOptions } from './actions';
import { createIntervalOptions } from './reducer';
import {
@@ -20,25 +19,37 @@ import { notifyApp } from '../../../core/actions';
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
import { variablesInitTransaction } from '../state/transactionReducer';
import { afterEach, beforeEach } from '../../../../test/lib/common';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
describe('interval actions', () => {
variableAdapters.setInit(() => [createIntervalVariableAdapter()]);
describe('when updateIntervalVariableOptions is dispatched', () => {
it('then correct actions are dispatched', async () => {
const interval = intervalBuilder().withId('0').withQuery('1s,1m,1h,1d').withAuto(false).build();
const interval = intervalBuilder()
.withId('0')
.withRootStateKey('key')
.withQuery('1s,1m,1h,1d')
.withAuto(false)
.build();
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval })))
.whenAsyncActionIsDispatched(updateIntervalVariableOptions(toVariableIdentifier(interval)), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval })))
)
.whenAsyncActionIsDispatched(updateIntervalVariableOptions(toKeyedVariableIdentifier(interval)), true);
tester.thenDispatchedActionsShouldEqual(
createIntervalOptions({ type: 'interval', id: '0', data: undefined }),
setCurrentVariableValue({
type: 'interval',
id: '0',
data: { option: { text: '1s', value: '1s', selected: false } },
})
toKeyedAction('key', createIntervalOptions({ type: 'interval', id: '0', data: undefined })),
toKeyedAction(
'key',
setCurrentVariableValue({
type: 'interval',
id: '0',
data: { option: { text: '1s', value: '1s', selected: false } },
})
)
);
});
});
@@ -67,6 +78,7 @@ describe('interval actions', () => {
it('then an notifyApp action should be dispatched', async () => {
const interval = intervalBuilder()
.withId('0')
.withRootStateKey('key')
.withQuery('1s,1m,1h,1d')
.withAuto(true)
.withAutoMin('1xyz') // illegal interval string
@@ -74,21 +86,26 @@ describe('interval actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval })))
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenAsyncActionIsDispatched(updateOptions(toVariableIdentifier(interval)), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval })))
)
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenAsyncActionIsDispatched(updateOptions(toKeyedVariableIdentifier(interval)), true);
tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
const expectedNumberOfActions = 4;
expect(dispatchedActions[0]).toEqual(variableStateFetching(toVariablePayload(interval)));
expect(dispatchedActions[1]).toEqual(createIntervalOptions(toVariablePayload(interval)));
expect(dispatchedActions[0]).toEqual(toKeyedAction('key', variableStateFetching(toVariablePayload(interval))));
expect(dispatchedActions[1]).toEqual(toKeyedAction('key', createIntervalOptions(toVariablePayload(interval))));
expect(dispatchedActions[2]).toEqual(
variableStateFailed(
toVariablePayload(interval, {
error: new Error(
'Invalid interval string, has to be either unit-less or end with one of the following units: "y, M, w, d, h, m, s, ms"'
),
})
toKeyedAction(
'key',
variableStateFailed(
toVariablePayload(interval, {
error: new Error(
'Invalid interval string, has to be either unit-less or end with one of the following units: "y, M, w, d, h, m, s, ms"'
),
})
)
)
);
@@ -107,6 +124,7 @@ describe('interval actions', () => {
it('then no actions are dispatched', async () => {
const interval = intervalBuilder()
.withId('0')
.withRootStateKey('key')
.withQuery('1s,1m,1h,1d')
.withAuto(true)
.withAutoMin('1xyz') // illegal interval string
@@ -115,9 +133,9 @@ describe('interval actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(
addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval }))
toKeyedAction('key', addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval })))
)
.whenAsyncActionIsDispatched(updateOptions(toVariableIdentifier(interval)), true);
.whenAsyncActionIsDispatched(updateOptions(toKeyedVariableIdentifier(interval)), true);
tester.thenNoActionsWhereDispatched();
});
@@ -127,7 +145,7 @@ describe('interval actions', () => {
describe('when updateAutoValue is dispatched', () => {
describe('and auto is false', () => {
it('then no dependencies are called', async () => {
const interval = intervalBuilder().withId('0').withAuto(false).build();
const interval = intervalBuilder().withId('0').withRootStateKey('key').withAuto(false).build();
const dependencies: UpdateAutoValueDependencies = {
calculateInterval: jest.fn(),
@@ -151,9 +169,9 @@ describe('interval actions', () => {
await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(
addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval }))
toKeyedAction('key', addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval })))
)
.whenAsyncActionIsDispatched(updateAutoValue(toVariableIdentifier(interval), dependencies), true);
.whenAsyncActionIsDispatched(updateAutoValue(toKeyedVariableIdentifier(interval), dependencies), true);
expect(dependencies.calculateInterval).toHaveBeenCalledTimes(0);
expect(dependencies.getTimeSrv().timeRange).toHaveBeenCalledTimes(0);
@@ -165,6 +183,7 @@ describe('interval actions', () => {
it('then correct dependencies are called', async () => {
const interval = intervalBuilder()
.withId('0')
.withRootStateKey('key')
.withName('intervalName')
.withAuto(true)
.withAutoCount(33)
@@ -195,9 +214,9 @@ describe('interval actions', () => {
await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(
addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval }))
toKeyedAction('key', addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval })))
)
.whenAsyncActionIsDispatched(updateAutoValue(toVariableIdentifier(interval), dependencies), true);
.whenAsyncActionIsDispatched(updateAutoValue(toKeyedVariableIdentifier(interval), dependencies), true);
expect(dependencies.calculateInterval).toHaveBeenCalledTimes(1);
expect(dependencies.calculateInterval).toHaveBeenCalledWith(
@@ -1,6 +1,6 @@
import { rangeUtil } from '@grafana/data';
import { toVariablePayload, VariableIdentifier } from '../state/types';
import { KeyedVariableIdentifier } from '../state/types';
import { ThunkResult } from '../../../types';
import { createIntervalOptions } from './reducer';
import { validateVariableSelectionState } from '../state/actions';
@@ -8,11 +8,14 @@ import { getVariable } from '../state/selectors';
import { IntervalVariableModel } from '../types';
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
import { getTemplateSrv, TemplateSrv } from '../../templating/template_srv';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { toVariablePayload } from '../utils';
export const updateIntervalVariableOptions =
(identifier: VariableIdentifier): ThunkResult<void> =>
(identifier: KeyedVariableIdentifier): ThunkResult<void> =>
async (dispatch) => {
await dispatch(createIntervalOptions(toVariablePayload(identifier)));
const { rootStateKey } = identifier;
await dispatch(toKeyedAction(rootStateKey, createIntervalOptions(toVariablePayload(identifier))));
await dispatch(updateAutoValue(identifier));
await dispatch(validateVariableSelectionState(identifier));
};
@@ -25,7 +28,7 @@ export interface UpdateAutoValueDependencies {
export const updateAutoValue =
(
identifier: VariableIdentifier,
identifier: KeyedVariableIdentifier,
dependencies: UpdateAutoValueDependencies = {
calculateInterval: rangeUtil.calculateInterval,
getTimeSrv: getTimeSrv,
@@ -33,7 +36,7 @@ export const updateAutoValue =
}
): ThunkResult<void> =>
(dispatch, getState) => {
const variableInState = getVariable<IntervalVariableModel>(identifier.id, getState());
const variableInState = getVariable<IntervalVariableModel>(identifier, getState());
if (variableInState.auto) {
const res = dependencies.calculateInterval(
dependencies.getTimeSrv().timeRange(),
@@ -4,10 +4,10 @@ import { dispatch } from '../../../store/store';
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions';
import { VariableAdapter } from '../adapters';
import { initialIntervalVariableModelState, intervalVariableReducer } from './reducer';
import { toVariableIdentifier } from '../state/types';
import { IntervalVariableEditor } from './IntervalVariableEditor';
import { updateAutoValue, updateIntervalVariableOptions } from './actions';
import { optionPickerFactory } from '../pickers';
import { toKeyedVariableIdentifier } from '../utils';
export const createIntervalVariableAdapter = (): VariableAdapter<IntervalVariableModel> => {
return {
@@ -22,18 +22,18 @@ export const createIntervalVariableAdapter = (): VariableAdapter<IntervalVariabl
return false;
},
setValue: async (variable, option, emitChanges = false) => {
await dispatch(updateAutoValue(toVariableIdentifier(variable)));
await dispatch(setOptionAsCurrent(toVariableIdentifier(variable), option, emitChanges));
await dispatch(updateAutoValue(toKeyedVariableIdentifier(variable)));
await dispatch(setOptionAsCurrent(toKeyedVariableIdentifier(variable), option, emitChanges));
},
setValueFromUrl: async (variable, urlValue) => {
await dispatch(updateAutoValue(toVariableIdentifier(variable)));
await dispatch(setOptionFromUrl(toVariableIdentifier(variable), urlValue));
await dispatch(updateAutoValue(toKeyedVariableIdentifier(variable)));
await dispatch(setOptionFromUrl(toKeyedVariableIdentifier(variable), urlValue));
},
updateOptions: async (variable) => {
await dispatch(updateIntervalVariableOptions(toVariableIdentifier(variable)));
await dispatch(updateIntervalVariableOptions(toKeyedVariableIdentifier(variable)));
},
getSaveModel: (variable) => {
const { index, id, state, global, ...rest } = cloneDeep(variable);
const { index, id, state, global, rootStateKey, ...rest } = cloneDeep(variable);
return rest;
},
getValueForUrl: (variable) => {
@@ -1,11 +1,12 @@
import { cloneDeep } from 'lodash';
import { getVariableTestContext } from '../state/helpers';
import { toVariablePayload, VariablesState } from '../state/types';
import { VariablesState } from '../state/types';
import { createIntervalVariableAdapter } from './adapter';
import { IntervalVariableModel } from '../types';
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { createIntervalOptions, intervalVariableReducer } from './reducer';
import { toVariablePayload } from '../utils';
describe('intervalVariableReducer', () => {
const adapter = createIntervalVariableAdapter();
@@ -1,7 +1,8 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { initialVariableModelState, IntervalVariableModel, VariableOption, VariableRefresh } from '../types';
import { getInstanceState, VariablePayload, initialVariablesState, VariablesState } from '../state/types';
import { initialVariablesState, VariablePayload, VariablesState } from '../state/types';
import { map } from 'lodash';
import { getInstanceState } from '../state/selectors';
export const initialIntervalVariableModelState: IntervalVariableModel = {
...initialVariableModelState,
@@ -9,7 +9,8 @@ import { VariablePickerProps } from '../types';
import { QueryVariableModel, VariableWithMultiSupport, VariableWithOptions } from '../../types';
import { queryBuilder } from '../../shared/testing/builders';
import { optionPickerFactory } from './OptionsPicker';
import { initialState, OptionsPickerState } from './reducer';
import { initialOptionPickerState, OptionsPickerState } from './reducer';
import { getPreloadedState } from '../../state/helpers';
interface Args {
pickerState?: Partial<OptionsPickerState>;
@@ -18,6 +19,7 @@ interface Args {
const defaultVariable = queryBuilder()
.withId('query0')
.withRootStateKey('key')
.withName('query0')
.withMulti()
.withCurrent(['A', 'C'])
@@ -35,17 +37,16 @@ function setupTestContext({ pickerState = {}, variable = {} }: Args = {}) {
onVariableChange,
};
const Picker = optionPickerFactory();
const optionsPicker: OptionsPickerState = { ...initialState, ...pickerState };
const optionsPicker: OptionsPickerState = { ...initialOptionPickerState, ...pickerState };
const dispatch = jest.fn();
const subscribe = jest.fn();
const getState = jest.fn().mockReturnValue({
templating: {
variables: {
[v.id]: { ...v },
},
optionsPicker,
const templatingState = {
variables: {
[v.id]: { ...v },
},
});
optionsPicker,
};
const getState = jest.fn().mockReturnValue(getPreloadedState('key', templatingState));
const store: any = { getState, dispatch, subscribe };
const { rerender } = render(
<Provider store={store}>
@@ -1,36 +1,58 @@
import React, { ComponentType, PureComponent } from 'react';
import { bindActionCreators } from 'redux';
import { connect, ConnectedProps } from 'react-redux';
import { ClickOutsideWrapper } from '@grafana/ui';
import { LoadingState } from '@grafana/data';
import { StoreState } from 'app/types';
import { StoreState, ThunkDispatch } from 'app/types';
import { VariableInput } from '../shared/VariableInput';
import { commitChangesToVariable, filterOrSearchOptions, navigateOptions, openOptions } from './actions';
import { OptionsPickerState, toggleAllOptions, toggleOption } from './reducer';
import { initialOptionPickerState, OptionsPickerState, toggleAllOptions, toggleOption } from './reducer';
import { VariableOption, VariableWithMultiSupport, VariableWithOptions } from '../../types';
import { VariableOptions } from '../shared/VariableOptions';
import { isMulti } from '../../guard';
import { VariablePickerProps } from '../types';
import { NavigationKey, VariablePickerProps } from '../types';
import { formatVariableLabel } from '../../shared/formatVariable';
import { toVariableIdentifier } from '../../state/types';
import { KeyedVariableIdentifier } from '../../state/types';
import { getVariableQueryRunner } from '../../query/VariableQueryRunner';
import { VariableLink } from '../shared/VariableLink';
import { getVariablesState } from '../../state/selectors';
import { toKeyedAction } from '../../state/keyedVariablesReducer';
import { toKeyedVariableIdentifier } from '../../utils';
export const optionPickerFactory = <Model extends VariableWithOptions | VariableWithMultiSupport>(): ComponentType<
VariablePickerProps<Model>
> => {
const mapDispatchToProps = {
openOptions,
commitChangesToVariable,
filterOrSearchOptions,
toggleAllOptions,
toggleOption,
navigateOptions,
const mapDispatchToProps = (dispatch: ThunkDispatch) => {
return {
...bindActionCreators({ openOptions, commitChangesToVariable, navigateOptions }, dispatch),
filterOrSearchOptions: (identifier: KeyedVariableIdentifier, filter = '') => {
dispatch(filterOrSearchOptions(identifier, filter));
},
toggleAllOptions: (identifier: KeyedVariableIdentifier) =>
dispatch(toKeyedAction(identifier.rootStateKey, toggleAllOptions())),
toggleOption: (
identifier: KeyedVariableIdentifier,
option: VariableOption,
clearOthers: boolean,
forceSelect: boolean
) => dispatch(toKeyedAction(identifier.rootStateKey, toggleOption({ option, clearOthers, forceSelect }))),
};
};
const mapStateToProps = (state: StoreState) => ({
picker: state.templating.optionsPicker,
});
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => {
const { rootStateKey } = ownProps.variable;
if (!rootStateKey) {
console.error('OptionPickerFactory: variable has no rootStateKey');
return {
picker: initialOptionPickerState,
};
}
return {
picker: getVariablesState(rootStateKey, state).optionsPicker,
};
};
const connector = connect(mapStateToProps, mapDispatchToProps);
@@ -40,8 +62,15 @@ export const optionPickerFactory = <Model extends VariableWithOptions | Variable
class OptionsPickerUnconnected extends PureComponent<Props> {
onShowOptions = () =>
this.props.openOptions(toVariableIdentifier(this.props.variable), this.props.onVariableChange);
onHideOptions = () => this.props.commitChangesToVariable(this.props.onVariableChange);
this.props.openOptions(toKeyedVariableIdentifier(this.props.variable), this.props.onVariableChange);
onHideOptions = () => {
if (!this.props.variable.rootStateKey) {
console.error('Variable has no rootStateKey');
return;
}
this.props.commitChangesToVariable(this.props.variable.rootStateKey, this.props.onVariableChange);
};
onToggleOption = (option: VariableOption, clearOthers: boolean) => {
const toggleFunc =
@@ -52,12 +81,29 @@ export const optionPickerFactory = <Model extends VariableWithOptions | Variable
};
onToggleSingleValueVariable = (option: VariableOption, clearOthers: boolean) => {
this.props.toggleOption({ option, clearOthers, forceSelect: false });
this.props.toggleOption(toKeyedVariableIdentifier(this.props.variable), option, clearOthers, false);
this.onHideOptions();
};
onToggleMultiValueVariable = (option: VariableOption, clearOthers: boolean) => {
this.props.toggleOption({ option, clearOthers, forceSelect: false });
this.props.toggleOption(toKeyedVariableIdentifier(this.props.variable), option, clearOthers, false);
};
onToggleAllOptions = () => {
this.props.toggleAllOptions(toKeyedVariableIdentifier(this.props.variable));
};
onFilterOrSearchOptions = (filter: string) => {
this.props.filterOrSearchOptions(toKeyedVariableIdentifier(this.props.variable), filter);
};
onNavigate = (key: NavigationKey, clearOthers: boolean) => {
if (!this.props.variable.rootStateKey) {
console.error('Variable has no rootStateKey');
return;
}
this.props.navigateOptions(this.props.variable.rootStateKey, key, clearOthers);
};
render() {
@@ -87,7 +133,7 @@ export const optionPickerFactory = <Model extends VariableWithOptions | Variable
}
onCancel = () => {
getVariableQueryRunner().cancelRequest(toVariableIdentifier(this.props.variable));
getVariableQueryRunner().cancelRequest(toKeyedVariableIdentifier(this.props.variable));
};
renderOptions(picker: OptionsPickerState) {
@@ -97,15 +143,15 @@ export const optionPickerFactory = <Model extends VariableWithOptions | Variable
<VariableInput
id={id}
value={picker.queryValue}
onChange={this.props.filterOrSearchOptions}
onNavigate={this.props.navigateOptions}
onChange={this.onFilterOrSearchOptions}
onNavigate={this.onNavigate}
aria-expanded={true}
aria-controls={`options-${id}`}
/>
<VariableOptions
values={picker.options}
onToggle={this.onToggleOption}
onToggleAll={this.props.toggleAllOptions}
onToggleAll={this.onToggleAllOptions}
highlightIndex={picker.highlightIndex}
multi={picker.multi}
selectedValues={picker.selectedValues}
@@ -1,9 +1,9 @@
import { reduxTester } from '../../../../../test/core/redux/reduxTester';
import { getRootReducer, RootReducerType } from '../../state/helpers';
import { getPreloadedState, getRootReducer, RootReducerType } from '../../state/helpers';
import { initialVariableModelState, QueryVariableModel, VariableRefresh, VariableSort } from '../../types';
import {
hideOptions,
initialState,
initialOptionPickerState,
moveOptionsHighlight,
showOptions,
toggleOption,
@@ -18,12 +18,13 @@ import {
toggleOptionByHighlight,
} from './actions';
import { NavigationKey } from '../types';
import { toVariablePayload } from '../../state/types';
import { addVariable, changeVariableProp, setCurrentVariableValue } from '../../state/sharedReducer';
import { variableAdapters } from '../../adapters';
import { createQueryVariableAdapter } from '../../query/adapter';
import { locationService } from '@grafana/runtime';
import { queryBuilder } from '../../shared/testing/builders';
import { toKeyedAction } from '../../state/keyedVariablesReducer';
import { toKeyedVariableIdentifier, toVariablePayload } from '../../utils';
const datasource = {
metricFindQuery: jest.fn(() => Promise.resolve([])),
@@ -59,9 +60,11 @@ describe('options picker actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(showOptions(variable))
.whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
.whenAsyncActionIsDispatched(navigateOptions('key', key, clearOthers), true);
const option = {
...createOption(['A']),
@@ -70,9 +73,12 @@ describe('options picker actions', () => {
};
tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(toVariablePayload(variable, { option })),
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' })),
hideOptions()
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
toKeyedAction(
'key',
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' }))
),
toKeyedAction('key', hideOptions())
);
});
});
@@ -91,12 +97,16 @@ describe('options picker actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(showOptions(variable))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, false))
.whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, false))
.whenAsyncActionIsDispatched(navigateOptions('key', key, clearOthers), true);
tester.thenDispatchedActionsShouldEqual(toggleOption({ option, forceSelect: false, clearOthers }));
tester.thenDispatchedActionsShouldEqual(
toKeyedAction('key', toggleOption({ option, forceSelect: false, clearOthers }))
);
});
});
@@ -114,12 +124,16 @@ describe('options picker actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(showOptions(variable))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenAsyncActionIsDispatched(navigateOptions('key', key, clearOthers), true);
tester.thenDispatchedActionsShouldEqual(toggleOption({ option, forceSelect: false, clearOthers }));
tester.thenDispatchedActionsShouldEqual(
toKeyedAction('key', toggleOption({ option, forceSelect: false, clearOthers }))
);
});
});
@@ -133,14 +147,18 @@ describe('options picker actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(showOptions(variable))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenAsyncActionIsDispatched(navigateOptions('key', key, clearOthers), true);
tester.thenDispatchedActionsShouldEqual(toggleOption({ option: options[2], forceSelect: false, clearOthers }));
tester.thenDispatchedActionsShouldEqual(
toKeyedAction('key', toggleOption({ option: options[2], forceSelect: false, clearOthers }))
);
});
});
@@ -154,15 +172,19 @@ describe('options picker actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(showOptions(variable))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveUp, clearOthers))
.whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveUp, clearOthers))
.whenAsyncActionIsDispatched(navigateOptions('key', key, clearOthers), true);
tester.thenDispatchedActionsShouldEqual(toggleOption({ option: options[1], forceSelect: false, clearOthers }));
tester.thenDispatchedActionsShouldEqual(
toKeyedAction('key', toggleOption({ option: options[1], forceSelect: false, clearOthers }))
);
});
});
@@ -176,13 +198,15 @@ describe('options picker actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(showOptions(variable))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveUp, clearOthers))
.whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveUp, clearOthers))
.whenAsyncActionIsDispatched(navigateOptions('key', key, clearOthers), true);
const option = {
...createOption(['B']),
@@ -191,11 +215,14 @@ describe('options picker actions', () => {
};
tester.thenDispatchedActionsShouldEqual(
toggleOption({ option: options[1], forceSelect: true, clearOthers }),
setCurrentVariableValue(toVariablePayload(variable, { option })),
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' })),
hideOptions(),
setCurrentVariableValue(toVariablePayload(variable, { option }))
toKeyedAction('key', toggleOption({ option: options[1], forceSelect: true, clearOthers })),
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
toKeyedAction(
'key',
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' }))
),
toKeyedAction('key', hideOptions()),
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
);
expect(locationService.partial).toHaveBeenLastCalledWith({ 'var-Constant': ['B'] });
});
@@ -209,11 +236,16 @@ describe('options picker actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(showOptions(variable))
.whenAsyncActionIsDispatched(filterOrSearchOptions(filter), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
.whenAsyncActionIsDispatched(filterOrSearchOptions(toKeyedVariableIdentifier(variable), filter), true);
tester.thenDispatchedActionsShouldEqual(updateSearchQuery(filter), updateOptionsAndFilter(variable.options));
tester.thenDispatchedActionsShouldEqual(
toKeyedAction('key', updateSearchQuery(filter)),
toKeyedAction('key', updateOptionsAndFilter(variable.options))
);
});
});
@@ -221,26 +253,25 @@ describe('options picker actions', () => {
it('then correct actions are dispatched', async () => {
const variable = queryBuilder()
.withId('query0')
.withRootStateKey('key')
.withName('query0')
.withMulti()
.withCurrent(['A', 'C'])
.withOptions('A', 'B', 'C')
.build();
const preloadedState: any = {
templating: {
variables: {
[variable.id]: { ...variable },
},
optionsPicker: { ...initialState },
const preloadedState = getPreloadedState('key', {
variables: {
[variable.id]: { ...variable },
},
};
optionsPicker: { ...initialOptionPickerState },
});
const tester = await reduxTester<RootReducerType>({ preloadedState })
.givenRootReducer(getRootReducer())
.whenAsyncActionIsDispatched(openOptions(variable, undefined));
.whenAsyncActionIsDispatched(openOptions(toKeyedVariableIdentifier(variable), undefined));
tester.thenDispatchedActionsShouldEqual(showOptions(variable));
tester.thenDispatchedActionsShouldEqual(toKeyedAction('key', showOptions(variable)));
});
});
@@ -248,26 +279,25 @@ describe('options picker actions', () => {
it('then correct actions are dispatched', async () => {
const variable = queryBuilder()
.withId('query0')
.withRootStateKey('key')
.withName('query0')
.withMulti()
.withCurrent(['A', 'C'])
.withOptions('A', 'B', 'C')
.build();
const preloadedState: any = {
templating: {
variables: {
[variable.id]: { ...variable },
},
optionsPicker: { ...initialState, id: variable.id },
const preloadedState = getPreloadedState('key', {
variables: {
[variable.id]: { ...variable },
},
};
optionsPicker: { ...initialOptionPickerState, id: variable.id },
});
const tester = await reduxTester<RootReducerType>({ preloadedState })
.givenRootReducer(getRootReducer())
.whenAsyncActionIsDispatched(openOptions(variable, undefined));
.whenAsyncActionIsDispatched(openOptions(toKeyedVariableIdentifier(variable), undefined));
tester.thenDispatchedActionsShouldEqual(showOptions(variable));
tester.thenDispatchedActionsShouldEqual(toKeyedAction('key', showOptions(variable)));
});
});
@@ -275,6 +305,7 @@ describe('options picker actions', () => {
it('then correct actions are dispatched', async () => {
const variableInPickerState = queryBuilder()
.withId('query1')
.withRootStateKey('key')
.withName('query1')
.withMulti()
.withCurrent(['A', 'C'])
@@ -283,31 +314,33 @@ describe('options picker actions', () => {
const variable = queryBuilder()
.withId('query0')
.withRootStateKey('key')
.withName('query0')
.withMulti()
.withCurrent(['A'])
.withOptions('A', 'B', 'C')
.build();
const preloadedState: any = {
templating: {
variables: {
[variable.id]: { ...variable },
[variableInPickerState.id]: { ...variableInPickerState },
},
optionsPicker: { ...initialState, id: variableInPickerState.id },
const preloadedState = getPreloadedState('key', {
variables: {
[variable.id]: { ...variable },
[variableInPickerState.id]: { ...variableInPickerState },
},
};
optionsPicker: { ...initialOptionPickerState, id: variableInPickerState.id },
});
const tester = await reduxTester<RootReducerType>({ preloadedState })
.givenRootReducer(getRootReducer())
.whenAsyncActionIsDispatched(openOptions(variable, undefined));
.whenAsyncActionIsDispatched(openOptions(toKeyedVariableIdentifier(variable), undefined));
tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue({ type: 'query', id: 'query1', data: { option: undefined } }),
changeVariableProp({ type: 'query', id: 'query1', data: { propName: 'queryValue', propValue: '' } }),
hideOptions(),
showOptions(variable)
toKeyedAction('key', setCurrentVariableValue({ type: 'query', id: 'query1', data: { option: undefined } })),
toKeyedAction(
'key',
changeVariableProp({ type: 'query', id: 'query1', data: { propName: 'queryValue', propValue: '' } })
),
toKeyedAction('key', hideOptions()),
toKeyedAction('key', showOptions(variable))
);
});
});
@@ -319,9 +352,11 @@ describe('options picker actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(showOptions(variable))
.whenAsyncActionIsDispatched(commitChangesToVariable(), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
.whenAsyncActionIsDispatched(commitChangesToVariable('key'), true);
const option = {
...createOption(['A']),
@@ -330,9 +365,12 @@ describe('options picker actions', () => {
};
tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(toVariablePayload(variable, { option })),
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' })),
hideOptions()
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
toKeyedAction(
'key',
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' }))
),
toKeyedAction('key', hideOptions())
);
});
});
@@ -345,11 +383,13 @@ describe('options picker actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(showOptions(variable))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(toggleOptionByHighlight(clearOthers))
.whenAsyncActionIsDispatched(commitChangesToVariable(), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(toggleOptionByHighlight('key', clearOthers))
.whenAsyncActionIsDispatched(commitChangesToVariable('key'), true);
const option = {
...createOption([]),
@@ -358,10 +398,13 @@ describe('options picker actions', () => {
};
tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(toVariablePayload(variable, { option })),
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' })),
hideOptions(),
setCurrentVariableValue(toVariablePayload(variable, { option }))
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
toKeyedAction(
'key',
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' }))
),
toKeyedAction('key', hideOptions()),
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
);
expect(locationService.partial).toHaveBeenLastCalledWith({ 'var-Constant': [] });
});
@@ -375,12 +418,14 @@ describe('options picker actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(showOptions(variable))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(toggleOptionByHighlight(clearOthers))
.whenActionIsDispatched(filterOrSearchOptions('C'))
.whenAsyncActionIsDispatched(commitChangesToVariable(), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(toggleOptionByHighlight('key', clearOthers))
.whenActionIsDispatched(filterOrSearchOptions(toKeyedVariableIdentifier(variable), 'C'))
.whenAsyncActionIsDispatched(commitChangesToVariable('key'), true);
const option = {
...createOption([]),
@@ -389,10 +434,13 @@ describe('options picker actions', () => {
};
tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(toVariablePayload(variable, { option })),
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: 'C' })),
hideOptions(),
setCurrentVariableValue(toVariablePayload(variable, { option }))
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
toKeyedAction(
'key',
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: 'C' }))
),
toKeyedAction('key', hideOptions()),
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
);
expect(locationService.partial).toHaveBeenLastCalledWith({ 'var-Constant': [] });
});
@@ -406,14 +454,18 @@ describe('options picker actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(showOptions(variable))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(toggleOptionByHighlight(clearOthers), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(toggleOptionByHighlight('key', clearOthers), true);
const option = createOption('A');
tester.thenDispatchedActionsShouldEqual(toggleOption({ option, forceSelect: false, clearOthers }));
tester.thenDispatchedActionsShouldEqual(
toKeyedAction('key', toggleOption({ option, forceSelect: false, clearOthers }))
);
});
});
@@ -425,25 +477,27 @@ describe('options picker actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(showOptions(variable))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(toggleOptionByHighlight(clearOthers), true)
.whenActionIsDispatched(filterOrSearchOptions('B'))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(toggleOptionByHighlight(clearOthers));
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(toggleOptionByHighlight('key', clearOthers), true)
.whenActionIsDispatched(filterOrSearchOptions(toKeyedVariableIdentifier(variable), 'B'))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
.whenActionIsDispatched(toggleOptionByHighlight('key', clearOthers));
const optionA = createOption('A');
const optionBC = createOption('BD');
tester.thenDispatchedActionsShouldEqual(
toggleOption({ option: optionA, forceSelect: false, clearOthers }),
updateSearchQuery('B'),
updateOptionsAndFilter(variable.options),
moveOptionsHighlight(1),
moveOptionsHighlight(1),
toggleOption({ option: optionBC, forceSelect: false, clearOthers })
toKeyedAction('key', toggleOption({ option: optionA, forceSelect: false, clearOthers })),
toKeyedAction('key', updateSearchQuery('B')),
toKeyedAction('key', updateOptionsAndFilter(variable.options)),
toKeyedAction('key', moveOptionsHighlight(1)),
toKeyedAction('key', moveOptionsHighlight(1)),
toKeyedAction('key', toggleOption({ option: optionBC, forceSelect: false, clearOthers }))
);
});
});
@@ -454,6 +508,7 @@ function createMultiVariable(extend?: Partial<QueryVariableModel>): QueryVariabl
...initialVariableModelState,
type: 'query',
id: '0',
rootStateKey: 'key',
index: 0,
current: createOption([]),
options: [],
@@ -2,7 +2,7 @@ import { debounce, trim } from 'lodash';
import { StoreState, ThunkDispatch, ThunkResult } from 'app/types';
import { VariableOption, VariableWithMultiSupport, VariableWithOptions } from '../../types';
import { variableAdapters } from '../../adapters';
import { getVariable } from '../../state/selectors';
import { getVariable, getVariablesState } from '../../state/selectors';
import { NavigationKey } from '../types';
import {
hideOptions,
@@ -15,50 +15,56 @@ import {
updateSearchQuery,
} from './reducer';
import { changeVariableProp, setCurrentVariableValue } from '../../state/sharedReducer';
import { toVariablePayload, VariableIdentifier } from '../../state/types';
import { containsSearchFilter, getCurrentText } from '../../utils';
import { KeyedVariableIdentifier } from '../../state/types';
import { containsSearchFilter, getCurrentText, toVariablePayload } from '../../utils';
import { toKeyedAction } from '../../state/keyedVariablesReducer';
export const navigateOptions = (key: NavigationKey, clearOthers: boolean): ThunkResult<void> => {
export const navigateOptions = (rootStateKey: string, key: NavigationKey, clearOthers: boolean): ThunkResult<void> => {
return async (dispatch, getState) => {
if (key === NavigationKey.cancel) {
return await dispatch(commitChangesToVariable());
return await dispatch(commitChangesToVariable(rootStateKey));
}
if (key === NavigationKey.select) {
return dispatch(toggleOptionByHighlight(clearOthers));
return dispatch(toggleOptionByHighlight(rootStateKey, clearOthers));
}
if (key === NavigationKey.selectAndClose) {
dispatch(toggleOptionByHighlight(clearOthers, true));
return await dispatch(commitChangesToVariable());
dispatch(toggleOptionByHighlight(rootStateKey, clearOthers, true));
return await dispatch(commitChangesToVariable(rootStateKey));
}
if (key === NavigationKey.moveDown) {
return dispatch(moveOptionsHighlight(1));
return dispatch(toKeyedAction(rootStateKey, moveOptionsHighlight(1)));
}
if (key === NavigationKey.moveUp) {
return dispatch(moveOptionsHighlight(-1));
return dispatch(toKeyedAction(rootStateKey, moveOptionsHighlight(-1)));
}
return undefined;
};
};
export const filterOrSearchOptions = (searchQuery = ''): ThunkResult<void> => {
export const filterOrSearchOptions = (
passedIdentifier: KeyedVariableIdentifier,
searchQuery = ''
): ThunkResult<void> => {
return async (dispatch, getState) => {
const { id, queryValue } = getState().templating.optionsPicker;
const { query, options } = getVariable<VariableWithOptions>(id, getState());
dispatch(updateSearchQuery(searchQuery));
const { rootStateKey } = passedIdentifier;
const { id, queryValue } = getVariablesState(rootStateKey, getState()).optionsPicker;
const identifier: KeyedVariableIdentifier = { id, rootStateKey: rootStateKey, type: 'query' };
const { query, options } = getVariable<VariableWithOptions>(identifier, getState());
dispatch(toKeyedAction(rootStateKey, updateSearchQuery(searchQuery)));
if (trim(queryValue) === trim(searchQuery)) {
return;
}
if (containsSearchFilter(query)) {
return searchForOptionsWithDebounce(dispatch, getState, searchQuery);
return searchForOptionsWithDebounce(dispatch, getState, searchQuery, rootStateKey);
}
return dispatch(updateOptionsAndFilter(options));
return dispatch(toKeyedAction(rootStateKey, updateOptionsAndFilter(options)));
};
};
@@ -68,17 +74,18 @@ const setVariable = async (updated: VariableWithMultiSupport) => {
return;
};
export const commitChangesToVariable = (callback?: (updated: any) => void): ThunkResult<void> => {
export const commitChangesToVariable = (key: string, callback?: (updated: any) => void): ThunkResult<void> => {
return async (dispatch, getState) => {
const picker = getState().templating.optionsPicker;
const existing = getVariable<VariableWithMultiSupport>(picker.id, getState());
const picker = getVariablesState(key, getState()).optionsPicker;
const identifier: KeyedVariableIdentifier = { id: picker.id, rootStateKey: key, type: 'query' };
const existing = getVariable<VariableWithMultiSupport>(identifier, getState());
const currentPayload = { option: mapToCurrent(picker) };
const searchQueryPayload = { propName: 'queryValue', propValue: picker.queryValue };
dispatch(setCurrentVariableValue(toVariablePayload(existing, currentPayload)));
dispatch(changeVariableProp(toVariablePayload(existing, searchQueryPayload)));
const updated = getVariable<VariableWithMultiSupport>(picker.id, getState());
dispatch(hideOptions());
dispatch(toKeyedAction(key, setCurrentVariableValue(toVariablePayload(existing, currentPayload))));
dispatch(toKeyedAction(key, changeVariableProp(toVariablePayload(existing, searchQueryPayload))));
const updated = getVariable<VariableWithMultiSupport>(identifier, getState());
dispatch(toKeyedAction(key, hideOptions()));
if (getCurrentText(existing) === getCurrentText(updated)) {
return;
@@ -93,36 +100,43 @@ export const commitChangesToVariable = (callback?: (updated: any) => void): Thun
};
export const openOptions =
({ id }: VariableIdentifier, callback?: (updated: any) => void): ThunkResult<void> =>
(identifier: KeyedVariableIdentifier, callback?: (updated: any) => void): ThunkResult<void> =>
async (dispatch, getState) => {
const picker = getState().templating.optionsPicker;
const { id, rootStateKey: uid } = identifier;
const picker = getVariablesState(uid, getState()).optionsPicker;
if (picker.id && picker.id !== id) {
await dispatch(commitChangesToVariable(callback));
await dispatch(commitChangesToVariable(uid, callback));
}
const variable = getVariable<VariableWithMultiSupport>(id, getState());
dispatch(showOptions(variable));
const variable = getVariable<VariableWithMultiSupport>(identifier, getState());
dispatch(toKeyedAction(uid, showOptions(variable)));
};
export const toggleOptionByHighlight = (clearOthers: boolean, forceSelect = false): ThunkResult<void> => {
export const toggleOptionByHighlight = (key: string, clearOthers: boolean, forceSelect = false): ThunkResult<void> => {
return (dispatch, getState) => {
const { highlightIndex, options } = getState().templating.optionsPicker;
const { highlightIndex, options } = getVariablesState(key, getState()).optionsPicker;
const option = options[highlightIndex];
dispatch(toggleOption({ option, forceSelect, clearOthers }));
dispatch(toKeyedAction(key, toggleOption({ option, forceSelect, clearOthers })));
};
};
const searchForOptions = async (dispatch: ThunkDispatch, getState: () => StoreState, searchQuery: string) => {
const searchForOptions = async (
dispatch: ThunkDispatch,
getState: () => StoreState,
searchQuery: string,
key: string
) => {
try {
const { id } = getState().templating.optionsPicker;
const existing = getVariable<VariableWithOptions>(id, getState());
const { id } = getVariablesState(key, getState()).optionsPicker;
const identifier: KeyedVariableIdentifier = { id, rootStateKey: key, type: 'query' };
const existing = getVariable<VariableWithOptions>(identifier, getState());
const adapter = variableAdapters.get(existing.type);
await adapter.updateOptions(existing, searchQuery);
const updated = getVariable<VariableWithOptions>(id, getState());
dispatch(updateOptionsFromSearch(updated.options));
const updated = getVariable<VariableWithOptions>(identifier, getState());
dispatch(toKeyedAction(key, updateOptionsFromSearch(updated.options)));
} catch (error) {
console.error(error);
}
@@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash';
import {
cleanPickerState,
hideOptions,
initialState as optionsPickerInitialState,
initialOptionPickerState as optionsPickerInitialState,
moveOptionsHighlight,
OPTIONS_LIMIT,
optionsPickerReducer,
@@ -21,7 +21,7 @@ export interface OptionsPickerState {
multi: boolean;
}
export const initialState: OptionsPickerState = {
export const initialOptionPickerState: OptionsPickerState = {
id: '',
highlightIndex: -1,
queryValue: '',
@@ -106,7 +106,7 @@ const updateAllSelection = (state: OptionsPickerState): OptionsPickerState => {
const optionsPickerSlice = createSlice({
name: 'templating/optionsPicker',
initialState,
initialState: initialOptionPickerState,
reducers: {
showOptions: (state, action: PayloadAction<VariableWithOptions>): OptionsPickerState => {
const { query, options } = action.payload;
@@ -131,7 +131,7 @@ const optionsPickerSlice = createSlice({
return applyStateChanges(state, updateDefaultSelection, updateOptions);
},
hideOptions: (state, action: PayloadAction): OptionsPickerState => {
return { ...initialState };
return { ...initialOptionPickerState };
},
toggleOption: (state, action: PayloadAction<ToggleOption>): OptionsPickerState => {
const { option, clearOthers, forceSelect } = action.payload;
@@ -212,7 +212,7 @@ const optionsPickerSlice = createSlice({
return applyStateChanges(state, updateDefaultSelection, updateOptions);
},
cleanPickerState: () => initialState,
cleanPickerState: () => initialOptionPickerState,
},
});
@@ -10,14 +10,17 @@ import { LegacyVariableQueryEditor } from '../editor/LegacyVariableQueryEditor';
import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
import { NEW_VARIABLE_ID } from '../constants';
import { VariableModel } from '../types';
import { KeyedVariableIdentifier } from '../state/types';
const setupTestContext = (options: Partial<Props>) => {
const variableDefaults: Partial<VariableModel> = { rootStateKey: 'key' };
const extended = {
VariableQueryEditor: LegacyVariableQueryEditor,
dataSource: {} as unknown as DataSourceApi,
};
const defaults: Props = {
variable: { ...initialQueryVariableModelState },
variable: { ...initialQueryVariableModelState, ...variableDefaults },
initQueryVariableEditor: jest.fn(),
changeQueryVariableDataSource: jest.fn(),
changeQueryVariableQuery: jest.fn(),
@@ -47,20 +50,22 @@ jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
};
});
const defaultIdentifier: KeyedVariableIdentifier = { type: 'query', rootStateKey: 'key', id: NEW_VARIABLE_ID };
describe('QueryVariableEditor', () => {
describe('when the component is mounted', () => {
it('then it should call initQueryVariableEditor', () => {
const { props } = setupTestContext({});
expect(props.initQueryVariableEditor).toHaveBeenCalledTimes(1);
expect(props.initQueryVariableEditor).toHaveBeenCalledWith({ type: 'query', id: NEW_VARIABLE_ID });
expect(props.initQueryVariableEditor).toHaveBeenCalledWith(defaultIdentifier);
});
});
describe('when the user changes', () => {
it.each`
fieldName | propName | expectedArgs
${'query'} | ${'changeQueryVariableQuery'} | ${[{ type: 'query', id: NEW_VARIABLE_ID }, 't', 't']}
${'query'} | ${'changeQueryVariableQuery'} | ${[defaultIdentifier, 't', 't']}
${'regex'} | ${'onPropChange'} | ${[{ propName: 'regex', propValue: 't', updateOptions: true }]}
`(
'$fieldName field and tabs away then $propName should be called with correct args',
@@ -9,9 +9,9 @@ import { DataSourceInstanceSettings, getDataSourceRef, LoadingState, SelectableV
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { QueryVariableModel, VariableRefresh, VariableSort, VariableWithMultiSupport } from '../types';
import { changeQueryVariableDataSource, changeQueryVariableQuery, initQueryVariableEditor } from './actions';
import { initialVariableEditorState } from '../editor/reducer';
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
import { StoreState } from '../../../types';
import { toVariableIdentifier } from '../state/types';
import { changeVariableMultiValue } from '../state/actions';
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
import { isLegacyQueryEditor, isQueryEditor } from '../guard';
@@ -20,10 +20,24 @@ import { VariableTextField } from '../editor/VariableTextField';
import { QueryVariableRefreshSelect } from './QueryVariableRefreshSelect';
import { QueryVariableSortSelect } from './QueryVariableSortSelect';
import { getQueryVariableEditorState } from '../editor/selectors';
import { getVariablesState } from '../state/selectors';
import { toKeyedVariableIdentifier } from '../utils';
const mapStateToProps = (state: StoreState) => ({
extended: getQueryVariableEditorState(state.templating.editor),
});
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => {
const { rootStateKey } = ownProps.variable;
if (!rootStateKey) {
console.error('QueryVariableEditor: variable has no rootStateKey');
return {
extended: getQueryVariableEditorState(initialVariableEditorState),
};
}
const { editor } = getVariablesState(rootStateKey, state);
return {
extended: getQueryVariableEditorState(editor),
};
};
const mapDispatchToProps = {
initQueryVariableEditor,
@@ -52,13 +66,13 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
};
async componentDidMount() {
await this.props.initQueryVariableEditor(toVariableIdentifier(this.props.variable));
await this.props.initQueryVariableEditor(toKeyedVariableIdentifier(this.props.variable));
}
componentDidUpdate(prevProps: Readonly<Props>): void {
if (prevProps.variable.datasource !== this.props.variable.datasource) {
this.props.changeQueryVariableDataSource(
toVariableIdentifier(this.props.variable),
toKeyedVariableIdentifier(this.props.variable),
this.props.variable.datasource
);
}
@@ -73,7 +87,7 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
onLegacyQueryChange = async (query: any, definition: string) => {
if (this.props.variable.query !== query) {
this.props.changeQueryVariableQuery(toVariableIdentifier(this.props.variable), query, definition);
this.props.changeQueryVariableQuery(toKeyedVariableIdentifier(this.props.variable), query, definition);
}
};
@@ -85,7 +99,7 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
definition = query.query;
}
this.props.changeQueryVariableQuery(toVariableIdentifier(this.props.variable), query, definition);
this.props.changeQueryVariableQuery(toKeyedVariableIdentifier(this.props.variable), query, definition);
}
};
@@ -5,9 +5,13 @@ import { delay } from 'rxjs/operators';
import { UpdateOptionsResults, VariableQueryRunner } from './VariableQueryRunner';
import { queryBuilder } from '../shared/testing/builders';
import { QueryRunner, QueryRunners } from './queryRunners';
import { toVariableIdentifier, VariableIdentifier } from '../state/types';
import { KeyedVariableIdentifier } from '../state/types';
import { QueryVariableModel } from '../types';
import { updateVariableOptions } from './reducer';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { initialTransactionState } from '../state/transactionReducer';
import { getPreloadedState } from '../state/helpers';
import { toKeyedVariableIdentifier } from '../utils';
type DoneCallback = {
(...args: any[]): any;
@@ -16,7 +20,7 @@ type DoneCallback = {
function expectOnResults(args: {
runner: VariableQueryRunner;
identifier: VariableIdentifier;
identifier: KeyedVariableIdentifier;
done: DoneCallback;
expect: (results: UpdateOptionsResults[]) => void;
}) {
@@ -32,7 +36,7 @@ function expectOnResults(args: {
done();
} catch (err) {
subscription.unsubscribe();
done.fail(err);
done(err);
}
}
},
@@ -40,25 +44,23 @@ function expectOnResults(args: {
}
function getTestContext(variable?: QueryVariableModel) {
variable = variable ?? queryBuilder().withId('query').build();
const getTimeSrv = jest.fn().mockReturnValue({
timeRange: jest.fn().mockReturnValue(getDefaultTimeRange()),
});
const key = '0123456789';
variable = variable ?? queryBuilder().withId('query').withRootStateKey(key).withName('query').build();
const datasource: any = { metricFindQuery: jest.fn().mockResolvedValue([]) };
const identifier = toVariableIdentifier(variable);
const identifier = toKeyedVariableIdentifier(variable);
const searchFilter = undefined;
const getTemplatedRegex = jest.fn().mockReturnValue('getTemplatedRegex result');
const dispatch = jest.fn().mockResolvedValue({});
const getState = jest.fn().mockReturnValue({
templating: {
transaction: {
uid: '0123456789',
},
},
const templatingState = {
transaction: { ...initialTransactionState, uid: key },
variables: {
[variable.id]: variable,
},
});
};
const getState = jest.fn().mockReturnValue(getPreloadedState(key, templatingState));
const queryRunner: QueryRunner = {
type: VariableSupportType.Standard,
canRun: jest.fn().mockReturnValue(true),
@@ -81,6 +83,7 @@ function getTestContext(variable?: QueryVariableModel) {
});
return {
key,
identifier,
datasource,
runner,
@@ -100,7 +103,7 @@ function getTestContext(variable?: QueryVariableModel) {
describe('VariableQueryRunner', () => {
describe('happy case', () => {
it('then it should work as expected', (done) => {
const { identifier, runner, datasource, getState, getVariable, queryRunners, queryRunner, dispatch } =
const { key, identifier, runner, datasource, getState, getVariable, queryRunners, queryRunner, dispatch } =
getTestContext();
expectOnResults({
@@ -124,11 +127,14 @@ describe('VariableQueryRunner', () => {
// updateVariableOptions and validateVariableSelectionState
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch.mock.calls[0][0]).toEqual(
updateVariableOptions({
id: 'query',
type: 'query',
data: { results: [], templatedRegex: 'getTemplatedRegex result' },
})
toKeyedAction(
key,
updateVariableOptions({
id: 'query',
type: 'query',
data: { results: [], templatedRegex: 'getTemplatedRegex result' },
})
)
);
},
done,
@@ -11,8 +11,8 @@ import {
ScopedVars,
} from '@grafana/data';
import { VariableIdentifier } from '../state/types';
import { getVariable } from '../state/selectors';
import { KeyedVariableIdentifier } from '../state/types';
import { getLastKey, getVariable } from '../state/selectors';
import { QueryVariableModel, VariableRefresh } from '../types';
import { StoreState, ThunkDispatch } from '../../../types';
import { dispatch, getState } from '../../../store/store';
@@ -24,14 +24,14 @@ import { runRequest } from '../../query/state/runRequest';
import { toMetricFindValues, updateOptionsState, validateVariableSelection } from './operators';
interface UpdateOptionsArgs {
identifier: VariableIdentifier;
identifier: KeyedVariableIdentifier;
datasource: DataSourceApi;
searchFilter?: string;
}
export interface UpdateOptionsResults {
state: LoadingState;
identifier: VariableIdentifier;
identifier: KeyedVariableIdentifier;
error?: any;
cancelled?: boolean;
}
@@ -49,7 +49,7 @@ interface VariableQueryRunnerArgs {
export class VariableQueryRunner {
private readonly updateOptionsRequests: Subject<UpdateOptionsArgs>;
private readonly updateOptionsResults: Subject<UpdateOptionsResults>;
private readonly cancelRequests: Subject<{ identifier: VariableIdentifier }>;
private readonly cancelRequests: Subject<{ identifier: KeyedVariableIdentifier }>;
private readonly subscription: Unsubscribable;
constructor(
@@ -65,7 +65,7 @@ export class VariableQueryRunner {
) {
this.updateOptionsRequests = new Subject<UpdateOptionsArgs>();
this.updateOptionsResults = new Subject<UpdateOptionsResults>();
this.cancelRequests = new Subject<{ identifier: VariableIdentifier }>();
this.cancelRequests = new Subject<{ identifier: KeyedVariableIdentifier }>();
this.onNewRequest = this.onNewRequest.bind(this);
this.subscription = this.updateOptionsRequests.subscribe(this.onNewRequest);
}
@@ -74,11 +74,11 @@ export class VariableQueryRunner {
this.updateOptionsRequests.next(args);
}
getResponse(identifier: VariableIdentifier): Observable<UpdateOptionsResults> {
getResponse(identifier: KeyedVariableIdentifier): Observable<UpdateOptionsResults> {
return this.updateOptionsResults.asObservable().pipe(filter((result) => result.identifier === identifier));
}
cancelRequest(identifier: VariableIdentifier): void {
cancelRequest(identifier: KeyedVariableIdentifier): void {
this.cancelRequests.next({ identifier });
}
@@ -99,11 +99,11 @@ export class VariableQueryRunner {
getState,
} = this.dependencies;
const beforeUid = getState().templating.transaction.uid;
const beforeKey = getLastKey(getState());
this.updateOptionsResults.next({ identifier, state: LoadingState.Loading });
const variable = getVariable<QueryVariableModel>(identifier.id, getState());
const variable = getVariable<QueryVariableModel>(identifier, getState());
const timeSrv = getTimeSrv();
const runnerArgs = { variable, datasource, searchFilter, timeSrv, runRequest };
const runner = queryRunners.getRunnerForDatasource(datasource);
@@ -115,9 +115,9 @@ export class VariableQueryRunner {
.pipe(
filter(() => {
// Lets check if we started another batch during the execution of the observable. If so we just want to abort the rest.
const afterUid = getState().templating.transaction.uid;
const afterKey = getLastKey(getState());
return beforeUid === afterUid;
return beforeKey === afterKey;
}),
filter((data) => data.state === LoadingState.Done || data.state === LoadingState.Error), // we only care about done or error for now
take(1), // take the first result, using first caused a bug where it in some situations throw an uncaught error because of no results had been received yet
@@ -4,9 +4,8 @@ import { DataSourceRef, getDefaultTimeRange, LoadingState } from '@grafana/data'
import { variableAdapters } from '../adapters';
import { createQueryVariableAdapter } from './adapter';
import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { getRootReducer, RootReducerType } from '../state/helpers';
import { getPreloadedState, getRootReducer, RootReducerType } from '../state/helpers';
import { QueryVariableModel, VariableHide, VariableQueryEditorProps, VariableRefresh, VariableSort } from '../types';
import { toVariablePayload } from '../state/types';
import {
addVariable,
changeVariableProp,
@@ -27,6 +26,7 @@ import { updateVariableOptions } from './reducer';
import {
addVariableEditorError,
changeVariableEditorExtended,
initialVariableEditorState,
removeVariableEditorError,
setIdInEditor,
} from '../editor/reducer';
@@ -40,6 +40,8 @@ import { setVariableQueryRunner, VariableQueryRunner } from './VariableQueryRunn
import { setDataSourceSrv } from '@grafana/runtime';
import { variablesInitTransaction } from '../state/transactionReducer';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
const mocks: Record<string, any> = {
datasource: {
@@ -93,8 +95,10 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toKeyedVariableIdentifier(variable)), true);
tester.thenNoActionsWhereDispatched();
});
@@ -109,16 +113,18 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toKeyedVariableIdentifier(variable)), true);
const option = createOption('A');
const update = { results: optionsMetrics, templatedRegex: '' };
tester.thenDispatchedActionsShouldEqual(
updateVariableOptions(toVariablePayload(variable, update)),
setCurrentVariableValue(toVariablePayload(variable, { option }))
toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
);
});
});
@@ -132,21 +138,19 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toKeyedVariableIdentifier(variable)), true);
const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
const update = { results: optionsMetrics, templatedRegex: '' };
tester.thenDispatchedActionsPredicateShouldEqual((actions) => {
const [updateOptions, setCurrentAction] = actions;
const expectedNumberOfActions = 2;
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, update)));
expect(setCurrentAction).toEqual(setCurrentVariableValue(toVariablePayload(variable, { option })));
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
);
});
});
@@ -159,23 +163,21 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenActionIsDispatched(setIdInEditor({ id: variable.id }))
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenActionIsDispatched(toKeyedAction('key', setIdInEditor({ id: variable.id })))
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toKeyedVariableIdentifier(variable)), true);
const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
const update = { results: optionsMetrics, templatedRegex: '' };
tester.thenDispatchedActionsPredicateShouldEqual((actions) => {
const [clearErrors, updateOptions, setCurrentAction] = actions;
const expectedNumberOfActions = 3;
expect(clearErrors).toEqual(removeVariableEditorError({ errorProp: 'update' }));
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, update)));
expect(setCurrentAction).toEqual(setCurrentVariableValue(toVariablePayload(variable, { option })));
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
toKeyedAction('key', removeVariableEditorError({ errorProp: 'update' })),
toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
);
});
});
@@ -188,21 +190,19 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenActionIsDispatched(setIdInEditor({ id: variable.id }))
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable), 'search'), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenActionIsDispatched(toKeyedAction('key', setIdInEditor({ id: variable.id })))
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toKeyedVariableIdentifier(variable), 'search'), true);
const update = { results: optionsMetrics, templatedRegex: '' };
tester.thenDispatchedActionsPredicateShouldEqual((actions) => {
const [clearErrors, updateOptions] = actions;
const expectedNumberOfActions = 2;
expect(clearErrors).toEqual(removeVariableEditorError({ errorProp: 'update' }));
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, update)));
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
toKeyedAction('key', removeVariableEditorError({ errorProp: 'update' })),
toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update)))
);
});
});
@@ -216,19 +216,26 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenActionIsDispatched(setIdInEditor({ id: variable.id }))
.whenAsyncActionIsDispatched(updateOptions(toVariablePayload(variable)), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenActionIsDispatched(toKeyedAction('key', setIdInEditor({ id: variable.id })))
.whenAsyncActionIsDispatched(updateOptions(toKeyedVariableIdentifier(variable)), true);
tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
const expectedNumberOfActions = 5;
expect(dispatchedActions[0]).toEqual(variableStateFetching(toVariablePayload(variable)));
expect(dispatchedActions[1]).toEqual(removeVariableEditorError({ errorProp: 'update' }));
expect(dispatchedActions[2]).toEqual(addVariableEditorError({ errorProp: 'update', errorText: error.message }));
expect(dispatchedActions[0]).toEqual(toKeyedAction('key', variableStateFetching(toVariablePayload(variable))));
expect(dispatchedActions[1]).toEqual(toKeyedAction('key', removeVariableEditorError({ errorProp: 'update' })));
expect(dispatchedActions[2]).toEqual(
toKeyedAction('key', addVariableEditorError({ errorProp: 'update', errorText: error.message }))
);
expect(dispatchedActions[3]).toEqual(
variableStateFailed(toVariablePayload(variable, { error: { message: 'failed to fetch metrics' } }))
toKeyedAction(
'key',
variableStateFailed(toVariablePayload(variable, { error: { message: 'failed to fetch metrics' } }))
)
);
expect(dispatchedActions[4].type).toEqual(notifyApp.type);
expect(dispatchedActions[4].payload.title).toEqual('Templating [0]');
@@ -253,12 +260,17 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenAsyncActionIsDispatched(initQueryVariableEditor(toKeyedVariableIdentifier(variable)), true);
tester.thenDispatchedActionsShouldEqual(
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
toKeyedAction(
'key',
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
)
);
});
});
@@ -276,12 +288,17 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenAsyncActionIsDispatched(initQueryVariableEditor(toKeyedVariableIdentifier(variable)), true);
tester.thenDispatchedActionsShouldEqual(
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
toKeyedAction(
'key',
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
)
);
});
});
@@ -298,12 +315,17 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenAsyncActionIsDispatched(initQueryVariableEditor(toKeyedVariableIdentifier(variable)), true);
tester.thenDispatchedActionsShouldEqual(
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
toKeyedAction(
'key',
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
)
);
});
});
@@ -319,15 +341,20 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenAsyncActionIsDispatched(
changeQueryVariableDataSource(toVariablePayload(variable), { uid: 'datasource' }),
changeQueryVariableDataSource(toKeyedVariableIdentifier(variable), { uid: 'datasource' }),
true
);
tester.thenDispatchedActionsShouldEqual(
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
toKeyedAction(
'key',
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
)
);
});
@@ -335,7 +362,14 @@ describe('query actions', () => {
it('then correct actions are dispatched', async () => {
const variable = createVariable({ datasource: { uid: 'other' } });
const editor = mocks.VariableQueryEditor;
const preloadedState: any = { templating: { editor: { extended: { dataSource: { type: 'previous' } } } } };
const previousDataSource: any = { type: 'previous' };
const templatingState = {
editor: {
...initialVariableEditorState,
extended: { dataSource: previousDataSource, VariableQueryEditor: editor },
},
};
const preloadedState = getPreloadedState('key', templatingState);
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
components: { VariableQueryEditor: editor },
@@ -344,17 +378,20 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>({ preloadedState })
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(
addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable }))
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenAsyncActionIsDispatched(
changeQueryVariableDataSource(toVariablePayload(variable), { uid: 'datasource' }),
changeQueryVariableDataSource(toKeyedVariableIdentifier(variable), { uid: 'datasource' }),
true
);
tester.thenDispatchedActionsShouldEqual(
changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: '' })),
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
toKeyedAction('key', changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: '' }))),
toKeyedAction(
'key',
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
)
);
});
});
@@ -371,15 +408,20 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenAsyncActionIsDispatched(
changeQueryVariableDataSource(toVariablePayload(variable), { uid: 'datasource' }),
changeQueryVariableDataSource(toKeyedVariableIdentifier(variable), { uid: 'datasource' }),
true
);
tester.thenDispatchedActionsShouldEqual(
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
toKeyedAction(
'key',
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
)
);
});
});
@@ -396,21 +438,29 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenAsyncActionIsDispatched(changeQueryVariableQuery(toVariablePayload(variable), query, definition), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenAsyncActionIsDispatched(
changeQueryVariableQuery(toKeyedVariableIdentifier(variable), query, definition),
true
);
const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
const update = { results: optionsMetrics, templatedRegex: '' };
tester.thenDispatchedActionsShouldEqual(
removeVariableEditorError({ errorProp: 'query' }),
changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query })),
changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition })),
variableStateFetching(toVariablePayload(variable)),
updateVariableOptions(toVariablePayload(variable, update)),
setCurrentVariableValue(toVariablePayload(variable, { option })),
variableStateCompleted(toVariablePayload(variable))
toKeyedAction('key', removeVariableEditorError({ errorProp: 'query' })),
toKeyedAction('key', changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query }))),
toKeyedAction(
'key',
changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition }))
),
toKeyedAction('key', variableStateFetching(toVariablePayload(variable))),
toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
toKeyedAction('key', variableStateCompleted(toVariablePayload(variable)))
);
});
});
@@ -427,21 +477,29 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenAsyncActionIsDispatched(changeQueryVariableQuery(toVariablePayload(variable), query, definition), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenAsyncActionIsDispatched(
changeQueryVariableQuery(toKeyedVariableIdentifier(variable), query, definition),
true
);
const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
const update = { results: optionsMetrics, templatedRegex: '' };
tester.thenDispatchedActionsShouldEqual(
removeVariableEditorError({ errorProp: 'query' }),
changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query })),
changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition })),
variableStateFetching(toVariablePayload(variable)),
updateVariableOptions(toVariablePayload(variable, update)),
setCurrentVariableValue(toVariablePayload(variable, { option })),
variableStateCompleted(toVariablePayload(variable))
toKeyedAction('key', removeVariableEditorError({ errorProp: 'query' })),
toKeyedAction('key', changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query }))),
toKeyedAction(
'key',
changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition }))
),
toKeyedAction('key', variableStateFetching(toVariablePayload(variable))),
toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
toKeyedAction('key', variableStateCompleted(toVariablePayload(variable)))
);
});
});
@@ -457,21 +515,29 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenAsyncActionIsDispatched(changeQueryVariableQuery(toVariablePayload(variable), query, definition), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenAsyncActionIsDispatched(
changeQueryVariableQuery(toKeyedVariableIdentifier(variable), query, definition),
true
);
const option = createOption('A');
const update = { results: optionsMetrics, templatedRegex: '' };
tester.thenDispatchedActionsShouldEqual(
removeVariableEditorError({ errorProp: 'query' }),
changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query })),
changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition })),
variableStateFetching(toVariablePayload(variable)),
updateVariableOptions(toVariablePayload(variable, update)),
setCurrentVariableValue(toVariablePayload(variable, { option })),
variableStateCompleted(toVariablePayload(variable))
toKeyedAction('key', removeVariableEditorError({ errorProp: 'query' })),
toKeyedAction('key', changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query }))),
toKeyedAction(
'key',
changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition }))
),
toKeyedAction('key', variableStateFetching(toVariablePayload(variable))),
toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
toKeyedAction('key', variableStateCompleted(toVariablePayload(variable)))
);
});
});
@@ -484,19 +550,20 @@ describe('query actions', () => {
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenAsyncActionIsDispatched(changeQueryVariableQuery(toVariablePayload(variable), query, definition), true);
.whenActionIsDispatched(
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
.whenAsyncActionIsDispatched(
changeQueryVariableQuery(toKeyedVariableIdentifier(variable), query, definition),
true
);
const errorText = 'Query cannot contain a reference to itself. Variable: $' + variable.name;
tester.thenDispatchedActionsPredicateShouldEqual((actions) => {
const [editorError] = actions;
const expectedNumberOfActions = 1;
expect(editorError).toEqual(addVariableEditorError({ errorProp: 'query', errorText }));
return actions.length === expectedNumberOfActions;
});
tester.thenDispatchedActionsShouldEqual(
toKeyedAction('key', addVariableEditorError({ errorProp: 'query', errorText }))
);
});
});
@@ -672,6 +739,7 @@ function createVariable(extend?: Partial<QueryVariableModel>): QueryVariableMode
return {
type: 'query',
id: '0',
rootStateKey: 'key',
global: false,
current: createOption(''),
options: [],
+58 -27
View File
@@ -5,30 +5,32 @@ import { DataSourceRef } from '@grafana/data';
import { updateOptions } from '../state/actions';
import { QueryVariableModel } from '../types';
import { ThunkResult } from '../../../types';
import { getVariable } from '../state/selectors';
import { getVariable, getVariablesState } from '../state/selectors';
import { addVariableEditorError, changeVariableEditorExtended, removeVariableEditorError } from '../editor/reducer';
import { changeVariableProp } from '../state/sharedReducer';
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
import { KeyedVariableIdentifier } from '../state/types';
import { getVariableQueryEditor } from '../editor/getVariableQueryEditor';
import { getVariableQueryRunner } from './VariableQueryRunner';
import { variableQueryObserver } from './variableQueryObserver';
import { hasOngoingTransaction } from '../utils';
import { hasOngoingTransaction, toKeyedVariableIdentifier, toVariablePayload } from '../utils';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { getQueryVariableEditorState } from '../editor/selectors';
export const updateQueryVariableOptions = (
identifier: VariableIdentifier,
identifier: KeyedVariableIdentifier,
searchFilter?: string
): ThunkResult<void> => {
return async (dispatch, getState) => {
try {
if (!hasOngoingTransaction(getState())) {
const { rootStateKey } = identifier;
if (!hasOngoingTransaction(rootStateKey, getState())) {
// we might have cancelled a batch so then variable state is removed
return;
}
const variableInState = getVariable<QueryVariableModel>(identifier.id, getState());
if (getState().templating.editor.id === variableInState.id) {
dispatch(removeVariableEditorError({ errorProp: 'update' }));
const variableInState = getVariable<QueryVariableModel>(identifier, getState());
if (getVariablesState(rootStateKey, getState()).editor.id === variableInState.id) {
dispatch(toKeyedAction(rootStateKey, removeVariableEditorError({ errorProp: 'update' })));
}
const datasource = await getDataSourceSrv().get(variableInState.datasource ?? '');
@@ -44,8 +46,11 @@ export const updateQueryVariableOptions = (
});
} catch (err) {
const error = toDataQueryError(err);
if (getState().templating.editor.id === identifier.id) {
dispatch(addVariableEditorError({ errorProp: 'update', errorText: error.message }));
const { rootStateKey } = identifier;
if (getVariablesState(rootStateKey, getState()).editor.id === identifier.id) {
dispatch(
toKeyedAction(rootStateKey, addVariableEditorError({ errorProp: 'update', errorText: error.message }))
);
}
throw error;
@@ -54,33 +59,43 @@ export const updateQueryVariableOptions = (
};
export const initQueryVariableEditor =
(identifier: VariableIdentifier): ThunkResult<void> =>
(identifier: KeyedVariableIdentifier): ThunkResult<void> =>
async (dispatch, getState) => {
const variable = getVariable<QueryVariableModel>(identifier.id, getState());
await dispatch(changeQueryVariableDataSource(toVariableIdentifier(variable), variable.datasource));
const variable = getVariable<QueryVariableModel>(identifier, getState());
await dispatch(changeQueryVariableDataSource(toKeyedVariableIdentifier(variable), variable.datasource));
};
export const changeQueryVariableDataSource = (
identifier: VariableIdentifier,
identifier: KeyedVariableIdentifier,
name: DataSourceRef | null
): ThunkResult<void> => {
return async (dispatch, getState) => {
try {
const extendedEditorState = getQueryVariableEditorState(getState().templating.editor);
const { rootStateKey } = identifier;
const { editor } = getVariablesState(rootStateKey, getState());
const extendedEditorState = getQueryVariableEditorState(editor);
const previousDatasource = extendedEditorState?.dataSource;
const dataSource = await getDataSourceSrv().get(name ?? '');
if (previousDatasource && previousDatasource.type !== dataSource?.type) {
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: '' })));
dispatch(
toKeyedAction(
rootStateKey,
changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: '' }))
)
);
}
const VariableQueryEditor = await getVariableQueryEditor(dataSource);
dispatch(
changeVariableEditorExtended({
dataSource: dataSource,
VariableQueryEditor: VariableQueryEditor,
})
toKeyedAction(
rootStateKey,
changeVariableEditorExtended({
dataSource,
VariableQueryEditor,
})
)
);
} catch (err) {
console.error(err);
@@ -89,22 +104,38 @@ export const changeQueryVariableDataSource = (
};
export const changeQueryVariableQuery =
(identifier: VariableIdentifier, query: any, definition?: string): ThunkResult<void> =>
(identifier: KeyedVariableIdentifier, query: any, definition?: string): ThunkResult<void> =>
async (dispatch, getState) => {
const variableInState = getVariable<QueryVariableModel>(identifier.id, getState());
const { rootStateKey } = identifier;
const variableInState = getVariable<QueryVariableModel>(identifier, getState());
if (hasSelfReferencingQuery(variableInState.name, query)) {
const errorText = 'Query cannot contain a reference to itself. Variable: $' + variableInState.name;
dispatch(addVariableEditorError({ errorProp: 'query', errorText }));
dispatch(toKeyedAction(rootStateKey, addVariableEditorError({ errorProp: 'query', errorText })));
return;
}
dispatch(removeVariableEditorError({ errorProp: 'query' }));
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: query })));
dispatch(toKeyedAction(rootStateKey, removeVariableEditorError({ errorProp: 'query' })));
dispatch(
toKeyedAction(
rootStateKey,
changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: query }))
)
);
if (definition) {
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: definition })));
dispatch(
toKeyedAction(
rootStateKey,
changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: definition }))
)
);
} else if (typeof query === 'string') {
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: query })));
dispatch(
toKeyedAction(
rootStateKey,
changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: query }))
)
);
}
await dispatch(updateOptions(identifier));
@@ -7,8 +7,7 @@ import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions';
import { VariableAdapter } from '../adapters';
import { QueryVariableEditor } from './QueryVariableEditor';
import { updateQueryVariableOptions } from './actions';
import { toVariableIdentifier } from '../state/types';
import { containsVariable, isAllVariable } from '../utils';
import { containsVariable, isAllVariable, toKeyedVariableIdentifier } from '../utils';
import { optionPickerFactory } from '../pickers';
import { ALL_VARIABLE_TEXT } from '../constants';
@@ -25,16 +24,16 @@ export const createQueryVariableAdapter = (): VariableAdapter<QueryVariableModel
return containsVariable(variable.query, variable.datasource?.uid, variable.regex, variableToTest.name);
},
setValue: async (variable, option, emitChanges = false) => {
await dispatch(setOptionAsCurrent(toVariableIdentifier(variable), option, emitChanges));
await dispatch(setOptionAsCurrent(toKeyedVariableIdentifier(variable), option, emitChanges));
},
setValueFromUrl: async (variable, urlValue) => {
await dispatch(setOptionFromUrl(toVariableIdentifier(variable), urlValue));
await dispatch(setOptionFromUrl(toKeyedVariableIdentifier(variable), urlValue));
},
updateOptions: async (variable, searchFilter) => {
await dispatch(updateQueryVariableOptions(toVariableIdentifier(variable), searchFilter));
await dispatch(updateQueryVariableOptions(toKeyedVariableIdentifier(variable), searchFilter));
},
getSaveModel: (variable) => {
const { index, id, state, global, queryValue, ...rest } = cloneDeep(variable);
const { index, id, state, global, queryValue, rootStateKey, ...rest } = cloneDeep(variable);
// remove options
if (variable.refresh !== VariableRefresh.never) {
return { ...rest, options: [] };
@@ -3,6 +3,7 @@ import { queryBuilder } from '../shared/testing/builders';
import { FieldType, toDataFrame } from '@grafana/data';
import { updateVariableOptions } from './reducer';
import { areMetricFindValues, toMetricFindValues, updateOptionsState, validateVariableSelection } from './operators';
import { toKeyedAction } from '../state/keyedVariablesReducer';
describe('operators', () => {
beforeEach(() => {
@@ -12,7 +13,7 @@ describe('operators', () => {
describe('validateVariableSelection', () => {
describe('when called', () => {
it('then the correct observable should be created', async () => {
const variable = queryBuilder().withId('query').build();
const variable = queryBuilder().withId('query').withRootStateKey('key').build();
const dispatch = jest.fn().mockResolvedValue({});
const observable = of(undefined).pipe(validateVariableSelection({ variable, dispatch }));
@@ -27,7 +28,7 @@ describe('operators', () => {
describe('updateOptionsState', () => {
describe('when called', () => {
it('then the correct observable should be created', async () => {
const variable = queryBuilder().withId('query').build();
const variable = queryBuilder().withId('query').withRootStateKey('key').build();
const dispatch = jest.fn();
const getTemplatedRegexFunc = jest.fn().mockReturnValue('getTemplatedRegexFunc result');
@@ -39,11 +40,14 @@ describe('operators', () => {
expect(getTemplatedRegexFunc).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenCalledWith(
updateVariableOptions({
id: 'query',
type: 'query',
data: { results: [{ text: 'A' }], templatedRegex: 'getTemplatedRegexFunc result' },
})
toKeyedAction(
'key',
updateVariableOptions({
id: 'query',
type: 'query',
data: { results: [{ text: 'A' }], templatedRegex: 'getTemplatedRegexFunc result' },
})
)
);
});
});
@@ -3,12 +3,12 @@ import { map, mergeMap } from 'rxjs/operators';
import { QueryVariableModel } from '../types';
import { ThunkDispatch } from '../../../types';
import { toVariableIdentifier, toVariablePayload } from '../state/types';
import { validateVariableSelectionState } from '../state/actions';
import { FieldType, getFieldDisplayName, isDataFrame, MetricFindValue, PanelData } from '@grafana/data';
import { updateVariableOptions } from './reducer';
import { getTemplatedRegex } from '../utils';
import { getTemplatedRegex, toKeyedVariableIdentifier, toVariablePayload } from '../utils';
import { getProcessedDataFrames } from 'app/features/query/state/runRequest';
import { toKeyedAction } from '../state/keyedVariablesReducer';
export function toMetricFindValues(): OperatorFunction<PanelData, MetricFindValue[]> {
return (source) =>
@@ -102,9 +102,13 @@ export function updateOptionsState(args: {
source.pipe(
map((results) => {
const { variable, dispatch, getTemplatedRegexFunc } = args;
if (!variable.rootStateKey) {
console.error('updateOptionsState: variable.rootStateKey is not defined');
return;
}
const templatedRegex = getTemplatedRegexFunc(variable);
const payload = toVariablePayload(variable, { results, templatedRegex });
dispatch(updateVariableOptions(payload));
dispatch(toKeyedAction(variable.rootStateKey, updateVariableOptions(payload)));
})
);
}
@@ -124,7 +128,7 @@ export function validateVariableSelection(args: {
// So after search and selection the current value is already update so no setValue, refresh and URL update is performed
// The if statement below fixes https://github.com/grafana/grafana/issues/25671
if (!searchFilter) {
return from(dispatch(validateVariableSelectionState(toVariableIdentifier(variable))));
return from(dispatch(validateVariableSelectionState(toKeyedVariableIdentifier(variable))));
}
return of<void>();
@@ -1,3 +1,6 @@
import { cloneDeep } from 'lodash';
import { MetricFindValue } from '@grafana/data';
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import {
metricNamesToVariableValues,
@@ -6,11 +9,10 @@ import {
updateVariableOptions,
} from './reducer';
import { QueryVariableModel, VariableSort } from '../types';
import { cloneDeep } from 'lodash';
import { getVariableTestContext } from '../state/helpers';
import { toVariablePayload, VariablesState } from '../state/types';
import { VariablesState } from '../state/types';
import { createQueryVariableAdapter } from './adapter';
import { MetricFindValue } from '@grafana/data';
import { toVariablePayload } from '../utils';
describe('queryVariableReducer', () => {
const adapter = createQueryVariableAdapter();
@@ -4,8 +4,9 @@ import { MetricFindValue, stringToJsRegex } from '@grafana/data';
import { initialVariableModelState, QueryVariableModel, VariableOption, VariableRefresh, VariableSort } from '../types';
import { getInstanceState, initialVariablesState, VariablePayload, VariablesState } from '../state/types';
import { initialVariablesState, VariablePayload, VariablesState } from '../state/types';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE, NONE_VARIABLE_TEXT, NONE_VARIABLE_VALUE } from '../constants';
import { getInstanceState } from '../state/selectors';
interface VariableOptionsUpdate {
templatedRegex: string;
@@ -1,6 +1,6 @@
import { variableQueryObserver } from './variableQueryObserver';
import { LoadingState } from '@grafana/data';
import { VariableIdentifier } from '../state/types';
import { KeyedVariableIdentifier } from '../state/types';
import { UpdateOptionsResults } from './VariableQueryRunner';
function getTestContext(args: { next?: UpdateOptionsResults; error?: any; complete?: boolean }) {
@@ -27,7 +27,7 @@ function getTestContext(args: { next?: UpdateOptionsResults; error?: any; comple
return { resolve, reject, subscription, observer };
}
const identifier: VariableIdentifier = { id: 'id', type: 'query' };
const identifier: KeyedVariableIdentifier = { id: 'id', type: 'query', rootStateKey: 'uid' };
describe('variableQueryObserver', () => {
describe('when receiving a Done state', () => {
@@ -19,6 +19,11 @@ export class VariableBuilder<T extends VariableModel> {
return this;
}
withRootStateKey(key: string) {
this.variable.rootStateKey = key;
return this;
}
build(): T {
return this.variable;
}
@@ -1,13 +1,12 @@
import { AnyAction } from 'redux';
import { getTemplatingRootReducer, TemplatingReducerType } from './helpers';
import { getPreloadedState, getTemplatingRootReducer, TemplatingReducerType } from './helpers';
import { variableAdapters } from '../adapters';
import { createQueryVariableAdapter } from '../query/adapter';
import { createCustomVariableAdapter } from '../custom/adapter';
import { createTextBoxVariableAdapter } from '../textbox/adapter';
import { createConstantVariableAdapter } from '../constant/adapter';
import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { TemplatingState } from 'app/features/variables/state/reducers';
import {
cancelVariables,
changeVariableMultiValue,
@@ -27,7 +26,6 @@ import {
variableStateFetching,
variableStateNotStarted,
} from './sharedReducer';
import { toVariableIdentifier, toVariablePayload } from './types';
import {
constantBuilder,
customBuilder,
@@ -52,6 +50,8 @@ import * as runtime from '@grafana/runtime';
import { LoadingState } from '@grafana/data';
import { toAsyncOfResult } from '../../query/state/DashboardQueryRunner/testHelpers';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE, NEW_VARIABLE_ID } from '../constants';
import { toKeyedAction } from './keyedVariablesReducer';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
variableAdapters.setInit(() => [
createQueryVariableAdapter(),
@@ -81,44 +81,66 @@ runtime.setDataSourceSrv({
describe('shared actions', () => {
describe('when initDashboardTemplating is dispatched', () => {
it('then correct actions are dispatched', () => {
const key = 'key';
const query = queryBuilder().build();
const constant = constantBuilder().build();
const datasource = datasourceBuilder().build();
const custom = customBuilder().build();
const textbox = textboxBuilder().build();
const list = [query, constant, datasource, custom, textbox];
const dashboard: any = { templating: { list } };
reduxTester<{ templating: TemplatingState }>()
reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(initDashboardTemplating(list))
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
expect(dispatchedActions.length).toEqual(8);
expect(dispatchedActions[0]).toEqual(
addVariable(toVariablePayload(query, { global: false, index: 0, model: query }))
toKeyedAction(key, addVariable(toVariablePayload(query, { global: false, index: 0, model: query })))
);
expect(dispatchedActions[1]).toEqual(
addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant }))
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
);
expect(dispatchedActions[2]).toEqual(
addVariable(toVariablePayload(custom, { global: false, index: 2, model: custom }))
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 2, model: custom })))
);
expect(dispatchedActions[3]).toEqual(
addVariable(toVariablePayload(textbox, { global: false, index: 3, model: textbox }))
toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 3, model: textbox })))
);
// because uuid are dynamic we need to get the uuid from the resulting state
// an alternative would be to add our own uuids in the model above instead
expect(dispatchedActions[4]).toEqual(
variableStateNotStarted(toVariablePayload({ ...query, id: dispatchedActions[4].payload.id }))
toKeyedAction(
key,
variableStateNotStarted(
toVariablePayload({ ...query, id: dispatchedActions[4].payload.action.payload.id })
)
)
);
expect(dispatchedActions[5]).toEqual(
variableStateNotStarted(toVariablePayload({ ...constant, id: dispatchedActions[5].payload.id }))
toKeyedAction(
key,
variableStateNotStarted(
toVariablePayload({ ...constant, id: dispatchedActions[5].payload.action.payload.id })
)
)
);
expect(dispatchedActions[6]).toEqual(
variableStateNotStarted(toVariablePayload({ ...custom, id: dispatchedActions[6].payload.id }))
toKeyedAction(
key,
variableStateNotStarted(
toVariablePayload({ ...custom, id: dispatchedActions[6].payload.action.payload.id })
)
)
);
expect(dispatchedActions[7]).toEqual(
variableStateNotStarted(toVariablePayload({ ...textbox, id: dispatchedActions[7].payload.id }))
toKeyedAction(
key,
variableStateNotStarted(
toVariablePayload({ ...textbox, id: dispatchedActions[7].payload.action.payload.id })
)
)
);
return true;
@@ -128,52 +150,71 @@ describe('shared actions', () => {
describe('when processVariables is dispatched', () => {
it('then correct actions are dispatched', async () => {
const key = 'key';
const query = queryBuilder().build();
const constant = constantBuilder().build();
const datasource = datasourceBuilder().build();
const custom = customBuilder().build();
const textbox = textboxBuilder().build();
const list = [query, constant, datasource, custom, textbox];
const preloadedState = {
templating: {} as unknown as TemplatingState,
};
const dashboard: any = { templating: { list } };
const preloadedState = getPreloadedState(key, {});
const locationService: any = { getSearchObject: () => ({}) };
runtime.setLocationService(locationService);
const variableQueryRunner: any = {
cancelRequest: jest.fn(),
queueRequest: jest.fn(),
getResponse: () => toAsyncOfResult({ state: LoadingState.Done, identifier: toVariableIdentifier(query) }),
getResponse: () => toAsyncOfResult({ state: LoadingState.Done, identifier: toKeyedVariableIdentifier(query) }),
destroy: jest.fn(),
};
setVariableQueryRunner(variableQueryRunner);
const tester = await reduxTester<TemplatingReducerType>({ preloadedState })
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
.whenActionIsDispatched(initDashboardTemplating(list))
.whenAsyncActionIsDispatched(processVariables(), true);
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
.whenAsyncActionIsDispatched(processVariables(key), true);
await tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
expect(dispatchedActions.length).toEqual(5);
expect(dispatchedActions[0]).toEqual(
variableStateFetching(toVariablePayload({ ...query, id: dispatchedActions[0].payload.id }))
toKeyedAction(
key,
variableStateFetching(toVariablePayload({ ...query, id: dispatchedActions[0].payload.action.payload.id }))
)
);
expect(dispatchedActions[1]).toEqual(
variableStateCompleted(toVariablePayload({ ...constant, id: dispatchedActions[1].payload.id }))
toKeyedAction(
key,
variableStateCompleted(
toVariablePayload({ ...constant, id: dispatchedActions[1].payload.action.payload.id })
)
)
);
expect(dispatchedActions[2]).toEqual(
variableStateCompleted(toVariablePayload({ ...custom, id: dispatchedActions[2].payload.id }))
toKeyedAction(
key,
variableStateCompleted(toVariablePayload({ ...custom, id: dispatchedActions[2].payload.action.payload.id }))
)
);
expect(dispatchedActions[3]).toEqual(
variableStateCompleted(toVariablePayload({ ...textbox, id: dispatchedActions[3].payload.id }))
toKeyedAction(
key,
variableStateCompleted(
toVariablePayload({ ...textbox, id: dispatchedActions[3].payload.action.payload.id })
)
)
);
expect(dispatchedActions[4]).toEqual(
variableStateCompleted(toVariablePayload({ ...query, id: dispatchedActions[4].payload.id }))
toKeyedAction(
key,
variableStateCompleted(toVariablePayload({ ...query, id: dispatchedActions[4].payload.action.payload.id }))
)
);
return true;
@@ -183,8 +224,10 @@ describe('shared actions', () => {
// Fix for https://github.com/grafana/grafana/issues/28791
it('fix for https://github.com/grafana/grafana/issues/28791', async () => {
setVariableQueryRunner(new VariableQueryRunner());
const key = 'key';
const stats = queryBuilder()
.withId('stats')
.withRootStateKey(key)
.withName('stats')
.withQuery('stats.*')
.withRefresh(VariableRefresh.onDashboardLoad)
@@ -195,6 +238,7 @@ describe('shared actions', () => {
const substats = queryBuilder()
.withId('substats')
.withRootStateKey(key)
.withName('substats')
.withQuery('stats.$stats.*')
.withRefresh(VariableRefresh.onDashboardLoad)
@@ -204,45 +248,64 @@ describe('shared actions', () => {
.build();
const list = [stats, substats];
const dashboard: any = { templating: { list } };
const query = { orgId: '1', 'var-stats': 'response', 'var-substats': ALL_VARIABLE_TEXT };
const locationService: any = { getSearchObject: () => query };
runtime.setLocationService(locationService);
const preloadedState = {
templating: {} as unknown as TemplatingState,
};
const preloadedState = getPreloadedState(key, {});
const tester = await reduxTester<TemplatingReducerType>({ preloadedState })
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
.whenActionIsDispatched(initDashboardTemplating(list))
.whenAsyncActionIsDispatched(processVariables(), true);
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
.whenAsyncActionIsDispatched(processVariables(key), true);
await tester.thenDispatchedActionsShouldEqual(
variableStateFetching(toVariablePayload(stats)),
updateVariableOptions(
toVariablePayload(stats, { results: [{ text: 'responses' }, { text: 'timers' }], templatedRegex: '' })
toKeyedAction(key, variableStateFetching(toVariablePayload(stats))),
toKeyedAction(
key,
updateVariableOptions(
toVariablePayload(stats, { results: [{ text: 'responses' }, { text: 'timers' }], templatedRegex: '' })
)
),
setCurrentVariableValue(
toVariablePayload(stats, { option: { text: ALL_VARIABLE_TEXT, value: ALL_VARIABLE_VALUE, selected: false } })
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(stats, {
option: { text: ALL_VARIABLE_TEXT, value: ALL_VARIABLE_VALUE, selected: false },
})
)
),
variableStateCompleted(toVariablePayload(stats)),
setCurrentVariableValue(
toVariablePayload(stats, { option: { text: ['response'], value: ['response'], selected: false } })
toKeyedAction(key, variableStateCompleted(toVariablePayload(stats))),
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(stats, { option: { text: ['response'], value: ['response'], selected: false } })
)
),
variableStateFetching(toVariablePayload(substats)),
updateVariableOptions(
toVariablePayload(substats, { results: [{ text: '200' }, { text: '500' }], templatedRegex: '' })
toKeyedAction(key, variableStateFetching(toVariablePayload(substats))),
toKeyedAction(
key,
updateVariableOptions(
toVariablePayload(substats, { results: [{ text: '200' }, { text: '500' }], templatedRegex: '' })
)
),
setCurrentVariableValue(
toVariablePayload(substats, {
option: { text: [ALL_VARIABLE_TEXT], value: [ALL_VARIABLE_VALUE], selected: true },
})
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(substats, {
option: { text: [ALL_VARIABLE_TEXT], value: [ALL_VARIABLE_VALUE], selected: true },
})
)
),
variableStateCompleted(toVariablePayload(substats)),
setCurrentVariableValue(
toVariablePayload(substats, {
option: { text: [ALL_VARIABLE_TEXT], value: [ALL_VARIABLE_VALUE], selected: false },
})
toKeyedAction(key, variableStateCompleted(toVariablePayload(substats))),
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(substats, {
option: { text: [ALL_VARIABLE_TEXT], value: [ALL_VARIABLE_VALUE], selected: false },
})
)
)
);
});
@@ -260,36 +323,42 @@ describe('shared actions', () => {
${undefined} | ${'B'} | ${undefined} | ${'should not dispatch setCurrentVariableValue'}
`('then correct actions are dispatched', async ({ withOptions, withCurrent, defaultValue, expected }) => {
let custom;
const key = 'key';
if (!withOptions) {
custom = customBuilder().withId('0').withCurrent(withCurrent).withoutOptions().build();
custom = customBuilder().withId('0').withRootStateKey(key).withCurrent(withCurrent).withoutOptions().build();
} else {
custom = customBuilder()
.withId('0')
.withRootStateKey(key)
.withOptions(...withOptions)
.withCurrent(withCurrent)
.build();
}
const tester = await reduxTester<{ templating: TemplatingState }>()
const tester = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
)
.whenAsyncActionIsDispatched(
validateVariableSelectionState(toVariableIdentifier(custom), defaultValue),
validateVariableSelectionState(toKeyedVariableIdentifier(custom), defaultValue),
true
);
await tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
const expectedActions: AnyAction[] = !withOptions
? []
: [
setCurrentVariableValue(
toVariablePayload(
{ type: 'custom', id: '0' },
{ option: { text: expected, value: expected, selected: false } }
const expectedActions: AnyAction[] = withOptions
? [
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'custom', id: '0' },
{ option: { text: expected, value: expected, selected: false } }
)
)
),
];
]
: [];
expect(dispatchedActions).toEqual(expectedActions);
return true;
});
@@ -309,37 +378,49 @@ describe('shared actions', () => {
'then correct actions are dispatched',
async ({ withOptions, withCurrent, defaultValue, expectedText, expectedSelected }) => {
let custom;
const key = 'key';
if (!withOptions) {
custom = customBuilder().withId('0').withMulti().withCurrent(withCurrent).withoutOptions().build();
custom = customBuilder()
.withId('0')
.withRootStateKey(key)
.withMulti()
.withCurrent(withCurrent)
.withoutOptions()
.build();
} else {
custom = customBuilder()
.withId('0')
.withRootStateKey(key)
.withMulti()
.withOptions(...withOptions)
.withCurrent(withCurrent)
.build();
}
const tester = await reduxTester<{ templating: TemplatingState }>()
const tester = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
)
.whenAsyncActionIsDispatched(
validateVariableSelectionState(toVariableIdentifier(custom), defaultValue),
validateVariableSelectionState(toKeyedVariableIdentifier(custom), defaultValue),
true
);
await tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
const expectedActions: AnyAction[] = !withOptions
? []
: [
setCurrentVariableValue(
toVariablePayload(
{ type: 'custom', id: '0' },
{ option: { text: expectedText, value: expectedText, selected: expectedSelected } }
const expectedActions: AnyAction[] = withOptions
? [
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'custom', id: '0' },
{ option: { text: expectedText, value: expectedText, selected: expectedSelected } }
)
)
),
];
]
: [];
expect(dispatchedActions).toEqual(expectedActions);
return true;
});
@@ -351,154 +432,196 @@ describe('shared actions', () => {
describe('changeVariableName', () => {
describe('when changeVariableName is dispatched with the same name', () => {
it('then the correct actions are dispatched', () => {
const textbox = textboxBuilder().withId('textbox').withName('textbox').build();
const constant = constantBuilder().withId('constant').withName('constant').build();
const key = 'key';
const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
const constant = constantBuilder().withId('constant').withRootStateKey(key).withName('constant').build();
reduxTester<{ templating: TemplatingState }>()
reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
.whenActionIsDispatched(
addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant }))
toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
)
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), constant.name), true)
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
)
.whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), constant.name), true)
.thenDispatchedActionsShouldEqual(
changeVariableNameSucceeded({ type: 'constant', id: 'constant', data: { newName: 'constant' } })
toKeyedAction(
key,
changeVariableNameSucceeded({ type: 'constant', id: 'constant', data: { newName: 'constant' } })
)
);
});
});
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();
const key = 'key';
const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
const constant = constantBuilder().withId('constant').withRootStateKey(key).withName('constant').build();
reduxTester<{ templating: TemplatingState }>()
reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
.whenActionIsDispatched(
addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant }))
toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
)
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), 'constant1'), true)
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
)
.whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), 'constant1'), true)
.thenDispatchedActionsShouldEqual(
addVariable({
type: 'constant',
id: 'constant1',
data: {
global: false,
index: 1,
model: {
...constant,
name: 'constant1',
id: 'constant1',
toKeyedAction(
key,
addVariable({
type: 'constant',
id: 'constant1',
data: {
global: false,
index: 1,
current: { selected: true, text: '', value: '' },
options: [{ selected: true, text: '', value: '' }],
} as ConstantVariableModel,
},
}),
changeVariableNameSucceeded({ type: 'constant', id: 'constant1', data: { newName: 'constant1' } }),
setIdInEditor({ id: 'constant1' }),
removeVariable({ type: 'constant', id: 'constant', data: { reIndex: false } })
model: {
...constant,
name: 'constant1',
id: 'constant1',
global: false,
index: 1,
current: { selected: true, text: '', value: '' },
options: [{ selected: true, text: '', value: '' }],
} as ConstantVariableModel,
},
})
),
toKeyedAction(
key,
changeVariableNameSucceeded({ type: 'constant', id: 'constant1', data: { newName: 'constant1' } })
),
toKeyedAction(key, setIdInEditor({ id: 'constant1' })),
toKeyedAction(key, 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();
const key = 'key';
const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
const constant = constantBuilder().withId(NEW_VARIABLE_ID).withRootStateKey(key).withName('constant').build();
reduxTester<{ templating: TemplatingState }>()
reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
.whenActionIsDispatched(
addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant }))
toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
)
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), 'constant1'), true)
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
)
.whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), 'constant1'), true)
.thenDispatchedActionsShouldEqual(
addVariable({
type: 'constant',
id: 'constant1',
data: {
global: false,
index: 1,
model: {
...constant,
name: 'constant1',
id: 'constant1',
toKeyedAction(
key,
addVariable({
type: 'constant',
id: 'constant1',
data: {
global: false,
index: 1,
current: { selected: true, text: '', value: '' },
options: [{ selected: true, text: '', value: '' }],
} as ConstantVariableModel,
},
}),
changeVariableNameSucceeded({ type: 'constant', id: 'constant1', data: { newName: 'constant1' } }),
setIdInEditor({ id: 'constant1' }),
removeVariable({ type: 'constant', id: NEW_VARIABLE_ID, data: { reIndex: false } })
model: {
...constant,
name: 'constant1',
id: 'constant1',
global: false,
index: 1,
current: { selected: true, text: '', value: '' },
options: [{ selected: true, text: '', value: '' }],
} as ConstantVariableModel,
},
})
),
toKeyedAction(
key,
changeVariableNameSucceeded({ type: 'constant', id: 'constant1', data: { newName: 'constant1' } })
),
toKeyedAction(key, setIdInEditor({ id: 'constant1' })),
toKeyedAction(key, removeVariable({ type: 'constant', id: NEW_VARIABLE_ID, data: { reIndex: false } }))
);
});
});
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();
const key = 'key';
const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
const constant = constantBuilder().withId('constant').withRootStateKey(key).withName('constant').build();
reduxTester<{ templating: TemplatingState }>()
reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
.whenActionIsDispatched(
addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant }))
toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
)
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), '__newName'), true)
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
)
.whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), '__newName'), true)
.thenDispatchedActionsShouldEqual(
changeVariableNameFailed({
newName: '__newName',
errorText: "Template names cannot begin with '__', that's reserved for Grafana's global variables",
})
toKeyedAction(
key,
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();
const key = 'key';
const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
const constant = constantBuilder().withId('constant').withRootStateKey(key).withName('constant').build();
reduxTester<{ templating: TemplatingState }>()
reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
.whenActionIsDispatched(
addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant }))
toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
)
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), '#constant!'), true)
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
)
.whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), '#constant!'), true)
.thenDispatchedActionsShouldEqual(
changeVariableNameFailed({
newName: '#constant!',
errorText: 'Only word and digit characters are allowed in variable names',
})
toKeyedAction(
key,
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();
const key = 'key';
const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
const constant = constantBuilder().withId('constant').withRootStateKey(key).withName('constant').build();
reduxTester<{ templating: TemplatingState }>()
reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
.whenActionIsDispatched(
addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant }))
toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
)
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), 'textbox'), true)
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
)
.whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), 'textbox'), true)
.thenDispatchedActionsShouldEqual(
changeVariableNameFailed({
newName: 'textbox',
errorText: 'Variable with the same name already exists',
})
toKeyedAction(
key,
changeVariableNameFailed({
newName: 'textbox',
errorText: 'Variable with the same name already exists',
})
)
);
});
});
@@ -507,28 +630,42 @@ describe('shared actions', () => {
describe('changeVariableMultiValue', () => {
describe('when changeVariableMultiValue is dispatched for variable with multi enabled', () => {
it('then correct actions are dispatched', () => {
const custom = customBuilder().withId('custom').withMulti(true).withCurrent(['A'], ['A']).build();
const key = 'key';
const custom = customBuilder()
.withId('custom')
.withRootStateKey(key)
.withMulti(true)
.withCurrent(['A'], ['A'])
.build();
reduxTester<{ templating: TemplatingState }>()
reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
.whenActionIsDispatched(changeVariableMultiValue(toVariableIdentifier(custom), false), true)
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
)
.whenActionIsDispatched(changeVariableMultiValue(toKeyedVariableIdentifier(custom), false), true)
.thenDispatchedActionsShouldEqual(
changeVariableProp(
toVariablePayload(custom, {
propName: 'multi',
propValue: false,
})
toKeyedAction(
key,
changeVariableProp(
toVariablePayload(custom, {
propName: 'multi',
propValue: false,
})
)
),
changeVariableProp(
toVariablePayload(custom, {
propName: 'current',
propValue: {
value: 'A',
text: 'A',
selected: true,
},
})
toKeyedAction(
key,
changeVariableProp(
toVariablePayload(custom, {
propName: 'current',
propValue: {
value: 'A',
text: 'A',
selected: true,
},
})
)
)
);
});
@@ -536,28 +673,42 @@ describe('shared actions', () => {
describe('when changeVariableMultiValue is dispatched for variable with multi disabled', () => {
it('then correct actions are dispatched', () => {
const custom = customBuilder().withId('custom').withMulti(false).withCurrent(['A'], ['A']).build();
const key = 'key';
const custom = customBuilder()
.withId('custom')
.withRootStateKey(key)
.withMulti(false)
.withCurrent(['A'], ['A'])
.build();
reduxTester<{ templating: TemplatingState }>()
reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
.whenActionIsDispatched(changeVariableMultiValue(toVariableIdentifier(custom), true), true)
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
)
.whenActionIsDispatched(changeVariableMultiValue(toKeyedVariableIdentifier(custom), true), true)
.thenDispatchedActionsShouldEqual(
changeVariableProp(
toVariablePayload(custom, {
propName: 'multi',
propValue: true,
})
toKeyedAction(
key,
changeVariableProp(
toVariablePayload(custom, {
propName: 'multi',
propValue: true,
})
)
),
changeVariableProp(
toVariablePayload(custom, {
propName: 'current',
propValue: {
value: ['A'],
text: ['A'],
selected: true,
},
})
toKeyedAction(
key,
changeVariableProp(
toVariablePayload(custom, {
propName: 'current',
propValue: {
value: ['A'],
text: ['A'],
selected: true,
},
})
)
)
);
});
@@ -567,14 +718,15 @@ describe('shared actions', () => {
describe('cleanUpVariables', () => {
describe('when called', () => {
it('then correct actions are dispatched', async () => {
reduxTester<{ templating: TemplatingState }>()
const key = 'key';
reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(cleanUpVariables())
.whenActionIsDispatched(cleanUpVariables(key))
.thenDispatchedActionsShouldEqual(
cleanVariables(),
cleanEditorState(),
cleanPickerState(),
variablesClearTransaction()
toKeyedAction(key, cleanVariables()),
toKeyedAction(key, cleanEditorState()),
toKeyedAction(key, cleanPickerState()),
toKeyedAction(key, variablesClearTransaction())
);
});
});
@@ -588,14 +740,15 @@ describe('shared actions', () => {
describe('when called', () => {
it('then cancelAllInFlightRequests should be called and correct actions are dispatched', async () => {
reduxTester<{ templating: TemplatingState }>()
const key = 'key';
reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(cancelVariables({ getBackendSrv: () => backendSrvMock }))
.whenActionIsDispatched(cancelVariables(key, { getBackendSrv: () => backendSrvMock }))
.thenDispatchedActionsShouldEqual(
cleanVariables(),
cleanEditorState(),
cleanPickerState(),
variablesClearTransaction()
toKeyedAction(key, cleanVariables()),
toKeyedAction(key, cleanEditorState()),
toKeyedAction(key, cleanPickerState()),
toKeyedAction(key, variablesClearTransaction())
);
expect(cancelAllInFlightRequestsMock).toHaveBeenCalledTimes(1);
+154 -98
View File
@@ -28,7 +28,7 @@ import {
VariableWithOptions,
} from '../types';
import { AppNotification, StoreState, ThunkResult } from '../../../types';
import { getVariable, getVariables } from './selectors';
import { getIfExistsLastKey, getVariable, getVariablesByKey, getVariablesState } from './selectors';
import { variableAdapters } from '../adapters';
import { Graph } from '../../../core/utils/dag';
import { notifyApp } from 'app/core/actions';
@@ -41,7 +41,7 @@ import {
variableStateFetching,
variableStateNotStarted,
} from './sharedReducer';
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from './types';
import { KeyedVariableIdentifier } from './types';
import { contextSrv } from 'app/core/services/context_srv';
import { getTemplateSrv, TemplateSrv } from '../../templating/template_srv';
import { alignCurrentWithMulti } from '../shared/multiOptions';
@@ -71,6 +71,9 @@ import {
getCurrentText,
getVariableRefresh,
hasOngoingTransaction,
toKeyedVariableIdentifier,
toStateKey,
toVariablePayload,
} from '../utils';
import { store } from 'app/store/store';
import { getDatasourceSrv } from '../../plugins/datasource_srv';
@@ -80,6 +83,7 @@ import { locationService } from '@grafana/runtime';
import { appEvents } from '../../../core/core';
import { getAllAffectedPanelIdsForVariableChange } from '../inspect/utils';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
import { toKeyedAction } from './keyedVariablesReducer';
// process flow queryVariable
// thunk => processVariables
@@ -111,24 +115,27 @@ import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
// thunk => variableUpdated
// adapter => updateOptions for dependent nodes
export const initDashboardTemplating = (list: VariableModel[]): ThunkResult<void> => {
export const initDashboardTemplating = (key: string, dashboard: DashboardModel): ThunkResult<void> => {
return (dispatch, getState) => {
let orderIndex = 0;
const list = dashboard.templating.list;
for (let index = 0; index < list.length; index++) {
const model = fixSelectedInconsistency(list[index]);
model.rootStateKey = key;
if (!variableAdapters.getIfExists(model.type)) {
continue;
}
dispatch(addVariable(toVariablePayload(model, { global: false, index: orderIndex++, model })));
dispatch(
toKeyedAction(key, addVariable(toVariablePayload(model, { global: false, index: orderIndex++, model })))
);
}
getTemplateSrv().updateTimeRange(getTimeSrv().timeRange());
const variables = getVariables(getState());
for (let index = 0; index < variables.length; index++) {
const variable = variables[index];
dispatch(variableStateNotStarted(toVariablePayload(variable)));
const variables = getVariablesByKey(key, getState());
for (const variable of variables) {
dispatch(toKeyedAction(key, variableStateNotStarted(toVariablePayload(variable))));
}
};
};
@@ -159,7 +166,7 @@ export function fixSelectedInconsistency(model: VariableModel): VariableModel |
return model;
}
export const addSystemTemplateVariables = (dashboard: DashboardModel): ThunkResult<void> => {
export const addSystemTemplateVariables = (key: string, dashboard: DashboardModel): ThunkResult<void> => {
return (dispatch) => {
const dashboardModel: DashboardVariableModel = {
...initialVariableModelState,
@@ -179,12 +186,15 @@ export const addSystemTemplateVariables = (dashboard: DashboardModel): ThunkResu
};
dispatch(
addVariable(
toVariablePayload(dashboardModel, {
global: dashboardModel.global,
index: dashboardModel.index,
model: dashboardModel,
})
toKeyedAction(
key,
addVariable(
toVariablePayload(dashboardModel, {
global: dashboardModel.global,
index: dashboardModel.index,
model: dashboardModel,
})
)
)
);
@@ -206,7 +216,10 @@ export const addSystemTemplateVariables = (dashboard: DashboardModel): ThunkResu
};
dispatch(
addVariable(toVariablePayload(orgModel, { global: orgModel.global, index: orgModel.index, model: orgModel }))
toKeyedAction(
key,
addVariable(toVariablePayload(orgModel, { global: orgModel.global, index: orgModel.index, model: orgModel }))
)
);
const userModel: UserVariableModel = {
@@ -228,25 +241,39 @@ export const addSystemTemplateVariables = (dashboard: DashboardModel): ThunkResu
};
dispatch(
addVariable(toVariablePayload(userModel, { global: userModel.global, index: userModel.index, model: userModel }))
toKeyedAction(
key,
addVariable(
toVariablePayload(userModel, { global: userModel.global, index: userModel.index, model: userModel })
)
)
);
};
};
export const changeVariableMultiValue = (identifier: VariableIdentifier, multi: boolean): ThunkResult<void> => {
export const changeVariableMultiValue = (identifier: KeyedVariableIdentifier, multi: boolean): ThunkResult<void> => {
return (dispatch, getState) => {
const variable = getVariable<VariableWithMultiSupport>(identifier.id, getState());
const { rootStateKey: key } = identifier;
const variable = getVariable<VariableWithMultiSupport>(identifier, getState());
const current = alignCurrentWithMulti(variable.current, multi);
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'multi', propValue: multi })));
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'current', propValue: current })));
dispatch(
toKeyedAction(key, changeVariableProp(toVariablePayload(identifier, { propName: 'multi', propValue: multi })))
);
dispatch(
toKeyedAction(key, changeVariableProp(toVariablePayload(identifier, { propName: 'current', propValue: current })))
);
};
};
export const processVariableDependencies = async (variable: VariableModel, state: StoreState) => {
if (!variable.rootStateKey) {
throw new Error(`rootStateKey not found for variable with id:${variable.id}`);
}
const dependencies: VariableModel[] = [];
for (const otherVariable of getVariables(state)) {
for (const otherVariable of getVariablesByKey(variable.rootStateKey, state)) {
if (variable === otherVariable) {
continue;
}
@@ -258,13 +285,17 @@ export const processVariableDependencies = async (variable: VariableModel, state
}
}
if (!isWaitingForDependencies(dependencies, state)) {
if (!isWaitingForDependencies(variable.rootStateKey, dependencies, state)) {
return;
}
await new Promise<void>((resolve) => {
const unsubscribe = store.subscribe(() => {
if (!isWaitingForDependencies(dependencies, store.getState())) {
if (!variable.rootStateKey) {
throw new Error(`rootStateKey not found for variable with id:${variable.id}`);
}
if (!isWaitingForDependencies(variable.rootStateKey, dependencies, store.getState())) {
unsubscribe();
resolve();
}
@@ -272,12 +303,12 @@ export const processVariableDependencies = async (variable: VariableModel, state
});
};
const isWaitingForDependencies = (dependencies: VariableModel[], state: StoreState): boolean => {
const isWaitingForDependencies = (key: string, dependencies: VariableModel[], state: StoreState): boolean => {
if (dependencies.length === 0) {
return false;
}
const variables = getVariables(state);
const variables = getVariablesByKey(key, state);
const notCompletedDependencies = dependencies.filter((d) =>
variables.some((v) => v.id === d.id && (v.state === LoadingState.NotStarted || v.state === LoadingState.Loading))
);
@@ -286,11 +317,11 @@ const isWaitingForDependencies = (dependencies: VariableModel[], state: StoreSta
};
export const processVariable = (
identifier: VariableIdentifier,
identifier: KeyedVariableIdentifier,
queryParams: UrlQueryMap
): ThunkResult<Promise<void>> => {
return async (dispatch, getState) => {
const variable = getVariable(identifier.id, getState());
const variable = getVariable(identifier, getState());
await processVariableDependencies(variable, getState());
const urlValue = queryParams['var-' + variable.name];
@@ -306,7 +337,7 @@ export const processVariable = (
refreshableVariable.refresh === VariableRefresh.onDashboardLoad ||
refreshableVariable.refresh === VariableRefresh.onTimeRangeChanged
) {
await dispatch(updateOptions(toVariableIdentifier(refreshableVariable)));
await dispatch(updateOptions(toKeyedVariableIdentifier(refreshableVariable)));
return;
}
}
@@ -316,11 +347,12 @@ export const processVariable = (
};
};
export const processVariables = (): ThunkResult<Promise<void>> => {
export const processVariables = (key: string): ThunkResult<Promise<void>> => {
return async (dispatch, getState) => {
const queryParams = locationService.getSearchObject();
const promises = getVariables(getState()).map(
async (variable: VariableModel) => await dispatch(processVariable(toVariableIdentifier(variable), queryParams))
const promises = getVariablesByKey(key, getState()).map(
async (variable: VariableModel) =>
await dispatch(processVariable(toKeyedVariableIdentifier(variable), queryParams))
);
await Promise.all(promises);
@@ -328,19 +360,19 @@ export const processVariables = (): ThunkResult<Promise<void>> => {
};
export const setOptionFromUrl = (
identifier: VariableIdentifier,
identifier: KeyedVariableIdentifier,
urlValue: UrlQueryValue
): ThunkResult<Promise<void>> => {
return async (dispatch, getState) => {
const stringUrlValue = ensureStringValues(urlValue);
const variable = getVariable(identifier.id, getState());
const variable = getVariable(identifier, getState());
if (getVariableRefresh(variable) !== VariableRefresh.never) {
// updates options
await dispatch(updateOptions(toVariableIdentifier(variable)));
await dispatch(updateOptions(toKeyedVariableIdentifier(variable)));
}
// get variable from state
const variableFromState = getVariable<VariableWithOptions>(variable.id, getState());
const variableFromState = getVariable<VariableWithOptions>(toKeyedVariableIdentifier(variable), getState());
if (!variableFromState) {
throw new Error(`Couldn't find variable with name: ${variable.name}`);
}
@@ -418,11 +450,11 @@ export const selectOptionsForCurrentValue = (variable: VariableWithOptions): Var
};
export const validateVariableSelectionState = (
identifier: VariableIdentifier,
identifier: KeyedVariableIdentifier,
defaultValue?: string
): ThunkResult<Promise<void>> => {
return (dispatch, getState) => {
const variableInState = getVariable<VariableWithOptions>(identifier.id, getState());
const variableInState = getVariable<VariableWithOptions>(identifier, getState());
const current = variableInState.current || ({} as unknown as VariableOption);
const setValue = variableAdapters.get(variableInState.type).setValue;
@@ -474,12 +506,13 @@ export const validateVariableSelectionState = (
};
export const setOptionAsCurrent = (
identifier: VariableIdentifier,
identifier: KeyedVariableIdentifier,
current: VariableOption,
emitChanges: boolean
): ThunkResult<Promise<void>> => {
return async (dispatch) => {
dispatch(setCurrentVariableValue(toVariablePayload(identifier, { option: current })));
const { rootStateKey: key } = identifier;
dispatch(toKeyedAction(key, setCurrentVariableValue(toVariablePayload(identifier, { option: current }))));
return await dispatch(variableUpdated(identifier, emitChanges));
};
};
@@ -507,25 +540,26 @@ const createGraph = (variables: VariableModel[]) => {
};
export const variableUpdated = (
identifier: VariableIdentifier,
identifier: KeyedVariableIdentifier,
emitChangeEvents: boolean,
events: typeof appEvents = appEvents
): ThunkResult<Promise<void>> => {
return async (dispatch, getState) => {
const state = getState();
const variableInState = getVariable(identifier.id, state);
const { rootStateKey } = identifier;
const variableInState = getVariable(identifier, state);
// if we're initializing variables ignore cascading update because we are in a boot up scenario
if (state.templating.transaction.status === TransactionStatus.Fetching) {
if (getVariablesState(rootStateKey, state).transaction.status === TransactionStatus.Fetching) {
if (getVariableRefresh(variableInState) === VariableRefresh.never) {
// for variable types with updates that go the setValueFromUrl path in the update let's make sure their state is set to Done.
await dispatch(upgradeLegacyQueries(toVariableIdentifier(variableInState)));
await dispatch(upgradeLegacyQueries(toKeyedVariableIdentifier(variableInState)));
dispatch(completeVariableLoading(identifier));
}
return Promise.resolve();
}
const variables = getVariables(state);
const variables = getVariablesByKey(rootStateKey, state);
const g = createGraph(variables);
const panels = state.dashboard?.getModel()?.panels ?? [];
const event: VariablesChangedEvent = isAdHoc(variableInState)
@@ -541,14 +575,14 @@ export const variableUpdated = (
return Promise.resolve();
}
return dispatch(updateOptions(toVariableIdentifier(variable)));
return dispatch(updateOptions(toKeyedVariableIdentifier(variable)));
});
}
return Promise.all(promises).then(() => {
if (emitChangeEvents) {
events.publish(new VariablesChanged(event));
locationService.partial(getQueryWithVariables(getState));
locationService.partial(getQueryWithVariables(rootStateKey, getState));
}
});
};
@@ -561,12 +595,13 @@ export interface OnTimeRangeUpdatedDependencies {
export const onTimeRangeUpdated =
(
key: string,
timeRange: TimeRange,
dependencies: OnTimeRangeUpdatedDependencies = { templateSrv: getTemplateSrv(), events: appEvents }
): ThunkResult<Promise<void>> =>
async (dispatch, getState) => {
dependencies.templateSrv.updateTimeRange(timeRange);
const variablesThatNeedRefresh = getVariables(getState()).filter((variable) => {
const variablesThatNeedRefresh = getVariablesByKey(key, getState()).filter((variable) => {
if (variable.hasOwnProperty('refresh') && variable.hasOwnProperty('options')) {
const variableWithRefresh = variable as unknown as QueryVariableModel;
return variableWithRefresh.refresh === VariableRefresh.onTimeRangeChanged;
@@ -577,7 +612,7 @@ export const onTimeRangeUpdated =
const variableIds = variablesThatNeedRefresh.map((variable) => variable.id);
const promises = variablesThatNeedRefresh.map((variable: VariableWithOptions) =>
dispatch(timeRangeUpdated(toVariableIdentifier(variable)))
dispatch(timeRangeUpdated(toKeyedVariableIdentifier(variable)))
);
try {
@@ -590,14 +625,14 @@ export const onTimeRangeUpdated =
};
const timeRangeUpdated =
(identifier: VariableIdentifier): ThunkResult<Promise<void>> =>
(identifier: KeyedVariableIdentifier): ThunkResult<Promise<void>> =>
async (dispatch, getState) => {
const variableInState = getVariable<VariableWithOptions>(identifier.id);
const variableInState = getVariable<VariableWithOptions>(identifier, getState());
const previousOptions = variableInState.options.slice();
await dispatch(updateOptions(toVariableIdentifier(variableInState), true));
await dispatch(updateOptions(toKeyedVariableIdentifier(variableInState), true));
const updatedVariable = getVariable<VariableWithOptions>(identifier.id, getState());
const updatedVariable = getVariable<VariableWithOptions>(identifier, getState());
const updatedOptions = updatedVariable.options;
if (JSON.stringify(previousOptions) !== JSON.stringify(updatedOptions)) {
@@ -607,11 +642,11 @@ const timeRangeUpdated =
};
export const templateVarsChangedInUrl =
(vars: ExtendedUrlQueryMap, events: typeof appEvents = appEvents): ThunkResult<void> =>
(key: string, vars: ExtendedUrlQueryMap, events: typeof appEvents = appEvents): ThunkResult<void> =>
async (dispatch, getState) => {
const update: Array<Promise<any>> = [];
const dashboard = getState().dashboard.getModel();
for (const variable of getVariables(getState())) {
for (const variable of getVariablesByKey(key, getState())) {
const key = `var-${variable.name}`;
if (!vars.hasOwnProperty(key)) {
// key not found quick exit
@@ -657,7 +692,7 @@ export function isVariableUrlValueDifferentFromCurrent(variable: VariableModel,
return !isEqual(variableValue, stringUrlValue);
}
const getQueryWithVariables = (getState: () => StoreState): UrlQueryMap => {
const getQueryWithVariables = (key: string, getState: () => StoreState): UrlQueryMap => {
const queryParams = locationService.getSearchObject();
const queryParamsNew = Object.keys(queryParams)
@@ -667,7 +702,7 @@ const getQueryWithVariables = (getState: () => StoreState): UrlQueryMap => {
return obj;
}, {} as UrlQueryMap);
for (const variable of getVariables(getState())) {
for (const variable of getVariablesByKey(key, getState())) {
if (variable.skipUrlSync) {
continue;
}
@@ -680,27 +715,32 @@ const getQueryWithVariables = (getState: () => StoreState): UrlQueryMap => {
};
export const initVariablesTransaction =
(dashboardUid: string, dashboard: DashboardModel): ThunkResult<void> =>
(urlUid: string, dashboard: DashboardModel): ThunkResult<Promise<void>> =>
async (dispatch, getState) => {
try {
const transactionState = getState().templating.transaction;
if (transactionState.status === TransactionStatus.Fetching) {
// previous dashboard is still fetching variables, cancel all requests
dispatch(cancelVariables());
const uid = toStateKey(urlUid);
const state = getState();
const lastKey = getIfExistsLastKey(state);
if (lastKey) {
const transactionState = getVariablesState(lastKey, state).transaction;
if (transactionState.status === TransactionStatus.Fetching) {
// previous dashboard is still fetching variables, cancel all requests
dispatch(cancelVariables(lastKey));
}
}
// Start init transaction
dispatch(variablesInitTransaction({ uid: dashboardUid }));
dispatch(toKeyedAction(uid, variablesInitTransaction({ uid })));
// Add system variables like __dashboard and __user
dispatch(addSystemTemplateVariables(dashboard));
dispatch(addSystemTemplateVariables(uid, dashboard));
// Load all variables into redux store
dispatch(initDashboardTemplating(dashboard.templating.list));
dispatch(initDashboardTemplating(uid, dashboard));
// Migrate data source name to ref
dispatch(migrateVariablesDatasourceNameToRef());
dispatch(migrateVariablesDatasourceNameToRef(uid));
// Process all variable updates
await dispatch(processVariables());
// Mark update as complete
dispatch(variablesCompleteTransaction({ uid: dashboardUid }));
await dispatch(processVariables(uid));
// Set transaction as complete
dispatch(toKeyedAction(uid, variablesCompleteTransaction({ uid })));
} catch (err) {
dispatch(notifyApp(createVariableErrorNotification('Templating init failed', err)));
console.error(err);
@@ -708,10 +748,11 @@ export const initVariablesTransaction =
};
export function migrateVariablesDatasourceNameToRef(
getDatasourceSrvFunc: typeof getDatasourceSrv = getDatasourceSrv
key: string,
getDatasourceSrvFunc = getDatasourceSrv
): ThunkResult<void> {
return function (dispatch, getState) {
const variables = getVariables(getState());
return (dispatch, getState) => {
const variables = getVariablesByKey(key, getState());
for (const variable of variables) {
if (!isAdHoc(variable) && !isQuery(variable)) {
continue;
@@ -726,43 +767,51 @@ export function migrateVariablesDatasourceNameToRef(
// the call to getInstanceSettings needs to be done after initDashboardTemplating because we might have
// datasource variables that need to be resolved
const ds = getDatasourceSrvFunc().getInstanceSettings(nameOrRef);
const dsRef = !ds ? { uid: nameOrRef } : getDataSourceRef(ds);
dispatch(changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: dsRef })));
const dsRef = ds ? getDataSourceRef(ds) : { uid: nameOrRef };
dispatch(
toKeyedAction(
key,
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: dsRef }))
)
);
}
};
}
export const cleanUpVariables = (): ThunkResult<void> => (dispatch) => {
dispatch(cleanVariables());
dispatch(cleanEditorState());
dispatch(cleanPickerState());
dispatch(variablesClearTransaction());
};
export const cleanUpVariables =
(key: string): ThunkResult<void> =>
(dispatch) => {
dispatch(toKeyedAction(key, cleanVariables()));
dispatch(toKeyedAction(key, cleanEditorState()));
dispatch(toKeyedAction(key, cleanPickerState()));
dispatch(toKeyedAction(key, variablesClearTransaction()));
};
type CancelVariablesDependencies = { getBackendSrv: typeof getBackendSrv };
export const cancelVariables =
(dependencies: CancelVariablesDependencies = { getBackendSrv: getBackendSrv }): ThunkResult<void> =>
(key: string, dependencies: CancelVariablesDependencies = { getBackendSrv: getBackendSrv }): ThunkResult<void> =>
(dispatch) => {
dependencies.getBackendSrv().cancelAllInFlightRequests();
dispatch(cleanUpVariables());
dispatch(cleanUpVariables(key));
};
export const updateOptions =
(identifier: VariableIdentifier, rethrow = false): ThunkResult<Promise<void>> =>
(identifier: KeyedVariableIdentifier, rethrow = false): ThunkResult<Promise<void>> =>
async (dispatch, getState) => {
const { rootStateKey } = identifier;
try {
if (!hasOngoingTransaction(getState())) {
if (!hasOngoingTransaction(rootStateKey, getState())) {
// we might have cancelled a batch so then variable state is removed
return;
}
const variableInState = getVariable(identifier.id, getState());
dispatch(variableStateFetching(toVariablePayload(variableInState)));
await dispatch(upgradeLegacyQueries(toVariableIdentifier(variableInState)));
const variableInState = getVariable(identifier, getState());
dispatch(toKeyedAction(rootStateKey, variableStateFetching(toVariablePayload(variableInState))));
await dispatch(upgradeLegacyQueries(toKeyedVariableIdentifier(variableInState)));
await variableAdapters.get(variableInState.type).updateOptions(variableInState);
dispatch(completeVariableLoading(identifier));
} catch (error) {
dispatch(variableStateFailed(toVariablePayload(identifier, { error })));
dispatch(toKeyedAction(rootStateKey, variableStateFailed(toVariablePayload(identifier, { error }))));
if (!rethrow) {
console.error(error);
@@ -778,7 +827,7 @@ export const updateOptions =
export const createVariableErrorNotification = (
message: string,
error: any,
identifier?: VariableIdentifier
identifier?: KeyedVariableIdentifier
): AppNotification =>
createErrorNotification(
`${identifier ? `Templating [${identifier.id}]` : 'Templating'}`,
@@ -786,31 +835,33 @@ export const createVariableErrorNotification = (
);
export const completeVariableLoading =
(identifier: VariableIdentifier): ThunkResult<void> =>
(identifier: KeyedVariableIdentifier): ThunkResult<void> =>
(dispatch, getState) => {
if (!hasOngoingTransaction(getState())) {
const { rootStateKey } = identifier;
if (!hasOngoingTransaction(rootStateKey, getState())) {
// we might have cancelled a batch so then variable state is removed
return;
}
const variableInState = getVariable(identifier.id, getState());
const variableInState = getVariable(identifier, getState());
if (variableInState.state !== LoadingState.Done) {
dispatch(variableStateCompleted(toVariablePayload(variableInState)));
dispatch(toKeyedAction(identifier.rootStateKey, variableStateCompleted(toVariablePayload(variableInState))));
}
};
export function upgradeLegacyQueries(
identifier: VariableIdentifier,
identifier: KeyedVariableIdentifier,
getDatasourceSrvFunc: typeof getDatasourceSrv = getDatasourceSrv
): ThunkResult<void> {
return async function (dispatch, getState) {
if (!hasOngoingTransaction(getState())) {
const { id, rootStateKey } = identifier;
if (!hasOngoingTransaction(rootStateKey, getState())) {
// we might have cancelled a batch so then variable state is removed
return;
}
const variable = getVariable<QueryVariableModel>(identifier.id, getState());
const variable = getVariable<QueryVariableModel>(identifier, getState());
if (!isQuery(variable)) {
return;
@@ -832,11 +883,16 @@ export function upgradeLegacyQueries(
}
const query = {
refId: `${datasource.name}-${identifier.id}-Variable-Query`,
refId: `${datasource.name}-${id}-Variable-Query`,
query: variable.query,
};
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: query })));
dispatch(
toKeyedAction(
rootStateKey,
changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: query }))
)
);
} catch (err) {
dispatch(notifyApp(createVariableErrorNotification('Failed to upgrade legacy queries', err)));
console.error(err);
+32 -8
View File
@@ -13,9 +13,10 @@ import {
import { VariableAdapter } from '../adapters';
import { dashboardReducer } from 'app/features/dashboard/state/reducers';
import { templatingReducers, TemplatingState } from './reducers';
import { DashboardState } from '../../../types';
import { DashboardState, StoreState } from '../../../types';
import { NEW_VARIABLE_ID } from '../constants';
import { keyedVariablesReducer, KeyedVariablesState } from './keyedVariablesReducer';
import { getInitialTemplatingState, TemplatingState } from './reducers';
export const getVariableState = (
noOfVariables: number,
@@ -86,6 +87,7 @@ export const getVariableState = (
for (let index = 0; index < noOfVariables; index++) {
variables[index] = {
id: index.toString(),
rootStateKey: 'key',
type: 'query',
name: `Name-${index}`,
hide: VariableHide.dontHide,
@@ -102,6 +104,7 @@ export const getVariableState = (
if (includeEmpty) {
variables[NEW_VARIABLE_ID] = {
id: NEW_VARIABLE_ID,
rootStateKey: 'key',
type: 'query',
name: `Name-${NEW_VARIABLE_ID}`,
hide: VariableHide.dontHide,
@@ -122,12 +125,16 @@ export const getVariableTestContext = <Model extends VariableModel>(
adapter: VariableAdapter<Model>,
variableOverrides: Partial<Model> = {}
) => {
const defaultVariable = {
...adapter.initialState,
const defaults: Partial<VariableModel> = {
id: '0',
rootStateKey: 'key',
index: 0,
name: '0',
};
const defaultVariable = {
...adapter.initialState,
...defaults,
};
const initialState: VariablesState = {
'0': { ...defaultVariable, ...variableOverrides },
@@ -139,14 +146,31 @@ export const getVariableTestContext = <Model extends VariableModel>(
export const getRootReducer = () =>
combineReducers({
dashboard: dashboardReducer,
templating: templatingReducers,
templating: keyedVariablesReducer,
});
export type RootReducerType = { dashboard: DashboardState; templating: TemplatingState };
export type RootReducerType = { dashboard: DashboardState; templating: KeyedVariablesState };
export const getTemplatingRootReducer = () =>
combineReducers({
templating: templatingReducers,
templating: keyedVariablesReducer,
});
export type TemplatingReducerType = { templating: TemplatingState };
export type TemplatingReducerType = { templating: KeyedVariablesState };
export function getPreloadedState(
key: string,
templatingState: Partial<TemplatingState>
): Pick<StoreState, 'templating'> {
return {
templating: {
lastKey: key,
keys: {
[key]: {
...getInitialTemplatingState(),
...templatingState,
},
},
},
};
}
@@ -1,4 +1,4 @@
import { getRootReducer, RootReducerType } from './helpers';
import { getPreloadedState, getRootReducer, RootReducerType } from './helpers';
import { variableAdapters } from '../adapters';
import { createQueryVariableAdapter } from '../query/adapter';
import { createConstantVariableAdapter } from '../constant/adapter';
@@ -11,15 +11,15 @@ import {
variableStateFetching,
variableStateNotStarted,
} from './sharedReducer';
import { toVariablePayload } from './types';
import { adHocBuilder, constantBuilder, datasourceBuilder, queryBuilder } from '../shared/testing/builders';
import { cleanEditorState, initialVariableEditorState } from '../editor/reducer';
import { cleanEditorState } from '../editor/reducer';
import {
initialTransactionState,
variablesClearTransaction,
variablesCompleteTransaction,
variablesInitTransaction,
} from './transactionReducer';
import { cleanPickerState, initialState } from '../pickers/OptionsPicker/reducer';
import { cleanPickerState } from '../pickers/OptionsPicker/reducer';
import { cleanVariables } from './variablesReducer';
import { createAdHocVariableAdapter } from '../adhoc/adapter';
import { createDataSourceVariableAdapter } from '../datasource/adapter';
@@ -30,6 +30,8 @@ import { toAsyncOfResult } from '../../query/state/DashboardQueryRunner/testHelp
import { setVariableQueryRunner } from '../query/VariableQueryRunner';
import { createDataSourceOptions } from '../datasource/reducer';
import { initVariablesTransaction } from './actions';
import { toKeyedAction } from './keyedVariablesReducer';
import { toVariablePayload } from '../utils';
variableAdapters.setInit(() => [
createQueryVariableAdapter(),
@@ -39,7 +41,7 @@ variableAdapters.setInit(() => [
]);
function getTestContext(variables?: VariableModel[]) {
const uid = 'uid';
const key = 'key';
const constant = constantBuilder().withId('constant').withName('constant').build();
const templating = { list: variables ?? [constant] };
const getInstanceSettingsMock = jest.fn().mockReturnValue(undefined);
@@ -57,34 +59,34 @@ function getTestContext(variables?: VariableModel[]) {
};
setVariableQueryRunner(variableQueryRunner);
const dashboard: any = { title: 'Some dash', uid, templating };
const dashboard: any = { title: 'Some dash', uid: key, templating };
return { constant, getInstanceSettingsMock, templating, uid, dashboard };
return { constant, getInstanceSettingsMock, templating, key, dashboard };
}
describe('initVariablesTransaction', () => {
describe('when called and the previous dashboard has completed', () => {
it('then correct actions are dispatched', async () => {
const { constant, uid, dashboard } = getTestContext();
const { constant, key, dashboard } = getTestContext();
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenAsyncActionIsDispatched(initVariablesTransaction(uid, dashboard));
.whenAsyncActionIsDispatched(initVariablesTransaction(key, dashboard));
tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
expect(dispatchedActions[0]).toEqual(variablesInitTransaction({ uid }));
expect(dispatchedActions[1].type).toEqual(addVariable.type);
expect(dispatchedActions[1].payload.id).toEqual('__dashboard');
expect(dispatchedActions[2].type).toEqual(addVariable.type);
expect(dispatchedActions[2].payload.id).toEqual('__org');
expect(dispatchedActions[3].type).toEqual(addVariable.type);
expect(dispatchedActions[3].payload.id).toEqual('__user');
expect(dispatchedActions[0]).toEqual(toKeyedAction(key, variablesInitTransaction({ uid: key })));
expect(dispatchedActions[1].payload.action.type).toEqual(addVariable.type);
expect(dispatchedActions[1].payload.action.payload.id).toEqual('__dashboard');
expect(dispatchedActions[2].payload.action.type).toEqual(addVariable.type);
expect(dispatchedActions[2].payload.action.payload.id).toEqual('__org');
expect(dispatchedActions[3].payload.action.type).toEqual(addVariable.type);
expect(dispatchedActions[3].payload.action.payload.id).toEqual('__user');
expect(dispatchedActions[4]).toEqual(
addVariable(toVariablePayload(constant, { global: false, index: 0, model: constant }))
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 0, model: constant })))
);
expect(dispatchedActions[5]).toEqual(variableStateNotStarted(toVariablePayload(constant)));
expect(dispatchedActions[6]).toEqual(variableStateCompleted(toVariablePayload(constant)));
expect(dispatchedActions[5]).toEqual(toKeyedAction(key, variableStateNotStarted(toVariablePayload(constant))));
expect(dispatchedActions[6]).toEqual(toKeyedAction(key, variableStateCompleted(toVariablePayload(constant))));
expect(dispatchedActions[7]).toEqual(variablesCompleteTransaction({ uid }));
expect(dispatchedActions[7]).toEqual(toKeyedAction(key, variablesCompleteTransaction({ uid: key })));
return dispatchedActions.length === 8;
});
});
@@ -92,54 +94,73 @@ describe('initVariablesTransaction', () => {
describe('and there are variables that have data source that need to be migrated', () => {
it('then correct actions are dispatched', async () => {
const legacyDs = '${ds}' as unknown as DataSourceRef;
const ds = datasourceBuilder().withId('ds').withName('ds').withQuery('prom').build();
const query = queryBuilder().withId('query').withName('query').withDatasource(legacyDs).build();
const adhoc = adHocBuilder().withId('adhoc').withName('adhoc').withDatasource(legacyDs).build();
const { uid, dashboard } = getTestContext([ds, query, adhoc]);
const ds = datasourceBuilder().withId('ds').withRootStateKey('key').withName('ds').withQuery('prom').build();
const query = queryBuilder()
.withId('query')
.withRootStateKey('key')
.withName('query')
.withDatasource(legacyDs)
.build();
const adhoc = adHocBuilder()
.withId('adhoc')
.withRootStateKey('key')
.withName('adhoc')
.withDatasource(legacyDs)
.build();
const { key, dashboard } = getTestContext([ds, query, adhoc]);
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenAsyncActionIsDispatched(initVariablesTransaction(uid, dashboard));
.whenAsyncActionIsDispatched(initVariablesTransaction(key, dashboard));
tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
expect(dispatchedActions[0]).toEqual(variablesInitTransaction({ uid }));
expect(dispatchedActions[1].type).toEqual(addVariable.type);
expect(dispatchedActions[1].payload.id).toEqual('__dashboard');
expect(dispatchedActions[2].type).toEqual(addVariable.type);
expect(dispatchedActions[2].payload.id).toEqual('__org');
expect(dispatchedActions[3].type).toEqual(addVariable.type);
expect(dispatchedActions[3].payload.id).toEqual('__user');
expect(dispatchedActions[0]).toEqual(toKeyedAction(key, variablesInitTransaction({ uid: key })));
expect(dispatchedActions[1].payload.action.type).toEqual(addVariable.type);
expect(dispatchedActions[1].payload.action.payload.id).toEqual('__dashboard');
expect(dispatchedActions[2].payload.action.type).toEqual(addVariable.type);
expect(dispatchedActions[2].payload.action.payload.id).toEqual('__org');
expect(dispatchedActions[3].payload.action.type).toEqual(addVariable.type);
expect(dispatchedActions[3].payload.action.payload.id).toEqual('__user');
expect(dispatchedActions[4]).toEqual(
addVariable(toVariablePayload(ds, { global: false, index: 0, model: ds }))
toKeyedAction(key, addVariable(toVariablePayload(ds, { global: false, index: 0, model: ds })))
);
expect(dispatchedActions[5]).toEqual(
addVariable(toVariablePayload(query, { global: false, index: 1, model: query }))
toKeyedAction(key, addVariable(toVariablePayload(query, { global: false, index: 1, model: query })))
);
expect(dispatchedActions[6]).toEqual(
addVariable(toVariablePayload(adhoc, { global: false, index: 2, model: adhoc }))
toKeyedAction(key, addVariable(toVariablePayload(adhoc, { global: false, index: 2, model: adhoc })))
);
expect(dispatchedActions[7]).toEqual(variableStateNotStarted(toVariablePayload(ds)));
expect(dispatchedActions[8]).toEqual(variableStateNotStarted(toVariablePayload(query)));
expect(dispatchedActions[9]).toEqual(variableStateNotStarted(toVariablePayload(adhoc)));
expect(dispatchedActions[7]).toEqual(toKeyedAction(key, variableStateNotStarted(toVariablePayload(ds))));
expect(dispatchedActions[8]).toEqual(toKeyedAction(key, variableStateNotStarted(toVariablePayload(query))));
expect(dispatchedActions[9]).toEqual(toKeyedAction(key, variableStateNotStarted(toVariablePayload(adhoc))));
expect(dispatchedActions[10]).toEqual(
changeVariableProp(toVariablePayload(query, { propName: 'datasource', propValue: { uid: '${ds}' } }))
);
expect(dispatchedActions[11]).toEqual(
changeVariableProp(toVariablePayload(adhoc, { propName: 'datasource', propValue: { uid: '${ds}' } }))
);
expect(dispatchedActions[12]).toEqual(variableStateFetching(toVariablePayload(ds)));
expect(dispatchedActions[13]).toEqual(variableStateCompleted(toVariablePayload(adhoc)));
expect(dispatchedActions[14]).toEqual(
createDataSourceOptions(toVariablePayload(ds, { sources: [], regex: undefined }))
);
expect(dispatchedActions[15]).toEqual(
setCurrentVariableValue(
toVariablePayload(ds, { option: { selected: false, text: 'No data sources found', value: '' } })
toKeyedAction(
key,
changeVariableProp(toVariablePayload(query, { propName: 'datasource', propValue: { uid: '${ds}' } }))
)
);
expect(dispatchedActions[16]).toEqual(variableStateCompleted(toVariablePayload(ds)));
expect(dispatchedActions[17]).toEqual(variableStateFetching(toVariablePayload(query)));
expect(dispatchedActions[18]).toEqual(variableStateCompleted(toVariablePayload(query)));
expect(dispatchedActions[19]).toEqual(variablesCompleteTransaction({ uid }));
expect(dispatchedActions[11]).toEqual(
toKeyedAction(
key,
changeVariableProp(toVariablePayload(adhoc, { propName: 'datasource', propValue: { uid: '${ds}' } }))
)
);
expect(dispatchedActions[12]).toEqual(toKeyedAction(key, variableStateFetching(toVariablePayload(ds))));
expect(dispatchedActions[13]).toEqual(toKeyedAction(key, variableStateCompleted(toVariablePayload(adhoc))));
expect(dispatchedActions[14]).toEqual(
toKeyedAction(key, createDataSourceOptions(toVariablePayload(ds, { sources: [], regex: undefined })))
);
expect(dispatchedActions[15]).toEqual(
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(ds, { option: { selected: false, text: 'No data sources found', value: '' } })
)
)
);
expect(dispatchedActions[16]).toEqual(toKeyedAction(key, variableStateCompleted(toVariablePayload(ds))));
expect(dispatchedActions[17]).toEqual(toKeyedAction(key, variableStateFetching(toVariablePayload(query))));
expect(dispatchedActions[18]).toEqual(toKeyedAction(key, variableStateCompleted(toVariablePayload(query))));
expect(dispatchedActions[19]).toEqual(toKeyedAction(key, variablesCompleteTransaction({ uid: key })));
return dispatchedActions.length === 20;
});
@@ -149,40 +170,32 @@ describe('initVariablesTransaction', () => {
describe('when called and the previous dashboard is still processing variables', () => {
it('then correct actions are dispatched', async () => {
const { constant, uid, dashboard } = getTestContext();
const transactionState = { uid: 'previous-uid', status: TransactionStatus.Fetching };
const { constant, key, dashboard } = getTestContext();
const transactionState = { ...initialTransactionState, uid: 'previous-uid', status: TransactionStatus.Fetching };
const preloadedState = getPreloadedState(key, { transaction: transactionState });
const tester = await reduxTester<RootReducerType>({
preloadedState: {
templating: {
transaction: transactionState,
variables: {},
optionsPicker: { ...initialState },
editor: { ...initialVariableEditorState },
},
} as unknown as RootReducerType,
})
const tester = await reduxTester<RootReducerType>({ preloadedState })
.givenRootReducer(getRootReducer())
.whenAsyncActionIsDispatched(initVariablesTransaction(uid, dashboard));
.whenAsyncActionIsDispatched(initVariablesTransaction(key, dashboard));
tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
expect(dispatchedActions[0]).toEqual(cleanVariables());
expect(dispatchedActions[1]).toEqual(cleanEditorState());
expect(dispatchedActions[2]).toEqual(cleanPickerState());
expect(dispatchedActions[3]).toEqual(variablesClearTransaction());
expect(dispatchedActions[4]).toEqual(variablesInitTransaction({ uid }));
expect(dispatchedActions[5].type).toEqual(addVariable.type);
expect(dispatchedActions[5].payload.id).toEqual('__dashboard');
expect(dispatchedActions[6].type).toEqual(addVariable.type);
expect(dispatchedActions[6].payload.id).toEqual('__org');
expect(dispatchedActions[7].type).toEqual(addVariable.type);
expect(dispatchedActions[7].payload.id).toEqual('__user');
expect(dispatchedActions[0]).toEqual(toKeyedAction(key, cleanVariables()));
expect(dispatchedActions[1]).toEqual(toKeyedAction(key, cleanEditorState()));
expect(dispatchedActions[2]).toEqual(toKeyedAction(key, cleanPickerState()));
expect(dispatchedActions[3]).toEqual(toKeyedAction(key, variablesClearTransaction()));
expect(dispatchedActions[4]).toEqual(toKeyedAction(key, variablesInitTransaction({ uid: key })));
expect(dispatchedActions[5].payload.action.type).toEqual(addVariable.type);
expect(dispatchedActions[5].payload.action.payload.id).toEqual('__dashboard');
expect(dispatchedActions[6].payload.action.type).toEqual(addVariable.type);
expect(dispatchedActions[6].payload.action.payload.id).toEqual('__org');
expect(dispatchedActions[7].payload.action.type).toEqual(addVariable.type);
expect(dispatchedActions[7].payload.action.payload.id).toEqual('__user');
expect(dispatchedActions[8]).toEqual(
addVariable(toVariablePayload(constant, { global: false, index: 0, model: constant }))
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 0, model: constant })))
);
expect(dispatchedActions[9]).toEqual(variableStateNotStarted(toVariablePayload(constant)));
expect(dispatchedActions[10]).toEqual(variableStateCompleted(toVariablePayload(constant)));
expect(dispatchedActions[11]).toEqual(variablesCompleteTransaction({ uid }));
expect(dispatchedActions[9]).toEqual(toKeyedAction(key, variableStateNotStarted(toVariablePayload(constant))));
expect(dispatchedActions[10]).toEqual(toKeyedAction(key, variableStateCompleted(toVariablePayload(constant))));
expect(dispatchedActions[11]).toEqual(toKeyedAction(key, variablesCompleteTransaction({ uid: key })));
return dispatchedActions.length === 12;
});
});
@@ -0,0 +1,128 @@
import {
initialKeyedVariablesState,
keyedVariablesReducer,
KeyedVariablesState,
toKeyedAction,
} from './keyedVariablesReducer';
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { initialTransactionState, variablesCompleteTransaction, variablesInitTransaction } from './transactionReducer';
import { TransactionStatus } from '../types';
import { getInitialTemplatingState } from './reducers';
describe('dashboardVariablesReducer', () => {
describe('when an toUidAction is dispatched', () => {
it('then state should be correct', () => {
const key = 'key';
reducerTester<KeyedVariablesState>()
.givenReducer(keyedVariablesReducer, {
...initialKeyedVariablesState,
lastKey: key,
keys: {
[key]: {
...getInitialTemplatingState(),
transaction: {
...initialTransactionState,
uid: key,
},
},
},
})
.whenActionIsDispatched(toKeyedAction(key, variablesCompleteTransaction({ uid: key })))
.thenStateShouldEqual({
...initialKeyedVariablesState,
lastKey: key,
keys: {
[key]: {
...getInitialTemplatingState(),
transaction: {
...initialTransactionState,
uid: key,
status: TransactionStatus.Completed,
},
},
},
});
});
});
describe('when an toUidAction with variablesInitTransaction is dispatched', () => {
it('then lastUid property should be correct', () => {
const lastUid = 'lastUid';
const key = 'key';
reducerTester<KeyedVariablesState>()
.givenReducer(keyedVariablesReducer, {
...initialKeyedVariablesState,
lastKey: lastUid,
keys: {
[lastUid]: {
...getInitialTemplatingState(),
transaction: {
...initialTransactionState,
uid: lastUid,
status: TransactionStatus.Completed,
},
},
},
})
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.thenStateShouldEqual({
...initialKeyedVariablesState,
lastKey: key,
keys: {
[key]: {
...getInitialTemplatingState(),
transaction: {
...initialTransactionState,
uid: key,
status: TransactionStatus.Fetching,
},
},
[lastUid]: {
...getInitialTemplatingState(),
transaction: {
...initialTransactionState,
uid: lastUid,
status: TransactionStatus.Completed,
},
},
},
});
});
});
describe('when action other than toUidAction is dispatched', () => {
it('then state should not be affected', () => {
const key = 'key';
reducerTester<KeyedVariablesState>()
.givenReducer(keyedVariablesReducer, {
...initialKeyedVariablesState,
lastKey: key,
keys: {
[key]: {
...getInitialTemplatingState(),
transaction: {
...initialTransactionState,
uid: key,
status: TransactionStatus.Completed,
},
},
},
})
.whenActionIsDispatched(variablesInitTransaction({ uid: 'newUid' }))
.thenStateShouldEqual({
...initialKeyedVariablesState,
lastKey: key,
keys: {
[key]: {
...getInitialTemplatingState(),
transaction: {
...initialTransactionState,
uid: key,
status: TransactionStatus.Completed,
},
},
},
});
});
});
});
@@ -0,0 +1,50 @@
import { AnyAction } from 'redux';
import { createAction, PayloadAction } from '@reduxjs/toolkit';
import { getTemplatingReducers, TemplatingState } from './reducers';
import { variablesInitTransaction } from './transactionReducer';
import { toStateKey } from '../utils';
export interface KeyedVariablesState {
lastKey?: string;
keys: Record<string, TemplatingState>;
}
export const initialKeyedVariablesState: KeyedVariablesState = { keys: {} };
export interface KeyedAction {
key: string;
action: PayloadAction<any>;
}
const keyedAction = createAction<KeyedAction>('templating/keyedAction');
export function toKeyedAction(key: string, action: PayloadAction<any>): PayloadAction<KeyedAction> {
const keyAsString = toStateKey(key);
return keyedAction({ key: keyAsString, action });
}
export function keyedVariablesReducer(state = initialKeyedVariablesState, outerAction: AnyAction): KeyedVariablesState {
if (keyedAction.match(outerAction)) {
const { key, action } = outerAction.payload;
const stringKey = toStateKey(key);
const lastKey = variablesInitTransaction.match(action) ? stringKey : state.lastKey;
const templatingReducers = getTemplatingReducers();
const prevKeyState = state.keys[stringKey];
const nextKeyState = templatingReducers(prevKeyState, action);
return {
...state,
lastKey,
keys: {
...state.keys,
[stringKey]: nextKeyState,
},
};
}
return state;
}
export default {
templating: keyedVariablesReducer,
};
@@ -2,13 +2,17 @@ import { migrateVariablesDatasourceNameToRef } from './actions';
import { adHocBuilder, queryBuilder } from '../shared/testing/builders';
import { DataSourceRef } from '@grafana/data/src';
import { changeVariableProp } from './sharedReducer';
import { toVariablePayload } from './types';
import { toKeyedAction } from './keyedVariablesReducer';
import { getPreloadedState } from './helpers';
import { toVariablePayload } from '../utils';
function getTestContext(ds: DataSourceRef, dsInstance?: { uid: string; type: string }) {
jest.clearAllMocks();
const query = queryBuilder().withId('query').withName('query').withDatasource(ds).build();
const adhoc = adHocBuilder().withId('adhoc').withName('adhoc').withDatasource(ds).build();
const state = { templating: { variables: [query, adhoc] } };
const key = 'key';
const query = queryBuilder().withId('query').withRootStateKey(key).withName('query').withDatasource(ds).build();
const adhoc = adHocBuilder().withId('adhoc').withRootStateKey(key).withName('adhoc').withDatasource(ds).build();
const templatingState = { variables: { query, adhoc } };
const state = getPreloadedState(key, templatingState);
const dispatch = jest.fn();
const getState = jest.fn().mockReturnValue(state);
const getInstanceSettingsMock = jest.fn().mockReturnValue(dsInstance);
@@ -18,7 +22,7 @@ function getTestContext(ds: DataSourceRef, dsInstance?: { uid: string; type: str
getInstanceSettings: getInstanceSettingsMock,
});
return { query, adhoc, dispatch, getState, getDatasourceSrvFunc };
return { key, query, adhoc, dispatch, getState, getDatasourceSrvFunc };
}
describe('migrateVariablesDatasourceNameToRef', () => {
@@ -26,22 +30,34 @@ describe('migrateVariablesDatasourceNameToRef', () => {
describe('and data source exists', () => {
it('then correct actions are dispatched', async () => {
const legacyDs = '${ds}' as unknown as DataSourceRef;
const { query, adhoc, dispatch, getState, getDatasourceSrvFunc } = getTestContext(legacyDs, {
const { query, adhoc, dispatch, getState, getDatasourceSrvFunc, key } = getTestContext(legacyDs, {
uid: 'a random uid',
type: 'prometheus',
});
migrateVariablesDatasourceNameToRef(getDatasourceSrvFunc)(dispatch, getState, undefined);
migrateVariablesDatasourceNameToRef(key, getDatasourceSrvFunc)(dispatch, getState, undefined);
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch.mock.calls[0][0]).toEqual(
changeVariableProp(
toVariablePayload(query, { propName: 'datasource', propValue: { uid: 'a random uid', type: 'prometheus' } })
toKeyedAction(
key,
changeVariableProp(
toVariablePayload(query, {
propName: 'datasource',
propValue: { uid: 'a random uid', type: 'prometheus' },
})
)
)
);
expect(dispatch.mock.calls[1][0]).toEqual(
changeVariableProp(
toVariablePayload(adhoc, { propName: 'datasource', propValue: { uid: 'a random uid', type: 'prometheus' } })
toKeyedAction(
key,
changeVariableProp(
toVariablePayload(adhoc, {
propName: 'datasource',
propValue: { uid: 'a random uid', type: 'prometheus' },
})
)
)
);
});
@@ -50,16 +66,22 @@ describe('migrateVariablesDatasourceNameToRef', () => {
describe('and data source does not exist', () => {
it('then correct actions are dispatched', async () => {
const legacyDs = '${ds}' as unknown as DataSourceRef;
const { query, adhoc, dispatch, getState, getDatasourceSrvFunc } = getTestContext(legacyDs, undefined);
const { query, adhoc, dispatch, getState, getDatasourceSrvFunc, key } = getTestContext(legacyDs, undefined);
migrateVariablesDatasourceNameToRef(getDatasourceSrvFunc)(dispatch, getState, undefined);
migrateVariablesDatasourceNameToRef(key, getDatasourceSrvFunc)(dispatch, getState, undefined);
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch.mock.calls[0][0]).toEqual(
changeVariableProp(toVariablePayload(query, { propName: 'datasource', propValue: { uid: '${ds}' } }))
toKeyedAction(
key,
changeVariableProp(toVariablePayload(query, { propName: 'datasource', propValue: { uid: '${ds}' } }))
)
);
expect(dispatch.mock.calls[1][0]).toEqual(
changeVariableProp(toVariablePayload(adhoc, { propName: 'datasource', propValue: { uid: '${ds}' } }))
toKeyedAction(
key,
changeVariableProp(toVariablePayload(adhoc, { propName: 'datasource', propValue: { uid: '${ds}' } }))
)
);
});
});
@@ -68,9 +90,9 @@ describe('migrateVariablesDatasourceNameToRef', () => {
describe('when called and variables have dataSourceRef', () => {
it('then no actions are dispatched', async () => {
const legacyDs = { uid: '${ds}', type: 'prometheus' };
const { dispatch, getState, getDatasourceSrvFunc } = getTestContext(legacyDs, undefined);
const { dispatch, getState, getDatasourceSrvFunc, key } = getTestContext(legacyDs, undefined);
migrateVariablesDatasourceNameToRef(getDatasourceSrvFunc)(dispatch, getState, undefined);
migrateVariablesDatasourceNameToRef(key, getDatasourceSrvFunc)(dispatch, getState, undefined);
expect(dispatch).toHaveBeenCalledTimes(0);
});
@@ -10,8 +10,7 @@ import { createConstantVariableAdapter } from '../constant/adapter';
import { VariableRefresh } from '../types';
import { constantBuilder, intervalBuilder } from '../shared/testing/builders';
import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { getRootReducer, RootReducerType } from './helpers';
import { toVariableIdentifier, toVariablePayload } from './types';
import { getPreloadedState, getRootReducer, RootReducerType } from './helpers';
import {
setCurrentVariableValue,
variableStateCompleted,
@@ -22,17 +21,20 @@ import { createIntervalOptions } from '../interval/reducer';
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
import { notifyApp } from '../../../core/reducers/appNotification';
import { expect } from '../../../../test/lib/common';
import { TemplatingState } from './reducers';
import { appEvents } from '../../../core/core';
import { variablesInitTransaction } from './transactionReducer';
import { toKeyedAction } from './keyedVariablesReducer';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
variableAdapters.setInit(() => [createIntervalVariableAdapter(), createConstantVariableAdapter()]);
const getTestContext = (dashboard: DashboardModel) => {
jest.clearAllMocks();
const key = 'key';
const interval = intervalBuilder()
.withId('interval-0')
.withRootStateKey(key)
.withName('interval-0')
.withOptions('1m', '10m', '30m', '1h', '6h', '12h', '1d', '7d', '14d', '30d')
.withCurrent('1m')
@@ -41,6 +43,7 @@ const getTestContext = (dashboard: DashboardModel) => {
const constant = constantBuilder()
.withId('constant-1')
.withRootStateKey(key)
.withName('constant-1')
.withOptions('a constant')
.withCurrent('a constant')
@@ -65,17 +68,19 @@ const getTestContext = (dashboard: DashboardModel) => {
getModel: () => dashboard,
} as unknown as DashboardState;
const adapter = variableAdapters.get('interval');
const templatingState = {
variables: {
'interval-0': { ...interval },
'constant-1': { ...constant },
},
};
const preloadedState = {
dashboard: dashboardState,
templating: {
variables: {
'interval-0': { ...interval },
'constant-1': { ...constant },
},
} as unknown as TemplatingState,
...getPreloadedState(key, templatingState),
} as unknown as RootReducerType;
return {
key,
interval,
range,
dependencies,
@@ -91,6 +96,7 @@ describe('when onTimeRangeUpdated is dispatched', () => {
describe('and options are changed by update', () => {
it('then correct actions are dispatched and correct dependencies are called', async () => {
const {
key,
preloadedState,
range,
dependencies,
@@ -101,20 +107,23 @@ describe('when onTimeRangeUpdated is dispatched', () => {
const tester = await reduxTester<RootReducerType>({ preloadedState })
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenAsyncActionIsDispatched(onTimeRangeUpdated(range, dependencies));
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenAsyncActionIsDispatched(onTimeRangeUpdated(key, range, dependencies));
tester.thenDispatchedActionsShouldEqual(
variablesInitTransaction({ uid: 'a uid' }),
variableStateFetching(toVariablePayload({ type: 'interval', id: 'interval-0' })),
createIntervalOptions(toVariablePayload({ type: 'interval', id: 'interval-0' })),
setCurrentVariableValue(
toVariablePayload(
{ type: 'interval', id: 'interval-0' },
{ option: { text: '1m', value: '1m', selected: false } }
toKeyedAction(key, variablesInitTransaction({ uid: key })),
toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'interval', id: 'interval-0' }))),
toKeyedAction(key, createIntervalOptions(toVariablePayload({ type: 'interval', id: 'interval-0' }))),
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'interval', id: 'interval-0' },
{ option: { text: '1m', value: '1m', selected: false } }
)
)
),
variableStateCompleted(toVariablePayload({ type: 'interval', id: 'interval-0' }))
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'interval', id: 'interval-0' })))
);
expect(updateTimeRangeMock).toHaveBeenCalledTimes(1);
@@ -127,6 +136,7 @@ describe('when onTimeRangeUpdated is dispatched', () => {
describe('and options are not changed by update', () => {
it('then correct actions are dispatched and correct dependencies are called', async () => {
const {
key,
interval,
preloadedState,
range,
@@ -138,21 +148,26 @@ describe('when onTimeRangeUpdated is dispatched', () => {
const base = await reduxTester<RootReducerType>({ preloadedState })
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenAsyncActionIsDispatched(setOptionAsCurrent(toVariableIdentifier(interval), interval.options[0], false));
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenAsyncActionIsDispatched(
setOptionAsCurrent(toKeyedVariableIdentifier(interval), interval.options[0], false)
);
const tester = await base.whenAsyncActionIsDispatched(onTimeRangeUpdated(range, dependencies), true);
const tester = await base.whenAsyncActionIsDispatched(onTimeRangeUpdated(key, range, dependencies), true);
tester.thenDispatchedActionsShouldEqual(
variableStateFetching(toVariablePayload({ type: 'interval', id: 'interval-0' })),
createIntervalOptions(toVariablePayload({ type: 'interval', id: 'interval-0' })),
setCurrentVariableValue(
toVariablePayload(
{ type: 'interval', id: 'interval-0' },
{ option: { text: '1m', value: '1m', selected: false } }
toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'interval', id: 'interval-0' }))),
toKeyedAction(key, createIntervalOptions(toVariablePayload({ type: 'interval', id: 'interval-0' }))),
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'interval', id: 'interval-0' },
{ option: { text: '1m', value: '1m', selected: false } }
)
)
),
variableStateCompleted(toVariablePayload({ type: 'interval', id: 'interval-0' }))
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'interval', id: 'interval-0' })))
);
expect(updateTimeRangeMock).toHaveBeenCalledTimes(1);
@@ -166,6 +181,7 @@ describe('when onTimeRangeUpdated is dispatched', () => {
silenceConsoleOutput();
it('then correct actions are dispatched and correct dependencies are called', async () => {
const {
key,
adapter,
preloadedState,
range,
@@ -179,16 +195,19 @@ describe('when onTimeRangeUpdated is dispatched', () => {
const tester = await reduxTester<RootReducerType>({ preloadedState, debug: true })
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
.whenAsyncActionIsDispatched(onTimeRangeUpdated(range, dependencies), true);
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenAsyncActionIsDispatched(onTimeRangeUpdated(key, range, dependencies), true);
tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
expect(dispatchedActions[0]).toEqual(
variableStateFetching(toVariablePayload({ type: 'interval', id: 'interval-0' }))
toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'interval', id: 'interval-0' })))
);
expect(dispatchedActions[1]).toEqual(
variableStateFailed(
toVariablePayload({ type: 'interval', id: 'interval-0' }, { error: new Error('Something broke') })
toKeyedAction(
key,
variableStateFailed(
toVariablePayload({ type: 'interval', id: 'interval-0' }, { error: new Error('Something broke') })
)
)
);
expect(dispatchedActions[2].type).toEqual(notifyApp.type);
@@ -1,20 +1,20 @@
import { UrlQueryMap } from '@grafana/data';
import { getTemplatingRootReducer } from './helpers';
import { getTemplatingRootReducer, TemplatingReducerType } from './helpers';
import { variableAdapters } from '../adapters';
import { createQueryVariableAdapter } from '../query/adapter';
import { createCustomVariableAdapter } from '../custom/adapter';
import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { TemplatingState } from 'app/features/variables/state/reducers';
import { initDashboardTemplating, processVariable } from './actions';
import { setCurrentVariableValue, variableStateCompleted, variableStateFetching } from './sharedReducer';
import { toVariableIdentifier, toVariablePayload } from './types';
import { VariableRefresh } from '../types';
import { updateVariableOptions } from '../query/reducer';
import { customBuilder, queryBuilder } from '../shared/testing/builders';
import { variablesInitTransaction } from './transactionReducer';
import { setVariableQueryRunner, VariableQueryRunner } from '../query/VariableQueryRunner';
import { setDataSourceSrv } from '@grafana/runtime';
import { toKeyedAction } from './keyedVariablesReducer';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
jest.mock('app/features/dashboard/services/TimeSrv', () => ({
getTimeSrv: jest.fn().mockReturnValue({
@@ -68,9 +68,11 @@ describe('processVariable', () => {
// custom doesn't depend on any other variable
// queryDependsOnCustom depends on custom
// queryNoDepends doesn't depend on any other variable
const getAndSetupProcessVariableContext = () => {
const key = 'key';
const getTestContext = () => {
const custom = customBuilder()
.withId('custom')
.withRootStateKey(key)
.withName('custom')
.withQuery('A,B,C')
.withOptions('A', 'B', 'C')
@@ -79,6 +81,7 @@ describe('processVariable', () => {
const queryDependsOnCustom = queryBuilder()
.withId('queryDependsOnCustom')
.withRootStateKey(key)
.withName('queryDependsOnCustom')
.withQuery('$custom.*')
.withOptions('AA', 'AB', 'AC')
@@ -87,6 +90,7 @@ describe('processVariable', () => {
const queryNoDepends = queryBuilder()
.withId('queryNoDepends')
.withRootStateKey(key)
.withName('queryNoDepends')
.withQuery('*')
.withOptions('A', 'B', 'C')
@@ -94,13 +98,15 @@ describe('processVariable', () => {
.build();
const list = [custom, queryDependsOnCustom, queryNoDepends];
const dashboard: any = { templating: { list } };
setVariableQueryRunner(new VariableQueryRunner());
return {
key,
custom,
queryDependsOnCustom,
queryNoDepends,
list,
dashboard,
};
};
@@ -108,33 +114,41 @@ describe('processVariable', () => {
describe('when processVariable is dispatched for a custom variable without dependencies', () => {
describe('and queryParams does not match variable', () => {
it('then correct actions are dispatched', async () => {
const { list, custom } = getAndSetupProcessVariableContext();
const { key, dashboard, custom } = getTestContext();
const queryParams: UrlQueryMap = {};
const tester = await reduxTester<{ templating: TemplatingState }>()
const tester = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
.whenActionIsDispatched(initDashboardTemplating(list))
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(custom), queryParams), true);
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams), true);
await tester.thenDispatchedActionsShouldEqual(variableStateCompleted(toVariablePayload(custom)));
await tester.thenDispatchedActionsShouldEqual(
toKeyedAction(key, variableStateCompleted(toVariablePayload(custom)))
);
});
});
describe('and queryParams does match variable', () => {
it('then correct actions are dispatched', async () => {
const { list, custom } = getAndSetupProcessVariableContext();
const { key, dashboard, custom } = getTestContext();
const queryParams: UrlQueryMap = { 'var-custom': 'B' };
const tester = await reduxTester<{ templating: TemplatingState }>()
const tester = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
.whenActionIsDispatched(initDashboardTemplating(list))
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(custom), queryParams), true);
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams), true);
await tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(
toVariablePayload({ type: 'custom', id: 'custom' }, { option: { text: 'B', value: 'B', selected: false } })
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'custom', id: 'custom' },
{ option: { text: 'B', value: 'B', selected: false } }
)
)
),
variableStateCompleted(toVariablePayload(custom))
toKeyedAction(key, variableStateCompleted(toVariablePayload(custom)))
);
});
});
@@ -148,15 +162,17 @@ describe('processVariable', () => {
describe('and refresh is VariableRefresh.never', () => {
const refresh = VariableRefresh.never;
it('then correct actions are dispatched', async () => {
const { list, queryNoDepends } = getAndSetupProcessVariableContext();
const { dashboard, key, queryNoDepends } = getTestContext();
queryNoDepends.refresh = refresh;
const tester = await reduxTester<{ templating: TemplatingState }>()
const tester = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
.whenActionIsDispatched(initDashboardTemplating(list))
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(queryNoDepends), queryParams), true);
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(queryNoDepends), queryParams), true);
await tester.thenDispatchedActionsShouldEqual(variableStateCompleted(toVariablePayload(queryNoDepends)));
await tester.thenDispatchedActionsShouldEqual(
toKeyedAction(key, variableStateCompleted(toVariablePayload(queryNoDepends)))
);
});
});
@@ -165,36 +181,42 @@ describe('processVariable', () => {
${VariableRefresh.onDashboardLoad}
${VariableRefresh.onTimeRangeChanged}
`('and refresh is $refresh then correct actions are dispatched', async ({ refresh }) => {
const { list, queryNoDepends } = getAndSetupProcessVariableContext();
const { dashboard, key, queryNoDepends } = getTestContext();
queryNoDepends.refresh = refresh;
const tester = await reduxTester<{ templating: TemplatingState }>()
const tester = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
.whenActionIsDispatched(initDashboardTemplating(list))
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(queryNoDepends), queryParams), true);
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(queryNoDepends), queryParams), true);
await tester.thenDispatchedActionsShouldEqual(
variableStateFetching(toVariablePayload({ type: 'query', id: 'queryNoDepends' })),
updateVariableOptions(
toVariablePayload(
{ type: 'query', id: 'queryNoDepends' },
{
results: [
{ value: 'A', text: 'A' },
{ value: 'B', text: 'B' },
{ value: 'C', text: 'C' },
],
templatedRegex: '',
}
toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))),
toKeyedAction(
key,
updateVariableOptions(
toVariablePayload(
{ type: 'query', id: 'queryNoDepends' },
{
results: [
{ value: 'A', text: 'A' },
{ value: 'B', text: 'B' },
{ value: 'C', text: 'C' },
],
templatedRegex: '',
}
)
)
),
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryNoDepends' },
{ option: { text: 'A', value: 'A', selected: false } }
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryNoDepends' },
{ option: { text: 'A', value: 'A', selected: false } }
)
)
),
variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryNoDepends' })))
);
});
});
@@ -205,22 +227,25 @@ describe('processVariable', () => {
describe('and refresh is VariableRefresh.never', () => {
const refresh = VariableRefresh.never;
it('then correct actions are dispatched', async () => {
const { list, queryNoDepends } = getAndSetupProcessVariableContext();
const { dashboard, key, queryNoDepends } = getTestContext();
queryNoDepends.refresh = refresh;
const tester = await reduxTester<{ templating: TemplatingState }>()
const tester = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
.whenActionIsDispatched(initDashboardTemplating(list))
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(queryNoDepends), queryParams), true);
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(queryNoDepends), queryParams), true);
await tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryNoDepends' },
{ option: { text: 'B', value: 'B', selected: false } }
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryNoDepends' },
{ option: { text: 'B', value: 'B', selected: false } }
)
)
),
variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryNoDepends' })))
);
});
});
@@ -230,40 +255,49 @@ describe('processVariable', () => {
${VariableRefresh.onDashboardLoad}
${VariableRefresh.onTimeRangeChanged}
`('and refresh is $refresh then correct actions are dispatched', async ({ refresh }) => {
const { list, queryNoDepends } = getAndSetupProcessVariableContext();
const { dashboard, key, queryNoDepends } = getTestContext();
queryNoDepends.refresh = refresh;
const tester = await reduxTester<{ templating: TemplatingState }>()
const tester = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
.whenActionIsDispatched(initDashboardTemplating(list))
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(queryNoDepends), queryParams), true);
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(queryNoDepends), queryParams), true);
await tester.thenDispatchedActionsShouldEqual(
variableStateFetching(toVariablePayload({ type: 'query', id: 'queryNoDepends' })),
updateVariableOptions(
toVariablePayload(
{ type: 'query', id: 'queryNoDepends' },
{
results: [
{ value: 'A', text: 'A' },
{ value: 'B', text: 'B' },
{ value: 'C', text: 'C' },
],
templatedRegex: '',
}
toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))),
toKeyedAction(
key,
updateVariableOptions(
toVariablePayload(
{ type: 'query', id: 'queryNoDepends' },
{
results: [
{ value: 'A', text: 'A' },
{ value: 'B', text: 'B' },
{ value: 'C', text: 'C' },
],
templatedRegex: '',
}
)
)
),
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryNoDepends' },
{ option: { text: 'A', value: 'A', selected: false } }
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryNoDepends' },
{ option: { text: 'A', value: 'A', selected: false } }
)
)
),
variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryNoDepends' })),
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryNoDepends' },
{ option: { text: 'B', value: 'B', selected: false } }
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))),
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryNoDepends' },
{ option: { text: 'B', value: 'B', selected: false } }
)
)
)
);
@@ -279,21 +313,21 @@ describe('processVariable', () => {
describe('and refresh is VariableRefresh.never', () => {
const refresh = VariableRefresh.never;
it('then correct actions are dispatched', async () => {
const { list, custom, queryDependsOnCustom } = getAndSetupProcessVariableContext();
const { key, dashboard, custom, queryDependsOnCustom } = getTestContext();
queryDependsOnCustom.refresh = refresh;
const customProcessed = await reduxTester<{ templating: TemplatingState }>()
const customProcessed = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
.whenActionIsDispatched(initDashboardTemplating(list))
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
const tester = await customProcessed.whenAsyncActionIsDispatched(
processVariable(toVariableIdentifier(queryDependsOnCustom), queryParams),
processVariable(toKeyedVariableIdentifier(queryDependsOnCustom), queryParams),
true
);
await tester.thenDispatchedActionsShouldEqual(
variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' })))
);
});
});
@@ -303,41 +337,47 @@ describe('processVariable', () => {
${VariableRefresh.onDashboardLoad}
${VariableRefresh.onTimeRangeChanged}
`('and refresh is $refresh then correct actions are dispatched', async ({ refresh }) => {
const { list, custom, queryDependsOnCustom } = getAndSetupProcessVariableContext();
const { key, dashboard, custom, queryDependsOnCustom } = getTestContext();
queryDependsOnCustom.refresh = refresh;
const customProcessed = await reduxTester<{ templating: TemplatingState }>()
const customProcessed = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
.whenActionIsDispatched(initDashboardTemplating(list))
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
const tester = await customProcessed.whenAsyncActionIsDispatched(
processVariable(toVariableIdentifier(queryDependsOnCustom), queryParams),
processVariable(toKeyedVariableIdentifier(queryDependsOnCustom), queryParams),
true
);
await tester.thenDispatchedActionsShouldEqual(
variableStateFetching(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' })),
updateVariableOptions(
toVariablePayload(
{ type: 'query', id: 'queryDependsOnCustom' },
{
results: [
{ value: 'AA', text: 'AA' },
{ value: 'AB', text: 'AB' },
{ value: 'AC', text: 'AC' },
],
templatedRegex: '',
}
toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))),
toKeyedAction(
key,
updateVariableOptions(
toVariablePayload(
{ type: 'query', id: 'queryDependsOnCustom' },
{
results: [
{ value: 'AA', text: 'AA' },
{ value: 'AB', text: 'AB' },
{ value: 'AC', text: 'AC' },
],
templatedRegex: '',
}
)
)
),
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryDependsOnCustom' },
{ option: { text: 'AA', value: 'AA', selected: false } }
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryDependsOnCustom' },
{ option: { text: 'AA', value: 'AA', selected: false } }
)
)
),
variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' })))
);
});
});
@@ -348,27 +388,30 @@ describe('processVariable', () => {
describe('and refresh is VariableRefresh.never', () => {
const refresh = VariableRefresh.never;
it('then correct actions are dispatched', async () => {
const { list, custom, queryDependsOnCustom } = getAndSetupProcessVariableContext();
const { key, dashboard, custom, queryDependsOnCustom } = getTestContext();
queryDependsOnCustom.refresh = refresh;
const customProcessed = await reduxTester<{ templating: TemplatingState }>()
const customProcessed = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
.whenActionIsDispatched(initDashboardTemplating(list))
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
const tester = await customProcessed.whenAsyncActionIsDispatched(
processVariable(toVariableIdentifier(queryDependsOnCustom), queryParams),
processVariable(toKeyedVariableIdentifier(queryDependsOnCustom), queryParams),
true
);
await tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryDependsOnCustom' },
{ option: { text: 'AB', value: 'AB', selected: false } }
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryDependsOnCustom' },
{ option: { text: 'AB', value: 'AB', selected: false } }
)
)
),
variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' })))
);
});
});
@@ -378,45 +421,54 @@ describe('processVariable', () => {
${VariableRefresh.onDashboardLoad}
${VariableRefresh.onTimeRangeChanged}
`('and refresh is $refresh then correct actions are dispatched', async ({ refresh }) => {
const { list, custom, queryDependsOnCustom } = getAndSetupProcessVariableContext();
const { key, dashboard, custom, queryDependsOnCustom } = getTestContext();
queryDependsOnCustom.refresh = refresh;
const customProcessed = await reduxTester<{ templating: TemplatingState }>()
const customProcessed = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
.whenActionIsDispatched(initDashboardTemplating(list))
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
const tester = await customProcessed.whenAsyncActionIsDispatched(
processVariable(toVariableIdentifier(queryDependsOnCustom), queryParams),
processVariable(toKeyedVariableIdentifier(queryDependsOnCustom), queryParams),
true
);
await tester.thenDispatchedActionsShouldEqual(
variableStateFetching(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' })),
updateVariableOptions(
toVariablePayload(
{ type: 'query', id: 'queryDependsOnCustom' },
{
results: [
{ value: 'AA', text: 'AA' },
{ value: 'AB', text: 'AB' },
{ value: 'AC', text: 'AC' },
],
templatedRegex: '',
}
toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))),
toKeyedAction(
key,
updateVariableOptions(
toVariablePayload(
{ type: 'query', id: 'queryDependsOnCustom' },
{
results: [
{ value: 'AA', text: 'AA' },
{ value: 'AB', text: 'AB' },
{ value: 'AC', text: 'AC' },
],
templatedRegex: '',
}
)
)
),
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryDependsOnCustom' },
{ option: { text: 'AA', value: 'AA', selected: false } }
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryDependsOnCustom' },
{ option: { text: 'AA', value: 'AA', selected: false } }
)
)
),
variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' })),
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryDependsOnCustom' },
{ option: { text: 'AB', value: 'AB', selected: false } }
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))),
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'query', id: 'queryDependsOnCustom' },
{ option: { text: 'AB', value: 'AB', selected: false } }
)
)
)
);
@@ -1,10 +1,12 @@
import { createAction } from '@reduxjs/toolkit';
import { VariableType } from '@grafana/data';
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { initialVariableModelState, QueryVariableModel } from '../types';
import { VariableAdapter, variableAdapters } from '../adapters';
import { createAction } from '@reduxjs/toolkit';
import { cleanVariables, variablesReducer } from './variablesReducer';
import { VariablesState, toVariablePayload, VariablePayload } from './types';
import { VariableType } from '@grafana/data';
import { VariablePayload, VariablesState } from './types';
import { toVariablePayload } from '../utils';
const variableAdapter: VariableAdapter<QueryVariableModel> = {
id: 'mock' as unknown as VariableType,
+37 -16
View File
@@ -1,20 +1,41 @@
import { combineReducers } from '@reduxjs/toolkit';
import { optionsPickerReducer } from '../pickers/OptionsPicker/reducer';
import { variableEditorReducer } from '../editor/reducer';
import { CombinedState, combineReducers, Reducer } from 'redux';
import { initialOptionPickerState, optionsPickerReducer, OptionsPickerState } from '../pickers/OptionsPicker/reducer';
import { initialVariableEditorState, variableEditorReducer, VariableEditorState } from '../editor/reducer';
import { variablesReducer } from './variablesReducer';
import { transactionReducer } from './transactionReducer';
import { variableInspectReducer } from '../inspect/reducer';
import { initialTransactionState, transactionReducer, TransactionState } from './transactionReducer';
import { initialVariableInspectState, variableInspectReducer, VariableInspectState } from '../inspect/reducer';
import { initialVariablesState, VariablesState } from './types';
export const templatingReducers = combineReducers({
editor: variableEditorReducer,
variables: variablesReducer,
optionsPicker: optionsPickerReducer,
transaction: transactionReducer,
inspect: variableInspectReducer,
});
export interface TemplatingState {
editor: VariableEditorState;
variables: VariablesState;
optionsPicker: OptionsPickerState;
transaction: TransactionState;
inspect: VariableInspectState;
}
export type TemplatingState = ReturnType<typeof templatingReducers>;
let templatingReducers: Reducer<CombinedState<TemplatingState>>;
export default {
templating: templatingReducers,
};
export function getTemplatingReducers() {
if (!templatingReducers) {
templatingReducers = combineReducers({
editor: variableEditorReducer,
variables: variablesReducer,
optionsPicker: optionsPickerReducer,
transaction: transactionReducer,
inspect: variableInspectReducer,
});
}
return templatingReducers;
}
export function getInitialTemplatingState() {
return {
editor: initialVariableEditorState,
variables: initialVariablesState,
optionsPicker: initialOptionPickerState,
transaction: initialTransactionState,
inspect: initialVariableInspectState,
};
}
@@ -2,52 +2,63 @@ import { StoreState } from '../../../types';
import { VariableModel } from '../types';
import { getState } from '../../../store/store';
import memoizeOne from 'memoize-one';
import { getInitialTemplatingState, TemplatingState } from './reducers';
import { toStateKey } from '../utils';
import { KeyedVariableIdentifier, VariablesState } from './types';
export const getVariable = <T extends VariableModel = VariableModel>(
id: string,
identifier: KeyedVariableIdentifier,
state: StoreState = getState(),
throwWhenMissing = true
): T => {
if (!state.templating.variables[id]) {
const { id, rootStateKey } = identifier;
const variablesState = getVariablesState(rootStateKey, state);
if (!variablesState.variables[id]) {
if (throwWhenMissing) {
throw new Error(`Couldn't find variable with id:${id}`);
}
return undefined as unknown as T;
}
return state.templating.variables[id] as T;
return variablesState.variables[id] as T;
};
export const getFilteredVariables = (filter: (model: VariableModel) => boolean, state: StoreState = getState()) => {
return Object.values(state.templating.variables)
function getFilteredVariablesByKey(
filter: (model: VariableModel) => boolean,
key: string,
state: StoreState = getState()
) {
return Object.values(getVariablesState(key, state).variables)
.filter(filter)
.sort((s1, s2) => s1.index - s2.index);
};
}
export const getVariableWithName = (name: string, state: StoreState = getState()) => {
return getVariable(name, state, false);
};
export function getVariablesState(key: string, state: StoreState = getState()): TemplatingState {
return state.templating.keys[toStateKey(key)] ?? getInitialTemplatingState();
}
export const getVariables = (state: StoreState = getState()): VariableModel[] => {
return getFilteredVariables(defaultVariablesFilter, state);
};
export function getVariablesByKey(key: string, state: StoreState = getState()): VariableModel[] {
return getFilteredVariablesByKey(defaultVariablesFilter, key, state);
}
export function defaultVariablesFilter(variable: VariableModel): boolean {
function defaultVariablesFilter(variable: VariableModel): boolean {
return variable.type !== 'system';
}
export const getSubMenuVariables = memoizeOne((variables: Record<string, VariableModel>): VariableModel[] => {
return getVariables(getState());
});
export const getSubMenuVariables = memoizeOne(
(key: string, variables: Record<string, VariableModel>): VariableModel[] => {
return getVariablesByKey(key, getState());
}
);
export const getEditorVariables = (state: StoreState): VariableModel[] => {
return getVariables(state);
export const getEditorVariables = (key: string, state: StoreState): VariableModel[] => {
return getVariablesByKey(key, state);
};
export type GetVariables = typeof getVariables;
export type GetVariables = typeof getVariablesByKey;
export function getNewVariableIndex(state: StoreState = getState()): number {
return getNextVariableIndex(Object.values(state.templating.variables));
export function getNewVariableIndex(key: string, state: StoreState = getState()): number {
return getNextVariableIndex(Object.values(getVariablesState(key, state).variables));
}
export function getNextVariableIndex(variables: VariableModel[]): number {
@@ -55,6 +66,47 @@ export function getNextVariableIndex(variables: VariableModel[]): number {
return sorted.length > 0 ? sorted[sorted.length - 1].index + 1 : 0;
}
export function getVariablesIsDirty(state: StoreState = getState()): boolean {
return state.templating.transaction.isDirty;
export function getVariablesIsDirty(key: string, state: StoreState = getState()): boolean {
return getVariablesState(key, state).transaction.isDirty;
}
export function getIfExistsLastKey(state: StoreState = getState()): string | undefined {
return state.templating?.lastKey;
}
export function getLastKey(state: StoreState = getState()): string {
if (!state.templating?.lastKey) {
throw new Error('Accessing lastKey without initializing it variables');
}
return state.templating.lastKey;
}
// selectors used by template srv, assumes that lastKey is in state. Needs to change when/if dashboard redux state becomes keyed too.
export function getFilteredVariables(filter: (model: VariableModel) => boolean, state: StoreState = getState()) {
const lastKey = getIfExistsLastKey(state);
if (!lastKey) {
return [];
}
return getFilteredVariablesByKey(filter, lastKey, state);
}
export function getVariables(state: StoreState = getState()) {
const lastKey = getIfExistsLastKey(state);
if (!lastKey) {
return [];
}
return getVariablesByKey(lastKey, state);
}
export function getVariableWithName(name: string, state: StoreState = getState()) {
const lastKey = getIfExistsLastKey(state);
if (!lastKey) {
return;
}
return getVariable({ id: name, rootStateKey: lastKey, type: 'query' }, state, false);
}
export function getInstanceState<Model extends VariableModel = VariableModel>(state: VariablesState, id: string) {
return state[id] as Model;
}
@@ -2,12 +2,12 @@ import { variableAdapters } from '../adapters';
import { createCustomVariableAdapter } from '../custom/adapter';
import { customBuilder } from '../shared/testing/builders';
import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { TemplatingState } from './reducers';
import { getTemplatingRootReducer } from './helpers';
import { getTemplatingRootReducer, TemplatingReducerType } from './helpers';
import { addVariable, setCurrentVariableValue } from './sharedReducer';
import { toVariableIdentifier, toVariablePayload } from './types';
import { setOptionFromUrl } from './actions';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
import { toKeyedAction } from './keyedVariablesReducer';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
variableAdapters.setInit(() => [createCustomVariableAdapter()]);
@@ -28,16 +28,31 @@ describe('when setOptionFromUrl is dispatched with a custom variable (no refresh
${null} | ${true} | ${['']}
${undefined} | ${true} | ${['']}
`('and urlValue is $urlValue then correct actions are dispatched', async ({ urlValue, expected, isMulti }) => {
const custom = customBuilder().withId('0').withMulti(isMulti).withOptions('A', 'B', 'C').withCurrent('A').build();
const key = 'key';
const custom = customBuilder()
.withId('0')
.withRootStateKey(key)
.withMulti(isMulti)
.withOptions('A', 'B', 'C')
.withCurrent('A')
.build();
const tester = await reduxTester<{ templating: TemplatingState }>()
const tester = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
.whenAsyncActionIsDispatched(setOptionFromUrl(toVariableIdentifier(custom), urlValue), true);
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
)
.whenAsyncActionIsDispatched(setOptionFromUrl(toKeyedVariableIdentifier(custom), urlValue), true);
await tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(
toVariablePayload({ type: 'custom', id: '0' }, { option: { text: expected, value: expected, selected: false } })
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'custom', id: '0' },
{ option: { text: expected, value: expected, selected: false } }
)
)
)
);
});
@@ -47,8 +62,10 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
it('and urlValue contains same all value then correct actions are dispatched', async () => {
const allValue = '.*';
const urlValue = allValue;
const key = 'key';
const custom = customBuilder()
.withId('0')
.withRootStateKey(key)
.withMulti(false)
.withIncludeAll()
.withAllValue(allValue)
@@ -56,16 +73,21 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
.withCurrent('A')
.build();
const tester = await reduxTester<{ templating: TemplatingState }>()
const tester = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
.whenAsyncActionIsDispatched(setOptionFromUrl(toVariableIdentifier(custom), urlValue), true);
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
)
.whenAsyncActionIsDispatched(setOptionFromUrl(toKeyedVariableIdentifier(custom), urlValue), true);
await tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(
toVariablePayload(
{ type: 'custom', id: '0' },
{ option: { text: ALL_VARIABLE_TEXT, value: ALL_VARIABLE_VALUE, selected: false } }
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'custom', id: '0' },
{ option: { text: ALL_VARIABLE_TEXT, value: ALL_VARIABLE_VALUE, selected: false } }
)
)
)
);
@@ -74,8 +96,10 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
it('and urlValue differs from all value then correct actions are dispatched', async () => {
const allValue = '.*';
const urlValue = 'X';
const key = 'key';
const custom = customBuilder()
.withId('0')
.withRootStateKey(key)
.withMulti(false)
.withIncludeAll()
.withAllValue(allValue)
@@ -83,14 +107,19 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
.withCurrent('A')
.build();
const tester = await reduxTester<{ templating: TemplatingState }>()
const tester = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
.whenAsyncActionIsDispatched(setOptionFromUrl(toVariableIdentifier(custom), urlValue), true);
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
)
.whenAsyncActionIsDispatched(setOptionFromUrl(toKeyedVariableIdentifier(custom), urlValue), true);
await tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(
toVariablePayload({ type: 'custom', id: '0' }, { option: { text: 'X', value: 'X', selected: false } })
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload({ type: 'custom', id: '0' }, { option: { text: 'X', value: 'X', selected: false } })
)
)
);
});
@@ -98,8 +127,10 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
it('and urlValue differs but matches an option then correct actions are dispatched', async () => {
const allValue = '.*';
const urlValue = 'B';
const key = 'key';
const custom = customBuilder()
.withId('0')
.withRootStateKey(key)
.withMulti(false)
.withIncludeAll()
.withAllValue(allValue)
@@ -107,14 +138,19 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
.withCurrent('A')
.build();
const tester = await reduxTester<{ templating: TemplatingState }>()
const tester = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
.whenAsyncActionIsDispatched(setOptionFromUrl(toVariableIdentifier(custom), urlValue), true);
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
)
.whenAsyncActionIsDispatched(setOptionFromUrl(toKeyedVariableIdentifier(custom), urlValue), true);
await tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(
toVariablePayload({ type: 'custom', id: '0' }, { option: { text: 'B', value: 'B', selected: false } })
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload({ type: 'custom', id: '0' }, { option: { text: 'B', value: 'B', selected: false } })
)
)
);
});
@@ -122,8 +158,10 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
it('and custom all value matches an option', async () => {
const allValue = '.*';
const urlValue = allValue;
const key = 'key';
const custom = customBuilder()
.withId('0')
.withRootStateKey(key)
.withMulti(false)
.withIncludeAll()
.withAllValue(allValue)
@@ -133,16 +171,21 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
custom.options[2].value = 'special value for .*';
const tester = await reduxTester<{ templating: TemplatingState }>()
const tester = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
.whenAsyncActionIsDispatched(setOptionFromUrl(toVariableIdentifier(custom), urlValue), true);
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
)
.whenAsyncActionIsDispatched(setOptionFromUrl(toKeyedVariableIdentifier(custom), urlValue), true);
await tester.thenDispatchedActionsShouldEqual(
setCurrentVariableValue(
toVariablePayload(
{ type: 'custom', id: '0' },
{ option: { text: '.*', value: 'special value for .*', selected: false } }
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'custom', id: '0' },
{ option: { text: '.*', value: 'special value for .*', selected: false } }
)
)
)
);
@@ -17,7 +17,7 @@ import {
variableStateNotStarted,
} from './sharedReducer';
import { ConstantVariableModel, QueryVariableModel, VariableHide, VariableOption } from '../types';
import { initialVariablesState, toVariablePayload, VariableIdentifier, VariablesState } from './types';
import { initialVariablesState, KeyedVariableIdentifier, VariablesState } from './types';
import { variableAdapters } from '../adapters';
import { createQueryVariableAdapter } from '../query/adapter';
import { initialQueryVariableModelState } from '../query/reducer';
@@ -26,6 +26,7 @@ import { changeVariableNameSucceeded } from '../editor/reducer';
import { createConstantVariableAdapter } from '../constant/adapter';
import { initialConstantVariableModelState } from '../constant/reducer';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
import { toVariablePayload } from '../utils';
variableAdapters.setInit(() => [createQueryVariableAdapter(), createConstantVariableAdapter()]);
@@ -102,6 +103,7 @@ describe('sharedReducer', () => {
.thenStateShouldEqual({
'0': {
id: '0',
rootStateKey: 'key',
type: 'query',
name: 'Name-0',
hide: VariableHide.dontHide,
@@ -115,6 +117,7 @@ describe('sharedReducer', () => {
},
'2': {
id: '2',
rootStateKey: 'key',
type: 'query',
name: 'Name-2',
hide: VariableHide.dontHide,
@@ -140,6 +143,7 @@ describe('sharedReducer', () => {
.thenStateShouldEqual({
'0': {
id: '0',
rootStateKey: 'key',
type: 'query',
name: 'Name-0',
hide: VariableHide.dontHide,
@@ -153,6 +157,7 @@ describe('sharedReducer', () => {
},
'2': {
id: '2',
rootStateKey: 'key',
type: 'query',
name: 'Name-2',
hide: VariableHide.dontHide,
@@ -179,6 +184,7 @@ describe('sharedReducer', () => {
...initialState,
'0': {
id: '0',
rootStateKey: 'key',
type: 'query',
name: 'Name-0',
hide: VariableHide.dontHide,
@@ -192,6 +198,7 @@ describe('sharedReducer', () => {
},
'1': {
id: '1',
rootStateKey: 'key',
type: 'query',
name: 'Name-1',
hide: VariableHide.dontHide,
@@ -205,6 +212,7 @@ describe('sharedReducer', () => {
},
'2': {
id: '2',
rootStateKey: 'key',
type: 'query',
name: 'Name-2',
hide: VariableHide.dontHide,
@@ -219,6 +227,7 @@ describe('sharedReducer', () => {
'11': {
...initialQueryVariableModelState,
id: '11',
rootStateKey: 'key',
name: 'copy_of_Name-1',
index: 3,
label: 'Label-1',
@@ -237,6 +246,7 @@ describe('sharedReducer', () => {
.thenStateShouldEqual({
'0': {
id: '0',
rootStateKey: 'key',
type: 'query',
name: 'Name-0',
hide: VariableHide.dontHide,
@@ -250,6 +260,7 @@ describe('sharedReducer', () => {
},
'1': {
id: '1',
rootStateKey: 'key',
type: 'query',
name: 'Name-1',
hide: VariableHide.dontHide,
@@ -263,6 +274,7 @@ describe('sharedReducer', () => {
},
'2': {
id: '2',
rootStateKey: 'key',
type: 'query',
name: 'Name-2',
hide: VariableHide.dontHide,
@@ -286,6 +298,7 @@ describe('sharedReducer', () => {
.thenStateShouldEqual({
'0': {
id: '0',
rootStateKey: 'key',
type: 'query',
name: 'Name-0',
hide: VariableHide.dontHide,
@@ -299,6 +312,7 @@ describe('sharedReducer', () => {
},
'1': {
id: '1',
rootStateKey: 'key',
type: 'query',
name: 'Name-1',
hide: VariableHide.dontHide,
@@ -312,6 +326,7 @@ describe('sharedReducer', () => {
},
'2': {
id: '2',
rootStateKey: 'key',
type: 'query',
name: 'Name-2',
hide: VariableHide.dontHide,
@@ -548,7 +563,7 @@ describe('sharedReducer', () => {
const constantAdapter = createConstantVariableAdapter();
const { initialState: constantAdapterState } = getVariableTestContext(constantAdapter);
const newType = 'constant' as VariableType;
const identifier: VariableIdentifier = { id: '0', type: 'query' };
const identifier: KeyedVariableIdentifier = { id: '0', type: 'query', rootStateKey: 'key' };
const payload = toVariablePayload(identifier, { newType });
reducerTester<VariablesState>()
.givenReducer(sharedReducer, cloneDeep(queryAdapterState))
@@ -564,6 +579,7 @@ describe('sharedReducer', () => {
...constantAdapterState,
'0': {
...constantAdapterState[0],
rootStateKey: 'key',
name: 'test',
description: 'new description',
label: 'new label',
@@ -2,11 +2,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { cloneDeep, defaults as lodashDefaults } from 'lodash';
import { LoadingState, VariableType } from '@grafana/data';
import { VariableModel, VariableOption, VariableWithOptions } from '../types';
import { AddVariable, getInstanceState, initialVariablesState, VariablePayload, VariablesState } from './types';
import { AddVariable, initialVariablesState, VariablePayload, VariablesState } from './types';
import { variableAdapters } from '../adapters';
import { changeVariableNameSucceeded } from '../editor/reducer';
import { ensureStringValues } from '../utils';
import { getNextVariableIndex } from './selectors';
import { getInstanceState, getNextVariableIndex } from './selectors';
const sharedReducerSlice = createSlice({
name: 'templating/shared',
@@ -100,11 +100,12 @@ const sharedReducerSlice = createSlice({
},
changeVariableType: (state: VariablesState, action: PayloadAction<VariablePayload<{ newType: VariableType }>>) => {
const { id } = action.payload;
const { label, name, index, description } = state[id];
const { label, name, index, description, rootStateKey } = state[id];
state[id] = {
...cloneDeep(variableAdapters.get(action.payload.data.newType).initialState),
id: id,
id,
rootStateKey: rootStateKey,
label,
name,
index,
@@ -2,12 +2,12 @@ import { variableAdapters } from '../adapters';
import { constantBuilder, customBuilder } from '../shared/testing/builders';
import { DashboardState, StoreState } from '../../../types';
import { initialState } from '../../dashboard/state/reducers';
import { TemplatingState } from './reducers';
import { ExtendedUrlQueryMap } from '../utils';
import { templateVarsChangedInUrl } from './actions';
import { createCustomVariableAdapter } from '../custom/adapter';
import { VariablesState } from './types';
import { DashboardModel } from '../../dashboard/state';
import { getPreloadedState } from './helpers';
import { createConstantVariableAdapter } from '../constant/adapter';
import { VariableModel } from '../types';
@@ -18,9 +18,11 @@ variableAdapters.setInit(() => [createCustomVariableAdapter(), createConstantVar
async function getTestContext(urlQueryMap: ExtendedUrlQueryMap = {}, variable: VariableModel | undefined = undefined) {
jest.clearAllMocks();
const key = 'key';
if (!variable) {
variable = customBuilder()
.withId('variable')
.withRootStateKey(key)
.withName('variable')
.withCurrent(['A', 'C'])
.withOptions('A', 'B', 'C')
@@ -43,15 +45,14 @@ async function getTestContext(urlQueryMap: ExtendedUrlQueryMap = {}, variable: V
};
const variables: VariablesState = { variable };
const templating = { variables } as unknown as TemplatingState;
const state: Partial<StoreState> = {
dashboard,
templating,
...getPreloadedState(key, { variables }),
};
const getState = () => state as unknown as StoreState;
const dispatch = jest.fn();
const thunk = templateVarsChangedInUrl(urlQueryMap);
const thunk = templateVarsChangedInUrl(key, urlQueryMap);
await thunk(dispatch, getState, undefined);
@@ -125,6 +126,7 @@ describe('templateVarsChangedInUrl', () => {
it('then the value should change to the value in dashboard json and dashboard should be refreshed', async () => {
const constant = constantBuilder()
.withId('variable')
.withRootStateKey('key')
.withName('variable')
.withQuery('default value in dash.json')
.build();
@@ -52,14 +52,14 @@ const transactionSlice = createSlice({
});
function actionAffectsDirtyState(action: AnyAction): boolean {
return [
removeVariable.type,
addVariable.type,
changeVariableProp.type,
changeVariableOrder.type,
duplicateVariable.type,
changeVariableType.type,
].includes(action.type);
return (
removeVariable.match(action) ||
addVariable.match(action) ||
changeVariableProp.match(action) ||
changeVariableOrder.match(action) ||
duplicateVariable.match(action) ||
changeVariableType.match(action)
);
}
export const { variablesInitTransaction, variablesClearTransaction, variablesCompleteTransaction } =
+6 -22
View File
@@ -6,15 +6,17 @@ export interface VariablesState extends Record<string, VariableModel> {}
export const initialVariablesState: VariablesState = {};
export const getInstanceState = <Model extends VariableModel = VariableModel>(state: VariablesState, id: string) => {
return state[id] as Model;
};
export interface VariableIdentifier {
type: VariableType;
id: string;
}
export interface KeyedVariableIdentifier {
type: VariableType;
id: string;
rootStateKey: string;
}
export interface VariablePayload<T extends any = undefined> extends VariableIdentifier {
data: T;
}
@@ -24,21 +26,3 @@ export interface AddVariable<T extends VariableModel = VariableModel> {
index: number; // the order in variables list
model: T;
}
export const toVariableIdentifier = (variable: VariableModel): VariableIdentifier => {
return { type: variable.type, id: variable.id };
};
export function toVariablePayload<T extends any = undefined>(
identifier: VariableIdentifier,
data?: T
): VariablePayload<T>;
// eslint-disable-next-line
export function toVariablePayload<T extends any = undefined>(model: VariableModel, data?: T): VariablePayload<T>;
// eslint-disable-next-line
export function toVariablePayload<T extends any = undefined>(
obj: VariableIdentifier | VariableModel,
data?: T
): VariablePayload<T> {
return { type: obj.type, id: obj.id, data: data as T };
}
@@ -1,10 +1,12 @@
import { customBuilder, queryBuilder } from '../shared/testing/builders';
import { VariableSupportType } from '@grafana/data';
import { toVariableIdentifier } from './types';
import { upgradeLegacyQueries } from './actions';
import { changeVariableProp } from './sharedReducer';
import { thunkTester } from '../../../../test/core/thunk/thunkTester';
import { TransactionStatus, VariableModel } from '../types';
import { toKeyedAction } from './keyedVariablesReducer';
import { getPreloadedState } from './helpers';
import { toKeyedVariableIdentifier } from '../utils';
interface Args {
query?: any;
@@ -18,22 +20,23 @@ function getTestContext({
datasource,
transactionStatus = TransactionStatus.Fetching,
}: Args = {}) {
const key = 'key';
variable =
variable ??
queryBuilder()
.withId('query')
.withRootStateKey(key)
.withName('query')
.withQuery(query)
.withDatasource({ uid: 'test-data', type: 'test-data' })
.build();
const state = {
templating: {
transaction: { status: transactionStatus },
variables: {
[variable.id]: variable,
},
const templatingState = {
transaction: { status: transactionStatus, uid: key, isDirty: false },
variables: {
[variable.id]: variable,
},
};
const state = getPreloadedState(key, templatingState);
datasource = datasource ?? {
name: 'TestData',
metricFindQuery: () => undefined,
@@ -41,32 +44,35 @@ function getTestContext({
};
const get = jest.fn().mockResolvedValue(datasource);
const getDatasourceSrv = jest.fn().mockReturnValue({ get });
const identifier = toVariableIdentifier(variable);
const identifier = toKeyedVariableIdentifier(variable);
return { state, get, getDatasourceSrv, identifier };
return { key, state, get, getDatasourceSrv, identifier };
}
describe('upgradeLegacyQueries', () => {
describe('when called with a query variable for a standard variable supported data source that has not been upgraded', () => {
it('then it should dispatch changeVariableProp', async () => {
const { state, identifier, get, getDatasourceSrv } = getTestContext({ query: '*' });
const { key, state, identifier, get, getDatasourceSrv } = getTestContext({ query: '*' });
const dispatchedActions = await thunkTester(state)
.givenThunk(upgradeLegacyQueries)
.whenThunkIsDispatched(identifier, getDatasourceSrv);
expect(dispatchedActions).toEqual([
changeVariableProp({
type: 'query',
id: 'query',
data: {
propName: 'query',
propValue: {
refId: 'TestData-query-Variable-Query',
query: '*',
toKeyedAction(
key,
changeVariableProp({
type: 'query',
id: 'query',
data: {
propName: 'query',
propValue: {
refId: 'TestData-query-Variable-Query',
query: '*',
},
},
},
}),
})
),
]);
expect(get).toHaveBeenCalledTimes(1);
expect(get).toHaveBeenCalledWith({ uid: 'test-data', type: 'test-data' });
@@ -161,7 +167,7 @@ describe('upgradeLegacyQueries', () => {
describe('when called with a custom variable', () => {
it('then it should not dispatch any actions', async () => {
const variable = customBuilder().withId('custom').withName('custom').build();
const variable = customBuilder().withId('custom').withRootStateKey('key').withName('custom').build();
const { state, identifier, get, getDatasourceSrv } = getTestContext({ variable });
const dispatchedActions = await thunkTester(state)
@@ -1,12 +1,13 @@
import React, { ChangeEvent, FocusEvent, KeyboardEvent, ReactElement, useCallback, useEffect, useState } from 'react';
import { TextBoxVariableModel } from '../types';
import { toVariablePayload } from '../state/types';
import { changeVariableProp } from '../state/sharedReducer';
import { VariablePickerProps } from '../pickers/types';
import { Input } from '@grafana/ui';
import { variableAdapters } from '../adapters';
import { useDispatch } from 'react-redux';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { toVariablePayload } from '../utils';
export interface Props extends VariablePickerProps<TextBoxVariableModel> {}
@@ -18,13 +19,21 @@ export function TextBoxVariablePicker({ variable, onVariableChange }: Props): Re
}, [variable]);
const updateVariable = useCallback(() => {
if (!variable.rootStateKey) {
console.error('Cannot update variable without rootStateKey');
return;
}
if (variable.current.value === updatedValue) {
return;
}
dispatch(
changeVariableProp(
toVariablePayload({ id: variable.id, type: variable.type }, { propName: 'query', propValue: updatedValue })
toKeyedAction(
variable.rootStateKey,
changeVariableProp(
toVariablePayload({ id: variable.id, type: variable.type }, { propName: 'query', propValue: updatedValue })
)
)
);
@@ -4,11 +4,12 @@ import { reduxTester } from '../../../../test/core/redux/reduxTester';
import { setTextBoxVariableOptionsFromUrl, updateTextBoxVariableOptions } from './actions';
import { getRootReducer, RootReducerType } from '../state/helpers';
import { VariableOption } from '../types';
import { toVariablePayload } from '../state/types';
import { createTextBoxOptions } from './reducer';
import { addVariable, changeVariableProp, setCurrentVariableValue } from '../state/sharedReducer';
import { textboxBuilder } from '../shared/testing/builders';
import { locationService } from '@grafana/runtime';
import { toKeyedAction } from '../state/keyedVariablesReducer';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
jest.mock('@grafana/runtime', () => {
const original = jest.requireActual('@grafana/runtime');
@@ -32,16 +33,25 @@ describe('textbox actions', () => {
selected: false,
};
const variable = textboxBuilder().withId('textbox').withName('textbox').withCurrent('A').withQuery('A').build();
const key = 'key';
const variable = textboxBuilder()
.withId('textbox')
.withRootStateKey(key)
.withName('textbox')
.withCurrent('A')
.withQuery('A')
.build();
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenAsyncActionIsDispatched(updateTextBoxVariableOptions(toVariablePayload(variable)), true);
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenAsyncActionIsDispatched(updateTextBoxVariableOptions(toKeyedVariableIdentifier(variable)), true);
tester.thenDispatchedActionsShouldEqual(
createTextBoxOptions(toVariablePayload(variable)),
setCurrentVariableValue(toVariablePayload(variable, { option }))
toKeyedAction(key, createTextBoxOptions(toVariablePayload(variable))),
toKeyedAction(key, setCurrentVariableValue(toVariablePayload(variable, { option })))
);
expect(locationService.partial).toHaveBeenLastCalledWith({ 'var-textbox': 'A' });
});
@@ -50,16 +60,31 @@ describe('textbox actions', () => {
describe('when setTextBoxVariableOptionsFromUrl is dispatched', () => {
it('then correct actions are dispatched', async () => {
const urlValue = 'bB';
const variable = textboxBuilder().withId('textbox').withName('textbox').withCurrent('A').withQuery('A').build();
const key = 'key';
const variable = textboxBuilder()
.withId('textbox')
.withRootStateKey(key)
.withName('textbox')
.withCurrent('A')
.withQuery('A')
.build();
const tester = await reduxTester<RootReducerType>()
.givenRootReducer(getRootReducer())
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
.whenAsyncActionIsDispatched(setTextBoxVariableOptionsFromUrl(toVariablePayload(variable), urlValue), true);
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
)
.whenAsyncActionIsDispatched(
setTextBoxVariableOptionsFromUrl(toKeyedVariableIdentifier(variable), urlValue),
true
);
tester.thenDispatchedActionsShouldEqual(
changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: 'bB' })),
setCurrentVariableValue(toVariablePayload(variable, { option: { text: 'bB', value: 'bB', selected: false } }))
toKeyedAction(key, changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: 'bB' }))),
toKeyedAction(
key,
setCurrentVariableValue(toVariablePayload(variable, { option: { text: 'bB', value: 'bB', selected: false } }))
)
);
});
});
@@ -3,28 +3,36 @@ import { ThunkResult } from '../../../types';
import { getVariable } from '../state/selectors';
import { variableAdapters } from '../adapters';
import { createTextBoxOptions } from './reducer';
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
import { KeyedVariableIdentifier } from '../state/types';
import { setOptionFromUrl } from '../state/actions';
import { UrlQueryValue } from '@grafana/data';
import { changeVariableProp } from '../state/sharedReducer';
import { ensureStringValues } from '../utils';
import { ensureStringValues, toKeyedVariableIdentifier, toVariablePayload } from '../utils';
import { toKeyedAction } from '../state/keyedVariablesReducer';
export const updateTextBoxVariableOptions = (identifier: VariableIdentifier): ThunkResult<void> => {
export const updateTextBoxVariableOptions = (identifier: KeyedVariableIdentifier): ThunkResult<void> => {
return async (dispatch, getState) => {
await dispatch(createTextBoxOptions(toVariablePayload(identifier)));
const { rootStateKey, type } = identifier;
dispatch(toKeyedAction(rootStateKey, createTextBoxOptions(toVariablePayload(identifier))));
const variableInState = getVariable<TextBoxVariableModel>(identifier.id, getState());
await variableAdapters.get(identifier.type).setValue(variableInState, variableInState.options[0], true);
const variableInState = getVariable<TextBoxVariableModel>(identifier, getState());
await variableAdapters.get(type).setValue(variableInState, variableInState.options[0], true);
};
};
export const setTextBoxVariableOptionsFromUrl =
(identifier: VariableIdentifier, urlValue: UrlQueryValue): ThunkResult<void> =>
(identifier: KeyedVariableIdentifier, urlValue: UrlQueryValue): ThunkResult<void> =>
async (dispatch, getState) => {
const variableInState = getVariable<TextBoxVariableModel>(identifier.id, getState());
const { rootStateKey } = identifier;
const variableInState = getVariable<TextBoxVariableModel>(identifier, getState());
const stringUrlValue = ensureStringValues(urlValue);
dispatch(changeVariableProp(toVariablePayload(variableInState, { propName: 'query', propValue: stringUrlValue })));
dispatch(
toKeyedAction(
rootStateKey,
changeVariableProp(toVariablePayload(variableInState, { propName: 'query', propValue: stringUrlValue }))
)
);
await dispatch(setOptionFromUrl(toVariableIdentifier(variableInState), stringUrlValue));
await dispatch(setOptionFromUrl(toKeyedVariableIdentifier(variableInState), stringUrlValue));
};
@@ -11,6 +11,7 @@ describe('createTextBoxVariableAdapter', () => {
it('then the model should be correct', () => {
const text = textboxBuilder()
.withId('text')
.withRootStateKey('key')
.withName('text')
.withQuery('query')
.withOriginalQuery('original')
@@ -41,6 +42,7 @@ describe('createTextBoxVariableAdapter', () => {
it('then the model should be correct', () => {
const text = textboxBuilder()
.withId('text')
.withRootStateKey('key')
.withName('text')
.withQuery('query')
.withOriginalQuery('original')
@@ -8,7 +8,7 @@ import { VariableAdapter } from '../adapters';
import { TextBoxVariablePicker } from './TextBoxVariablePicker';
import { TextBoxVariableEditor } from './TextBoxVariableEditor';
import { setTextBoxVariableOptionsFromUrl, updateTextBoxVariableOptions } from './actions';
import { toVariableIdentifier } from '../state/types';
import { toKeyedVariableIdentifier } from '../utils';
export const createTextBoxVariableAdapter = (): VariableAdapter<TextBoxVariableModel> => {
return {
@@ -23,16 +23,16 @@ export const createTextBoxVariableAdapter = (): VariableAdapter<TextBoxVariableM
return false;
},
setValue: async (variable, option, emitChanges = false) => {
await dispatch(setOptionAsCurrent(toVariableIdentifier(variable), option, emitChanges));
await dispatch(setOptionAsCurrent(toKeyedVariableIdentifier(variable), option, emitChanges));
},
setValueFromUrl: async (variable, urlValue) => {
await dispatch(setTextBoxVariableOptionsFromUrl(toVariableIdentifier(variable), urlValue));
await dispatch(setTextBoxVariableOptionsFromUrl(toKeyedVariableIdentifier(variable), urlValue));
},
updateOptions: async (variable) => {
await dispatch(updateTextBoxVariableOptions(toVariableIdentifier(variable)));
await dispatch(updateTextBoxVariableOptions(toKeyedVariableIdentifier(variable)));
},
getSaveModel: (variable, saveCurrentAsDefault) => {
const { index, id, state, global, originalQuery, ...rest } = cloneDeep(variable);
const { index, id, state, global, originalQuery, rootStateKey, ...rest } = cloneDeep(variable);
if (variable.query !== originalQuery && !saveCurrentAsDefault) {
const origQuery = originalQuery ?? '';
@@ -1,10 +1,11 @@
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { cloneDeep } from 'lodash';
import { getVariableTestContext } from '../state/helpers';
import { toVariablePayload, VariablesState } from '../state/types';
import { VariablesState } from '../state/types';
import { createTextBoxOptions, textBoxVariableReducer } from './reducer';
import { TextBoxVariableModel } from '../types';
import { createTextBoxVariableAdapter } from './adapter';
import { toVariablePayload } from '../utils';
describe('textBoxVariableReducer', () => {
const adapter = createTextBoxVariableAdapter();
@@ -1,7 +1,8 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { initialVariableModelState, TextBoxVariableModel, VariableOption } from '../types';
import { getInstanceState, VariablePayload, initialVariablesState, VariablesState } from '../state/types';
import { initialVariablesState, VariablePayload, VariablesState } from '../state/types';
import { getInstanceState } from '../state/selectors';
export const initialTextBoxVariableModelState: TextBoxVariableModel = {
...initialVariableModelState,
+2
View File
@@ -131,6 +131,7 @@ export interface SystemVariable<TProps extends { toString: () => string }> exten
export interface VariableModel extends BaseVariableModel {
id: string;
rootStateKey: string | null;
global: boolean;
hide: VariableHide;
skipUrlSync: boolean;
@@ -142,6 +143,7 @@ export interface VariableModel extends BaseVariableModel {
export const initialVariableModelState: VariableModel = {
id: NEW_VARIABLE_ID,
rootStateKey: null,
name: '',
label: null,
type: '' as unknown as VariableType,

Some files were not shown because too many files have changed in this diff Show More