diff --git a/.betterer.results b/.betterer.results
index 3a1f939d9d5..3c9d28c1017 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -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": [
diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts
index 88ff7d0fdfb..de3b901986f 100644
--- a/public/app/core/reducers/root.ts
+++ b/public/app/core/reducers/root.ts
@@ -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,
};
diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx
index 125beaab782..486cacbe7c6 100644
--- a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx
+++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx
@@ -70,7 +70,7 @@ export function DashboardSettings({ dashboard, editview }: Props) {
title: 'Variables',
id: 'templating',
icon: 'calculator-alt',
- component: ,
+ component: ,
});
pages.push({
diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx
index 568c7ba1ede..1f50138843c 100644
--- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx
+++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx
@@ -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),
};
};
diff --git a/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx b/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx
index 9dda02baca4..0ad229d26df 100644
--- a/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx
+++ b/public/app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect.tsx
@@ -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 = ({ 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) => {
diff --git a/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx b/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx
index 3ef55c9c1b1..b93827ad01a 100644
--- a/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx
+++ b/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx
@@ -96,7 +96,7 @@ function shareLinkScenario(description: string, scenarioFn: (ctx: ScenarioContex
}
describe('ShareModal', () => {
- let templateSrv = initTemplateSrv([]);
+ let templateSrv = initTemplateSrv('key', []);
beforeAll(() => {
variableAdapters.register(createQueryVariableAdapter());
diff --git a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx
index 01aeca07e61..14839796da0 100644
--- a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx
+++ b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx
@@ -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 {
}
}
-const mapStateToProps: MapStateToProps = (state) => {
+const mapStateToProps: MapStateToProps = (state, ownProps) => {
+ const { uid } = ownProps.dashboard;
+ const templatingState = getVariablesState(uid, state);
return {
- variables: getSubMenuVariables(state.templating.variables),
+ variables: getSubMenuVariables(uid, templatingState.variables),
};
};
diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx
index 309adc30170..09ab1695faf 100644
--- a/public/app/features/dashboard/containers/DashboardPage.tsx
+++ b/public/app/features/dashboard/containers/DashboardPage.tsx
@@ -168,7 +168,7 @@ export class UnthemedDashboardPage extends PureComponent {
const templateVarChanges = findTemplateVarChanges(this.props.queryParams, prevProps.queryParams);
if (templateVarChanges) {
- templateVarsChangedInUrl(templateVarChanges);
+ templateVarsChangedInUrl(dashboard.uid, templateVarChanges);
}
}
diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts
index bbda7085b5d..e3afab19aea 100644
--- a/public/app/features/dashboard/state/DashboardModel.ts
+++ b/public/app/features/dashboard/state/DashboardModel.ts
@@ -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 {
diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts
index 1a503a276ad..9ae183fc475 100644
--- a/public/app/features/dashboard/state/actions.ts
+++ b/public/app/features/dashboard/state/actions.ts
@@ -119,12 +119,12 @@ export const cleanUpDashboardAndVariables = (): ThunkResult => (dispatch,
if (dashboard) {
dashboard.destroy();
+ dispatch(cancelVariables(dashboard.uid));
}
getTimeSrv().stopAutoRefresh();
dispatch(cleanUpDashboard());
- dispatch(cancelVariables());
};
export const updateTimeZoneDashboard =
diff --git a/public/app/features/dashboard/state/initDashboard.test.ts b/public/app/features/dashboard/state/initDashboard.test.ts
index f7083989a6b..0bcb74ef8d2 100644
--- a/public/app/features/dashboard/state/initDashboard.test.ts
+++ b/public/app/features/dashboard/state/initDashboard.test.ts
@@ -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);
+ });
+});
diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts
index 876e2170e3a..91dc7c58280 100644
--- a/public/app/features/dashboard/state/initDashboard.ts
+++ b/public/app/features/dashboard/state/initDashboard.ts
@@ -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 {
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.
diff --git a/public/app/features/panel/panellinks/specs/link_srv.test.ts b/public/app/features/panel/panellinks/specs/link_srv.test.ts
index b32b5b62b15..b2b30096045 100644
--- a/public/app/features/panel/panellinks/specs/link_srv.test.ts
+++ b/public/app/features/panel/panellinks/specs/link_srv.test.ts
@@ -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' } },
]);
diff --git a/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.test.ts b/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.test.ts
index daf7bc5dba1..8f6917004c9 100644
--- a/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.test.ts
+++ b/public/app/features/query/state/DashboardQueryRunner/AnnotationsWorker.test.ts
@@ -67,7 +67,7 @@ function expectOnResults(args: {
done();
} catch (err) {
subscription.unsubscribe();
- done.fail(err);
+ done(err);
}
},
});
diff --git a/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts b/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts
index e68011186a4..ba65d463d4f 100644
--- a/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts
+++ b/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts
@@ -69,7 +69,7 @@ function expectOnResults(args: {
expectCallback(value);
done();
} catch (err) {
- done.fail(err);
+ done(err);
}
},
});
diff --git a/public/app/features/templating/template_srv.test.ts b/public/app/features/templating/template_srv.test.ts
index ebe981efc8f..c99d43b1a9f 100644
--- a/public/app/features/templating/template_srv.test.ts
+++ b/public/app/features/templating/template_srv.test.ts
@@ -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,
createAdHocVariableAdapter() as unknown as VariableAdapter,
@@ -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: '' } },
]);
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',
diff --git a/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx b/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx
index 82944657f9e..2bb3a2d7289 100644
--- a/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx
+++ b/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx
@@ -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();
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', () => {
diff --git a/public/app/features/variables/adhoc/AdHocVariableEditor.tsx b/public/app/features/variables/adhoc/AdHocVariableEditor.tsx
index f00a074c21d..3e4ca258479 100644
--- a/public/app/features/variables/adhoc/AdHocVariableEditor.tsx
+++ b/public/app/features/variables/adhoc/AdHocVariableEditor.tsx
@@ -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;
export class AdHocVariableEditorUnConnected extends PureComponent {
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) => {
- this.props.changeVariableDatasource(option.value);
+ this.props.changeVariableDatasource(toKeyedVariableIdentifier(this.props.variable), option.value);
};
render() {
diff --git a/public/app/features/variables/adhoc/actions.test.ts b/public/app/features/variables/adhoc/actions.test.ts
index 1a7d69c63c7..becf0bb4a76 100644
--- a/public/app/features/variables/adhoc/actions.test.ts
+++ b/public/app/features/variables/adhoc/actions.test.ts
@@ -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()
+ const tester = await reduxTester({ 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()
+ const tester = await reduxTester({ 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()
+ const tester = await reduxTester({ 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()
+ const tester = await reduxTester({ 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()
.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()
.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()
.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()
.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()
.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()
.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()
.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()
.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()
.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 {
diff --git a/public/app/features/variables/adhoc/actions.ts b/public/app/features/variables/adhoc/actions.ts
index e004c66f86e..b49e398e524 100644
--- a/public/app/features/variables/adhoc/actions.ts
+++ b/public/app/features/variables/adhoc/actions.ts
@@ -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 f.key === options.key && f.value === options.value);
@@ -42,52 +47,66 @@ export const applyFilterFromTable = (options: AdHocTableOptions): ThunkResult => {
+export const changeFilter = (
+ identifier: KeyedVariableIdentifier,
+ update: AdHocVariabelFilterUpdate
+): ThunkResult => {
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 => {
+export const removeFilter = (identifier: KeyedVariableIdentifier, index: number): ThunkResult => {
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 => {
+export const addFilter = (identifier: KeyedVariableIdentifier, filter: AdHocVariableFilter): ThunkResult => {
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 => {
+export const setFiltersFromUrl = (
+ identifier: KeyedVariableIdentifier,
+ filters: AdHocVariableFilter[]
+): ThunkResult => {
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 => {
+export const changeVariableDatasource = (
+ identifier: KeyedVariableIdentifier,
+ datasource?: DataSourceRef
+): ThunkResult => {
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 => (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 =>
+ (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 => {
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(identifier, { global, model, index })));
+ dispatch(toKeyedAction(key, addVariable(toVariablePayload(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;
};
diff --git a/public/app/features/variables/adhoc/adapter.ts b/public/app/features/variables/adhoc/adapter.ts
index 10adfce70b7..6f2ed40e074 100644
--- a/public/app/features/variables/adhoc/adapter.ts
+++ b/public/app/features/variables/adhoc/adapter.ts
@@ -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 {
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) => {
diff --git a/public/app/features/variables/adhoc/picker/AdHocPicker.tsx b/public/app/features/variables/adhoc/picker/AdHocPicker.tsx
index 8abb7a6fbf3..1fa6e55e1ce 100644
--- a/public/app/features/variables/adhoc/picker/AdHocPicker.tsx
+++ b/public/app/features/variables/adhoc/picker/AdHocPicker.tsx
@@ -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;
*/
export class AdHocPickerUnconnected extends PureComponent {
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,
});
diff --git a/public/app/features/variables/adhoc/reducer.test.ts b/public/app/features/variables/adhoc/reducer.test.ts
index b3645e38b1a..ce4b7948cd4 100644
--- a/public/app/features/variables/adhoc/reducer.test.ts
+++ b/public/app/features/variables/adhoc/reducer.test.ts
@@ -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();
diff --git a/public/app/features/variables/adhoc/reducer.ts b/public/app/features/variables/adhoc/reducer.ts
index 3f5aa9f9222..f5ff0945961 100644
--- a/public/app/features/variables/adhoc/reducer.ts
+++ b/public/app/features/variables/adhoc/reducer.ts
@@ -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;
diff --git a/public/app/features/variables/constant/actions.test.ts b/public/app/features/variables/constant/actions.test.ts
index b483c8ad547..a609cdc69ce 100644
--- a/public/app/features/variables/constant/actions.test.ts
+++ b/public/app/features/variables/constant/actions.test.ts
@@ -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()
.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 })))
);
});
});
diff --git a/public/app/features/variables/constant/actions.ts b/public/app/features/variables/constant/actions.ts
index db27b06739a..6f865bcaa19 100644
--- a/public/app/features/variables/constant/actions.ts
+++ b/public/app/features/variables/constant/actions.ts
@@ -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 => {
+export const updateConstantVariableOptions = (identifier: KeyedVariableIdentifier): ThunkResult => {
return async (dispatch) => {
- await dispatch(createConstantOptionsFromQuery(toVariablePayload(identifier)));
+ const { rootStateKey } = identifier;
+ await dispatch(toKeyedAction(rootStateKey, createConstantOptionsFromQuery(toVariablePayload(identifier))));
await dispatch(validateVariableSelectionState(identifier));
};
};
diff --git a/public/app/features/variables/constant/adapter.ts b/public/app/features/variables/constant/adapter.ts
index 12aa0e85073..4555a63c788 100644
--- a/public/app/features/variables/constant/adapter.ts
+++ b/public/app/features/variables/constant/adapter.ts
@@ -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 => {
return {
@@ -22,16 +22,16 @@ export const createConstantVariableAdapter = (): VariableAdapter {
- 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) => {
diff --git a/public/app/features/variables/constant/reducer.test.ts b/public/app/features/variables/constant/reducer.test.ts
index b6c937c52ea..932593582b2 100644
--- a/public/app/features/variables/constant/reducer.test.ts
+++ b/public/app/features/variables/constant/reducer.test.ts
@@ -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();
diff --git a/public/app/features/variables/constant/reducer.ts b/public/app/features/variables/constant/reducer.ts
index bf2f2d38be7..8a830d779d1 100644
--- a/public/app/features/variables/constant/reducer.ts
+++ b/public/app/features/variables/constant/reducer.ts
@@ -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,
diff --git a/public/app/features/variables/custom/actions.test.ts b/public/app/features/variables/custom/actions.test.ts
index c6508944b0d..9df0916264d 100644
--- a/public/app/features/variables/custom/actions.test.ts
+++ b/public/app/features/variables/custom/actions.test.ts
@@ -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()
.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 })))
);
});
});
diff --git a/public/app/features/variables/custom/actions.ts b/public/app/features/variables/custom/actions.ts
index bc3ed3905a8..557d2044dca 100644
--- a/public/app/features/variables/custom/actions.ts
+++ b/public/app/features/variables/custom/actions.ts
@@ -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 => {
+export const updateCustomVariableOptions = (identifier: KeyedVariableIdentifier): ThunkResult => {
return async (dispatch) => {
- await dispatch(createCustomOptionsFromQuery(toVariablePayload(identifier)));
+ const { rootStateKey } = identifier;
+ await dispatch(toKeyedAction(rootStateKey, createCustomOptionsFromQuery(toVariablePayload(identifier))));
await dispatch(validateVariableSelectionState(identifier));
};
};
diff --git a/public/app/features/variables/custom/adapter.ts b/public/app/features/variables/custom/adapter.ts
index b8aed7172cc..47af02927bc 100644
--- a/public/app/features/variables/custom/adapter.ts
+++ b/public/app/features/variables/custom/adapter.ts
@@ -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 {
- 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) => {
diff --git a/public/app/features/variables/custom/reducer.test.ts b/public/app/features/variables/custom/reducer.test.ts
index 0cf66830900..39f0ef09815 100644
--- a/public/app/features/variables/custom/reducer.test.ts
+++ b/public/app/features/variables/custom/reducer.test.ts
@@ -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();
diff --git a/public/app/features/variables/custom/reducer.ts b/public/app/features/variables/custom/reducer.ts
index a579f412f96..9a897030c30 100644
--- a/public/app/features/variables/custom/reducer.ts
+++ b/public/app/features/variables/custom/reducer.ts
@@ -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,
diff --git a/public/app/features/variables/datasource/DataSourceVariableEditor.tsx b/public/app/features/variables/datasource/DataSourceVariableEditor.tsx
index 0be82c77056..51f9a827f71 100644
--- a/public/app/features/variables/datasource/DataSourceVariableEditor.tsx
+++ b/public/app/features/variables/datasource/DataSourceVariableEditor.tsx
@@ -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;
export class DataSourceVariableEditorUnConnected extends PureComponent {
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) => {
diff --git a/public/app/features/variables/datasource/actions.test.ts b/public/app/features/variables/datasource/actions.test.ts
index d8226da37ef..67f62788eae 100644
--- a/public/app/features/variables/datasource/actions.test.ts
+++ b/public/app/features/variables/datasource/actions.test.ts
@@ -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()
.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()
.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()
.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);
diff --git a/public/app/features/variables/datasource/actions.ts b/public/app/features/variables/datasource/actions.ts
index f7a9b031828..dc7b34cdd75 100644
--- a/public/app/features/variables/datasource/actions.ts
+++ b/public/app/features/variables/datasource/actions.ts
@@ -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 =>
async (dispatch, getState) => {
+ const { rootStateKey } = identifier;
const sources = dependencies.getDatasourceSrv().getList({ metrics: true, variables: false });
- const variableInState = getVariable(identifier.id, getState());
+ const variableInState = getVariable(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 =>
+ (
+ key: string,
+ dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrv }
+ ): ThunkResult =>
(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 })));
};
diff --git a/public/app/features/variables/datasource/adapter.ts b/public/app/features/variables/datasource/adapter.ts
index a3c8171558a..9b4da36805a 100644
--- a/public/app/features/variables/datasource/adapter.ts
+++ b/public/app/features/variables/datasource/adapter.ts
@@ -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 {
- 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) => {
diff --git a/public/app/features/variables/datasource/reducer.test.ts b/public/app/features/variables/datasource/reducer.test.ts
index 7fc17be7d41..dce1a1ed93f 100644
--- a/public/app/features/variables/datasource/reducer.test.ts
+++ b/public/app/features/variables/datasource/reducer.test.ts
@@ -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();
diff --git a/public/app/features/variables/datasource/reducer.ts b/public/app/features/variables/datasource/reducer.ts
index 1a6cb0619d3..3e2370dd495 100644
--- a/public/app/features/variables/datasource/reducer.ts
+++ b/public/app/features/variables/datasource/reducer.ts
@@ -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,
diff --git a/public/app/features/variables/editor/SelectionOptionsEditor.tsx b/public/app/features/variables/editor/SelectionOptionsEditor.tsx
index 00ff36f0966..d1e2688cc40 100644
--- a/public/app/features/variables/editor/SelectionOptionsEditor.tsx
+++ b/public/app/features/variables/editor/SelectionOptionsEditor.tsx
@@ -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
extends VariableEditorProps {
- onMultiChanged: (identifier: VariableIdentifier, value: boolean) => void;
+ onMultiChanged: (identifier: KeyedVariableIdentifier, value: boolean) => void;
}
export const SelectionOptionsEditor: FunctionComponent = ({
@@ -21,7 +22,7 @@ export const SelectionOptionsEditor: FunctionComponent {
const onMultiChanged = useCallback(
(event: ChangeEvent) => {
- onMultiChangedProps(toVariableIdentifier(variable), event.target.checked);
+ onMultiChangedProps(toKeyedVariableIdentifier(variable), event.target.checked);
},
[onMultiChangedProps, variable]
);
diff --git a/public/app/features/variables/editor/VariableEditorContainer.tsx b/public/app/features/variables/editor/VariableEditorContainer.tsx
index 6a82dd09b4a..745e368a37c 100644
--- a/public/app/features/variables/editor/VariableEditorContainer.tsx
+++ b/public/app/features/variables/editor/VariableEditorContainer.tsx
@@ -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;
class VariableEditorContainerUnconnected extends PureComponent {
componentDidMount(): void {
- this.props.switchToListMode();
+ this.props.switchToListMode(this.props.dashboard.uid);
}
onChangeToListMode = (event: MouseEvent) => {
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 {
{!variableToEdit && this.props.variables.length > 0 && (
)}
- {variableToEdit && }
+ {variableToEdit && }
);
}
diff --git a/public/app/features/variables/editor/VariableEditorEditor.tsx b/public/app/features/variables/editor/VariableEditorEditor.tsx
index a1e6455f1e3..58441e2eff1 100644
--- a/public/app/features/variables/editor/VariableEditorEditor.tsx
+++ b/public/app/features/variables/editor/VariableEditorEditor.tsx
@@ -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;
export class VariableEditorEditorUnConnected extends PureComponent {
componentDidMount(): void {
@@ -71,35 +82,26 @@ export class VariableEditorEditorUnConnected extends PureComponent {
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) => {
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) => {
- 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) => {
- 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 {
}
}
-const mapStateToProps: MapStateToProps = (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 = {
- variableEditorMount,
- variableEditorUnMount,
- changeVariableName,
- changeVariableProp,
- onEditorUpdate,
- changeVariableType,
- updateOptions,
-};
-
-export const VariableEditorEditor = connectWithStore(
- VariableEditorEditorUnConnected,
- mapStateToProps,
- mapDispatchToProps
-);
+export const VariableEditorEditor = connector(VariableEditorEditorUnConnected);
diff --git a/public/app/features/variables/editor/VariableEditorList.tsx b/public/app/features/variables/editor/VariableEditorList.tsx
index b6f27b4fe9c..c3ac0e585cd 100644
--- a/public/app/features/variables/editor/VariableEditorList.tsx
+++ b/public/app/features/variables/editor/VariableEditorList.tsx
@@ -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({
diff --git a/public/app/features/variables/editor/VariableEditorListRow.tsx b/public/app/features/variables/editor/VariableEditorListRow.tsx
index 6d117fdcd1a..68b589cbdd9 100644
--- a/public/app/features/variables/editor/VariableEditorListRow.tsx
+++ b/public/app/features/variables/editor/VariableEditorListRow.tsx
@@ -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 (
diff --git a/public/app/features/variables/editor/actions.ts b/public/app/features/variables/editor/actions.ts
index 1aa5af6f93e..55d9b12c2a4 100644
--- a/public/app/features/variables/editor/actions.ts
+++ b/public/app/features/variables/editor/actions.ts
@@ -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 => {
+export const variableEditorMount = (identifier: KeyedVariableIdentifier): ThunkResult => {
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 => {
+export const variableEditorUnMount = (identifier: KeyedVariableIdentifier): ThunkResult => {
return async (dispatch, getState) => {
- dispatch(variableEditorUnMounted(toVariablePayload(identifier)));
+ const { rootStateKey } = identifier;
+ dispatch(toKeyedAction(rootStateKey, variableEditorUnMounted(toVariablePayload(identifier))));
};
};
-export const onEditorUpdate = (identifier: VariableIdentifier): ThunkResult => {
+export const onEditorUpdate = (identifier: KeyedVariableIdentifier): ThunkResult => {
return async (dispatch) => {
await dispatch(updateOptions(identifier));
- dispatch(switchToListMode());
+ dispatch(switchToListMode(identifier.rootStateKey));
};
};
-export const changeVariableName = (identifier: VariableIdentifier, newName: string): ThunkResult => {
+export const changeVariableName = (identifier: KeyedVariableIdentifier, newName: string): ThunkResult => {
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 =>
+ (identifier: KeyedVariableIdentifier, newName: string): ThunkResult =>
(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 =>
+ (key: string, type: VariableType = 'query'): ThunkResult =>
(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(identifier, { global, model, index })));
- dispatch(setIdInEditor({ id: identifier.id }));
+ model.rootStateKey = key;
+ dispatch(toKeyedAction(key, addVariable(toVariablePayload(identifier, { global, model, index }))));
+ dispatch(toKeyedAction(key, setIdInEditor({ id: identifier.id })));
};
export const switchToEditMode =
- (identifier: VariableIdentifier): ThunkResult =>
+ (identifier: KeyedVariableIdentifier): ThunkResult =>
(dispatch) => {
- dispatch(setIdInEditor({ id: identifier.id }));
+ const { rootStateKey } = identifier;
+ dispatch(toKeyedAction(rootStateKey, setIdInEditor({ id: identifier.id })));
};
-export const switchToListMode = (): ThunkResult => (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 =>
+ (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;
diff --git a/public/app/features/variables/editor/reducer.test.ts b/public/app/features/variables/editor/reducer.test.ts
index 1fa8002a659..e1ed467bb3f 100644
--- a/public/app/features/variables/editor/reducer.test.ts
+++ b/public/app/features/variables/editor/reducer.test.ts
@@ -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', () => {
diff --git a/public/app/features/variables/getAllVariableValuesForUrl.test.ts b/public/app/features/variables/getAllVariableValuesForUrl.test.ts
index 7eb44ac7be0..dbd04260212 100644
--- a/public/app/features/variables/getAllVariableValuesForUrl.test.ts
+++ b/public/app/features/variables/getAllVariableValuesForUrl.test.ts
@@ -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', () => {
diff --git a/public/app/features/variables/getAllVariableValuesForUrl.ts b/public/app/features/variables/getAllVariableValuesForUrl.ts
index 4cfbfd55d33..e775394f287 100644
--- a/public/app/features/variables/getAllVariableValuesForUrl.ts
+++ b/public/app/features/variables/getAllVariableValuesForUrl.ts
@@ -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) {
diff --git a/public/app/features/variables/interval/actions.test.ts b/public/app/features/variables/interval/actions.test.ts
index 8e320fa6b18..007caacc7a7 100644
--- a/public/app/features/variables/interval/actions.test.ts
+++ b/public/app/features/variables/interval/actions.test.ts
@@ -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()
.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()
.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()
.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()
.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()
.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(
diff --git a/public/app/features/variables/interval/actions.ts b/public/app/features/variables/interval/actions.ts
index c75a724e8dd..bc3f4d8d521 100644
--- a/public/app/features/variables/interval/actions.ts
+++ b/public/app/features/variables/interval/actions.ts
@@ -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 =>
+ (identifier: KeyedVariableIdentifier): ThunkResult =>
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 =>
(dispatch, getState) => {
- const variableInState = getVariable(identifier.id, getState());
+ const variableInState = getVariable(identifier, getState());
if (variableInState.auto) {
const res = dependencies.calculateInterval(
dependencies.getTimeSrv().timeRange(),
diff --git a/public/app/features/variables/interval/adapter.ts b/public/app/features/variables/interval/adapter.ts
index 36402c2e866..69e523836a6 100644
--- a/public/app/features/variables/interval/adapter.ts
+++ b/public/app/features/variables/interval/adapter.ts
@@ -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 => {
return {
@@ -22,18 +22,18 @@ export const createIntervalVariableAdapter = (): VariableAdapter {
- 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) => {
diff --git a/public/app/features/variables/interval/reducer.test.ts b/public/app/features/variables/interval/reducer.test.ts
index 3faf98a3281..e5b04df6ae8 100644
--- a/public/app/features/variables/interval/reducer.test.ts
+++ b/public/app/features/variables/interval/reducer.test.ts
@@ -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();
diff --git a/public/app/features/variables/interval/reducer.ts b/public/app/features/variables/interval/reducer.ts
index 09e65357a8a..faadce99a6a 100644
--- a/public/app/features/variables/interval/reducer.ts
+++ b/public/app/features/variables/interval/reducer.ts
@@ -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,
diff --git a/public/app/features/variables/pickers/OptionsPicker/OptionPicker.test.tsx b/public/app/features/variables/pickers/OptionsPicker/OptionPicker.test.tsx
index 075908f023d..36a1a50e17e 100644
--- a/public/app/features/variables/pickers/OptionsPicker/OptionPicker.test.tsx
+++ b/public/app/features/variables/pickers/OptionsPicker/OptionPicker.test.tsx
@@ -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;
@@ -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(
diff --git a/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx b/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx
index 9c082a44658..c33a9afc05b 100644
--- a/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx
+++ b/public/app/features/variables/pickers/OptionsPicker/OptionsPicker.tsx
@@ -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 = (): ComponentType<
VariablePickerProps
> => {
- 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 = {
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 = {
- 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 = {
- getVariableQueryRunner().cancelRequest(toVariableIdentifier(this.props.variable));
+ getVariableQueryRunner().cancelRequest(toKeyedVariableIdentifier(this.props.variable));
};
renderOptions(picker: OptionsPickerState) {
@@ -97,15 +143,15 @@ export const optionPickerFactory =
Promise.resolve([])),
@@ -59,9 +60,11 @@ describe('options picker actions', () => {
const tester = await reduxTester()
.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()
.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()
.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()
.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()
.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()
.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()
.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({ 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({ 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({ 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()
.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()
.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()
.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()
.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()
.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): QueryVariabl
...initialVariableModelState,
type: 'query',
id: '0',
+ rootStateKey: 'key',
index: 0,
current: createOption([]),
options: [],
diff --git a/public/app/features/variables/pickers/OptionsPicker/actions.ts b/public/app/features/variables/pickers/OptionsPicker/actions.ts
index d6a634689bd..7d25290fb22 100644
--- a/public/app/features/variables/pickers/OptionsPicker/actions.ts
+++ b/public/app/features/variables/pickers/OptionsPicker/actions.ts
@@ -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 => {
+export const navigateOptions = (rootStateKey: string, key: NavigationKey, clearOthers: boolean): ThunkResult => {
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 => {
+export const filterOrSearchOptions = (
+ passedIdentifier: KeyedVariableIdentifier,
+ searchQuery = ''
+): ThunkResult => {
return async (dispatch, getState) => {
- const { id, queryValue } = getState().templating.optionsPicker;
- const { query, options } = getVariable(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(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 => {
+export const commitChangesToVariable = (key: string, callback?: (updated: any) => void): ThunkResult => {
return async (dispatch, getState) => {
- const picker = getState().templating.optionsPicker;
- const existing = getVariable(picker.id, getState());
+ const picker = getVariablesState(key, getState()).optionsPicker;
+ const identifier: KeyedVariableIdentifier = { id: picker.id, rootStateKey: key, type: 'query' };
+ const existing = getVariable(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(picker.id, getState());
- dispatch(hideOptions());
+ dispatch(toKeyedAction(key, setCurrentVariableValue(toVariablePayload(existing, currentPayload))));
+ dispatch(toKeyedAction(key, changeVariableProp(toVariablePayload(existing, searchQueryPayload))));
+ const updated = getVariable(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 =>
+ (identifier: KeyedVariableIdentifier, callback?: (updated: any) => void): ThunkResult =>
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(id, getState());
- dispatch(showOptions(variable));
+ const variable = getVariable(identifier, getState());
+ dispatch(toKeyedAction(uid, showOptions(variable)));
};
-export const toggleOptionByHighlight = (clearOthers: boolean, forceSelect = false): ThunkResult => {
+export const toggleOptionByHighlight = (key: string, clearOthers: boolean, forceSelect = false): ThunkResult => {
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(id, getState());
+ const { id } = getVariablesState(key, getState()).optionsPicker;
+ const identifier: KeyedVariableIdentifier = { id, rootStateKey: key, type: 'query' };
+ const existing = getVariable(identifier, getState());
const adapter = variableAdapters.get(existing.type);
await adapter.updateOptions(existing, searchQuery);
- const updated = getVariable(id, getState());
- dispatch(updateOptionsFromSearch(updated.options));
+ const updated = getVariable(identifier, getState());
+ dispatch(toKeyedAction(key, updateOptionsFromSearch(updated.options)));
} catch (error) {
console.error(error);
}
diff --git a/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts b/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts
index e28f8a51792..b706a6c3a83 100644
--- a/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts
+++ b/public/app/features/variables/pickers/OptionsPicker/reducer.test.ts
@@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash';
import {
cleanPickerState,
hideOptions,
- initialState as optionsPickerInitialState,
+ initialOptionPickerState as optionsPickerInitialState,
moveOptionsHighlight,
OPTIONS_LIMIT,
optionsPickerReducer,
diff --git a/public/app/features/variables/pickers/OptionsPicker/reducer.ts b/public/app/features/variables/pickers/OptionsPicker/reducer.ts
index 92d87996682..ce9f009f384 100644
--- a/public/app/features/variables/pickers/OptionsPicker/reducer.ts
+++ b/public/app/features/variables/pickers/OptionsPicker/reducer.ts
@@ -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): 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): OptionsPickerState => {
const { option, clearOthers, forceSelect } = action.payload;
@@ -212,7 +212,7 @@ const optionsPickerSlice = createSlice({
return applyStateChanges(state, updateDefaultSelection, updateOptions);
},
- cleanPickerState: () => initialState,
+ cleanPickerState: () => initialOptionPickerState,
},
});
diff --git a/public/app/features/variables/query/QueryVariableEditor.test.tsx b/public/app/features/variables/query/QueryVariableEditor.test.tsx
index 69c4cd08810..987446dd904 100644
--- a/public/app/features/variables/query/QueryVariableEditor.test.tsx
+++ b/public/app/features/variables/query/QueryVariableEditor.test.tsx
@@ -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) => {
+ const variableDefaults: Partial = { 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',
diff --git a/public/app/features/variables/query/QueryVariableEditor.tsx b/public/app/features/variables/query/QueryVariableEditor.tsx
index 521748f237f..94ff3cb539b 100644
--- a/public/app/features/variables/query/QueryVariableEditor.tsx
+++ b/public/app/features/variables/query/QueryVariableEditor.tsx
@@ -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
};
async componentDidMount() {
- await this.props.initQueryVariableEditor(toVariableIdentifier(this.props.variable));
+ await this.props.initQueryVariableEditor(toKeyedVariableIdentifier(this.props.variable));
}
componentDidUpdate(prevProps: Readonly): 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
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
definition = query.query;
}
- this.props.changeQueryVariableQuery(toVariableIdentifier(this.props.variable), query, definition);
+ this.props.changeQueryVariableQuery(toKeyedVariableIdentifier(this.props.variable), query, definition);
}
};
diff --git a/public/app/features/variables/query/VariableQueryRunner.test.ts b/public/app/features/variables/query/VariableQueryRunner.test.ts
index e212b625f0c..e1670479de2 100644
--- a/public/app/features/variables/query/VariableQueryRunner.test.ts
+++ b/public/app/features/variables/query/VariableQueryRunner.test.ts
@@ -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,
diff --git a/public/app/features/variables/query/VariableQueryRunner.ts b/public/app/features/variables/query/VariableQueryRunner.ts
index ea17c2ce2ea..866cc92ca85 100644
--- a/public/app/features/variables/query/VariableQueryRunner.ts
+++ b/public/app/features/variables/query/VariableQueryRunner.ts
@@ -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;
private readonly updateOptionsResults: Subject;
- 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();
this.updateOptionsResults = new Subject();
- 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 {
+ getResponse(identifier: KeyedVariableIdentifier): Observable {
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(identifier.id, getState());
+ const variable = getVariable(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
diff --git a/public/app/features/variables/query/actions.test.tsx b/public/app/features/variables/query/actions.test.tsx
index 03623609ebb..7f8afffcd8b 100644
--- a/public/app/features/variables/query/actions.test.tsx
+++ b/public/app/features/variables/query/actions.test.tsx
@@ -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 = {
datasource: {
@@ -93,8 +95,10 @@ describe('query actions', () => {
const tester = await reduxTester()
.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()
.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()
.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()
.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()
.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()
.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()
.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()
.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()
.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()
.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({ 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()
.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()
.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()
.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()
.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()
.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): QueryVariableMode
return {
type: 'query',
id: '0',
+ rootStateKey: 'key',
global: false,
current: createOption(''),
options: [],
diff --git a/public/app/features/variables/query/actions.ts b/public/app/features/variables/query/actions.ts
index 8c9f9c0fb6f..45d42c5fec8 100644
--- a/public/app/features/variables/query/actions.ts
+++ b/public/app/features/variables/query/actions.ts
@@ -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 => {
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(identifier.id, getState());
- if (getState().templating.editor.id === variableInState.id) {
- dispatch(removeVariableEditorError({ errorProp: 'update' }));
+ const variableInState = getVariable(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 =>
+ (identifier: KeyedVariableIdentifier): ThunkResult =>
async (dispatch, getState) => {
- const variable = getVariable(identifier.id, getState());
- await dispatch(changeQueryVariableDataSource(toVariableIdentifier(variable), variable.datasource));
+ const variable = getVariable(identifier, getState());
+ await dispatch(changeQueryVariableDataSource(toKeyedVariableIdentifier(variable), variable.datasource));
};
export const changeQueryVariableDataSource = (
- identifier: VariableIdentifier,
+ identifier: KeyedVariableIdentifier,
name: DataSourceRef | null
): ThunkResult => {
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 =>
+ (identifier: KeyedVariableIdentifier, query: any, definition?: string): ThunkResult =>
async (dispatch, getState) => {
- const variableInState = getVariable(identifier.id, getState());
+ const { rootStateKey } = identifier;
+ const variableInState = getVariable(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));
diff --git a/public/app/features/variables/query/adapter.ts b/public/app/features/variables/query/adapter.ts
index 7edd533e685..06a2442ab69 100644
--- a/public/app/features/variables/query/adapter.ts
+++ b/public/app/features/variables/query/adapter.ts
@@ -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 {
- 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: [] };
diff --git a/public/app/features/variables/query/operators.test.ts b/public/app/features/variables/query/operators.test.ts
index fa7eba38e49..ad3cdcce819 100644
--- a/public/app/features/variables/query/operators.test.ts
+++ b/public/app/features/variables/query/operators.test.ts
@@ -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' },
+ })
+ )
);
});
});
diff --git a/public/app/features/variables/query/operators.ts b/public/app/features/variables/query/operators.ts
index 3f57960c529..2085df4bd23 100644
--- a/public/app/features/variables/query/operators.ts
+++ b/public/app/features/variables/query/operators.ts
@@ -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 {
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();
diff --git a/public/app/features/variables/query/reducer.test.ts b/public/app/features/variables/query/reducer.test.ts
index dd143490b2c..106f8c50903 100644
--- a/public/app/features/variables/query/reducer.test.ts
+++ b/public/app/features/variables/query/reducer.test.ts
@@ -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();
diff --git a/public/app/features/variables/query/reducer.ts b/public/app/features/variables/query/reducer.ts
index 03e2e0d89e8..e4afc5c609d 100644
--- a/public/app/features/variables/query/reducer.ts
+++ b/public/app/features/variables/query/reducer.ts
@@ -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;
diff --git a/public/app/features/variables/query/variableQueryObserver.test.ts b/public/app/features/variables/query/variableQueryObserver.test.ts
index 7b2e41fc8c8..8232e74df55 100644
--- a/public/app/features/variables/query/variableQueryObserver.test.ts
+++ b/public/app/features/variables/query/variableQueryObserver.test.ts
@@ -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', () => {
diff --git a/public/app/features/variables/shared/testing/variableBuilder.ts b/public/app/features/variables/shared/testing/variableBuilder.ts
index a1bae77fc59..f966605787b 100644
--- a/public/app/features/variables/shared/testing/variableBuilder.ts
+++ b/public/app/features/variables/shared/testing/variableBuilder.ts
@@ -19,6 +19,11 @@ export class VariableBuilder {
return this;
}
+ withRootStateKey(key: string) {
+ this.variable.rootStateKey = key;
+ return this;
+ }
+
build(): T {
return this.variable;
}
diff --git a/public/app/features/variables/state/actions.test.ts b/public/app/features/variables/state/actions.test.ts
index 1b90b4d77ce..81b9d40b130 100644
--- a/public/app/features/variables/state/actions.test.ts
+++ b/public/app/features/variables/state/actions.test.ts
@@ -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()
.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({ 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({ 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()
.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()
.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()
.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()
.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()
.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()
.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()
.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()
.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()
.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()
.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()
.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()
.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);
diff --git a/public/app/features/variables/state/actions.ts b/public/app/features/variables/state/actions.ts
index 902fbba742e..b1d57723a8b 100644
--- a/public/app/features/variables/state/actions.ts
+++ b/public/app/features/variables/state/actions.ts
@@ -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 => {
+export const initDashboardTemplating = (key: string, dashboard: DashboardModel): ThunkResult => {
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 => {
+export const addSystemTemplateVariables = (key: string, dashboard: DashboardModel): ThunkResult => {
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 => {
+export const changeVariableMultiValue = (identifier: KeyedVariableIdentifier, multi: boolean): ThunkResult => {
return (dispatch, getState) => {
- const variable = getVariable(identifier.id, getState());
+ const { rootStateKey: key } = identifier;
+ const variable = getVariable(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((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> => {
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> => {
+export const processVariables = (key: string): ThunkResult> => {
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> => {
};
export const setOptionFromUrl = (
- identifier: VariableIdentifier,
+ identifier: KeyedVariableIdentifier,
urlValue: UrlQueryValue
): ThunkResult> => {
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(variable.id, getState());
+ const variableFromState = getVariable(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> => {
return (dispatch, getState) => {
- const variableInState = getVariable(identifier.id, getState());
+ const variableInState = getVariable(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> => {
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> => {
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> =>
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> =>
+ (identifier: KeyedVariableIdentifier): ThunkResult> =>
async (dispatch, getState) => {
- const variableInState = getVariable(identifier.id);
+ const variableInState = getVariable