Variables: move state tree into a keyed state (#44642)
* Variables: move state tree into a keyed state * Update public/app/features/variables/state/transactionReducer.ts Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> * Chore: fix prettier error * Chore: renamed slices and lastUid * Chore: rename toUidAction * Chore: rename dashboardVariableReducer * Chore: rename state prop back to templating * Chore renames variable.dashboardUid * Chore: rename toDashboardVariableIdentifier * Chore: rename getDashboardVariable * Chore: rename getDashboardVariablesState * Chore: rename getDashboardVariables * Chore: some more renames * Chore: small clean up * Chore: small rename * Chore: removes unused function * Chore: rename VariableModel.stateKey * Chore: rename KeyedVariableIdentifier.stateKey * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> Co-authored-by: kay delaney <kay@grafana.com> Co-authored-by: Alexandra Vargas <alexa1866@gmail.com> Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
co-authored by
kay delaney
kay delaney
Alexandra Vargas
Ashley Harrison
parent
a9de33601c
commit
dbec2b02fd
+1
-1
@@ -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": [
|
||||
|
||||
@@ -13,11 +13,11 @@ import usersReducers from 'app/features/users/state/reducers';
|
||||
import userReducers from 'app/features/profile/state/reducers';
|
||||
import organizationReducers from 'app/features/org/state/reducers';
|
||||
import ldapReducers from 'app/features/admin/state/reducers';
|
||||
import templatingReducers from 'app/features/variables/state/reducers';
|
||||
import importDashboardReducers from 'app/features/manage-dashboards/state/reducers';
|
||||
import panelEditorReducers from 'app/features/dashboard/components/PanelEditor/state/reducers';
|
||||
import panelsReducers from 'app/features/panel/state/reducers';
|
||||
import serviceAccountsReducer from 'app/features/serviceaccounts/state/reducers';
|
||||
import templatingReducers from 'app/features/variables/state/keyedVariablesReducer';
|
||||
|
||||
const rootReducers = {
|
||||
...sharedReducers,
|
||||
@@ -33,10 +33,10 @@ const rootReducers = {
|
||||
...userReducers,
|
||||
...organizationReducers,
|
||||
...ldapReducers,
|
||||
...templatingReducers,
|
||||
...importDashboardReducers,
|
||||
...panelEditorReducers,
|
||||
...panelsReducers,
|
||||
...templatingReducers,
|
||||
plugins: pluginsReducer,
|
||||
};
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ export function DashboardSettings({ dashboard, editview }: Props) {
|
||||
title: 'Variables',
|
||||
id: 'templating',
|
||||
icon: 'calculator-alt',
|
||||
component: <VariableEditorContainer />,
|
||||
component: <VariableEditorContainer dashboard={dashboard} />,
|
||||
});
|
||||
|
||||
pages.push({
|
||||
|
||||
@@ -34,7 +34,6 @@ import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
|
||||
import { toggleTableView } from './state/reducers';
|
||||
|
||||
import { getPanelEditorTabs } from './state/selectors';
|
||||
import { getVariables } from 'app/features/variables/state/selectors';
|
||||
|
||||
import { StoreState } from 'app/types';
|
||||
import { DisplayMode, displayModes, PanelEditorTab } from './types';
|
||||
@@ -55,6 +54,7 @@ import { notifyApp } from '../../../../core/actions';
|
||||
import { PanelEditorTableView } from './PanelEditorTableView';
|
||||
import { PanelModelWithLibraryPanel } from 'app/features/library-panels/types';
|
||||
import { getPanelStateForModel } from 'app/features/panel/state/selectors';
|
||||
import { getVariablesByKey } from '../../../variables/state/selectors';
|
||||
|
||||
interface OwnProps {
|
||||
dashboard: DashboardModel;
|
||||
@@ -62,7 +62,7 @@ interface OwnProps {
|
||||
tab?: string;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState) => {
|
||||
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => {
|
||||
const panel = state.panelEditor.getPanel();
|
||||
const panelState = getPanelStateForModel(state, panel);
|
||||
|
||||
@@ -73,7 +73,7 @@ const mapStateToProps = (state: StoreState) => {
|
||||
initDone: state.panelEditor.initDone,
|
||||
uiState: state.panelEditor.ui,
|
||||
tableViewEnabled: state.panelEditor.tableViewEnabled,
|
||||
variables: getVariables(state),
|
||||
variables: getVariablesByKey(ownProps.dashboard.uid, state),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { getVariables } from '../../../variables/state/selectors';
|
||||
import { getLastKey, getVariablesByKey } from '../../../variables/state/selectors';
|
||||
import { StoreState } from '../../../../types';
|
||||
|
||||
export interface Props {
|
||||
@@ -13,7 +13,9 @@ export interface Props {
|
||||
}
|
||||
|
||||
export const RepeatRowSelect: FC<Props> = ({ repeat, onChange, id }) => {
|
||||
const variables = useSelector((state: StoreState) => getVariables(state));
|
||||
const variables = useSelector((state: StoreState) => {
|
||||
return getVariablesByKey(getLastKey(state), state);
|
||||
});
|
||||
|
||||
const variableOptions = useMemo(() => {
|
||||
const options = variables.map((item: any) => {
|
||||
|
||||
@@ -96,7 +96,7 @@ function shareLinkScenario(description: string, scenarioFn: (ctx: ScenarioContex
|
||||
}
|
||||
|
||||
describe('ShareModal', () => {
|
||||
let templateSrv = initTemplateSrv([]);
|
||||
let templateSrv = initTemplateSrv('key', []);
|
||||
|
||||
beforeAll(() => {
|
||||
variableAdapters.register(createQueryVariableAdapter());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect, MapStateToProps } from 'react-redux';
|
||||
import { StoreState } from '../../../../types';
|
||||
import { getSubMenuVariables } from '../../../variables/state/selectors';
|
||||
import { getSubMenuVariables, getVariablesState } from '../../../variables/state/selectors';
|
||||
import { VariableModel } from '../../../variables/types';
|
||||
import { DashboardModel } from '../../state';
|
||||
import { DashboardLinks } from './DashboardLinks';
|
||||
@@ -64,9 +64,11 @@ class SubMenuUnConnected extends PureComponent<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state) => {
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, ownProps) => {
|
||||
const { uid } = ownProps.dashboard;
|
||||
const templatingState = getVariablesState(uid, state);
|
||||
return {
|
||||
variables: getSubMenuVariables(state.templating.variables),
|
||||
variables: getSubMenuVariables(uid, templatingState.variables),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
const templateVarChanges = findTemplateVarChanges(this.props.queryParams, prevProps.queryParams);
|
||||
|
||||
if (templateVarChanges) {
|
||||
templateVarsChangedInUrl(templateVarChanges);
|
||||
templateVarsChangedInUrl(dashboard.uid, templateVarChanges);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
UrlQueryValue,
|
||||
} from '@grafana/data';
|
||||
import { CoreEvents, DashboardMeta, KioskMode } from 'app/types';
|
||||
import { GetVariables, getVariables } from 'app/features/variables/state/selectors';
|
||||
import { GetVariables, getVariablesByKey } from 'app/features/variables/state/selectors';
|
||||
import { variableAdapters } from 'app/features/variables/adapters';
|
||||
import { onTimeRangeUpdated } from 'app/features/variables/state/actions';
|
||||
import { dispatch } from '../../../store/store';
|
||||
@@ -139,7 +139,7 @@ export class DashboardModel implements TimeModel {
|
||||
lastRefresh: true,
|
||||
};
|
||||
|
||||
constructor(data: any, meta?: DashboardMeta, private getVariablesFromState: GetVariables = getVariables) {
|
||||
constructor(data: any, meta?: DashboardMeta, private getVariablesFromState: GetVariables = getVariablesByKey) {
|
||||
if (!data) {
|
||||
data = {};
|
||||
}
|
||||
@@ -346,7 +346,7 @@ export class DashboardModel implements TimeModel {
|
||||
defaults: { saveTimerange: boolean; saveVariables: boolean } & CloneOptions
|
||||
) {
|
||||
const originalVariables = this.originalTemplating;
|
||||
const currentVariables = this.getVariablesFromState();
|
||||
const currentVariables = this.getVariablesFromState(this.uid);
|
||||
|
||||
copy.templating = {
|
||||
list: currentVariables.map((variable) =>
|
||||
@@ -374,7 +374,7 @@ export class DashboardModel implements TimeModel {
|
||||
|
||||
timeRangeUpdated(timeRange: TimeRange) {
|
||||
this.events.publish(new TimeRangeUpdatedEvent(timeRange));
|
||||
dispatch(onTimeRangeUpdated(timeRange));
|
||||
dispatch(onTimeRangeUpdated(this.uid, timeRange));
|
||||
}
|
||||
|
||||
startRefresh(event: VariablesChangedEvent = { refreshAll: true, panelIds: [] }) {
|
||||
@@ -1085,11 +1085,11 @@ export class DashboardModel implements TimeModel {
|
||||
return;
|
||||
}
|
||||
|
||||
this.originalTemplating = this.cloneVariablesFrom(this.getVariablesFromState());
|
||||
this.originalTemplating = this.cloneVariablesFrom(this.getVariablesFromState(this.uid));
|
||||
}
|
||||
|
||||
hasVariableValuesChanged() {
|
||||
return this.hasVariablesChanged(this.originalTemplating, this.getVariablesFromState());
|
||||
return this.hasVariablesChanged(this.originalTemplating, this.getVariablesFromState(this.uid));
|
||||
}
|
||||
|
||||
autoFitPanels(viewHeight: number, kioskMode?: UrlQueryValue) {
|
||||
@@ -1164,7 +1164,7 @@ export class DashboardModel implements TimeModel {
|
||||
}
|
||||
|
||||
getVariables = () => {
|
||||
return this.getVariablesFromState();
|
||||
return this.getVariablesFromState(this.uid);
|
||||
};
|
||||
|
||||
canAddAnnotations() {
|
||||
@@ -1179,7 +1179,7 @@ export class DashboardModel implements TimeModel {
|
||||
}
|
||||
|
||||
private getPanelRepeatVariable(panel: PanelModel) {
|
||||
return this.getVariablesFromState().find((variable) => variable.name === panel.repeat);
|
||||
return this.getVariablesFromState(this.uid).find((variable) => variable.name === panel.repeat);
|
||||
}
|
||||
|
||||
private isSnapshotTruthy() {
|
||||
@@ -1187,7 +1187,7 @@ export class DashboardModel implements TimeModel {
|
||||
}
|
||||
|
||||
private hasVariables() {
|
||||
return this.getVariablesFromState().length > 0;
|
||||
return this.getVariablesFromState(this.uid).length > 0;
|
||||
}
|
||||
|
||||
private hasVariablesChanged(originalVariables: any[], currentVariables: any[]): boolean {
|
||||
|
||||
@@ -119,12 +119,12 @@ export const cleanUpDashboardAndVariables = (): ThunkResult<void> => (dispatch,
|
||||
|
||||
if (dashboard) {
|
||||
dashboard.destroy();
|
||||
dispatch(cancelVariables(dashboard.uid));
|
||||
}
|
||||
|
||||
getTimeSrv().stopAutoRefresh();
|
||||
|
||||
dispatch(cleanUpDashboard());
|
||||
dispatch(cancelVariables());
|
||||
};
|
||||
|
||||
export const updateTimeZoneDashboard =
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Echo } from '../../../core/services/echo/Echo';
|
||||
import { variableAdapters } from 'app/features/variables/adapters';
|
||||
import { createConstantVariableAdapter } from 'app/features/variables/constant/adapter';
|
||||
import { constantBuilder } from 'app/features/variables/shared/testing/builders';
|
||||
import { variablesInitTransaction } from '../../variables/state/transactionReducer';
|
||||
import { initialTransactionState, variablesInitTransaction } from '../../variables/state/transactionReducer';
|
||||
import { keybindingSrv } from 'app/core/services/keybindingSrv';
|
||||
import { getTimeSrv, setTimeSrv } from '../services/TimeSrv';
|
||||
import { DashboardLoaderSrv, setDashboardLoaderSrv } from '../services/DashboardLoaderSrv';
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '../../query/state/DashboardQueryRunner/DashboardQueryRunner';
|
||||
import { emptyResult } from '../../query/state/DashboardQueryRunner/utils';
|
||||
import { TransactionStatus } from '../../variables/types';
|
||||
import { getPreloadedState } from '../../variables/state/helpers';
|
||||
|
||||
jest.mock('app/core/services/backend_srv');
|
||||
jest.mock('app/features/dashboard/services/TimeSrv', () => {
|
||||
@@ -53,7 +54,7 @@ interface ScenarioContext {
|
||||
}
|
||||
|
||||
type ScenarioFn = (ctx: ScenarioContext) => void;
|
||||
|
||||
const DASH_UID = 'DGmvKKxZz';
|
||||
function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
|
||||
describe(description, () => {
|
||||
const loaderSrv = {
|
||||
@@ -83,6 +84,7 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
|
||||
templating: {
|
||||
list: [constantBuilder().build()],
|
||||
},
|
||||
uid: DASH_UID,
|
||||
},
|
||||
})),
|
||||
};
|
||||
@@ -100,7 +102,7 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
|
||||
|
||||
const ctx: ScenarioContext = {
|
||||
args: {
|
||||
urlUid: 'DGmvKKxZz',
|
||||
urlUid: DASH_UID,
|
||||
fixUrl: false,
|
||||
routeName: DashboardRoutes.Normal,
|
||||
},
|
||||
@@ -120,10 +122,10 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
|
||||
queries: [],
|
||||
},
|
||||
},
|
||||
templating: {
|
||||
...getPreloadedState(DASH_UID, {
|
||||
variables: {},
|
||||
transaction: { uid: 'DGmvKKxZz', status: TransactionStatus.Completed },
|
||||
},
|
||||
transaction: { ...initialTransactionState, uid: DASH_UID, status: TransactionStatus.Completed },
|
||||
}),
|
||||
},
|
||||
setup: (fn: () => void) => {
|
||||
setupFn = fn;
|
||||
@@ -258,7 +260,7 @@ describeInitScenario('Initializing existing dashboard', (ctx) => {
|
||||
});
|
||||
|
||||
it('Should initialize redux variables if newVariables is enabled', () => {
|
||||
expect(ctx.actions[2].type).toBe(variablesInitTransaction.type);
|
||||
expect(ctx.actions[2].payload.action.type).toBe(variablesInitTransaction.type);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -287,3 +289,14 @@ describeInitScenario('Initializing previously canceled dashboard initialization'
|
||||
expect(getDashboardQueryRunner().run).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describeInitScenario('Initializing snapshot dashboard', (ctx) => {
|
||||
ctx.setup(() => {
|
||||
ctx.args.urlUid = undefined;
|
||||
});
|
||||
|
||||
it('Should send action initVariablesTransaction with correct payload', () => {
|
||||
expect(ctx.actions[2].payload.action.type).toBe(variablesInitTransaction.type);
|
||||
expect(ctx.actions[2].payload.action.payload.uid).toBe(DASH_UID);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,8 @@ import { emitDashboardViewEvent } from './analyticsProcessor';
|
||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { createDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner';
|
||||
import { getIfExistsLastKey } from '../../variables/state/selectors';
|
||||
import { toStateKey } from 'app/features/variables/utils';
|
||||
|
||||
export interface InitDashboardArgs {
|
||||
urlUid?: string;
|
||||
@@ -155,15 +157,16 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
|
||||
|
||||
timeSrv.init(dashboard);
|
||||
|
||||
const dashboardUid = toStateKey(args.urlUid ?? dashboard.uid);
|
||||
// template values service needs to initialize completely before the rest of the dashboard can load
|
||||
await dispatch(initVariablesTransaction(args.urlUid!, dashboard));
|
||||
await dispatch(initVariablesTransaction(dashboardUid, dashboard));
|
||||
|
||||
// DashboardQueryRunner needs to run after all variables have been resolved so that any annotation query including a variable
|
||||
// will be correctly resolved
|
||||
const runner = createDashboardQueryRunner({ dashboard, timeSrv });
|
||||
runner.run({ dashboard, range: timeSrv.timeRange() });
|
||||
|
||||
if (getState().templating.transaction.uid !== args.urlUid) {
|
||||
if (getIfExistsLastKey(getState()) !== dashboardUid) {
|
||||
// if a previous dashboard has slow running variable queries the batch uid will be the new one
|
||||
// but the args.urlUid will be the same as before initVariablesTransaction was called so then we can't continue initializing
|
||||
// the previous dashboard.
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('linkSrv', () => {
|
||||
_dashboard.refresh = false;
|
||||
setTimeSrv(timeSrv);
|
||||
|
||||
templateSrv = initTemplateSrv([
|
||||
templateSrv = initTemplateSrv('key', [
|
||||
{ type: 'query', name: 'home', current: { value: '127.0.0.1' } },
|
||||
{ type: 'query', name: 'server1', current: { value: '192.168.0.100' } },
|
||||
]);
|
||||
|
||||
@@ -67,7 +67,7 @@ function expectOnResults(args: {
|
||||
done();
|
||||
} catch (err) {
|
||||
subscription.unsubscribe();
|
||||
done.fail(err);
|
||||
done(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ function expectOnResults(args: {
|
||||
expectCallback(value);
|
||||
done();
|
||||
} catch (err) {
|
||||
done.fail(err);
|
||||
done(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,6 +9,8 @@ import { FormatRegistryID } from './formatRegistry';
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import { mockDataSource, MockDataSourceSrv } from '../alerting/unified/mocks';
|
||||
|
||||
const key = 'key';
|
||||
|
||||
variableAdapters.setInit(() => [
|
||||
createQueryVariableAdapter() as unknown as VariableAdapter<VariableModel>,
|
||||
createAdHocVariableAdapter() as unknown as VariableAdapter<VariableModel>,
|
||||
@@ -20,7 +22,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('init', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
|
||||
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
|
||||
});
|
||||
|
||||
it('should initialize template data', () => {
|
||||
@@ -31,7 +33,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('replace can pass scoped vars', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
|
||||
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
|
||||
});
|
||||
|
||||
it('scoped vars should support objects', () => {
|
||||
@@ -115,7 +117,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('getAdhocFilters', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([
|
||||
_templateSrv = initTemplateSrv(key, [
|
||||
{
|
||||
type: 'datasource',
|
||||
name: 'ds',
|
||||
@@ -152,7 +154,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('replace can pass multi / all format', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([
|
||||
_templateSrv = initTemplateSrv(key, [
|
||||
{
|
||||
type: 'query',
|
||||
name: 'test',
|
||||
@@ -168,7 +170,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('when the globbed variable only has one value', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([
|
||||
_templateSrv = initTemplateSrv(key, [
|
||||
{
|
||||
type: 'query',
|
||||
name: 'test',
|
||||
@@ -216,7 +218,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('variable with all option', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([
|
||||
_templateSrv = initTemplateSrv(key, [
|
||||
{
|
||||
type: 'query',
|
||||
name: 'test',
|
||||
@@ -254,7 +256,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('variable with all option and custom value', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([
|
||||
_templateSrv = initTemplateSrv(key, [
|
||||
{
|
||||
type: 'query',
|
||||
name: 'test',
|
||||
@@ -298,19 +300,19 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('lucene format', () => {
|
||||
it('should properly escape $test with lucene escape sequences', () => {
|
||||
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
|
||||
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
|
||||
const target = _templateSrv.replace('this:$test', {}, 'lucene');
|
||||
expect(target).toBe('this:value\\/4');
|
||||
});
|
||||
|
||||
it('should properly escape ${test} with lucene escape sequences', () => {
|
||||
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
|
||||
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
|
||||
const target = _templateSrv.replace('this:${test}', {}, 'lucene');
|
||||
expect(target).toBe('this:value\\/4');
|
||||
});
|
||||
|
||||
it('should properly escape ${test:lucene} with lucene escape sequences', () => {
|
||||
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
|
||||
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
|
||||
const target = _templateSrv.replace('this:${test:lucene}', {});
|
||||
expect(target).toBe('this:value\\/4');
|
||||
});
|
||||
@@ -318,7 +320,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('html format', () => {
|
||||
it('should encode values html escape sequences', () => {
|
||||
_templateSrv = initTemplateSrv([
|
||||
_templateSrv = initTemplateSrv(key, [
|
||||
{ type: 'query', name: 'test', current: { value: '<script>alert(asd)</script>' } },
|
||||
]);
|
||||
const target = _templateSrv.replace('$test', {}, 'html');
|
||||
@@ -426,7 +428,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('can check if variable exists', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
|
||||
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
|
||||
});
|
||||
|
||||
it('should return true if $test exists', () => {
|
||||
@@ -467,7 +469,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('can highlight variables in string', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
|
||||
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
|
||||
});
|
||||
|
||||
it('should insert html', () => {
|
||||
@@ -488,7 +490,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('updateIndex with simple value', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'muuuu' } }]);
|
||||
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'muuuu' } }]);
|
||||
});
|
||||
|
||||
it('should set current value and update template data', () => {
|
||||
@@ -499,7 +501,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('replaceWithText', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([
|
||||
_templateSrv = initTemplateSrv(key, [
|
||||
{
|
||||
type: 'query',
|
||||
name: 'server',
|
||||
@@ -544,7 +546,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('replaceWithText can pass all / multi value', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([
|
||||
_templateSrv = initTemplateSrv(key, [
|
||||
{
|
||||
type: 'query',
|
||||
name: 'server',
|
||||
@@ -595,7 +597,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('built in interval variables', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([]);
|
||||
_templateSrv = initTemplateSrv(key, []);
|
||||
});
|
||||
|
||||
it('should replace $__interval_ms with interval milliseconds', () => {
|
||||
@@ -608,7 +610,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('date formating', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([], {
|
||||
_templateSrv = initTemplateSrv(key, [], {
|
||||
from: dateTime(1594671549254),
|
||||
to: dateTime(1595237229747),
|
||||
} as TimeRange);
|
||||
@@ -642,7 +644,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('handle objects gracefully', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: { test: 'A' } } }]);
|
||||
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: { test: 'A' } } }]);
|
||||
});
|
||||
|
||||
it('should not pass object to custom function', () => {
|
||||
@@ -658,7 +660,7 @@ describe('templateSrv', () => {
|
||||
describe('handle objects gracefully and call toString if defined', () => {
|
||||
beforeEach(() => {
|
||||
const value = { test: 'A', toString: () => 'hello' };
|
||||
_templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value } }]);
|
||||
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value } }]);
|
||||
});
|
||||
|
||||
it('should not pass object to custom function', () => {
|
||||
@@ -673,7 +675,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('adhoc variables', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([
|
||||
_templateSrv = initTemplateSrv(key, [
|
||||
{
|
||||
type: 'adhoc',
|
||||
name: 'adhoc',
|
||||
@@ -713,7 +715,7 @@ describe('templateSrv', () => {
|
||||
|
||||
describe('queryparam', () => {
|
||||
beforeEach(() => {
|
||||
_templateSrv = initTemplateSrv([
|
||||
_templateSrv = initTemplateSrv(key, [
|
||||
{
|
||||
type: 'query',
|
||||
name: 'single',
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { AdHocVariableEditorUnConnected as AdHocVariableEditor } from './AdHocVariableEditor';
|
||||
import { initialAdHocVariableModelState } from './reducer';
|
||||
import { selectOptionInTest } from '@grafana/ui';
|
||||
import { getSelectParent } from '@grafana/ui/src/components/Select/test-utils';
|
||||
|
||||
import { AdHocVariableEditorUnConnected as AdHocVariableEditor } from './AdHocVariableEditor';
|
||||
import { adHocBuilder } from '../shared/testing/builders';
|
||||
|
||||
const props = {
|
||||
extended: {
|
||||
dataSources: [
|
||||
@@ -13,7 +13,7 @@ const props = {
|
||||
{ text: 'Loki', value: { type: 'loki-ds', uid: 'abc' } },
|
||||
],
|
||||
},
|
||||
variable: { ...initialAdHocVariableModelState },
|
||||
variable: adHocBuilder().withId('adhoc').withRootStateKey('key').withName('adhoc').build(),
|
||||
onPropChange: jest.fn(),
|
||||
|
||||
// connected actions
|
||||
@@ -37,7 +37,10 @@ describe('AdHocVariableEditor', () => {
|
||||
render(<AdHocVariableEditor {...props} />);
|
||||
await selectOptionInTest(screen.getByLabelText('Data source'), 'Loki');
|
||||
|
||||
expect(props.changeVariableDatasource).toBeCalledWith({ type: 'loki-ds', uid: 'abc' });
|
||||
expect(props.changeVariableDatasource).toBeCalledWith(
|
||||
{ type: 'adhoc', id: 'adhoc', rootStateKey: 'key' },
|
||||
{ type: 'loki-ds', uid: 'abc' }
|
||||
);
|
||||
});
|
||||
|
||||
it('renders informational text', () => {
|
||||
|
||||
@@ -5,15 +5,31 @@ import { DataSourceRef, SelectableValue } from '@grafana/data';
|
||||
|
||||
import { AdHocVariableModel } from '../types';
|
||||
import { VariableEditorProps } from '../editor/types';
|
||||
import { initialVariableEditorState } from '../editor/reducer';
|
||||
import { changeVariableDatasource, initAdHocVariableEditor } from './actions';
|
||||
import { StoreState } from 'app/types';
|
||||
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
|
||||
import { VariableSelectField } from '../editor/VariableSelectField';
|
||||
import { getAdhocVariableEditorState } from '../editor/selectors';
|
||||
import { getVariablesState } from '../state/selectors';
|
||||
import { toKeyedVariableIdentifier } from '../utils';
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
extended: getAdhocVariableEditorState(state.templating.editor),
|
||||
});
|
||||
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => {
|
||||
const { rootStateKey } = ownProps.variable;
|
||||
|
||||
if (!rootStateKey) {
|
||||
console.error('AdHocVariableEditor: variable has no rootStateKey');
|
||||
return {
|
||||
extended: getAdhocVariableEditorState(initialVariableEditorState),
|
||||
};
|
||||
}
|
||||
|
||||
const { editor } = getVariablesState(rootStateKey, state);
|
||||
|
||||
return {
|
||||
extended: getAdhocVariableEditorState(editor),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
initAdHocVariableEditor,
|
||||
@@ -28,11 +44,17 @@ type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
|
||||
export class AdHocVariableEditorUnConnected extends PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
this.props.initAdHocVariableEditor();
|
||||
const { rootStateKey } = this.props.variable;
|
||||
if (!rootStateKey) {
|
||||
console.error('AdHocVariableEditor: variable has no rootStateKey');
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.initAdHocVariableEditor(rootStateKey);
|
||||
}
|
||||
|
||||
onDatasourceChanged = (option: SelectableValue<DataSourceRef>) => {
|
||||
this.props.changeVariableDatasource(option.value);
|
||||
this.props.changeVariableDatasource(toKeyedVariableIdentifier(this.props.variable), option.value);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -3,8 +3,7 @@ import { DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data'
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { createAdHocVariableAdapter } from './adapter';
|
||||
import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||
import { getRootReducer, RootReducerType } from '../state/helpers';
|
||||
import { toVariableIdentifier, toVariablePayload } from '../state/types';
|
||||
import { getPreloadedState, getRootReducer, RootReducerType } from '../state/helpers';
|
||||
import {
|
||||
addFilter,
|
||||
AdHocTableOptions,
|
||||
@@ -21,6 +20,8 @@ import { VariableModel } from 'app/features/variables/types';
|
||||
import { changeVariableEditorExtended, setIdInEditor } from '../editor/reducer';
|
||||
import { adHocBuilder } from '../shared/testing/builders';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
|
||||
const getList = jest.fn().mockReturnValue([]);
|
||||
const getDatasource = jest.fn().mockResolvedValue({});
|
||||
@@ -55,6 +56,7 @@ const expectedDatasources = [
|
||||
describe('adhoc actions', () => {
|
||||
describe('when applyFilterFromTable is dispatched and filter already exist', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const key = 'key';
|
||||
const options: AdHocTableOptions = {
|
||||
datasource: { uid: 'influxdb' },
|
||||
key: 'filter-key',
|
||||
@@ -71,12 +73,13 @@ describe('adhoc actions', () => {
|
||||
|
||||
const variable = adHocBuilder()
|
||||
.withId('Filters')
|
||||
.withRootStateKey(key)
|
||||
.withName('Filters')
|
||||
.withFilters([existingFilter])
|
||||
.withDatasource(options.datasource)
|
||||
.build();
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
const tester = await reduxTester<RootReducerType>({ preloadedState: getPreloadedState(key, {}) })
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(createAddVariableAction(variable))
|
||||
.whenAsyncActionIsDispatched(applyFilterFromTable(options), true);
|
||||
@@ -84,7 +87,9 @@ describe('adhoc actions', () => {
|
||||
const expectedQuery = { 'var-Filters': ['filter-key|!=|filter-existing', 'filter-key|=|filter-value'] };
|
||||
const expectedFilter = { key: 'filter-key', value: 'filter-value', operator: '=', condition: '' };
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(filterAdded(toVariablePayload(variable, expectedFilter)));
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction(key, filterAdded(toVariablePayload(variable, expectedFilter)))
|
||||
);
|
||||
|
||||
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
|
||||
});
|
||||
@@ -92,6 +97,7 @@ describe('adhoc actions', () => {
|
||||
|
||||
describe('when applyFilterFromTable is dispatched and previously no variable or filter exists', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const key = 'key';
|
||||
const options: AdHocTableOptions = {
|
||||
datasource: { uid: 'influxdb' },
|
||||
key: 'filter-key',
|
||||
@@ -99,18 +105,23 @@ describe('adhoc actions', () => {
|
||||
operator: '=',
|
||||
};
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
const tester = await reduxTester<RootReducerType>({ preloadedState: getPreloadedState(key, {}) })
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenAsyncActionIsDispatched(applyFilterFromTable(options), true);
|
||||
|
||||
const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource(options.datasource).build();
|
||||
const variable = adHocBuilder()
|
||||
.withId('Filters')
|
||||
.withRootStateKey(key)
|
||||
.withName('Filters')
|
||||
.withDatasource(options.datasource)
|
||||
.build();
|
||||
|
||||
const expectedQuery = { 'var-Filters': ['filter-key|=|filter-value'] };
|
||||
const expectedFilter = { key: 'filter-key', value: 'filter-value', operator: '=', condition: '' };
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
createAddVariableAction(variable),
|
||||
filterAdded(toVariablePayload(variable, expectedFilter))
|
||||
toKeyedAction(key, filterAdded(toVariablePayload(variable, expectedFilter)))
|
||||
);
|
||||
|
||||
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
|
||||
@@ -119,6 +130,7 @@ describe('adhoc actions', () => {
|
||||
|
||||
describe('when applyFilterFromTable is dispatched and previously no filter exists', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const key = 'key';
|
||||
const options: AdHocTableOptions = {
|
||||
datasource: { uid: 'influxdb' },
|
||||
key: 'filter-key',
|
||||
@@ -128,12 +140,13 @@ describe('adhoc actions', () => {
|
||||
|
||||
const variable = adHocBuilder()
|
||||
.withId('Filters')
|
||||
.withRootStateKey(key)
|
||||
.withName('Filters')
|
||||
.withFilters([])
|
||||
.withDatasource(options.datasource)
|
||||
.build();
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
const tester = await reduxTester<RootReducerType>({ preloadedState: getPreloadedState(key, {}) })
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(createAddVariableAction(variable))
|
||||
.whenAsyncActionIsDispatched(applyFilterFromTable(options), true);
|
||||
@@ -141,13 +154,16 @@ describe('adhoc actions', () => {
|
||||
const expectedFilter = { key: 'filter-key', value: 'filter-value', operator: '=', condition: '' };
|
||||
const expectedQuery = { 'var-Filters': ['filter-key|=|filter-value'] };
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(filterAdded(toVariablePayload(variable, expectedFilter)));
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction(key, filterAdded(toVariablePayload(variable, expectedFilter)))
|
||||
);
|
||||
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when applyFilterFromTable is dispatched and adhoc variable with other datasource exists', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const key = 'key';
|
||||
const options: AdHocTableOptions = {
|
||||
datasource: { uid: 'influxdb' },
|
||||
key: 'filter-key',
|
||||
@@ -157,13 +173,19 @@ describe('adhoc actions', () => {
|
||||
|
||||
const existing = adHocBuilder()
|
||||
.withId('elastic-filter')
|
||||
.withRootStateKey(key)
|
||||
.withName('elastic-filter')
|
||||
.withDatasource({ uid: 'elasticsearch' })
|
||||
.build();
|
||||
|
||||
const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource(options.datasource).build();
|
||||
const variable = adHocBuilder()
|
||||
.withId('Filters')
|
||||
.withRootStateKey(key)
|
||||
.withName('Filters')
|
||||
.withDatasource(options.datasource)
|
||||
.build();
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
const tester = await reduxTester<RootReducerType>({ preloadedState: getPreloadedState(key, {}) })
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(createAddVariableAction(existing))
|
||||
.whenAsyncActionIsDispatched(applyFilterFromTable(options), true);
|
||||
@@ -173,7 +195,7 @@ describe('adhoc actions', () => {
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
createAddVariableAction(variable, 1),
|
||||
filterAdded(toVariablePayload(variable, expectedFilter))
|
||||
toKeyedAction(key, filterAdded(toVariablePayload(variable, expectedFilter)))
|
||||
);
|
||||
|
||||
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
|
||||
@@ -182,6 +204,7 @@ describe('adhoc actions', () => {
|
||||
|
||||
describe('when changeFilter is dispatched', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const key = 'key';
|
||||
const existing = {
|
||||
key: 'key',
|
||||
value: 'value',
|
||||
@@ -196,6 +219,7 @@ describe('adhoc actions', () => {
|
||||
|
||||
const variable = adHocBuilder()
|
||||
.withId('elastic-filter')
|
||||
.withRootStateKey(key)
|
||||
.withName('elastic-filter')
|
||||
.withFilters([existing])
|
||||
.withDatasource({ uid: 'elasticsearch' })
|
||||
@@ -206,12 +230,14 @@ describe('adhoc actions', () => {
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(createAddVariableAction(variable))
|
||||
.whenAsyncActionIsDispatched(changeFilter('elastic-filter', update), true);
|
||||
.whenAsyncActionIsDispatched(changeFilter(toKeyedVariableIdentifier(variable), update), true);
|
||||
|
||||
const expectedQuery = { 'var-elastic-filter': ['key|!=|value'] };
|
||||
const expectedUpdate = { index: 0, filter: updated };
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(filterUpdated(toVariablePayload(variable, expectedUpdate)));
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction(key, filterUpdated(toVariablePayload(variable, expectedUpdate)))
|
||||
);
|
||||
|
||||
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
|
||||
});
|
||||
@@ -219,6 +245,7 @@ describe('adhoc actions', () => {
|
||||
|
||||
describe('when addFilter is dispatched on variable with existing filter', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const key = 'key';
|
||||
const existing = {
|
||||
key: 'key',
|
||||
value: 'value',
|
||||
@@ -233,6 +260,7 @@ describe('adhoc actions', () => {
|
||||
|
||||
const variable = adHocBuilder()
|
||||
.withId('elastic-filter')
|
||||
.withRootStateKey(key)
|
||||
.withName('elastic-filter')
|
||||
.withFilters([existing])
|
||||
.withDatasource({ uid: 'elasticsearch' })
|
||||
@@ -241,18 +269,21 @@ describe('adhoc actions', () => {
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(createAddVariableAction(variable))
|
||||
.whenAsyncActionIsDispatched(addFilter('elastic-filter', adding), true);
|
||||
.whenAsyncActionIsDispatched(addFilter(toKeyedVariableIdentifier(variable), adding), true);
|
||||
|
||||
const expectedQuery = { 'var-elastic-filter': ['key|=|value', 'key|!=|value'] };
|
||||
const expectedFilter = { key: 'key', value: 'value', operator: '!=', condition: '' };
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(filterAdded(toVariablePayload(variable, expectedFilter)));
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction(key, filterAdded(toVariablePayload(variable, expectedFilter)))
|
||||
);
|
||||
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when addFilter is dispatched on variable with no existing filter', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const key = 'key';
|
||||
const adding = {
|
||||
key: 'key',
|
||||
value: 'value',
|
||||
@@ -262,6 +293,7 @@ describe('adhoc actions', () => {
|
||||
|
||||
const variable = adHocBuilder()
|
||||
.withId('elastic-filter')
|
||||
.withRootStateKey(key)
|
||||
.withName('elastic-filter')
|
||||
.withFilters([])
|
||||
.withDatasource({ uid: 'elasticsearch' })
|
||||
@@ -270,19 +302,21 @@ describe('adhoc actions', () => {
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(createAddVariableAction(variable))
|
||||
.whenAsyncActionIsDispatched(addFilter('elastic-filter', adding), true);
|
||||
.whenAsyncActionIsDispatched(addFilter(toKeyedVariableIdentifier(variable), adding), true);
|
||||
|
||||
const expectedQuery = { 'var-elastic-filter': ['key|=|value'] };
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(filterAdded(toVariablePayload(variable, adding)));
|
||||
tester.thenDispatchedActionsShouldEqual(toKeyedAction(key, filterAdded(toVariablePayload(variable, adding))));
|
||||
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when removeFilter is dispatched on variable with no existing filter', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const key = 'key';
|
||||
const variable = adHocBuilder()
|
||||
.withId('elastic-filter')
|
||||
.withRootStateKey(key)
|
||||
.withName('elastic-filter')
|
||||
.withFilters([])
|
||||
.withDatasource({ uid: 'elasticsearch' })
|
||||
@@ -291,17 +325,18 @@ describe('adhoc actions', () => {
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(createAddVariableAction(variable))
|
||||
.whenAsyncActionIsDispatched(removeFilter('elastic-filter', 0), true);
|
||||
.whenAsyncActionIsDispatched(removeFilter(toKeyedVariableIdentifier(variable), 0), true);
|
||||
|
||||
const expectedQuery = { 'var-elastic-filter': [] as string[] };
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(filterRemoved(toVariablePayload(variable, 0)));
|
||||
tester.thenDispatchedActionsShouldEqual(toKeyedAction(key, filterRemoved(toVariablePayload(variable, 0))));
|
||||
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when removeFilter is dispatched on variable with existing filter', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const key = 'key';
|
||||
const filter = {
|
||||
key: 'key',
|
||||
value: 'value',
|
||||
@@ -311,6 +346,7 @@ describe('adhoc actions', () => {
|
||||
|
||||
const variable = adHocBuilder()
|
||||
.withId('elastic-filter')
|
||||
.withRootStateKey(key)
|
||||
.withName('elastic-filter')
|
||||
.withFilters([filter])
|
||||
.withDatasource({ uid: 'elasticsearch' })
|
||||
@@ -319,17 +355,18 @@ describe('adhoc actions', () => {
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(createAddVariableAction(variable))
|
||||
.whenAsyncActionIsDispatched(removeFilter('elastic-filter', 0), true);
|
||||
.whenAsyncActionIsDispatched(removeFilter(toKeyedVariableIdentifier(variable), 0), true);
|
||||
|
||||
const expectedQuery = { 'var-elastic-filter': [] as string[] };
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(filterRemoved(toVariablePayload(variable, 0)));
|
||||
tester.thenDispatchedActionsShouldEqual(toKeyedAction(key, filterRemoved(toVariablePayload(variable, 0))));
|
||||
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when setFiltersFromUrl is dispatched', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const key = 'key';
|
||||
const existing = {
|
||||
key: 'key',
|
||||
value: 'value',
|
||||
@@ -339,6 +376,7 @@ describe('adhoc actions', () => {
|
||||
|
||||
const variable = adHocBuilder()
|
||||
.withId('elastic-filter')
|
||||
.withRootStateKey(key)
|
||||
.withName('elastic-filter')
|
||||
.withFilters([existing])
|
||||
.withDatasource({ uid: 'elasticsearch' })
|
||||
@@ -352,7 +390,7 @@ describe('adhoc actions', () => {
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(createAddVariableAction(variable))
|
||||
.whenAsyncActionIsDispatched(setFiltersFromUrl('elastic-filter', fromUrl), true);
|
||||
.whenAsyncActionIsDispatched(setFiltersFromUrl(toKeyedVariableIdentifier(variable), fromUrl), true);
|
||||
|
||||
const expectedQuery = { 'var-elastic-filter': ['key|=|value', 'key|=|value'] };
|
||||
const expectedFilters = [
|
||||
@@ -360,28 +398,40 @@ describe('adhoc actions', () => {
|
||||
{ key: 'key', value: 'value', operator: '=', condition: '', name: 'value-2' },
|
||||
];
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(filtersRestored(toVariablePayload(variable, expectedFilters)));
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction(key, filtersRestored(toVariablePayload(variable, expectedFilters)))
|
||||
);
|
||||
expect(locationService.partial).toHaveBeenLastCalledWith(expectedQuery);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when initAdHocVariableEditor is dispatched', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const key = 'key';
|
||||
|
||||
getList.mockRestore();
|
||||
getList.mockReturnValue(datasources);
|
||||
|
||||
const tester = reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(initAdHocVariableEditor());
|
||||
.whenActionIsDispatched(initAdHocVariableEditor(key));
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(changeVariableEditorExtended({ dataSources: expectedDatasources }));
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction(key, changeVariableEditorExtended({ dataSources: expectedDatasources }))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeVariableDatasource is dispatched with unsupported datasource', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const key = 'key';
|
||||
const datasource = { uid: 'mysql' };
|
||||
const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource({ uid: 'influxdb' }).build();
|
||||
const variable = adHocBuilder()
|
||||
.withId('Filters')
|
||||
.withRootStateKey(key)
|
||||
.withName('Filters')
|
||||
.withDatasource({ uid: 'influxdb' })
|
||||
.build();
|
||||
|
||||
getDatasource.mockRestore();
|
||||
getDatasource.mockResolvedValue(null);
|
||||
@@ -391,25 +441,37 @@ describe('adhoc actions', () => {
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(createAddVariableAction(variable))
|
||||
.whenActionIsDispatched(setIdInEditor({ id: variable.id }))
|
||||
.whenActionIsDispatched(initAdHocVariableEditor())
|
||||
.whenAsyncActionIsDispatched(changeVariableDatasource(datasource), true);
|
||||
.whenActionIsDispatched(toKeyedAction(key, setIdInEditor({ id: variable.id })))
|
||||
.whenActionIsDispatched(initAdHocVariableEditor(key))
|
||||
.whenAsyncActionIsDispatched(changeVariableDatasource(toKeyedVariableIdentifier(variable), datasource), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource })),
|
||||
changeVariableEditorExtended({
|
||||
infoText: 'This data source does not support ad hoc filters yet.',
|
||||
dataSources: expectedDatasources,
|
||||
})
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource }))
|
||||
),
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableEditorExtended({
|
||||
infoText: 'This data source does not support ad hoc filters yet.',
|
||||
dataSources: expectedDatasources,
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeVariableDatasource is dispatched with datasource', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const key = 'key';
|
||||
const datasource = { uid: 'elasticsearch' };
|
||||
const loadingText = 'Ad hoc filters are applied automatically to all queries that target this data source';
|
||||
const variable = adHocBuilder().withId('Filters').withName('Filters').withDatasource({ uid: 'influxdb' }).build();
|
||||
const variable = adHocBuilder()
|
||||
.withId('Filters')
|
||||
.withRootStateKey(key)
|
||||
.withName('Filters')
|
||||
.withDatasource({ uid: 'influxdb' })
|
||||
.build();
|
||||
|
||||
getDatasource.mockRestore();
|
||||
getDatasource.mockResolvedValue({
|
||||
@@ -421,23 +483,26 @@ describe('adhoc actions', () => {
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(createAddVariableAction(variable))
|
||||
.whenActionIsDispatched(setIdInEditor({ id: variable.id }))
|
||||
.whenActionIsDispatched(initAdHocVariableEditor())
|
||||
.whenAsyncActionIsDispatched(changeVariableDatasource(datasource), true);
|
||||
.whenActionIsDispatched(toKeyedAction(key, setIdInEditor({ id: variable.id })))
|
||||
.whenActionIsDispatched(initAdHocVariableEditor(key))
|
||||
.whenAsyncActionIsDispatched(changeVariableDatasource(toKeyedVariableIdentifier(variable), datasource), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource })),
|
||||
changeVariableEditorExtended({ infoText: loadingText, dataSources: expectedDatasources })
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource }))
|
||||
),
|
||||
toKeyedAction(key, changeVariableEditorExtended({ infoText: loadingText, dataSources: expectedDatasources }))
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createAddVariableAction(variable: VariableModel, index = 0) {
|
||||
const identifier = toVariableIdentifier(variable);
|
||||
const identifier = toKeyedVariableIdentifier(variable);
|
||||
const global = false;
|
||||
const data = { global, index, model: { ...variable, index: -1, global } };
|
||||
return addVariable(toVariablePayload(identifier, data));
|
||||
return toKeyedAction(variable.rootStateKey!, addVariable(toVariablePayload(identifier, data)));
|
||||
}
|
||||
|
||||
function createDatasource(name: string, selectable = true, isDefault = false): DataSourceInstanceSettings {
|
||||
|
||||
@@ -3,8 +3,8 @@ import { StoreState, ThunkResult } from 'app/types';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { changeVariableEditorExtended } from '../editor/reducer';
|
||||
import { addVariable, changeVariableProp } from '../state/sharedReducer';
|
||||
import { getNewVariableIndex, getVariable } from '../state/selectors';
|
||||
import { AddVariable, toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
|
||||
import { getLastKey, getNewVariableIndex, getVariable, getVariablesState } from '../state/selectors';
|
||||
import { AddVariable, KeyedVariableIdentifier } from '../state/types';
|
||||
import {
|
||||
AdHocVariabelFilterUpdate,
|
||||
filterAdded,
|
||||
@@ -18,6 +18,8 @@ import { variableUpdated } from '../state/actions';
|
||||
import { isAdHoc } from '../guard';
|
||||
import { DataSourceRef, getDataSourceRef } from '@grafana/data';
|
||||
import { getAdhocVariableEditorState } from '../editor/selectors';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
|
||||
export interface AdHocTableOptions {
|
||||
datasource: DataSourceRef;
|
||||
@@ -35,6 +37,9 @@ export const applyFilterFromTable = (options: AdHocTableOptions): ThunkResult<vo
|
||||
if (!variable) {
|
||||
dispatch(createAdHocVariable(options));
|
||||
variable = getVariableByOptions(options, getState());
|
||||
if (!variable) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const index = variable.filters.findIndex((f) => f.key === options.key && f.value === options.value);
|
||||
@@ -42,52 +47,66 @@ export const applyFilterFromTable = (options: AdHocTableOptions): ThunkResult<vo
|
||||
if (index === -1) {
|
||||
const { value, key, operator } = options;
|
||||
const filter = { value, key, operator, condition: '' };
|
||||
return await dispatch(addFilter(variable.id, filter));
|
||||
return await dispatch(addFilter(toKeyedVariableIdentifier(variable), filter));
|
||||
}
|
||||
|
||||
const filter = { ...variable.filters[index], operator: options.operator };
|
||||
return await dispatch(changeFilter(variable.id, { index, filter }));
|
||||
return await dispatch(changeFilter(toKeyedVariableIdentifier(variable), { index, filter }));
|
||||
};
|
||||
};
|
||||
|
||||
export const changeFilter = (id: string, update: AdHocVariabelFilterUpdate): ThunkResult<void> => {
|
||||
export const changeFilter = (
|
||||
identifier: KeyedVariableIdentifier,
|
||||
update: AdHocVariabelFilterUpdate
|
||||
): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
const variable = getVariable(id, getState());
|
||||
dispatch(filterUpdated(toVariablePayload(variable, update)));
|
||||
await dispatch(variableUpdated(toVariableIdentifier(variable), true));
|
||||
const variable = getVariable(identifier, getState());
|
||||
dispatch(toKeyedAction(identifier.rootStateKey, filterUpdated(toVariablePayload(variable, update))));
|
||||
await dispatch(variableUpdated(toKeyedVariableIdentifier(variable), true));
|
||||
};
|
||||
};
|
||||
|
||||
export const removeFilter = (id: string, index: number): ThunkResult<void> => {
|
||||
export const removeFilter = (identifier: KeyedVariableIdentifier, index: number): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
const variable = getVariable(id, getState());
|
||||
dispatch(filterRemoved(toVariablePayload(variable, index)));
|
||||
await dispatch(variableUpdated(toVariableIdentifier(variable), true));
|
||||
const variable = getVariable(identifier, getState());
|
||||
dispatch(toKeyedAction(identifier.rootStateKey, filterRemoved(toVariablePayload(variable, index))));
|
||||
await dispatch(variableUpdated(toKeyedVariableIdentifier(variable), true));
|
||||
};
|
||||
};
|
||||
|
||||
export const addFilter = (id: string, filter: AdHocVariableFilter): ThunkResult<void> => {
|
||||
export const addFilter = (identifier: KeyedVariableIdentifier, filter: AdHocVariableFilter): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
const variable = getVariable(id, getState());
|
||||
dispatch(filterAdded(toVariablePayload(variable, filter)));
|
||||
await dispatch(variableUpdated(toVariableIdentifier(variable), true));
|
||||
const variable = getVariable(identifier, getState());
|
||||
dispatch(toKeyedAction(identifier.rootStateKey, filterAdded(toVariablePayload(variable, filter))));
|
||||
await dispatch(variableUpdated(toKeyedVariableIdentifier(variable), true));
|
||||
};
|
||||
};
|
||||
|
||||
export const setFiltersFromUrl = (id: string, filters: AdHocVariableFilter[]): ThunkResult<void> => {
|
||||
export const setFiltersFromUrl = (
|
||||
identifier: KeyedVariableIdentifier,
|
||||
filters: AdHocVariableFilter[]
|
||||
): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
const variable = getVariable(id, getState());
|
||||
dispatch(filtersRestored(toVariablePayload(variable, filters)));
|
||||
await dispatch(variableUpdated(toVariableIdentifier(variable), true));
|
||||
const variable = getVariable(identifier, getState());
|
||||
dispatch(toKeyedAction(identifier.rootStateKey, filtersRestored(toVariablePayload(variable, filters))));
|
||||
await dispatch(variableUpdated(toKeyedVariableIdentifier(variable), true));
|
||||
};
|
||||
};
|
||||
|
||||
export const changeVariableDatasource = (datasource?: DataSourceRef): ThunkResult<void> => {
|
||||
export const changeVariableDatasource = (
|
||||
identifier: KeyedVariableIdentifier,
|
||||
datasource?: DataSourceRef
|
||||
): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
const { editor } = getState().templating;
|
||||
const { editor } = getVariablesState(identifier.rootStateKey, getState());
|
||||
const extended = getAdhocVariableEditorState(editor);
|
||||
const variable = getVariable(editor.id, getState());
|
||||
dispatch(changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource })));
|
||||
const variable = getVariable(identifier, getState());
|
||||
dispatch(
|
||||
toKeyedAction(
|
||||
identifier.rootStateKey,
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: datasource }))
|
||||
)
|
||||
);
|
||||
|
||||
const ds = await getDatasourceSrv().get(datasource);
|
||||
|
||||
@@ -97,57 +116,70 @@ export const changeVariableDatasource = (datasource?: DataSourceRef): ThunkResul
|
||||
: 'This data source does not support ad hoc filters yet.';
|
||||
|
||||
dispatch(
|
||||
changeVariableEditorExtended({
|
||||
infoText: message,
|
||||
dataSources: extended?.dataSources ?? [],
|
||||
})
|
||||
toKeyedAction(
|
||||
identifier.rootStateKey,
|
||||
changeVariableEditorExtended({
|
||||
infoText: message,
|
||||
dataSources: extended?.dataSources ?? [],
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const initAdHocVariableEditor = (): ThunkResult<void> => (dispatch) => {
|
||||
const dataSources = getDatasourceSrv().getList({ metrics: true, variables: true });
|
||||
const selectable = dataSources.reduce(
|
||||
(all: Array<{ text: string; value: DataSourceRef | null }>, ds) => {
|
||||
if (ds.meta.mixed) {
|
||||
export const initAdHocVariableEditor =
|
||||
(key: string): ThunkResult<void> =>
|
||||
(dispatch) => {
|
||||
const dataSources = getDatasourceSrv().getList({ metrics: true, variables: true });
|
||||
const selectable = dataSources.reduce(
|
||||
(all: Array<{ text: string; value: DataSourceRef | null }>, ds) => {
|
||||
if (ds.meta.mixed) {
|
||||
return all;
|
||||
}
|
||||
|
||||
const text = ds.isDefault ? `${ds.name} (default)` : ds.name;
|
||||
const value = getDataSourceRef(ds);
|
||||
all.push({ text, value });
|
||||
|
||||
return all;
|
||||
}
|
||||
},
|
||||
[{ text: '', value: {} }]
|
||||
);
|
||||
|
||||
const text = ds.isDefault ? `${ds.name} (default)` : ds.name;
|
||||
const value = getDataSourceRef(ds);
|
||||
all.push({ text, value });
|
||||
|
||||
return all;
|
||||
},
|
||||
[{ text: '', value: {} }]
|
||||
);
|
||||
|
||||
dispatch(
|
||||
changeVariableEditorExtended({
|
||||
dataSources: selectable,
|
||||
})
|
||||
);
|
||||
};
|
||||
dispatch(
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableEditorExtended({
|
||||
dataSources: selectable,
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const createAdHocVariable = (options: AdHocTableOptions): ThunkResult<void> => {
|
||||
return (dispatch, getState) => {
|
||||
const model = {
|
||||
const key = getLastKey(getState());
|
||||
|
||||
const model: AdHocVariableModel = {
|
||||
...cloneDeep(initialAdHocVariableModelState),
|
||||
datasource: options.datasource,
|
||||
name: filterTableName,
|
||||
id: filterTableName,
|
||||
rootStateKey: key,
|
||||
};
|
||||
|
||||
const global = false;
|
||||
const index = getNewVariableIndex(getState());
|
||||
const identifier: VariableIdentifier = { type: 'adhoc', id: model.id };
|
||||
const index = getNewVariableIndex(key, getState());
|
||||
const identifier: KeyedVariableIdentifier = { type: 'adhoc', id: model.id, rootStateKey: key };
|
||||
|
||||
dispatch(addVariable(toVariablePayload<AddVariable>(identifier, { global, model, index })));
|
||||
dispatch(toKeyedAction(key, addVariable(toVariablePayload<AddVariable>(identifier, { global, model, index }))));
|
||||
};
|
||||
};
|
||||
|
||||
const getVariableByOptions = (options: AdHocTableOptions, state: StoreState): AdHocVariableModel => {
|
||||
return Object.values(state.templating.variables).find(
|
||||
const getVariableByOptions = (options: AdHocTableOptions, state: StoreState): AdHocVariableModel | undefined => {
|
||||
const key = getLastKey(state);
|
||||
const templatingState = getVariablesState(key, state);
|
||||
return Object.values(templatingState.variables).find(
|
||||
(v) => isAdHoc(v) && v.datasource?.uid === options.datasource.uid
|
||||
) as AdHocVariableModel;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import { adHocVariableReducer, initialAdHocVariableModelState } from './reducer'
|
||||
import { AdHocVariableEditor } from './AdHocVariableEditor';
|
||||
import { setFiltersFromUrl } from './actions';
|
||||
import * as urlParser from './urlParser';
|
||||
import { toKeyedVariableIdentifier } from '../utils';
|
||||
|
||||
const noop = async () => {};
|
||||
|
||||
@@ -24,11 +25,11 @@ export const createAdHocVariableAdapter = (): VariableAdapter<AdHocVariableModel
|
||||
setValue: noop,
|
||||
setValueFromUrl: async (variable, urlValue) => {
|
||||
const filters = urlParser.toFilters(urlValue);
|
||||
await dispatch(setFiltersFromUrl(variable.id, filters));
|
||||
await dispatch(setFiltersFromUrl(toKeyedVariableIdentifier(variable), filters));
|
||||
},
|
||||
updateOptions: noop,
|
||||
getSaveModel: (variable) => {
|
||||
const { index, id, state, global, ...rest } = cloneDeep(variable);
|
||||
const { index, id, state, global, rootStateKey, ...rest } = cloneDeep(variable);
|
||||
return rest;
|
||||
},
|
||||
getValueForUrl: (variable) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AdHocVariableFilter, AdHocVariableModel } from 'app/features/variables/
|
||||
import { VariablePickerProps } from '../../pickers/types';
|
||||
import { addFilter, changeFilter, removeFilter } from '../actions';
|
||||
import { AdHocFilter } from './AdHocFilter';
|
||||
import { toKeyedVariableIdentifier } from '../../utils';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
addFilter,
|
||||
@@ -23,15 +24,15 @@ type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
*/
|
||||
export class AdHocPickerUnconnected extends PureComponent<Props> {
|
||||
addFilter = (filter: AdHocVariableFilter) => {
|
||||
this.props.addFilter(this.props.variable.id, filter);
|
||||
this.props.addFilter(toKeyedVariableIdentifier(this.props.variable), filter);
|
||||
};
|
||||
|
||||
removeFilter = (index: number) => {
|
||||
this.props.removeFilter(this.props.variable.id, index);
|
||||
this.props.removeFilter(toKeyedVariableIdentifier(this.props.variable), index);
|
||||
};
|
||||
|
||||
changeFilter = (index: number, filter: AdHocVariableFilter) => {
|
||||
this.props.changeFilter(this.props.variable.id, {
|
||||
this.props.changeFilter(toKeyedVariableIdentifier(this.props.variable), {
|
||||
index,
|
||||
filter,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { getVariableTestContext } from '../state/helpers';
|
||||
import { toVariablePayload, VariablesState } from '../state/types';
|
||||
import { VariablesState } from '../state/types';
|
||||
import { adHocVariableReducer, filterAdded, filterRemoved, filtersRestored, filterUpdated } from './reducer';
|
||||
import { AdHocVariableFilter, AdHocVariableModel } from '../types';
|
||||
import { createAdHocVariableAdapter } from './adapter';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
describe('adHocVariableReducer', () => {
|
||||
const adapter = createAdHocVariableAdapter();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { AdHocVariableFilter, AdHocVariableModel, initialVariableModelState } from 'app/features/variables/types';
|
||||
import { getInstanceState, initialVariablesState, VariablePayload, VariablesState } from '../state/types';
|
||||
import { initialVariablesState, VariablePayload, VariablesState } from '../state/types';
|
||||
import { getInstanceState } from '../state/selectors';
|
||||
|
||||
export interface AdHocVariabelFilterUpdate {
|
||||
index: number;
|
||||
|
||||
@@ -4,9 +4,10 @@ import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||
import { updateConstantVariableOptions } from './actions';
|
||||
import { getRootReducer, RootReducerType } from '../state/helpers';
|
||||
import { ConstantVariableModel, initialVariableModelState, VariableOption } from '../types';
|
||||
import { toVariablePayload } from '../state/types';
|
||||
import { createConstantOptionsFromQuery } from './reducer';
|
||||
import { addVariable, setCurrentVariableValue } from '../state/sharedReducer';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
|
||||
describe('constant actions', () => {
|
||||
variableAdapters.setInit(() => [createConstantVariableAdapter()]);
|
||||
@@ -22,6 +23,7 @@ describe('constant actions', () => {
|
||||
const variable: ConstantVariableModel = {
|
||||
...initialVariableModelState,
|
||||
id: '0',
|
||||
rootStateKey: 'key',
|
||||
index: 0,
|
||||
type: 'constant',
|
||||
name: 'Constant',
|
||||
@@ -36,12 +38,14 @@ describe('constant actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenAsyncActionIsDispatched(updateConstantVariableOptions(toVariablePayload(variable)), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(updateConstantVariableOptions(toKeyedVariableIdentifier(variable)), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
createConstantOptionsFromQuery(toVariablePayload(variable)),
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option }))
|
||||
toKeyedAction('key', createConstantOptionsFromQuery(toVariablePayload(variable))),
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { validateVariableSelectionState } from '../state/actions';
|
||||
import { ThunkResult } from 'app/types';
|
||||
import { createConstantOptionsFromQuery } from './reducer';
|
||||
import { toVariablePayload, VariableIdentifier } from '../state/types';
|
||||
import { KeyedVariableIdentifier } from '../state/types';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
export const updateConstantVariableOptions = (identifier: VariableIdentifier): ThunkResult<void> => {
|
||||
export const updateConstantVariableOptions = (identifier: KeyedVariableIdentifier): ThunkResult<void> => {
|
||||
return async (dispatch) => {
|
||||
await dispatch(createConstantOptionsFromQuery(toVariablePayload(identifier)));
|
||||
const { rootStateKey } = identifier;
|
||||
await dispatch(toKeyedAction(rootStateKey, createConstantOptionsFromQuery(toVariablePayload(identifier))));
|
||||
await dispatch(validateVariableSelectionState(identifier));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,8 +6,8 @@ import { VariableAdapter } from '../adapters';
|
||||
import { constantVariableReducer, initialConstantVariableModelState } from './reducer';
|
||||
import { ConstantVariableEditor } from './ConstantVariableEditor';
|
||||
import { updateConstantVariableOptions } from './actions';
|
||||
import { toVariableIdentifier } from '../state/types';
|
||||
import { optionPickerFactory } from '../pickers';
|
||||
import { toKeyedVariableIdentifier } from '../utils';
|
||||
|
||||
export const createConstantVariableAdapter = (): VariableAdapter<ConstantVariableModel> => {
|
||||
return {
|
||||
@@ -22,16 +22,16 @@ export const createConstantVariableAdapter = (): VariableAdapter<ConstantVariabl
|
||||
return false;
|
||||
},
|
||||
setValue: async (variable, option, emitChanges = false) => {
|
||||
await dispatch(setOptionAsCurrent(toVariableIdentifier(variable), option, emitChanges));
|
||||
await dispatch(setOptionAsCurrent(toKeyedVariableIdentifier(variable), option, emitChanges));
|
||||
},
|
||||
setValueFromUrl: async (variable, urlValue) => {
|
||||
await dispatch(setOptionFromUrl(toVariableIdentifier(variable), urlValue));
|
||||
await dispatch(setOptionFromUrl(toKeyedVariableIdentifier(variable), urlValue));
|
||||
},
|
||||
updateOptions: async (variable) => {
|
||||
await dispatch(updateConstantVariableOptions(toVariableIdentifier(variable)));
|
||||
await dispatch(updateConstantVariableOptions(toKeyedVariableIdentifier(variable)));
|
||||
},
|
||||
getSaveModel: (variable) => {
|
||||
const { index, id, state, global, current, options, ...rest } = cloneDeep(variable);
|
||||
const { index, id, state, global, current, options, rootStateKey, ...rest } = cloneDeep(variable);
|
||||
return rest;
|
||||
},
|
||||
getValueForUrl: (variable) => {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { getVariableTestContext } from '../state/helpers';
|
||||
import { toVariablePayload, VariablesState } from '../state/types';
|
||||
import { VariablesState } from '../state/types';
|
||||
import { constantVariableReducer, createConstantOptionsFromQuery } from './reducer';
|
||||
import { ConstantVariableModel } from '../types';
|
||||
import { createConstantVariableAdapter } from './adapter';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
describe('constantVariableReducer', () => {
|
||||
const adapter = createConstantVariableAdapter();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { ConstantVariableModel, initialVariableModelState, VariableHide, VariableOption } from '../types';
|
||||
import { getInstanceState, VariablePayload, initialVariablesState, VariablesState } from '../state/types';
|
||||
import { initialVariablesState, VariablePayload, VariablesState } from '../state/types';
|
||||
import { getInstanceState } from '../state/selectors';
|
||||
|
||||
export const initialConstantVariableModelState: ConstantVariableModel = {
|
||||
...initialVariableModelState,
|
||||
|
||||
@@ -4,9 +4,10 @@ import { createCustomVariableAdapter } from './adapter';
|
||||
import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||
import { getRootReducer, RootReducerType } from '../state/helpers';
|
||||
import { CustomVariableModel, initialVariableModelState, VariableOption } from '../types';
|
||||
import { toVariablePayload } from '../state/types';
|
||||
import { addVariable, setCurrentVariableValue } from '../state/sharedReducer';
|
||||
import { createCustomOptionsFromQuery } from './reducer';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
|
||||
describe('custom actions', () => {
|
||||
variableAdapters.setInit(() => [createCustomVariableAdapter()]);
|
||||
@@ -22,6 +23,7 @@ describe('custom actions', () => {
|
||||
const variable: CustomVariableModel = {
|
||||
...initialVariableModelState,
|
||||
id: '0',
|
||||
rootStateKey: 'key',
|
||||
index: 0,
|
||||
type: 'custom',
|
||||
name: 'Custom',
|
||||
@@ -49,12 +51,14 @@ describe('custom actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenAsyncActionIsDispatched(updateCustomVariableOptions(toVariablePayload(variable)), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(updateCustomVariableOptions(toKeyedVariableIdentifier(variable)), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
createCustomOptionsFromQuery(toVariablePayload(variable)),
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option }))
|
||||
toKeyedAction('key', createCustomOptionsFromQuery(toVariablePayload(variable))),
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { validateVariableSelectionState } from '../state/actions';
|
||||
import { ThunkResult } from 'app/types';
|
||||
import { createCustomOptionsFromQuery } from './reducer';
|
||||
import { toVariablePayload, VariableIdentifier } from '../state/types';
|
||||
import { KeyedVariableIdentifier } from '../state/types';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
export const updateCustomVariableOptions = (identifier: VariableIdentifier): ThunkResult<void> => {
|
||||
export const updateCustomVariableOptions = (identifier: KeyedVariableIdentifier): ThunkResult<void> => {
|
||||
return async (dispatch) => {
|
||||
await dispatch(createCustomOptionsFromQuery(toVariablePayload(identifier)));
|
||||
const { rootStateKey } = identifier;
|
||||
await dispatch(toKeyedAction(rootStateKey, createCustomOptionsFromQuery(toVariablePayload(identifier))));
|
||||
await dispatch(validateVariableSelectionState(identifier));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,8 +6,7 @@ import { VariableAdapter } from '../adapters';
|
||||
import { customVariableReducer, initialCustomVariableModelState } from './reducer';
|
||||
import { CustomVariableEditor } from './CustomVariableEditor';
|
||||
import { updateCustomVariableOptions } from './actions';
|
||||
import { toVariableIdentifier } from '../state/types';
|
||||
import { isAllVariable } from '../utils';
|
||||
import { isAllVariable, toKeyedVariableIdentifier } from '../utils';
|
||||
import { optionPickerFactory } from '../pickers';
|
||||
import { ALL_VARIABLE_TEXT } from '../constants';
|
||||
|
||||
@@ -24,16 +23,16 @@ export const createCustomVariableAdapter = (): VariableAdapter<CustomVariableMod
|
||||
return false;
|
||||
},
|
||||
setValue: async (variable, option, emitChanges = false) => {
|
||||
await dispatch(setOptionAsCurrent(toVariableIdentifier(variable), option, emitChanges));
|
||||
await dispatch(setOptionAsCurrent(toKeyedVariableIdentifier(variable), option, emitChanges));
|
||||
},
|
||||
setValueFromUrl: async (variable, urlValue) => {
|
||||
await dispatch(setOptionFromUrl(toVariableIdentifier(variable), urlValue));
|
||||
await dispatch(setOptionFromUrl(toKeyedVariableIdentifier(variable), urlValue));
|
||||
},
|
||||
updateOptions: async (variable) => {
|
||||
await dispatch(updateCustomVariableOptions(toVariableIdentifier(variable)));
|
||||
await dispatch(updateCustomVariableOptions(toKeyedVariableIdentifier(variable)));
|
||||
},
|
||||
getSaveModel: (variable) => {
|
||||
const { index, id, state, global, ...rest } = cloneDeep(variable);
|
||||
const { index, id, state, global, rootStateKey, ...rest } = cloneDeep(variable);
|
||||
return rest;
|
||||
},
|
||||
getValueForUrl: (variable) => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { getVariableTestContext } from '../state/helpers';
|
||||
import { toVariablePayload, VariablesState } from '../state/types';
|
||||
import { VariablesState } from '../state/types';
|
||||
import { createCustomOptionsFromQuery, customVariableReducer } from './reducer';
|
||||
import { createCustomVariableAdapter } from './adapter';
|
||||
import { CustomVariableModel } from '../types';
|
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
describe('customVariableReducer', () => {
|
||||
const adapter = createCustomVariableAdapter();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { CustomVariableModel, initialVariableModelState, VariableOption } from '../types';
|
||||
import { getInstanceState, VariablePayload, initialVariablesState, VariablesState } from '../state/types';
|
||||
import { initialVariablesState, VariablePayload, VariablesState } from '../state/types';
|
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
|
||||
import { getInstanceState } from '../state/selectors';
|
||||
|
||||
export const initialCustomVariableModelState: CustomVariableModel = {
|
||||
...initialVariableModelState,
|
||||
|
||||
@@ -1,23 +1,38 @@
|
||||
import React, { FormEvent, PureComponent } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { InlineFieldRow, VerticalGroup } from '@grafana/ui';
|
||||
|
||||
import { DataSourceVariableModel, VariableWithMultiSupport } from '../types';
|
||||
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
|
||||
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
|
||||
import { initialVariableEditorState } from '../editor/reducer';
|
||||
import { initDataSourceVariableEditor } from './actions';
|
||||
import { StoreState } from '../../../types';
|
||||
import { changeVariableMultiValue } from '../state/actions';
|
||||
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
|
||||
import { VariableSelectField } from '../editor/VariableSelectField';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { VariableTextField } from '../editor/VariableTextField';
|
||||
import { getVariablesState } from '../state/selectors';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { getDatasourceVariableEditorState } from '../editor/selectors';
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
extended: getDatasourceVariableEditorState(state.templating.editor),
|
||||
});
|
||||
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => {
|
||||
const {
|
||||
variable: { rootStateKey },
|
||||
} = ownProps;
|
||||
if (!rootStateKey) {
|
||||
console.error('DataSourceVariableEditor: variable has no rootStateKey');
|
||||
return {
|
||||
extended: getDatasourceVariableEditorState(initialVariableEditorState),
|
||||
};
|
||||
}
|
||||
|
||||
const { editor } = getVariablesState(rootStateKey, state);
|
||||
return {
|
||||
extended: getDatasourceVariableEditorState(editor),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
initDataSourceVariableEditor,
|
||||
@@ -32,7 +47,13 @@ type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
|
||||
export class DataSourceVariableEditorUnConnected extends PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
this.props.initDataSourceVariableEditor();
|
||||
const { rootStateKey } = this.props.variable;
|
||||
if (!rootStateKey) {
|
||||
console.error('DataSourceVariableEditor: variable has no rootStateKey');
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.initDataSourceVariableEditor(rootStateKey);
|
||||
}
|
||||
|
||||
onRegExChange = (event: FormEvent<HTMLInputElement>) => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
|
||||
import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||
import { getRootReducer, RootReducerType } from '../state/helpers';
|
||||
import { toVariableIdentifier, toVariablePayload } from '../state/types';
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { createDataSourceVariableAdapter } from './adapter';
|
||||
import {
|
||||
@@ -16,6 +15,8 @@ import { addVariable, setCurrentVariableValue } from '../state/sharedReducer';
|
||||
import { changeVariableEditorExtended } from '../editor/reducer';
|
||||
import { datasourceBuilder } from '../shared/testing/builders';
|
||||
import { getDataSourceInstanceSetting } from '../shared/testing/helpers';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
|
||||
interface Args {
|
||||
sources?: DataSourceInstanceSettings[];
|
||||
@@ -27,7 +28,7 @@ function getTestContext({ sources = [], query, regex }: Args = {}) {
|
||||
const getListMock = jest.fn().mockReturnValue(sources);
|
||||
const getDatasourceSrvMock = jest.fn().mockReturnValue({ getList: getListMock });
|
||||
const dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrvMock };
|
||||
const datasource = datasourceBuilder().withId('0').withQuery(query).withRegEx(regex).build();
|
||||
const datasource = datasourceBuilder().withId('0').withRootStateKey('key').withQuery(query).withRegEx(regex).build();
|
||||
|
||||
return { getListMock, getDatasourceSrvMock, dependencies, datasource };
|
||||
}
|
||||
@@ -51,27 +52,36 @@ describe('data source actions', () => {
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(
|
||||
addVariable(toVariablePayload(datasource, { global: false, index: 0, model: datasource }))
|
||||
toKeyedAction(
|
||||
'key',
|
||||
addVariable(toVariablePayload(datasource, { global: false, index: 0, model: datasource }))
|
||||
)
|
||||
)
|
||||
.whenAsyncActionIsDispatched(
|
||||
updateDataSourceVariableOptions(toVariableIdentifier(datasource), dependencies),
|
||||
updateDataSourceVariableOptions(toKeyedVariableIdentifier(datasource), dependencies),
|
||||
true
|
||||
);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
createDataSourceOptions(
|
||||
toVariablePayload(
|
||||
{ type: 'datasource', id: '0' },
|
||||
{
|
||||
sources,
|
||||
regex: undefined as unknown as RegExp,
|
||||
}
|
||||
toKeyedAction(
|
||||
'key',
|
||||
createDataSourceOptions(
|
||||
toVariablePayload(
|
||||
{ type: 'datasource', id: '0' },
|
||||
{
|
||||
sources,
|
||||
regex: undefined as unknown as RegExp,
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'datasource', id: '0' },
|
||||
{ option: { text: 'first-name', value: 'first-name', selected: false } }
|
||||
toKeyedAction(
|
||||
'key',
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'datasource', id: '0' },
|
||||
{ option: { text: 'first-name', value: 'first-name', selected: false } }
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -99,27 +109,36 @@ describe('data source actions', () => {
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(
|
||||
addVariable(toVariablePayload(datasource, { global: false, index: 0, model: datasource }))
|
||||
toKeyedAction(
|
||||
'key',
|
||||
addVariable(toVariablePayload(datasource, { global: false, index: 0, model: datasource }))
|
||||
)
|
||||
)
|
||||
.whenAsyncActionIsDispatched(
|
||||
updateDataSourceVariableOptions(toVariableIdentifier(datasource), dependencies),
|
||||
updateDataSourceVariableOptions(toKeyedVariableIdentifier(datasource), dependencies),
|
||||
true
|
||||
);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
createDataSourceOptions(
|
||||
toVariablePayload(
|
||||
{ type: 'datasource', id: '0' },
|
||||
{
|
||||
sources,
|
||||
regex: /.*(second-name).*/,
|
||||
}
|
||||
toKeyedAction(
|
||||
'key',
|
||||
createDataSourceOptions(
|
||||
toVariablePayload(
|
||||
{ type: 'datasource', id: '0' },
|
||||
{
|
||||
sources,
|
||||
regex: /.*(second-name).*/,
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'datasource', id: '0' },
|
||||
{ option: { text: 'second-name', value: 'second-name', selected: false } }
|
||||
toKeyedAction(
|
||||
'key',
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'datasource', id: '0' },
|
||||
{ option: { text: 'second-name', value: 'second-name', selected: false } }
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -143,14 +162,17 @@ describe('data source actions', () => {
|
||||
|
||||
reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(initDataSourceVariableEditor(dependencies))
|
||||
.whenActionIsDispatched(initDataSourceVariableEditor('key', dependencies))
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
changeVariableEditorExtended({
|
||||
dataSourceTypes: [
|
||||
{ text: '', value: '' },
|
||||
{ text: 'mock-data-name', value: 'mock-data-id' },
|
||||
],
|
||||
})
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableEditorExtended({
|
||||
dataSourceTypes: [
|
||||
{ text: '', value: '' },
|
||||
{ text: 'mock-data-name', value: 'mock-data-id' },
|
||||
],
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
expect(getListMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { chain } from 'lodash';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { stringToJsRegex } from '@grafana/data';
|
||||
|
||||
import { toVariablePayload, VariableIdentifier } from '../state/types';
|
||||
import { KeyedVariableIdentifier } from '../state/types';
|
||||
import { ThunkResult } from '../../../types';
|
||||
import { createDataSourceOptions } from './reducer';
|
||||
import { validateVariableSelectionState } from '../state/actions';
|
||||
@@ -10,6 +10,8 @@ import { getDatasourceSrv } from '../../plugins/datasource_srv';
|
||||
import { getVariable } from '../state/selectors';
|
||||
import { DataSourceVariableModel } from '../types';
|
||||
import { changeVariableEditorExtended } from '../editor/reducer';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
export interface DataSourceVariableActionDependencies {
|
||||
getDatasourceSrv: typeof getDatasourceSrv;
|
||||
@@ -17,12 +19,13 @@ export interface DataSourceVariableActionDependencies {
|
||||
|
||||
export const updateDataSourceVariableOptions =
|
||||
(
|
||||
identifier: VariableIdentifier,
|
||||
identifier: KeyedVariableIdentifier,
|
||||
dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrv }
|
||||
): ThunkResult<void> =>
|
||||
async (dispatch, getState) => {
|
||||
const { rootStateKey } = identifier;
|
||||
const sources = dependencies.getDatasourceSrv().getList({ metrics: true, variables: false });
|
||||
const variableInState = getVariable<DataSourceVariableModel>(identifier.id, getState());
|
||||
const variableInState = getVariable<DataSourceVariableModel>(identifier, getState());
|
||||
let regex;
|
||||
|
||||
if (variableInState.regex) {
|
||||
@@ -30,12 +33,15 @@ export const updateDataSourceVariableOptions =
|
||||
regex = stringToJsRegex(regex);
|
||||
}
|
||||
|
||||
dispatch(createDataSourceOptions(toVariablePayload(identifier, { sources, regex })));
|
||||
dispatch(toKeyedAction(rootStateKey, createDataSourceOptions(toVariablePayload(identifier, { sources, regex }))));
|
||||
await dispatch(validateVariableSelectionState(identifier));
|
||||
};
|
||||
|
||||
export const initDataSourceVariableEditor =
|
||||
(dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrv }): ThunkResult<void> =>
|
||||
(
|
||||
key: string,
|
||||
dependencies: DataSourceVariableActionDependencies = { getDatasourceSrv: getDatasourceSrv }
|
||||
): ThunkResult<void> =>
|
||||
(dispatch) => {
|
||||
const dataSources = dependencies.getDatasourceSrv().getList({ metrics: true, variables: true });
|
||||
const dataSourceTypes = chain(dataSources)
|
||||
@@ -47,5 +53,5 @@ export const initDataSourceVariableEditor =
|
||||
|
||||
dataSourceTypes.unshift({ text: '', value: '' });
|
||||
|
||||
dispatch(changeVariableEditorExtended({ dataSourceTypes }));
|
||||
dispatch(toKeyedAction(key, changeVariableEditorExtended({ dataSourceTypes })));
|
||||
};
|
||||
|
||||
@@ -4,10 +4,9 @@ import { dispatch } from '../../../store/store';
|
||||
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions';
|
||||
import { VariableAdapter } from '../adapters';
|
||||
import { dataSourceVariableReducer, initialDataSourceVariableModelState } from './reducer';
|
||||
import { toVariableIdentifier } from '../state/types';
|
||||
import { DataSourceVariableEditor } from './DataSourceVariableEditor';
|
||||
import { updateDataSourceVariableOptions } from './actions';
|
||||
import { containsVariable, isAllVariable } from '../utils';
|
||||
import { containsVariable, isAllVariable, toKeyedVariableIdentifier } from '../utils';
|
||||
import { optionPickerFactory } from '../pickers';
|
||||
import { ALL_VARIABLE_TEXT } from '../constants';
|
||||
|
||||
@@ -27,16 +26,16 @@ export const createDataSourceVariableAdapter = (): VariableAdapter<DataSourceVar
|
||||
return false;
|
||||
},
|
||||
setValue: async (variable, option, emitChanges = false) => {
|
||||
await dispatch(setOptionAsCurrent(toVariableIdentifier(variable), option, emitChanges));
|
||||
await dispatch(setOptionAsCurrent(toKeyedVariableIdentifier(variable), option, emitChanges));
|
||||
},
|
||||
setValueFromUrl: async (variable, urlValue) => {
|
||||
await dispatch(setOptionFromUrl(toVariableIdentifier(variable), urlValue));
|
||||
await dispatch(setOptionFromUrl(toKeyedVariableIdentifier(variable), urlValue));
|
||||
},
|
||||
updateOptions: async (variable) => {
|
||||
await dispatch(updateDataSourceVariableOptions(toVariableIdentifier(variable)));
|
||||
await dispatch(updateDataSourceVariableOptions(toKeyedVariableIdentifier(variable)));
|
||||
},
|
||||
getSaveModel: (variable) => {
|
||||
const { index, id, state, global, ...rest } = cloneDeep(variable);
|
||||
const { index, id, state, global, rootStateKey, ...rest } = cloneDeep(variable);
|
||||
return { ...rest, options: [] };
|
||||
},
|
||||
getValueForUrl: (variable) => {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { createDataSourceOptions, dataSourceVariableReducer } from './reducer';
|
||||
import { DataSourceVariableModel } from '../types';
|
||||
import { getVariableTestContext } from '../state/helpers';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { createDataSourceVariableAdapter } from './adapter';
|
||||
import { toVariablePayload, VariablesState } from '../state/types';
|
||||
import { VariablesState } from '../state/types';
|
||||
import { getMockPlugins } from '../../plugins/__mocks__/pluginMocks';
|
||||
import { getDataSourceInstanceSetting } from '../shared/testing/helpers';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
describe('dataSourceVariableReducer', () => {
|
||||
const adapter = createDataSourceVariableAdapter();
|
||||
|
||||
@@ -2,8 +2,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
|
||||
import { DataSourceVariableModel, initialVariableModelState, VariableOption, VariableRefresh } from '../types';
|
||||
import { getInstanceState, initialVariablesState, VariablePayload, VariablesState } from '../state/types';
|
||||
import { initialVariablesState, VariablePayload, VariablesState } from '../state/types';
|
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
|
||||
import { getInstanceState } from '../state/selectors';
|
||||
|
||||
export const initialDataSourceVariableModelState: DataSourceVariableModel = {
|
||||
...initialVariableModelState,
|
||||
|
||||
@@ -4,14 +4,15 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { VariableWithMultiSupport } from '../types';
|
||||
import { VariableEditorProps } from './types';
|
||||
import { toVariableIdentifier, VariableIdentifier } from '../state/types';
|
||||
import { KeyedVariableIdentifier } from '../state/types';
|
||||
import { VariableSectionHeader } from './VariableSectionHeader';
|
||||
import { VariableSwitchField } from './VariableSwitchField';
|
||||
import { VariableTextField } from './VariableTextField';
|
||||
import { toKeyedVariableIdentifier } from '../utils';
|
||||
|
||||
export interface SelectionOptionsEditorProps<Model extends VariableWithMultiSupport = VariableWithMultiSupport>
|
||||
extends VariableEditorProps<Model> {
|
||||
onMultiChanged: (identifier: VariableIdentifier, value: boolean) => void;
|
||||
onMultiChanged: (identifier: KeyedVariableIdentifier, value: boolean) => void;
|
||||
}
|
||||
|
||||
export const SelectionOptionsEditor: FunctionComponent<SelectionOptionsEditorProps> = ({
|
||||
@@ -21,7 +22,7 @@ export const SelectionOptionsEditor: FunctionComponent<SelectionOptionsEditorPro
|
||||
}) => {
|
||||
const onMultiChanged = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
onMultiChangedProps(toVariableIdentifier(variable), event.target.checked);
|
||||
onMultiChangedProps(toKeyedVariableIdentifier(variable), event.target.checked);
|
||||
},
|
||||
[onMultiChangedProps, variable]
|
||||
);
|
||||
|
||||
@@ -1,36 +1,61 @@
|
||||
import React, { MouseEvent, PureComponent } from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { Icon, LinkButton } from '@grafana/ui';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
|
||||
import { StoreState } from '../../../types';
|
||||
import { KeyedVariableIdentifier } from '../state/types';
|
||||
import { StoreState, ThunkDispatch } from '../../../types';
|
||||
import { VariableEditorEditor } from './VariableEditorEditor';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { getEditorVariables } from '../state/selectors';
|
||||
import { getEditorVariables, getVariablesState } from '../state/selectors';
|
||||
import { switchToEditMode, switchToListMode, switchToNewMode } from './actions';
|
||||
import { changeVariableOrder, duplicateVariable, removeVariable } from '../state/sharedReducer';
|
||||
import { VariableEditorList } from './VariableEditorList';
|
||||
import { VariablesUnknownTable } from '../inspect/VariablesUnknownTable';
|
||||
import { VariablesDependenciesButton } from '../inspect/VariablesDependenciesButton';
|
||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
variables: getEditorVariables(state),
|
||||
idInEditor: state.templating.editor.id,
|
||||
dashboard: state.dashboard.getModel(),
|
||||
usagesNetwork: state.templating.inspect.usagesNetwork,
|
||||
usages: state.templating.inspect.usages,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
changeVariableOrder,
|
||||
duplicateVariable,
|
||||
removeVariable,
|
||||
switchToNewMode,
|
||||
switchToEditMode,
|
||||
switchToListMode,
|
||||
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => {
|
||||
const { uid } = ownProps.dashboard;
|
||||
const templatingState = getVariablesState(uid, state);
|
||||
return {
|
||||
variables: getEditorVariables(uid, state),
|
||||
idInEditor: templatingState.editor.id,
|
||||
usagesNetwork: templatingState.inspect.usagesNetwork,
|
||||
usages: templatingState.inspect.usages,
|
||||
};
|
||||
};
|
||||
|
||||
interface OwnProps {}
|
||||
const mapDispatchToProps = (dispatch: ThunkDispatch) => {
|
||||
return {
|
||||
...bindActionCreators({ switchToNewMode, switchToEditMode, switchToListMode }, dispatch),
|
||||
changeVariableOrder: (identifier: KeyedVariableIdentifier, fromIndex: number, toIndex: number) =>
|
||||
dispatch(
|
||||
toKeyedAction(
|
||||
identifier.rootStateKey,
|
||||
changeVariableOrder(toVariablePayload(identifier, { fromIndex, toIndex }))
|
||||
)
|
||||
),
|
||||
duplicateVariable: (identifier: KeyedVariableIdentifier) =>
|
||||
dispatch(
|
||||
toKeyedAction(
|
||||
identifier.rootStateKey,
|
||||
duplicateVariable(toVariablePayload(identifier, { newId: undefined as unknown as string }))
|
||||
)
|
||||
),
|
||||
removeVariable: (identifier: KeyedVariableIdentifier) => {
|
||||
dispatch(
|
||||
toKeyedAction(identifier.rootStateKey, removeVariable(toVariablePayload(identifier, { reIndex: true })))
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface OwnProps {
|
||||
dashboard: DashboardModel;
|
||||
}
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
@@ -38,32 +63,32 @@ type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
|
||||
class VariableEditorContainerUnconnected extends PureComponent<Props> {
|
||||
componentDidMount(): void {
|
||||
this.props.switchToListMode();
|
||||
this.props.switchToListMode(this.props.dashboard.uid);
|
||||
}
|
||||
|
||||
onChangeToListMode = (event: MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault();
|
||||
this.props.switchToListMode();
|
||||
this.props.switchToListMode(this.props.dashboard.uid);
|
||||
};
|
||||
|
||||
onEditVariable = (identifier: VariableIdentifier) => {
|
||||
onEditVariable = (identifier: KeyedVariableIdentifier) => {
|
||||
this.props.switchToEditMode(identifier);
|
||||
};
|
||||
|
||||
onNewVariable = () => {
|
||||
this.props.switchToNewMode();
|
||||
this.props.switchToNewMode(this.props.dashboard.uid);
|
||||
};
|
||||
|
||||
onChangeVariableOrder = (identifier: VariableIdentifier, fromIndex: number, toIndex: number) => {
|
||||
this.props.changeVariableOrder(toVariablePayload(identifier, { fromIndex, toIndex }));
|
||||
onChangeVariableOrder = (identifier: KeyedVariableIdentifier, fromIndex: number, toIndex: number) => {
|
||||
this.props.changeVariableOrder(identifier, fromIndex, toIndex);
|
||||
};
|
||||
|
||||
onDuplicateVariable = (identifier: VariableIdentifier) => {
|
||||
this.props.duplicateVariable(toVariablePayload(identifier, { newId: undefined as unknown as string }));
|
||||
onDuplicateVariable = (identifier: KeyedVariableIdentifier) => {
|
||||
this.props.duplicateVariable(identifier);
|
||||
};
|
||||
|
||||
onRemoveVariable = (identifier: VariableIdentifier) => {
|
||||
this.props.removeVariable(toVariablePayload(identifier, { reIndex: true }));
|
||||
onRemoveVariable = (identifier: KeyedVariableIdentifier) => {
|
||||
this.props.removeVariable(identifier);
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -117,7 +142,7 @@ class VariableEditorContainerUnconnected extends PureComponent<Props> {
|
||||
{!variableToEdit && this.props.variables.length > 0 && (
|
||||
<VariablesUnknownTable variables={this.props.variables} dashboard={this.props.dashboard} />
|
||||
)}
|
||||
{variableToEdit && <VariableEditorEditor identifier={toVariableIdentifier(variableToEdit)} />}
|
||||
{variableToEdit && <VariableEditorEditor identifier={toKeyedVariableIdentifier(variableToEdit)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import React, { FormEvent, PureComponent } from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { isEqual } from 'lodash';
|
||||
import { AppEvents, LoadingState, SelectableValue, VariableType } from '@grafana/data';
|
||||
import { Button, Icon, InlineFieldRow, VerticalGroup } from '@grafana/ui';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
|
||||
import { VariableHide, VariableModel } from '../types';
|
||||
import { KeyedVariableIdentifier } from '../state/types';
|
||||
import { VariableHide } from '../types';
|
||||
import { appEvents } from '../../../core/core';
|
||||
import { VariableValuesPreview } from './VariableValuesPreview';
|
||||
import { changeVariableName, onEditorUpdate, variableEditorMount, variableEditorUnMount } from './actions';
|
||||
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||
import { StoreState } from '../../../types';
|
||||
import { VariableEditorState } from './reducer';
|
||||
import { getVariable } from '../state/selectors';
|
||||
import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
|
||||
import { OnPropChangeArguments } from './types';
|
||||
import { changeVariableProp, changeVariableType } from '../state/sharedReducer';
|
||||
import { updateOptions } from '../state/actions';
|
||||
@@ -23,27 +20,41 @@ import { VariableSectionHeader } from './VariableSectionHeader';
|
||||
import { hasOptions } from '../guard';
|
||||
import { VariableTypeSelect } from './VariableTypeSelect';
|
||||
import { VariableHideSelect } from './VariableHideSelect';
|
||||
import { getVariable, getVariablesState } from '../state/selectors';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { StoreState, ThunkDispatch } from '../../../types';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
|
||||
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => ({
|
||||
editor: getVariablesState(ownProps.identifier.rootStateKey, state).editor,
|
||||
variable: getVariable(ownProps.identifier, state, false), // we could be renaming a variable and we don't want this to throw
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: ThunkDispatch) => {
|
||||
return {
|
||||
...bindActionCreators(
|
||||
{ variableEditorMount, variableEditorUnMount, changeVariableName, onEditorUpdate, updateOptions },
|
||||
dispatch
|
||||
),
|
||||
changeVariableProp: (identifier: KeyedVariableIdentifier, propName: string, propValue: any) =>
|
||||
dispatch(
|
||||
toKeyedAction(
|
||||
identifier.rootStateKey,
|
||||
changeVariableProp(toVariablePayload(identifier, { propName, propValue }))
|
||||
)
|
||||
),
|
||||
changeVariableType: (identifier: KeyedVariableIdentifier, newType: VariableType) =>
|
||||
dispatch(toKeyedAction(identifier.rootStateKey, changeVariableType(toVariablePayload(identifier, { newType })))),
|
||||
};
|
||||
};
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export interface OwnProps {
|
||||
identifier: VariableIdentifier;
|
||||
identifier: KeyedVariableIdentifier;
|
||||
}
|
||||
|
||||
interface ConnectedProps {
|
||||
editor: VariableEditorState;
|
||||
variable: VariableModel;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
variableEditorMount: typeof variableEditorMount;
|
||||
variableEditorUnMount: typeof variableEditorUnMount;
|
||||
changeVariableName: typeof changeVariableName;
|
||||
changeVariableProp: typeof changeVariableProp;
|
||||
onEditorUpdate: typeof onEditorUpdate;
|
||||
changeVariableType: typeof changeVariableType;
|
||||
updateOptions: typeof updateOptions;
|
||||
}
|
||||
|
||||
type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||
type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
|
||||
export class VariableEditorEditorUnConnected extends PureComponent<Props> {
|
||||
componentDidMount(): void {
|
||||
@@ -71,35 +82,26 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
|
||||
if (!option.value) {
|
||||
return;
|
||||
}
|
||||
this.props.changeVariableType(toVariablePayload(this.props.identifier, { newType: option.value }));
|
||||
this.props.changeVariableType(this.props.identifier, option.value);
|
||||
};
|
||||
|
||||
onLabelChange = (event: FormEvent<HTMLInputElement>) => {
|
||||
event.preventDefault();
|
||||
this.props.changeVariableProp(
|
||||
toVariablePayload(this.props.identifier, { propName: 'label', propValue: event.currentTarget.value })
|
||||
);
|
||||
this.props.changeVariableProp(this.props.identifier, 'label', event.currentTarget.value);
|
||||
};
|
||||
|
||||
onDescriptionChange = (event: FormEvent<HTMLInputElement>) => {
|
||||
this.props.changeVariableProp(
|
||||
toVariablePayload(this.props.identifier, { propName: 'description', propValue: event.currentTarget.value })
|
||||
);
|
||||
this.props.changeVariableProp(this.props.identifier, 'description', event.currentTarget.value);
|
||||
};
|
||||
|
||||
onHideChange = (option: SelectableValue<VariableHide>) => {
|
||||
this.props.changeVariableProp(
|
||||
toVariablePayload(this.props.identifier, {
|
||||
propName: 'hide',
|
||||
propValue: option.value,
|
||||
})
|
||||
);
|
||||
this.props.changeVariableProp(this.props.identifier, 'hide', option.value);
|
||||
};
|
||||
|
||||
onPropChanged = async ({ propName, propValue, updateOptions = false }: OnPropChangeArguments) => {
|
||||
this.props.changeVariableProp(toVariablePayload(this.props.identifier, { propName, propValue }));
|
||||
this.props.changeVariableProp(this.props.identifier, propName, propValue);
|
||||
if (updateOptions) {
|
||||
await this.props.updateOptions(toVariableIdentifier(this.props.variable));
|
||||
await this.props.updateOptions(toKeyedVariableIdentifier(this.props.variable));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -191,23 +193,4 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, ownProps) => ({
|
||||
editor: state.templating.editor,
|
||||
variable: getVariable(ownProps.identifier.id, state, false), // we could be renaming a variable and we don't want this to throw
|
||||
});
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
|
||||
variableEditorMount,
|
||||
variableEditorUnMount,
|
||||
changeVariableName,
|
||||
changeVariableProp,
|
||||
onEditorUpdate,
|
||||
changeVariableType,
|
||||
updateOptions,
|
||||
};
|
||||
|
||||
export const VariableEditorEditor = connectWithStore(
|
||||
VariableEditorEditorUnConnected,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
);
|
||||
export const VariableEditorEditor = connector(VariableEditorEditorUnConnected);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
|
||||
import { VariableModel } from '../types';
|
||||
import { VariableIdentifier } from '../state/types';
|
||||
import { KeyedVariableIdentifier } from '../state/types';
|
||||
import { UsagesToNetwork, VariableUsageTree } from '../inspect/utils';
|
||||
import { VariableEditorListRow } from './VariableEditorListRow';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
@@ -14,10 +14,10 @@ export interface Props {
|
||||
usages: VariableUsageTree[];
|
||||
usagesNetwork: UsagesToNetwork[];
|
||||
onAdd: () => void;
|
||||
onEdit: (identifier: VariableIdentifier) => void;
|
||||
onChangeOrder: (identifier: VariableIdentifier, fromIndex: number, toIndex: number) => void;
|
||||
onDuplicate: (identifier: VariableIdentifier) => void;
|
||||
onDelete: (identifier: VariableIdentifier) => void;
|
||||
onEdit: (identifier: KeyedVariableIdentifier) => void;
|
||||
onChangeOrder: (identifier: KeyedVariableIdentifier, fromIndex: number, toIndex: number) => void;
|
||||
onDuplicate: (identifier: KeyedVariableIdentifier) => void;
|
||||
onDelete: (identifier: KeyedVariableIdentifier) => void;
|
||||
}
|
||||
|
||||
export function VariableEditorList({
|
||||
|
||||
@@ -8,18 +8,19 @@ import { reportInteraction } from '@grafana/runtime';
|
||||
|
||||
import { getVariableUsages, UsagesToNetwork, VariableUsageTree } from '../inspect/utils';
|
||||
import { hasOptions, isAdHoc, isQuery } from '../guard';
|
||||
import { toVariableIdentifier, VariableIdentifier } from '../state/types';
|
||||
import { KeyedVariableIdentifier } from '../state/types';
|
||||
import { VariableUsagesButton } from '../inspect/VariableUsagesButton';
|
||||
import { VariableModel } from '../types';
|
||||
import { toKeyedVariableIdentifier } from '../utils';
|
||||
|
||||
export interface VariableEditorListRowProps {
|
||||
index: number;
|
||||
variable: VariableModel;
|
||||
usageTree: VariableUsageTree[];
|
||||
usagesNetwork: UsagesToNetwork[];
|
||||
onEdit: (identifier: VariableIdentifier) => void;
|
||||
onDuplicate: (identifier: VariableIdentifier) => void;
|
||||
onDelete: (identifier: VariableIdentifier) => void;
|
||||
onEdit: (identifier: KeyedVariableIdentifier) => void;
|
||||
onDuplicate: (identifier: KeyedVariableIdentifier) => void;
|
||||
onDelete: (identifier: KeyedVariableIdentifier) => void;
|
||||
}
|
||||
|
||||
export function VariableEditorListRow({
|
||||
@@ -36,7 +37,7 @@ export function VariableEditorListRow({
|
||||
const definition = getDefinition(variable);
|
||||
const usages = getVariableUsages(variable.id, usageTree);
|
||||
const passed = usages > 0 || isAdHoc(variable);
|
||||
const identifier = toVariableIdentifier(variable);
|
||||
const identifier = toKeyedVariableIdentifier(variable);
|
||||
|
||||
return (
|
||||
<Draggable draggableId={JSON.stringify(identifier)} index={index}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ThunkResult } from '../../../types';
|
||||
import { getEditorVariables, getNewVariableIndex, getVariable, getVariables } from '../state/selectors';
|
||||
import { getEditorVariables, getNewVariableIndex, getVariable, getVariablesByKey } from '../state/selectors';
|
||||
import {
|
||||
changeVariableNameFailed,
|
||||
changeVariableNameSucceeded,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
variableEditorUnMounted,
|
||||
} from './reducer';
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { AddVariable, toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
|
||||
import { AddVariable, KeyedVariableIdentifier, VariableIdentifier } from '../state/types';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { VariableType } from '@grafana/data';
|
||||
import { addVariable, removeVariable } from '../state/sharedReducer';
|
||||
@@ -17,28 +17,33 @@ import { updateOptions } from '../state/actions';
|
||||
import { VariableModel } from '../types';
|
||||
import { initInspect } from '../inspect/reducer';
|
||||
import { createUsagesNetwork, transformUsagesToNetwork } from '../inspect/utils';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
|
||||
export const variableEditorMount = (identifier: VariableIdentifier): ThunkResult<void> => {
|
||||
export const variableEditorMount = (identifier: KeyedVariableIdentifier): ThunkResult<void> => {
|
||||
return async (dispatch) => {
|
||||
dispatch(variableEditorMounted({ name: getVariable(identifier.id).name }));
|
||||
const { rootStateKey } = identifier;
|
||||
dispatch(toKeyedAction(rootStateKey, variableEditorMounted({ name: getVariable(identifier).name })));
|
||||
};
|
||||
};
|
||||
|
||||
export const variableEditorUnMount = (identifier: VariableIdentifier): ThunkResult<void> => {
|
||||
export const variableEditorUnMount = (identifier: KeyedVariableIdentifier): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch(variableEditorUnMounted(toVariablePayload(identifier)));
|
||||
const { rootStateKey } = identifier;
|
||||
dispatch(toKeyedAction(rootStateKey, variableEditorUnMounted(toVariablePayload(identifier))));
|
||||
};
|
||||
};
|
||||
|
||||
export const onEditorUpdate = (identifier: VariableIdentifier): ThunkResult<void> => {
|
||||
export const onEditorUpdate = (identifier: KeyedVariableIdentifier): ThunkResult<void> => {
|
||||
return async (dispatch) => {
|
||||
await dispatch(updateOptions(identifier));
|
||||
dispatch(switchToListMode());
|
||||
dispatch(switchToListMode(identifier.rootStateKey));
|
||||
};
|
||||
};
|
||||
|
||||
export const changeVariableName = (identifier: VariableIdentifier, newName: string): ThunkResult<void> => {
|
||||
export const changeVariableName = (identifier: KeyedVariableIdentifier, newName: string): ThunkResult<void> => {
|
||||
return (dispatch, getState) => {
|
||||
const { id, rootStateKey: uid } = identifier;
|
||||
let errorText = null;
|
||||
if (!newName.match(/^(?!__).*$/)) {
|
||||
errorText = "Template names cannot begin with '__', that's reserved for Grafana's global variables";
|
||||
@@ -48,15 +53,15 @@ export const changeVariableName = (identifier: VariableIdentifier, newName: stri
|
||||
errorText = 'Only word and digit characters are allowed in variable names';
|
||||
}
|
||||
|
||||
const variables = getVariables(getState());
|
||||
const foundVariables = variables.filter((v) => v.name === newName && v.id !== identifier.id);
|
||||
const variables = getVariablesByKey(uid, getState());
|
||||
const foundVariables = variables.filter((v) => v.name === newName && v.id !== id);
|
||||
|
||||
if (foundVariables.length) {
|
||||
errorText = 'Variable with the same name already exists';
|
||||
}
|
||||
|
||||
if (errorText) {
|
||||
dispatch(changeVariableNameFailed({ newName, errorText }));
|
||||
dispatch(toKeyedAction(uid, changeVariableNameFailed({ newName, errorText })));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -65,54 +70,61 @@ export const changeVariableName = (identifier: VariableIdentifier, newName: stri
|
||||
};
|
||||
|
||||
export const completeChangeVariableName =
|
||||
(identifier: VariableIdentifier, newName: string): ThunkResult<void> =>
|
||||
(identifier: KeyedVariableIdentifier, newName: string): ThunkResult<void> =>
|
||||
(dispatch, getState) => {
|
||||
const originalVariable = getVariable(identifier.id, getState());
|
||||
const { rootStateKey } = identifier;
|
||||
const originalVariable = getVariable(identifier, getState());
|
||||
if (originalVariable.name === newName) {
|
||||
dispatch(changeVariableNameSucceeded(toVariablePayload(identifier, { newName })));
|
||||
dispatch(toKeyedAction(rootStateKey, changeVariableNameSucceeded(toVariablePayload(identifier, { newName }))));
|
||||
return;
|
||||
}
|
||||
const model = { ...cloneDeep(originalVariable), name: newName, id: newName };
|
||||
const global = originalVariable.global;
|
||||
const index = originalVariable.index;
|
||||
const renamedIdentifier = toVariableIdentifier(model);
|
||||
const renamedIdentifier = toKeyedVariableIdentifier(model);
|
||||
|
||||
dispatch(addVariable(toVariablePayload(renamedIdentifier, { global, index, model })));
|
||||
dispatch(changeVariableNameSucceeded(toVariablePayload(renamedIdentifier, { newName })));
|
||||
dispatch(toKeyedAction(rootStateKey, addVariable(toVariablePayload(renamedIdentifier, { global, index, model }))));
|
||||
dispatch(
|
||||
toKeyedAction(rootStateKey, changeVariableNameSucceeded(toVariablePayload(renamedIdentifier, { newName })))
|
||||
);
|
||||
dispatch(switchToEditMode(renamedIdentifier));
|
||||
dispatch(removeVariable(toVariablePayload(identifier, { reIndex: false })));
|
||||
dispatch(toKeyedAction(rootStateKey, removeVariable(toVariablePayload(identifier, { reIndex: false }))));
|
||||
};
|
||||
|
||||
export const switchToNewMode =
|
||||
(type: VariableType = 'query'): ThunkResult<void> =>
|
||||
(key: string, type: VariableType = 'query'): ThunkResult<void> =>
|
||||
(dispatch, getState) => {
|
||||
const id = getNextAvailableId(type, getVariables(getState()));
|
||||
const identifier = { type, id };
|
||||
const id = getNextAvailableId(type, getVariablesByKey(key, getState()));
|
||||
const identifier: VariableIdentifier = { type, id };
|
||||
const global = false;
|
||||
const index = getNewVariableIndex(getState());
|
||||
const model = cloneDeep(variableAdapters.get(type).initialState);
|
||||
const index = getNewVariableIndex(key, getState());
|
||||
const model: VariableModel = cloneDeep(variableAdapters.get(type).initialState);
|
||||
model.id = id;
|
||||
model.name = id;
|
||||
dispatch(addVariable(toVariablePayload<AddVariable>(identifier, { global, model, index })));
|
||||
dispatch(setIdInEditor({ id: identifier.id }));
|
||||
model.rootStateKey = key;
|
||||
dispatch(toKeyedAction(key, addVariable(toVariablePayload<AddVariable>(identifier, { global, model, index }))));
|
||||
dispatch(toKeyedAction(key, setIdInEditor({ id: identifier.id })));
|
||||
};
|
||||
|
||||
export const switchToEditMode =
|
||||
(identifier: VariableIdentifier): ThunkResult<void> =>
|
||||
(identifier: KeyedVariableIdentifier): ThunkResult<void> =>
|
||||
(dispatch) => {
|
||||
dispatch(setIdInEditor({ id: identifier.id }));
|
||||
const { rootStateKey } = identifier;
|
||||
dispatch(toKeyedAction(rootStateKey, setIdInEditor({ id: identifier.id })));
|
||||
};
|
||||
|
||||
export const switchToListMode = (): ThunkResult<void> => (dispatch, getState) => {
|
||||
dispatch(clearIdInEditor());
|
||||
const state = getState();
|
||||
const variables = getEditorVariables(state);
|
||||
const dashboard = state.dashboard.getModel();
|
||||
const { usages } = createUsagesNetwork(variables, dashboard);
|
||||
const usagesNetwork = transformUsagesToNetwork(usages);
|
||||
export const switchToListMode =
|
||||
(key: string): ThunkResult<void> =>
|
||||
(dispatch, getState) => {
|
||||
dispatch(toKeyedAction(key, clearIdInEditor()));
|
||||
const state = getState();
|
||||
const variables = getEditorVariables(key, state);
|
||||
const dashboard = state.dashboard.getModel();
|
||||
const { usages } = createUsagesNetwork(variables, dashboard);
|
||||
const usagesNetwork = transformUsagesToNetwork(usages);
|
||||
|
||||
dispatch(initInspect({ usages, usagesNetwork }));
|
||||
};
|
||||
dispatch(toKeyedAction(key, initInspect({ usages, usagesNetwork })));
|
||||
};
|
||||
|
||||
export function getNextAvailableId(type: VariableType, variables: VariableModel[]): string {
|
||||
let counter = 0;
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
VariableEditorState,
|
||||
variableEditorUnMounted,
|
||||
} from './reducer';
|
||||
import { toVariablePayload } from '../state/types';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
describe('variableEditorReducer', () => {
|
||||
describe('when setIdInEditor is dispatched', () => {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { createQueryVariableAdapter } from './query/adapter';
|
||||
import { getVariablesUrlParams } from './getAllVariableValuesForUrl';
|
||||
import { initTemplateSrv } from '../../../test/helpers/initTemplateSrv';
|
||||
|
||||
const key = 'key';
|
||||
|
||||
describe('getAllVariableValuesForUrl', () => {
|
||||
beforeAll(() => {
|
||||
variableAdapters.register(createQueryVariableAdapter());
|
||||
@@ -12,10 +14,11 @@ describe('getAllVariableValuesForUrl', () => {
|
||||
describe('with multi value', () => {
|
||||
beforeEach(() => {
|
||||
setTemplateSrv(
|
||||
initTemplateSrv([
|
||||
initTemplateSrv(key, [
|
||||
{
|
||||
type: 'query',
|
||||
name: 'test',
|
||||
rootStateKey: key,
|
||||
current: { value: ['val1', 'val2'] },
|
||||
getValueForUrl: function () {
|
||||
return this.current.value;
|
||||
@@ -34,9 +37,10 @@ describe('getAllVariableValuesForUrl', () => {
|
||||
describe('skip url sync', () => {
|
||||
beforeEach(() => {
|
||||
setTemplateSrv(
|
||||
initTemplateSrv([
|
||||
initTemplateSrv(key, [
|
||||
{
|
||||
name: 'test',
|
||||
rootStateKey: key,
|
||||
skipUrlSync: true,
|
||||
current: { value: 'value' },
|
||||
getValueForUrl: function () {
|
||||
@@ -56,10 +60,11 @@ describe('getAllVariableValuesForUrl', () => {
|
||||
describe('with multi value with skip url sync', () => {
|
||||
beforeEach(() => {
|
||||
setTemplateSrv(
|
||||
initTemplateSrv([
|
||||
initTemplateSrv(key, [
|
||||
{
|
||||
type: 'query',
|
||||
name: 'test',
|
||||
rootStateKey: key,
|
||||
skipUrlSync: true,
|
||||
current: { value: ['val1', 'val2'] },
|
||||
getValueForUrl: function () {
|
||||
@@ -78,7 +83,9 @@ describe('getAllVariableValuesForUrl', () => {
|
||||
|
||||
describe('fillVariableValuesForUrl with multi value and scopedVars', () => {
|
||||
beforeEach(() => {
|
||||
setTemplateSrv(initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]));
|
||||
setTemplateSrv(
|
||||
initTemplateSrv(key, [{ type: 'query', name: 'test', rootStateKey: key, current: { value: ['val1', 'val2'] } }])
|
||||
);
|
||||
});
|
||||
|
||||
it('should set scoped value as url params', () => {
|
||||
@@ -91,7 +98,9 @@ describe('getAllVariableValuesForUrl', () => {
|
||||
|
||||
describe('fillVariableValuesForUrl with multi value, scopedVars and skip url sync', () => {
|
||||
beforeEach(() => {
|
||||
setTemplateSrv(initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]));
|
||||
setTemplateSrv(
|
||||
initTemplateSrv(key, [{ type: 'query', name: 'test', rootStateKey: key, current: { value: ['val1', 'val2'] } }])
|
||||
);
|
||||
});
|
||||
|
||||
it('should not set scoped value as url params', () => {
|
||||
|
||||
@@ -6,7 +6,6 @@ export function getVariablesUrlParams(scopedVars?: ScopedVars): UrlQueryMap {
|
||||
const params: UrlQueryMap = {};
|
||||
const variables = getTemplateSrv().getVariables();
|
||||
|
||||
// console.log(variables)
|
||||
for (let i = 0; i < variables.length; i++) {
|
||||
const variable = variables[i];
|
||||
if (scopedVars && scopedVars[variable.name] !== void 0) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getRootReducer, RootReducerType } from '../state/helpers';
|
||||
import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||
import { toVariableIdentifier, toVariablePayload } from '../state/types';
|
||||
import { updateAutoValue, UpdateAutoValueDependencies, updateIntervalVariableOptions } from './actions';
|
||||
import { createIntervalOptions } from './reducer';
|
||||
import {
|
||||
@@ -20,25 +19,37 @@ import { notifyApp } from '../../../core/actions';
|
||||
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
|
||||
import { variablesInitTransaction } from '../state/transactionReducer';
|
||||
import { afterEach, beforeEach } from '../../../../test/lib/common';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
|
||||
describe('interval actions', () => {
|
||||
variableAdapters.setInit(() => [createIntervalVariableAdapter()]);
|
||||
describe('when updateIntervalVariableOptions is dispatched', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const interval = intervalBuilder().withId('0').withQuery('1s,1m,1h,1d').withAuto(false).build();
|
||||
const interval = intervalBuilder()
|
||||
.withId('0')
|
||||
.withRootStateKey('key')
|
||||
.withQuery('1s,1m,1h,1d')
|
||||
.withAuto(false)
|
||||
.build();
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval })))
|
||||
.whenAsyncActionIsDispatched(updateIntervalVariableOptions(toVariableIdentifier(interval)), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(updateIntervalVariableOptions(toKeyedVariableIdentifier(interval)), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
createIntervalOptions({ type: 'interval', id: '0', data: undefined }),
|
||||
setCurrentVariableValue({
|
||||
type: 'interval',
|
||||
id: '0',
|
||||
data: { option: { text: '1s', value: '1s', selected: false } },
|
||||
})
|
||||
toKeyedAction('key', createIntervalOptions({ type: 'interval', id: '0', data: undefined })),
|
||||
toKeyedAction(
|
||||
'key',
|
||||
setCurrentVariableValue({
|
||||
type: 'interval',
|
||||
id: '0',
|
||||
data: { option: { text: '1s', value: '1s', selected: false } },
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -67,6 +78,7 @@ describe('interval actions', () => {
|
||||
it('then an notifyApp action should be dispatched', async () => {
|
||||
const interval = intervalBuilder()
|
||||
.withId('0')
|
||||
.withRootStateKey('key')
|
||||
.withQuery('1s,1m,1h,1d')
|
||||
.withAuto(true)
|
||||
.withAutoMin('1xyz') // illegal interval string
|
||||
@@ -74,21 +86,26 @@ describe('interval actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval })))
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenAsyncActionIsDispatched(updateOptions(toVariableIdentifier(interval)), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenAsyncActionIsDispatched(updateOptions(toKeyedVariableIdentifier(interval)), true);
|
||||
|
||||
tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
|
||||
const expectedNumberOfActions = 4;
|
||||
expect(dispatchedActions[0]).toEqual(variableStateFetching(toVariablePayload(interval)));
|
||||
expect(dispatchedActions[1]).toEqual(createIntervalOptions(toVariablePayload(interval)));
|
||||
expect(dispatchedActions[0]).toEqual(toKeyedAction('key', variableStateFetching(toVariablePayload(interval))));
|
||||
expect(dispatchedActions[1]).toEqual(toKeyedAction('key', createIntervalOptions(toVariablePayload(interval))));
|
||||
expect(dispatchedActions[2]).toEqual(
|
||||
variableStateFailed(
|
||||
toVariablePayload(interval, {
|
||||
error: new Error(
|
||||
'Invalid interval string, has to be either unit-less or end with one of the following units: "y, M, w, d, h, m, s, ms"'
|
||||
),
|
||||
})
|
||||
toKeyedAction(
|
||||
'key',
|
||||
variableStateFailed(
|
||||
toVariablePayload(interval, {
|
||||
error: new Error(
|
||||
'Invalid interval string, has to be either unit-less or end with one of the following units: "y, M, w, d, h, m, s, ms"'
|
||||
),
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -107,6 +124,7 @@ describe('interval actions', () => {
|
||||
it('then no actions are dispatched', async () => {
|
||||
const interval = intervalBuilder()
|
||||
.withId('0')
|
||||
.withRootStateKey('key')
|
||||
.withQuery('1s,1m,1h,1d')
|
||||
.withAuto(true)
|
||||
.withAutoMin('1xyz') // illegal interval string
|
||||
@@ -115,9 +133,9 @@ describe('interval actions', () => {
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(
|
||||
addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval }))
|
||||
toKeyedAction('key', addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(updateOptions(toVariableIdentifier(interval)), true);
|
||||
.whenAsyncActionIsDispatched(updateOptions(toKeyedVariableIdentifier(interval)), true);
|
||||
|
||||
tester.thenNoActionsWhereDispatched();
|
||||
});
|
||||
@@ -127,7 +145,7 @@ describe('interval actions', () => {
|
||||
describe('when updateAutoValue is dispatched', () => {
|
||||
describe('and auto is false', () => {
|
||||
it('then no dependencies are called', async () => {
|
||||
const interval = intervalBuilder().withId('0').withAuto(false).build();
|
||||
const interval = intervalBuilder().withId('0').withRootStateKey('key').withAuto(false).build();
|
||||
|
||||
const dependencies: UpdateAutoValueDependencies = {
|
||||
calculateInterval: jest.fn(),
|
||||
@@ -151,9 +169,9 @@ describe('interval actions', () => {
|
||||
await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(
|
||||
addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval }))
|
||||
toKeyedAction('key', addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(updateAutoValue(toVariableIdentifier(interval), dependencies), true);
|
||||
.whenAsyncActionIsDispatched(updateAutoValue(toKeyedVariableIdentifier(interval), dependencies), true);
|
||||
|
||||
expect(dependencies.calculateInterval).toHaveBeenCalledTimes(0);
|
||||
expect(dependencies.getTimeSrv().timeRange).toHaveBeenCalledTimes(0);
|
||||
@@ -165,6 +183,7 @@ describe('interval actions', () => {
|
||||
it('then correct dependencies are called', async () => {
|
||||
const interval = intervalBuilder()
|
||||
.withId('0')
|
||||
.withRootStateKey('key')
|
||||
.withName('intervalName')
|
||||
.withAuto(true)
|
||||
.withAutoCount(33)
|
||||
@@ -195,9 +214,9 @@ describe('interval actions', () => {
|
||||
await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(
|
||||
addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval }))
|
||||
toKeyedAction('key', addVariable(toVariablePayload(interval, { global: false, index: 0, model: interval })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(updateAutoValue(toVariableIdentifier(interval), dependencies), true);
|
||||
.whenAsyncActionIsDispatched(updateAutoValue(toKeyedVariableIdentifier(interval), dependencies), true);
|
||||
|
||||
expect(dependencies.calculateInterval).toHaveBeenCalledTimes(1);
|
||||
expect(dependencies.calculateInterval).toHaveBeenCalledWith(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { rangeUtil } from '@grafana/data';
|
||||
|
||||
import { toVariablePayload, VariableIdentifier } from '../state/types';
|
||||
import { KeyedVariableIdentifier } from '../state/types';
|
||||
import { ThunkResult } from '../../../types';
|
||||
import { createIntervalOptions } from './reducer';
|
||||
import { validateVariableSelectionState } from '../state/actions';
|
||||
@@ -8,11 +8,14 @@ import { getVariable } from '../state/selectors';
|
||||
import { IntervalVariableModel } from '../types';
|
||||
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
import { getTemplateSrv, TemplateSrv } from '../../templating/template_srv';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
export const updateIntervalVariableOptions =
|
||||
(identifier: VariableIdentifier): ThunkResult<void> =>
|
||||
(identifier: KeyedVariableIdentifier): ThunkResult<void> =>
|
||||
async (dispatch) => {
|
||||
await dispatch(createIntervalOptions(toVariablePayload(identifier)));
|
||||
const { rootStateKey } = identifier;
|
||||
await dispatch(toKeyedAction(rootStateKey, createIntervalOptions(toVariablePayload(identifier))));
|
||||
await dispatch(updateAutoValue(identifier));
|
||||
await dispatch(validateVariableSelectionState(identifier));
|
||||
};
|
||||
@@ -25,7 +28,7 @@ export interface UpdateAutoValueDependencies {
|
||||
|
||||
export const updateAutoValue =
|
||||
(
|
||||
identifier: VariableIdentifier,
|
||||
identifier: KeyedVariableIdentifier,
|
||||
dependencies: UpdateAutoValueDependencies = {
|
||||
calculateInterval: rangeUtil.calculateInterval,
|
||||
getTimeSrv: getTimeSrv,
|
||||
@@ -33,7 +36,7 @@ export const updateAutoValue =
|
||||
}
|
||||
): ThunkResult<void> =>
|
||||
(dispatch, getState) => {
|
||||
const variableInState = getVariable<IntervalVariableModel>(identifier.id, getState());
|
||||
const variableInState = getVariable<IntervalVariableModel>(identifier, getState());
|
||||
if (variableInState.auto) {
|
||||
const res = dependencies.calculateInterval(
|
||||
dependencies.getTimeSrv().timeRange(),
|
||||
|
||||
@@ -4,10 +4,10 @@ import { dispatch } from '../../../store/store';
|
||||
import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions';
|
||||
import { VariableAdapter } from '../adapters';
|
||||
import { initialIntervalVariableModelState, intervalVariableReducer } from './reducer';
|
||||
import { toVariableIdentifier } from '../state/types';
|
||||
import { IntervalVariableEditor } from './IntervalVariableEditor';
|
||||
import { updateAutoValue, updateIntervalVariableOptions } from './actions';
|
||||
import { optionPickerFactory } from '../pickers';
|
||||
import { toKeyedVariableIdentifier } from '../utils';
|
||||
|
||||
export const createIntervalVariableAdapter = (): VariableAdapter<IntervalVariableModel> => {
|
||||
return {
|
||||
@@ -22,18 +22,18 @@ export const createIntervalVariableAdapter = (): VariableAdapter<IntervalVariabl
|
||||
return false;
|
||||
},
|
||||
setValue: async (variable, option, emitChanges = false) => {
|
||||
await dispatch(updateAutoValue(toVariableIdentifier(variable)));
|
||||
await dispatch(setOptionAsCurrent(toVariableIdentifier(variable), option, emitChanges));
|
||||
await dispatch(updateAutoValue(toKeyedVariableIdentifier(variable)));
|
||||
await dispatch(setOptionAsCurrent(toKeyedVariableIdentifier(variable), option, emitChanges));
|
||||
},
|
||||
setValueFromUrl: async (variable, urlValue) => {
|
||||
await dispatch(updateAutoValue(toVariableIdentifier(variable)));
|
||||
await dispatch(setOptionFromUrl(toVariableIdentifier(variable), urlValue));
|
||||
await dispatch(updateAutoValue(toKeyedVariableIdentifier(variable)));
|
||||
await dispatch(setOptionFromUrl(toKeyedVariableIdentifier(variable), urlValue));
|
||||
},
|
||||
updateOptions: async (variable) => {
|
||||
await dispatch(updateIntervalVariableOptions(toVariableIdentifier(variable)));
|
||||
await dispatch(updateIntervalVariableOptions(toKeyedVariableIdentifier(variable)));
|
||||
},
|
||||
getSaveModel: (variable) => {
|
||||
const { index, id, state, global, ...rest } = cloneDeep(variable);
|
||||
const { index, id, state, global, rootStateKey, ...rest } = cloneDeep(variable);
|
||||
return rest;
|
||||
},
|
||||
getValueForUrl: (variable) => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { getVariableTestContext } from '../state/helpers';
|
||||
import { toVariablePayload, VariablesState } from '../state/types';
|
||||
import { VariablesState } from '../state/types';
|
||||
import { createIntervalVariableAdapter } from './adapter';
|
||||
import { IntervalVariableModel } from '../types';
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { createIntervalOptions, intervalVariableReducer } from './reducer';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
describe('intervalVariableReducer', () => {
|
||||
const adapter = createIntervalVariableAdapter();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { initialVariableModelState, IntervalVariableModel, VariableOption, VariableRefresh } from '../types';
|
||||
import { getInstanceState, VariablePayload, initialVariablesState, VariablesState } from '../state/types';
|
||||
import { initialVariablesState, VariablePayload, VariablesState } from '../state/types';
|
||||
import { map } from 'lodash';
|
||||
import { getInstanceState } from '../state/selectors';
|
||||
|
||||
export const initialIntervalVariableModelState: IntervalVariableModel = {
|
||||
...initialVariableModelState,
|
||||
|
||||
@@ -9,7 +9,8 @@ import { VariablePickerProps } from '../types';
|
||||
import { QueryVariableModel, VariableWithMultiSupport, VariableWithOptions } from '../../types';
|
||||
import { queryBuilder } from '../../shared/testing/builders';
|
||||
import { optionPickerFactory } from './OptionsPicker';
|
||||
import { initialState, OptionsPickerState } from './reducer';
|
||||
import { initialOptionPickerState, OptionsPickerState } from './reducer';
|
||||
import { getPreloadedState } from '../../state/helpers';
|
||||
|
||||
interface Args {
|
||||
pickerState?: Partial<OptionsPickerState>;
|
||||
@@ -18,6 +19,7 @@ interface Args {
|
||||
|
||||
const defaultVariable = queryBuilder()
|
||||
.withId('query0')
|
||||
.withRootStateKey('key')
|
||||
.withName('query0')
|
||||
.withMulti()
|
||||
.withCurrent(['A', 'C'])
|
||||
@@ -35,17 +37,16 @@ function setupTestContext({ pickerState = {}, variable = {} }: Args = {}) {
|
||||
onVariableChange,
|
||||
};
|
||||
const Picker = optionPickerFactory();
|
||||
const optionsPicker: OptionsPickerState = { ...initialState, ...pickerState };
|
||||
const optionsPicker: OptionsPickerState = { ...initialOptionPickerState, ...pickerState };
|
||||
const dispatch = jest.fn();
|
||||
const subscribe = jest.fn();
|
||||
const getState = jest.fn().mockReturnValue({
|
||||
templating: {
|
||||
variables: {
|
||||
[v.id]: { ...v },
|
||||
},
|
||||
optionsPicker,
|
||||
const templatingState = {
|
||||
variables: {
|
||||
[v.id]: { ...v },
|
||||
},
|
||||
});
|
||||
optionsPicker,
|
||||
};
|
||||
const getState = jest.fn().mockReturnValue(getPreloadedState('key', templatingState));
|
||||
const store: any = { getState, dispatch, subscribe };
|
||||
const { rerender } = render(
|
||||
<Provider store={store}>
|
||||
|
||||
@@ -1,36 +1,58 @@
|
||||
import React, { ComponentType, PureComponent } from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { ClickOutsideWrapper } from '@grafana/ui';
|
||||
import { LoadingState } from '@grafana/data';
|
||||
|
||||
import { StoreState } from 'app/types';
|
||||
import { StoreState, ThunkDispatch } from 'app/types';
|
||||
import { VariableInput } from '../shared/VariableInput';
|
||||
import { commitChangesToVariable, filterOrSearchOptions, navigateOptions, openOptions } from './actions';
|
||||
import { OptionsPickerState, toggleAllOptions, toggleOption } from './reducer';
|
||||
import { initialOptionPickerState, OptionsPickerState, toggleAllOptions, toggleOption } from './reducer';
|
||||
import { VariableOption, VariableWithMultiSupport, VariableWithOptions } from '../../types';
|
||||
import { VariableOptions } from '../shared/VariableOptions';
|
||||
import { isMulti } from '../../guard';
|
||||
import { VariablePickerProps } from '../types';
|
||||
import { NavigationKey, VariablePickerProps } from '../types';
|
||||
import { formatVariableLabel } from '../../shared/formatVariable';
|
||||
import { toVariableIdentifier } from '../../state/types';
|
||||
import { KeyedVariableIdentifier } from '../../state/types';
|
||||
import { getVariableQueryRunner } from '../../query/VariableQueryRunner';
|
||||
import { VariableLink } from '../shared/VariableLink';
|
||||
import { getVariablesState } from '../../state/selectors';
|
||||
import { toKeyedAction } from '../../state/keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier } from '../../utils';
|
||||
|
||||
export const optionPickerFactory = <Model extends VariableWithOptions | VariableWithMultiSupport>(): ComponentType<
|
||||
VariablePickerProps<Model>
|
||||
> => {
|
||||
const mapDispatchToProps = {
|
||||
openOptions,
|
||||
commitChangesToVariable,
|
||||
filterOrSearchOptions,
|
||||
toggleAllOptions,
|
||||
toggleOption,
|
||||
navigateOptions,
|
||||
const mapDispatchToProps = (dispatch: ThunkDispatch) => {
|
||||
return {
|
||||
...bindActionCreators({ openOptions, commitChangesToVariable, navigateOptions }, dispatch),
|
||||
filterOrSearchOptions: (identifier: KeyedVariableIdentifier, filter = '') => {
|
||||
dispatch(filterOrSearchOptions(identifier, filter));
|
||||
},
|
||||
toggleAllOptions: (identifier: KeyedVariableIdentifier) =>
|
||||
dispatch(toKeyedAction(identifier.rootStateKey, toggleAllOptions())),
|
||||
toggleOption: (
|
||||
identifier: KeyedVariableIdentifier,
|
||||
option: VariableOption,
|
||||
clearOthers: boolean,
|
||||
forceSelect: boolean
|
||||
) => dispatch(toKeyedAction(identifier.rootStateKey, toggleOption({ option, clearOthers, forceSelect }))),
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
picker: state.templating.optionsPicker,
|
||||
});
|
||||
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => {
|
||||
const { rootStateKey } = ownProps.variable;
|
||||
if (!rootStateKey) {
|
||||
console.error('OptionPickerFactory: variable has no rootStateKey');
|
||||
return {
|
||||
picker: initialOptionPickerState,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
picker: getVariablesState(rootStateKey, state).optionsPicker,
|
||||
};
|
||||
};
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
@@ -40,8 +62,15 @@ export const optionPickerFactory = <Model extends VariableWithOptions | Variable
|
||||
|
||||
class OptionsPickerUnconnected extends PureComponent<Props> {
|
||||
onShowOptions = () =>
|
||||
this.props.openOptions(toVariableIdentifier(this.props.variable), this.props.onVariableChange);
|
||||
onHideOptions = () => this.props.commitChangesToVariable(this.props.onVariableChange);
|
||||
this.props.openOptions(toKeyedVariableIdentifier(this.props.variable), this.props.onVariableChange);
|
||||
onHideOptions = () => {
|
||||
if (!this.props.variable.rootStateKey) {
|
||||
console.error('Variable has no rootStateKey');
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.commitChangesToVariable(this.props.variable.rootStateKey, this.props.onVariableChange);
|
||||
};
|
||||
|
||||
onToggleOption = (option: VariableOption, clearOthers: boolean) => {
|
||||
const toggleFunc =
|
||||
@@ -52,12 +81,29 @@ export const optionPickerFactory = <Model extends VariableWithOptions | Variable
|
||||
};
|
||||
|
||||
onToggleSingleValueVariable = (option: VariableOption, clearOthers: boolean) => {
|
||||
this.props.toggleOption({ option, clearOthers, forceSelect: false });
|
||||
this.props.toggleOption(toKeyedVariableIdentifier(this.props.variable), option, clearOthers, false);
|
||||
this.onHideOptions();
|
||||
};
|
||||
|
||||
onToggleMultiValueVariable = (option: VariableOption, clearOthers: boolean) => {
|
||||
this.props.toggleOption({ option, clearOthers, forceSelect: false });
|
||||
this.props.toggleOption(toKeyedVariableIdentifier(this.props.variable), option, clearOthers, false);
|
||||
};
|
||||
|
||||
onToggleAllOptions = () => {
|
||||
this.props.toggleAllOptions(toKeyedVariableIdentifier(this.props.variable));
|
||||
};
|
||||
|
||||
onFilterOrSearchOptions = (filter: string) => {
|
||||
this.props.filterOrSearchOptions(toKeyedVariableIdentifier(this.props.variable), filter);
|
||||
};
|
||||
|
||||
onNavigate = (key: NavigationKey, clearOthers: boolean) => {
|
||||
if (!this.props.variable.rootStateKey) {
|
||||
console.error('Variable has no rootStateKey');
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.navigateOptions(this.props.variable.rootStateKey, key, clearOthers);
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -87,7 +133,7 @@ export const optionPickerFactory = <Model extends VariableWithOptions | Variable
|
||||
}
|
||||
|
||||
onCancel = () => {
|
||||
getVariableQueryRunner().cancelRequest(toVariableIdentifier(this.props.variable));
|
||||
getVariableQueryRunner().cancelRequest(toKeyedVariableIdentifier(this.props.variable));
|
||||
};
|
||||
|
||||
renderOptions(picker: OptionsPickerState) {
|
||||
@@ -97,15 +143,15 @@ export const optionPickerFactory = <Model extends VariableWithOptions | Variable
|
||||
<VariableInput
|
||||
id={id}
|
||||
value={picker.queryValue}
|
||||
onChange={this.props.filterOrSearchOptions}
|
||||
onNavigate={this.props.navigateOptions}
|
||||
onChange={this.onFilterOrSearchOptions}
|
||||
onNavigate={this.onNavigate}
|
||||
aria-expanded={true}
|
||||
aria-controls={`options-${id}`}
|
||||
/>
|
||||
<VariableOptions
|
||||
values={picker.options}
|
||||
onToggle={this.onToggleOption}
|
||||
onToggleAll={this.props.toggleAllOptions}
|
||||
onToggleAll={this.onToggleAllOptions}
|
||||
highlightIndex={picker.highlightIndex}
|
||||
multi={picker.multi}
|
||||
selectedValues={picker.selectedValues}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { reduxTester } from '../../../../../test/core/redux/reduxTester';
|
||||
import { getRootReducer, RootReducerType } from '../../state/helpers';
|
||||
import { getPreloadedState, getRootReducer, RootReducerType } from '../../state/helpers';
|
||||
import { initialVariableModelState, QueryVariableModel, VariableRefresh, VariableSort } from '../../types';
|
||||
import {
|
||||
hideOptions,
|
||||
initialState,
|
||||
initialOptionPickerState,
|
||||
moveOptionsHighlight,
|
||||
showOptions,
|
||||
toggleOption,
|
||||
@@ -18,12 +18,13 @@ import {
|
||||
toggleOptionByHighlight,
|
||||
} from './actions';
|
||||
import { NavigationKey } from '../types';
|
||||
import { toVariablePayload } from '../../state/types';
|
||||
import { addVariable, changeVariableProp, setCurrentVariableValue } from '../../state/sharedReducer';
|
||||
import { variableAdapters } from '../../adapters';
|
||||
import { createQueryVariableAdapter } from '../../query/adapter';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { queryBuilder } from '../../shared/testing/builders';
|
||||
import { toKeyedAction } from '../../state/keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../../utils';
|
||||
|
||||
const datasource = {
|
||||
metricFindQuery: jest.fn(() => Promise.resolve([])),
|
||||
@@ -59,9 +60,11 @@ describe('options picker actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(showOptions(variable))
|
||||
.whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
|
||||
.whenAsyncActionIsDispatched(navigateOptions('key', key, clearOthers), true);
|
||||
|
||||
const option = {
|
||||
...createOption(['A']),
|
||||
@@ -70,9 +73,12 @@ describe('options picker actions', () => {
|
||||
};
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option })),
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' })),
|
||||
hideOptions()
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' }))
|
||||
),
|
||||
toKeyedAction('key', hideOptions())
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -91,12 +97,16 @@ describe('options picker actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(showOptions(variable))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, false))
|
||||
.whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, false))
|
||||
.whenAsyncActionIsDispatched(navigateOptions('key', key, clearOthers), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(toggleOption({ option, forceSelect: false, clearOthers }));
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction('key', toggleOption({ option, forceSelect: false, clearOthers }))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,12 +124,16 @@ describe('options picker actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(showOptions(variable))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenAsyncActionIsDispatched(navigateOptions('key', key, clearOthers), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(toggleOption({ option, forceSelect: false, clearOthers }));
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction('key', toggleOption({ option, forceSelect: false, clearOthers }))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,14 +147,18 @@ describe('options picker actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(showOptions(variable))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenAsyncActionIsDispatched(navigateOptions('key', key, clearOthers), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(toggleOption({ option: options[2], forceSelect: false, clearOthers }));
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction('key', toggleOption({ option: options[2], forceSelect: false, clearOthers }))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -154,15 +172,19 @@ describe('options picker actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(showOptions(variable))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveUp, clearOthers))
|
||||
.whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveUp, clearOthers))
|
||||
.whenAsyncActionIsDispatched(navigateOptions('key', key, clearOthers), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(toggleOption({ option: options[1], forceSelect: false, clearOthers }));
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction('key', toggleOption({ option: options[1], forceSelect: false, clearOthers }))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -176,13 +198,15 @@ describe('options picker actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(showOptions(variable))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveUp, clearOthers))
|
||||
.whenAsyncActionIsDispatched(navigateOptions(key, clearOthers), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveUp, clearOthers))
|
||||
.whenAsyncActionIsDispatched(navigateOptions('key', key, clearOthers), true);
|
||||
|
||||
const option = {
|
||||
...createOption(['B']),
|
||||
@@ -191,11 +215,14 @@ describe('options picker actions', () => {
|
||||
};
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toggleOption({ option: options[1], forceSelect: true, clearOthers }),
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option })),
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' })),
|
||||
hideOptions(),
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option }))
|
||||
toKeyedAction('key', toggleOption({ option: options[1], forceSelect: true, clearOthers })),
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' }))
|
||||
),
|
||||
toKeyedAction('key', hideOptions()),
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
|
||||
);
|
||||
expect(locationService.partial).toHaveBeenLastCalledWith({ 'var-Constant': ['B'] });
|
||||
});
|
||||
@@ -209,11 +236,16 @@ describe('options picker actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(showOptions(variable))
|
||||
.whenAsyncActionIsDispatched(filterOrSearchOptions(filter), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
|
||||
.whenAsyncActionIsDispatched(filterOrSearchOptions(toKeyedVariableIdentifier(variable), filter), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(updateSearchQuery(filter), updateOptionsAndFilter(variable.options));
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction('key', updateSearchQuery(filter)),
|
||||
toKeyedAction('key', updateOptionsAndFilter(variable.options))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -221,26 +253,25 @@ describe('options picker actions', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = queryBuilder()
|
||||
.withId('query0')
|
||||
.withRootStateKey('key')
|
||||
.withName('query0')
|
||||
.withMulti()
|
||||
.withCurrent(['A', 'C'])
|
||||
.withOptions('A', 'B', 'C')
|
||||
.build();
|
||||
|
||||
const preloadedState: any = {
|
||||
templating: {
|
||||
variables: {
|
||||
[variable.id]: { ...variable },
|
||||
},
|
||||
optionsPicker: { ...initialState },
|
||||
const preloadedState = getPreloadedState('key', {
|
||||
variables: {
|
||||
[variable.id]: { ...variable },
|
||||
},
|
||||
};
|
||||
optionsPicker: { ...initialOptionPickerState },
|
||||
});
|
||||
|
||||
const tester = await reduxTester<RootReducerType>({ preloadedState })
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenAsyncActionIsDispatched(openOptions(variable, undefined));
|
||||
.whenAsyncActionIsDispatched(openOptions(toKeyedVariableIdentifier(variable), undefined));
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(showOptions(variable));
|
||||
tester.thenDispatchedActionsShouldEqual(toKeyedAction('key', showOptions(variable)));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -248,26 +279,25 @@ describe('options picker actions', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = queryBuilder()
|
||||
.withId('query0')
|
||||
.withRootStateKey('key')
|
||||
.withName('query0')
|
||||
.withMulti()
|
||||
.withCurrent(['A', 'C'])
|
||||
.withOptions('A', 'B', 'C')
|
||||
.build();
|
||||
|
||||
const preloadedState: any = {
|
||||
templating: {
|
||||
variables: {
|
||||
[variable.id]: { ...variable },
|
||||
},
|
||||
optionsPicker: { ...initialState, id: variable.id },
|
||||
const preloadedState = getPreloadedState('key', {
|
||||
variables: {
|
||||
[variable.id]: { ...variable },
|
||||
},
|
||||
};
|
||||
optionsPicker: { ...initialOptionPickerState, id: variable.id },
|
||||
});
|
||||
|
||||
const tester = await reduxTester<RootReducerType>({ preloadedState })
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenAsyncActionIsDispatched(openOptions(variable, undefined));
|
||||
.whenAsyncActionIsDispatched(openOptions(toKeyedVariableIdentifier(variable), undefined));
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(showOptions(variable));
|
||||
tester.thenDispatchedActionsShouldEqual(toKeyedAction('key', showOptions(variable)));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -275,6 +305,7 @@ describe('options picker actions', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variableInPickerState = queryBuilder()
|
||||
.withId('query1')
|
||||
.withRootStateKey('key')
|
||||
.withName('query1')
|
||||
.withMulti()
|
||||
.withCurrent(['A', 'C'])
|
||||
@@ -283,31 +314,33 @@ describe('options picker actions', () => {
|
||||
|
||||
const variable = queryBuilder()
|
||||
.withId('query0')
|
||||
.withRootStateKey('key')
|
||||
.withName('query0')
|
||||
.withMulti()
|
||||
.withCurrent(['A'])
|
||||
.withOptions('A', 'B', 'C')
|
||||
.build();
|
||||
|
||||
const preloadedState: any = {
|
||||
templating: {
|
||||
variables: {
|
||||
[variable.id]: { ...variable },
|
||||
[variableInPickerState.id]: { ...variableInPickerState },
|
||||
},
|
||||
optionsPicker: { ...initialState, id: variableInPickerState.id },
|
||||
const preloadedState = getPreloadedState('key', {
|
||||
variables: {
|
||||
[variable.id]: { ...variable },
|
||||
[variableInPickerState.id]: { ...variableInPickerState },
|
||||
},
|
||||
};
|
||||
optionsPicker: { ...initialOptionPickerState, id: variableInPickerState.id },
|
||||
});
|
||||
|
||||
const tester = await reduxTester<RootReducerType>({ preloadedState })
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenAsyncActionIsDispatched(openOptions(variable, undefined));
|
||||
.whenAsyncActionIsDispatched(openOptions(toKeyedVariableIdentifier(variable), undefined));
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
setCurrentVariableValue({ type: 'query', id: 'query1', data: { option: undefined } }),
|
||||
changeVariableProp({ type: 'query', id: 'query1', data: { propName: 'queryValue', propValue: '' } }),
|
||||
hideOptions(),
|
||||
showOptions(variable)
|
||||
toKeyedAction('key', setCurrentVariableValue({ type: 'query', id: 'query1', data: { option: undefined } })),
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableProp({ type: 'query', id: 'query1', data: { propName: 'queryValue', propValue: '' } })
|
||||
),
|
||||
toKeyedAction('key', hideOptions()),
|
||||
toKeyedAction('key', showOptions(variable))
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -319,9 +352,11 @@ describe('options picker actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(showOptions(variable))
|
||||
.whenAsyncActionIsDispatched(commitChangesToVariable(), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
|
||||
.whenAsyncActionIsDispatched(commitChangesToVariable('key'), true);
|
||||
|
||||
const option = {
|
||||
...createOption(['A']),
|
||||
@@ -330,9 +365,12 @@ describe('options picker actions', () => {
|
||||
};
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option })),
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' })),
|
||||
hideOptions()
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' }))
|
||||
),
|
||||
toKeyedAction('key', hideOptions())
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -345,11 +383,13 @@ describe('options picker actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(showOptions(variable))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(toggleOptionByHighlight(clearOthers))
|
||||
.whenAsyncActionIsDispatched(commitChangesToVariable(), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(toggleOptionByHighlight('key', clearOthers))
|
||||
.whenAsyncActionIsDispatched(commitChangesToVariable('key'), true);
|
||||
|
||||
const option = {
|
||||
...createOption([]),
|
||||
@@ -358,10 +398,13 @@ describe('options picker actions', () => {
|
||||
};
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option })),
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' })),
|
||||
hideOptions(),
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option }))
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: '' }))
|
||||
),
|
||||
toKeyedAction('key', hideOptions()),
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
|
||||
);
|
||||
expect(locationService.partial).toHaveBeenLastCalledWith({ 'var-Constant': [] });
|
||||
});
|
||||
@@ -375,12 +418,14 @@ describe('options picker actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(showOptions(variable))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(toggleOptionByHighlight(clearOthers))
|
||||
.whenActionIsDispatched(filterOrSearchOptions('C'))
|
||||
.whenAsyncActionIsDispatched(commitChangesToVariable(), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(toggleOptionByHighlight('key', clearOthers))
|
||||
.whenActionIsDispatched(filterOrSearchOptions(toKeyedVariableIdentifier(variable), 'C'))
|
||||
.whenAsyncActionIsDispatched(commitChangesToVariable('key'), true);
|
||||
|
||||
const option = {
|
||||
...createOption([]),
|
||||
@@ -389,10 +434,13 @@ describe('options picker actions', () => {
|
||||
};
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option })),
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: 'C' })),
|
||||
hideOptions(),
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option }))
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'queryValue', propValue: 'C' }))
|
||||
),
|
||||
toKeyedAction('key', hideOptions()),
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
|
||||
);
|
||||
expect(locationService.partial).toHaveBeenLastCalledWith({ 'var-Constant': [] });
|
||||
});
|
||||
@@ -406,14 +454,18 @@ describe('options picker actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(showOptions(variable))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(toggleOptionByHighlight(clearOthers), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(toggleOptionByHighlight('key', clearOthers), true);
|
||||
|
||||
const option = createOption('A');
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(toggleOption({ option, forceSelect: false, clearOthers }));
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction('key', toggleOption({ option, forceSelect: false, clearOthers }))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -425,25 +477,27 @@ describe('options picker actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(showOptions(variable))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(toggleOptionByHighlight(clearOthers), true)
|
||||
.whenActionIsDispatched(filterOrSearchOptions('B'))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions(NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(toggleOptionByHighlight(clearOthers));
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', showOptions(variable)))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(toggleOptionByHighlight('key', clearOthers), true)
|
||||
.whenActionIsDispatched(filterOrSearchOptions(toKeyedVariableIdentifier(variable), 'B'))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(navigateOptions('key', NavigationKey.moveDown, clearOthers))
|
||||
.whenActionIsDispatched(toggleOptionByHighlight('key', clearOthers));
|
||||
|
||||
const optionA = createOption('A');
|
||||
const optionBC = createOption('BD');
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toggleOption({ option: optionA, forceSelect: false, clearOthers }),
|
||||
updateSearchQuery('B'),
|
||||
updateOptionsAndFilter(variable.options),
|
||||
moveOptionsHighlight(1),
|
||||
moveOptionsHighlight(1),
|
||||
toggleOption({ option: optionBC, forceSelect: false, clearOthers })
|
||||
toKeyedAction('key', toggleOption({ option: optionA, forceSelect: false, clearOthers })),
|
||||
toKeyedAction('key', updateSearchQuery('B')),
|
||||
toKeyedAction('key', updateOptionsAndFilter(variable.options)),
|
||||
toKeyedAction('key', moveOptionsHighlight(1)),
|
||||
toKeyedAction('key', moveOptionsHighlight(1)),
|
||||
toKeyedAction('key', toggleOption({ option: optionBC, forceSelect: false, clearOthers }))
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -454,6 +508,7 @@ function createMultiVariable(extend?: Partial<QueryVariableModel>): QueryVariabl
|
||||
...initialVariableModelState,
|
||||
type: 'query',
|
||||
id: '0',
|
||||
rootStateKey: 'key',
|
||||
index: 0,
|
||||
current: createOption([]),
|
||||
options: [],
|
||||
|
||||
@@ -2,7 +2,7 @@ import { debounce, trim } from 'lodash';
|
||||
import { StoreState, ThunkDispatch, ThunkResult } from 'app/types';
|
||||
import { VariableOption, VariableWithMultiSupport, VariableWithOptions } from '../../types';
|
||||
import { variableAdapters } from '../../adapters';
|
||||
import { getVariable } from '../../state/selectors';
|
||||
import { getVariable, getVariablesState } from '../../state/selectors';
|
||||
import { NavigationKey } from '../types';
|
||||
import {
|
||||
hideOptions,
|
||||
@@ -15,50 +15,56 @@ import {
|
||||
updateSearchQuery,
|
||||
} from './reducer';
|
||||
import { changeVariableProp, setCurrentVariableValue } from '../../state/sharedReducer';
|
||||
import { toVariablePayload, VariableIdentifier } from '../../state/types';
|
||||
import { containsSearchFilter, getCurrentText } from '../../utils';
|
||||
import { KeyedVariableIdentifier } from '../../state/types';
|
||||
import { containsSearchFilter, getCurrentText, toVariablePayload } from '../../utils';
|
||||
import { toKeyedAction } from '../../state/keyedVariablesReducer';
|
||||
|
||||
export const navigateOptions = (key: NavigationKey, clearOthers: boolean): ThunkResult<void> => {
|
||||
export const navigateOptions = (rootStateKey: string, key: NavigationKey, clearOthers: boolean): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
if (key === NavigationKey.cancel) {
|
||||
return await dispatch(commitChangesToVariable());
|
||||
return await dispatch(commitChangesToVariable(rootStateKey));
|
||||
}
|
||||
|
||||
if (key === NavigationKey.select) {
|
||||
return dispatch(toggleOptionByHighlight(clearOthers));
|
||||
return dispatch(toggleOptionByHighlight(rootStateKey, clearOthers));
|
||||
}
|
||||
|
||||
if (key === NavigationKey.selectAndClose) {
|
||||
dispatch(toggleOptionByHighlight(clearOthers, true));
|
||||
return await dispatch(commitChangesToVariable());
|
||||
dispatch(toggleOptionByHighlight(rootStateKey, clearOthers, true));
|
||||
return await dispatch(commitChangesToVariable(rootStateKey));
|
||||
}
|
||||
|
||||
if (key === NavigationKey.moveDown) {
|
||||
return dispatch(moveOptionsHighlight(1));
|
||||
return dispatch(toKeyedAction(rootStateKey, moveOptionsHighlight(1)));
|
||||
}
|
||||
|
||||
if (key === NavigationKey.moveUp) {
|
||||
return dispatch(moveOptionsHighlight(-1));
|
||||
return dispatch(toKeyedAction(rootStateKey, moveOptionsHighlight(-1)));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
};
|
||||
|
||||
export const filterOrSearchOptions = (searchQuery = ''): ThunkResult<void> => {
|
||||
export const filterOrSearchOptions = (
|
||||
passedIdentifier: KeyedVariableIdentifier,
|
||||
searchQuery = ''
|
||||
): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
const { id, queryValue } = getState().templating.optionsPicker;
|
||||
const { query, options } = getVariable<VariableWithOptions>(id, getState());
|
||||
dispatch(updateSearchQuery(searchQuery));
|
||||
const { rootStateKey } = passedIdentifier;
|
||||
const { id, queryValue } = getVariablesState(rootStateKey, getState()).optionsPicker;
|
||||
const identifier: KeyedVariableIdentifier = { id, rootStateKey: rootStateKey, type: 'query' };
|
||||
const { query, options } = getVariable<VariableWithOptions>(identifier, getState());
|
||||
dispatch(toKeyedAction(rootStateKey, updateSearchQuery(searchQuery)));
|
||||
|
||||
if (trim(queryValue) === trim(searchQuery)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (containsSearchFilter(query)) {
|
||||
return searchForOptionsWithDebounce(dispatch, getState, searchQuery);
|
||||
return searchForOptionsWithDebounce(dispatch, getState, searchQuery, rootStateKey);
|
||||
}
|
||||
return dispatch(updateOptionsAndFilter(options));
|
||||
return dispatch(toKeyedAction(rootStateKey, updateOptionsAndFilter(options)));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -68,17 +74,18 @@ const setVariable = async (updated: VariableWithMultiSupport) => {
|
||||
return;
|
||||
};
|
||||
|
||||
export const commitChangesToVariable = (callback?: (updated: any) => void): ThunkResult<void> => {
|
||||
export const commitChangesToVariable = (key: string, callback?: (updated: any) => void): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
const picker = getState().templating.optionsPicker;
|
||||
const existing = getVariable<VariableWithMultiSupport>(picker.id, getState());
|
||||
const picker = getVariablesState(key, getState()).optionsPicker;
|
||||
const identifier: KeyedVariableIdentifier = { id: picker.id, rootStateKey: key, type: 'query' };
|
||||
const existing = getVariable<VariableWithMultiSupport>(identifier, getState());
|
||||
const currentPayload = { option: mapToCurrent(picker) };
|
||||
const searchQueryPayload = { propName: 'queryValue', propValue: picker.queryValue };
|
||||
|
||||
dispatch(setCurrentVariableValue(toVariablePayload(existing, currentPayload)));
|
||||
dispatch(changeVariableProp(toVariablePayload(existing, searchQueryPayload)));
|
||||
const updated = getVariable<VariableWithMultiSupport>(picker.id, getState());
|
||||
dispatch(hideOptions());
|
||||
dispatch(toKeyedAction(key, setCurrentVariableValue(toVariablePayload(existing, currentPayload))));
|
||||
dispatch(toKeyedAction(key, changeVariableProp(toVariablePayload(existing, searchQueryPayload))));
|
||||
const updated = getVariable<VariableWithMultiSupport>(identifier, getState());
|
||||
dispatch(toKeyedAction(key, hideOptions()));
|
||||
|
||||
if (getCurrentText(existing) === getCurrentText(updated)) {
|
||||
return;
|
||||
@@ -93,36 +100,43 @@ export const commitChangesToVariable = (callback?: (updated: any) => void): Thun
|
||||
};
|
||||
|
||||
export const openOptions =
|
||||
({ id }: VariableIdentifier, callback?: (updated: any) => void): ThunkResult<void> =>
|
||||
(identifier: KeyedVariableIdentifier, callback?: (updated: any) => void): ThunkResult<void> =>
|
||||
async (dispatch, getState) => {
|
||||
const picker = getState().templating.optionsPicker;
|
||||
const { id, rootStateKey: uid } = identifier;
|
||||
const picker = getVariablesState(uid, getState()).optionsPicker;
|
||||
|
||||
if (picker.id && picker.id !== id) {
|
||||
await dispatch(commitChangesToVariable(callback));
|
||||
await dispatch(commitChangesToVariable(uid, callback));
|
||||
}
|
||||
|
||||
const variable = getVariable<VariableWithMultiSupport>(id, getState());
|
||||
dispatch(showOptions(variable));
|
||||
const variable = getVariable<VariableWithMultiSupport>(identifier, getState());
|
||||
dispatch(toKeyedAction(uid, showOptions(variable)));
|
||||
};
|
||||
|
||||
export const toggleOptionByHighlight = (clearOthers: boolean, forceSelect = false): ThunkResult<void> => {
|
||||
export const toggleOptionByHighlight = (key: string, clearOthers: boolean, forceSelect = false): ThunkResult<void> => {
|
||||
return (dispatch, getState) => {
|
||||
const { highlightIndex, options } = getState().templating.optionsPicker;
|
||||
const { highlightIndex, options } = getVariablesState(key, getState()).optionsPicker;
|
||||
const option = options[highlightIndex];
|
||||
dispatch(toggleOption({ option, forceSelect, clearOthers }));
|
||||
dispatch(toKeyedAction(key, toggleOption({ option, forceSelect, clearOthers })));
|
||||
};
|
||||
};
|
||||
|
||||
const searchForOptions = async (dispatch: ThunkDispatch, getState: () => StoreState, searchQuery: string) => {
|
||||
const searchForOptions = async (
|
||||
dispatch: ThunkDispatch,
|
||||
getState: () => StoreState,
|
||||
searchQuery: string,
|
||||
key: string
|
||||
) => {
|
||||
try {
|
||||
const { id } = getState().templating.optionsPicker;
|
||||
const existing = getVariable<VariableWithOptions>(id, getState());
|
||||
const { id } = getVariablesState(key, getState()).optionsPicker;
|
||||
const identifier: KeyedVariableIdentifier = { id, rootStateKey: key, type: 'query' };
|
||||
const existing = getVariable<VariableWithOptions>(identifier, getState());
|
||||
|
||||
const adapter = variableAdapters.get(existing.type);
|
||||
await adapter.updateOptions(existing, searchQuery);
|
||||
|
||||
const updated = getVariable<VariableWithOptions>(id, getState());
|
||||
dispatch(updateOptionsFromSearch(updated.options));
|
||||
const updated = getVariable<VariableWithOptions>(identifier, getState());
|
||||
dispatch(toKeyedAction(key, updateOptionsFromSearch(updated.options)));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash';
|
||||
import {
|
||||
cleanPickerState,
|
||||
hideOptions,
|
||||
initialState as optionsPickerInitialState,
|
||||
initialOptionPickerState as optionsPickerInitialState,
|
||||
moveOptionsHighlight,
|
||||
OPTIONS_LIMIT,
|
||||
optionsPickerReducer,
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface OptionsPickerState {
|
||||
multi: boolean;
|
||||
}
|
||||
|
||||
export const initialState: OptionsPickerState = {
|
||||
export const initialOptionPickerState: OptionsPickerState = {
|
||||
id: '',
|
||||
highlightIndex: -1,
|
||||
queryValue: '',
|
||||
@@ -106,7 +106,7 @@ const updateAllSelection = (state: OptionsPickerState): OptionsPickerState => {
|
||||
|
||||
const optionsPickerSlice = createSlice({
|
||||
name: 'templating/optionsPicker',
|
||||
initialState,
|
||||
initialState: initialOptionPickerState,
|
||||
reducers: {
|
||||
showOptions: (state, action: PayloadAction<VariableWithOptions>): OptionsPickerState => {
|
||||
const { query, options } = action.payload;
|
||||
@@ -131,7 +131,7 @@ const optionsPickerSlice = createSlice({
|
||||
return applyStateChanges(state, updateDefaultSelection, updateOptions);
|
||||
},
|
||||
hideOptions: (state, action: PayloadAction): OptionsPickerState => {
|
||||
return { ...initialState };
|
||||
return { ...initialOptionPickerState };
|
||||
},
|
||||
toggleOption: (state, action: PayloadAction<ToggleOption>): OptionsPickerState => {
|
||||
const { option, clearOthers, forceSelect } = action.payload;
|
||||
@@ -212,7 +212,7 @@ const optionsPickerSlice = createSlice({
|
||||
|
||||
return applyStateChanges(state, updateDefaultSelection, updateOptions);
|
||||
},
|
||||
cleanPickerState: () => initialState,
|
||||
cleanPickerState: () => initialOptionPickerState,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -10,14 +10,17 @@ import { LegacyVariableQueryEditor } from '../editor/LegacyVariableQueryEditor';
|
||||
import { mockDataSource } from 'app/features/alerting/unified/mocks';
|
||||
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
|
||||
import { NEW_VARIABLE_ID } from '../constants';
|
||||
import { VariableModel } from '../types';
|
||||
import { KeyedVariableIdentifier } from '../state/types';
|
||||
|
||||
const setupTestContext = (options: Partial<Props>) => {
|
||||
const variableDefaults: Partial<VariableModel> = { rootStateKey: 'key' };
|
||||
const extended = {
|
||||
VariableQueryEditor: LegacyVariableQueryEditor,
|
||||
dataSource: {} as unknown as DataSourceApi,
|
||||
};
|
||||
const defaults: Props = {
|
||||
variable: { ...initialQueryVariableModelState },
|
||||
variable: { ...initialQueryVariableModelState, ...variableDefaults },
|
||||
initQueryVariableEditor: jest.fn(),
|
||||
changeQueryVariableDataSource: jest.fn(),
|
||||
changeQueryVariableQuery: jest.fn(),
|
||||
@@ -47,20 +50,22 @@ jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
|
||||
};
|
||||
});
|
||||
|
||||
const defaultIdentifier: KeyedVariableIdentifier = { type: 'query', rootStateKey: 'key', id: NEW_VARIABLE_ID };
|
||||
|
||||
describe('QueryVariableEditor', () => {
|
||||
describe('when the component is mounted', () => {
|
||||
it('then it should call initQueryVariableEditor', () => {
|
||||
const { props } = setupTestContext({});
|
||||
|
||||
expect(props.initQueryVariableEditor).toHaveBeenCalledTimes(1);
|
||||
expect(props.initQueryVariableEditor).toHaveBeenCalledWith({ type: 'query', id: NEW_VARIABLE_ID });
|
||||
expect(props.initQueryVariableEditor).toHaveBeenCalledWith(defaultIdentifier);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user changes', () => {
|
||||
it.each`
|
||||
fieldName | propName | expectedArgs
|
||||
${'query'} | ${'changeQueryVariableQuery'} | ${[{ type: 'query', id: NEW_VARIABLE_ID }, 't', 't']}
|
||||
${'query'} | ${'changeQueryVariableQuery'} | ${[defaultIdentifier, 't', 't']}
|
||||
${'regex'} | ${'onPropChange'} | ${[{ propName: 'regex', propValue: 't', updateOptions: true }]}
|
||||
`(
|
||||
'$fieldName field and tabs away then $propName should be called with correct args',
|
||||
|
||||
@@ -9,9 +9,9 @@ import { DataSourceInstanceSettings, getDataSourceRef, LoadingState, SelectableV
|
||||
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
|
||||
import { QueryVariableModel, VariableRefresh, VariableSort, VariableWithMultiSupport } from '../types';
|
||||
import { changeQueryVariableDataSource, changeQueryVariableQuery, initQueryVariableEditor } from './actions';
|
||||
import { initialVariableEditorState } from '../editor/reducer';
|
||||
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
|
||||
import { StoreState } from '../../../types';
|
||||
import { toVariableIdentifier } from '../state/types';
|
||||
import { changeVariableMultiValue } from '../state/actions';
|
||||
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
|
||||
import { isLegacyQueryEditor, isQueryEditor } from '../guard';
|
||||
@@ -20,10 +20,24 @@ import { VariableTextField } from '../editor/VariableTextField';
|
||||
import { QueryVariableRefreshSelect } from './QueryVariableRefreshSelect';
|
||||
import { QueryVariableSortSelect } from './QueryVariableSortSelect';
|
||||
import { getQueryVariableEditorState } from '../editor/selectors';
|
||||
import { getVariablesState } from '../state/selectors';
|
||||
import { toKeyedVariableIdentifier } from '../utils';
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
extended: getQueryVariableEditorState(state.templating.editor),
|
||||
});
|
||||
const mapStateToProps = (state: StoreState, ownProps: OwnProps) => {
|
||||
const { rootStateKey } = ownProps.variable;
|
||||
if (!rootStateKey) {
|
||||
console.error('QueryVariableEditor: variable has no rootStateKey');
|
||||
return {
|
||||
extended: getQueryVariableEditorState(initialVariableEditorState),
|
||||
};
|
||||
}
|
||||
|
||||
const { editor } = getVariablesState(rootStateKey, state);
|
||||
|
||||
return {
|
||||
extended: getQueryVariableEditorState(editor),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
initQueryVariableEditor,
|
||||
@@ -52,13 +66,13 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
await this.props.initQueryVariableEditor(toVariableIdentifier(this.props.variable));
|
||||
await this.props.initQueryVariableEditor(toKeyedVariableIdentifier(this.props.variable));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<Props>): void {
|
||||
if (prevProps.variable.datasource !== this.props.variable.datasource) {
|
||||
this.props.changeQueryVariableDataSource(
|
||||
toVariableIdentifier(this.props.variable),
|
||||
toKeyedVariableIdentifier(this.props.variable),
|
||||
this.props.variable.datasource
|
||||
);
|
||||
}
|
||||
@@ -73,7 +87,7 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
|
||||
|
||||
onLegacyQueryChange = async (query: any, definition: string) => {
|
||||
if (this.props.variable.query !== query) {
|
||||
this.props.changeQueryVariableQuery(toVariableIdentifier(this.props.variable), query, definition);
|
||||
this.props.changeQueryVariableQuery(toKeyedVariableIdentifier(this.props.variable), query, definition);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -85,7 +99,7 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
|
||||
definition = query.query;
|
||||
}
|
||||
|
||||
this.props.changeQueryVariableQuery(toVariableIdentifier(this.props.variable), query, definition);
|
||||
this.props.changeQueryVariableQuery(toKeyedVariableIdentifier(this.props.variable), query, definition);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,9 +5,13 @@ import { delay } from 'rxjs/operators';
|
||||
import { UpdateOptionsResults, VariableQueryRunner } from './VariableQueryRunner';
|
||||
import { queryBuilder } from '../shared/testing/builders';
|
||||
import { QueryRunner, QueryRunners } from './queryRunners';
|
||||
import { toVariableIdentifier, VariableIdentifier } from '../state/types';
|
||||
import { KeyedVariableIdentifier } from '../state/types';
|
||||
import { QueryVariableModel } from '../types';
|
||||
import { updateVariableOptions } from './reducer';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { initialTransactionState } from '../state/transactionReducer';
|
||||
import { getPreloadedState } from '../state/helpers';
|
||||
import { toKeyedVariableIdentifier } from '../utils';
|
||||
|
||||
type DoneCallback = {
|
||||
(...args: any[]): any;
|
||||
@@ -16,7 +20,7 @@ type DoneCallback = {
|
||||
|
||||
function expectOnResults(args: {
|
||||
runner: VariableQueryRunner;
|
||||
identifier: VariableIdentifier;
|
||||
identifier: KeyedVariableIdentifier;
|
||||
done: DoneCallback;
|
||||
expect: (results: UpdateOptionsResults[]) => void;
|
||||
}) {
|
||||
@@ -32,7 +36,7 @@ function expectOnResults(args: {
|
||||
done();
|
||||
} catch (err) {
|
||||
subscription.unsubscribe();
|
||||
done.fail(err);
|
||||
done(err);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -40,25 +44,23 @@ function expectOnResults(args: {
|
||||
}
|
||||
|
||||
function getTestContext(variable?: QueryVariableModel) {
|
||||
variable = variable ?? queryBuilder().withId('query').build();
|
||||
const getTimeSrv = jest.fn().mockReturnValue({
|
||||
timeRange: jest.fn().mockReturnValue(getDefaultTimeRange()),
|
||||
});
|
||||
const key = '0123456789';
|
||||
variable = variable ?? queryBuilder().withId('query').withRootStateKey(key).withName('query').build();
|
||||
const datasource: any = { metricFindQuery: jest.fn().mockResolvedValue([]) };
|
||||
const identifier = toVariableIdentifier(variable);
|
||||
const identifier = toKeyedVariableIdentifier(variable);
|
||||
const searchFilter = undefined;
|
||||
const getTemplatedRegex = jest.fn().mockReturnValue('getTemplatedRegex result');
|
||||
const dispatch = jest.fn().mockResolvedValue({});
|
||||
const getState = jest.fn().mockReturnValue({
|
||||
templating: {
|
||||
transaction: {
|
||||
uid: '0123456789',
|
||||
},
|
||||
},
|
||||
const templatingState = {
|
||||
transaction: { ...initialTransactionState, uid: key },
|
||||
variables: {
|
||||
[variable.id]: variable,
|
||||
},
|
||||
});
|
||||
};
|
||||
const getState = jest.fn().mockReturnValue(getPreloadedState(key, templatingState));
|
||||
const queryRunner: QueryRunner = {
|
||||
type: VariableSupportType.Standard,
|
||||
canRun: jest.fn().mockReturnValue(true),
|
||||
@@ -81,6 +83,7 @@ function getTestContext(variable?: QueryVariableModel) {
|
||||
});
|
||||
|
||||
return {
|
||||
key,
|
||||
identifier,
|
||||
datasource,
|
||||
runner,
|
||||
@@ -100,7 +103,7 @@ function getTestContext(variable?: QueryVariableModel) {
|
||||
describe('VariableQueryRunner', () => {
|
||||
describe('happy case', () => {
|
||||
it('then it should work as expected', (done) => {
|
||||
const { identifier, runner, datasource, getState, getVariable, queryRunners, queryRunner, dispatch } =
|
||||
const { key, identifier, runner, datasource, getState, getVariable, queryRunners, queryRunner, dispatch } =
|
||||
getTestContext();
|
||||
|
||||
expectOnResults({
|
||||
@@ -124,11 +127,14 @@ describe('VariableQueryRunner', () => {
|
||||
// updateVariableOptions and validateVariableSelectionState
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch.mock.calls[0][0]).toEqual(
|
||||
updateVariableOptions({
|
||||
id: 'query',
|
||||
type: 'query',
|
||||
data: { results: [], templatedRegex: 'getTemplatedRegex result' },
|
||||
})
|
||||
toKeyedAction(
|
||||
key,
|
||||
updateVariableOptions({
|
||||
id: 'query',
|
||||
type: 'query',
|
||||
data: { results: [], templatedRegex: 'getTemplatedRegex result' },
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
done,
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { VariableIdentifier } from '../state/types';
|
||||
import { getVariable } from '../state/selectors';
|
||||
import { KeyedVariableIdentifier } from '../state/types';
|
||||
import { getLastKey, getVariable } from '../state/selectors';
|
||||
import { QueryVariableModel, VariableRefresh } from '../types';
|
||||
import { StoreState, ThunkDispatch } from '../../../types';
|
||||
import { dispatch, getState } from '../../../store/store';
|
||||
@@ -24,14 +24,14 @@ import { runRequest } from '../../query/state/runRequest';
|
||||
import { toMetricFindValues, updateOptionsState, validateVariableSelection } from './operators';
|
||||
|
||||
interface UpdateOptionsArgs {
|
||||
identifier: VariableIdentifier;
|
||||
identifier: KeyedVariableIdentifier;
|
||||
datasource: DataSourceApi;
|
||||
searchFilter?: string;
|
||||
}
|
||||
|
||||
export interface UpdateOptionsResults {
|
||||
state: LoadingState;
|
||||
identifier: VariableIdentifier;
|
||||
identifier: KeyedVariableIdentifier;
|
||||
error?: any;
|
||||
cancelled?: boolean;
|
||||
}
|
||||
@@ -49,7 +49,7 @@ interface VariableQueryRunnerArgs {
|
||||
export class VariableQueryRunner {
|
||||
private readonly updateOptionsRequests: Subject<UpdateOptionsArgs>;
|
||||
private readonly updateOptionsResults: Subject<UpdateOptionsResults>;
|
||||
private readonly cancelRequests: Subject<{ identifier: VariableIdentifier }>;
|
||||
private readonly cancelRequests: Subject<{ identifier: KeyedVariableIdentifier }>;
|
||||
private readonly subscription: Unsubscribable;
|
||||
|
||||
constructor(
|
||||
@@ -65,7 +65,7 @@ export class VariableQueryRunner {
|
||||
) {
|
||||
this.updateOptionsRequests = new Subject<UpdateOptionsArgs>();
|
||||
this.updateOptionsResults = new Subject<UpdateOptionsResults>();
|
||||
this.cancelRequests = new Subject<{ identifier: VariableIdentifier }>();
|
||||
this.cancelRequests = new Subject<{ identifier: KeyedVariableIdentifier }>();
|
||||
this.onNewRequest = this.onNewRequest.bind(this);
|
||||
this.subscription = this.updateOptionsRequests.subscribe(this.onNewRequest);
|
||||
}
|
||||
@@ -74,11 +74,11 @@ export class VariableQueryRunner {
|
||||
this.updateOptionsRequests.next(args);
|
||||
}
|
||||
|
||||
getResponse(identifier: VariableIdentifier): Observable<UpdateOptionsResults> {
|
||||
getResponse(identifier: KeyedVariableIdentifier): Observable<UpdateOptionsResults> {
|
||||
return this.updateOptionsResults.asObservable().pipe(filter((result) => result.identifier === identifier));
|
||||
}
|
||||
|
||||
cancelRequest(identifier: VariableIdentifier): void {
|
||||
cancelRequest(identifier: KeyedVariableIdentifier): void {
|
||||
this.cancelRequests.next({ identifier });
|
||||
}
|
||||
|
||||
@@ -99,11 +99,11 @@ export class VariableQueryRunner {
|
||||
getState,
|
||||
} = this.dependencies;
|
||||
|
||||
const beforeUid = getState().templating.transaction.uid;
|
||||
const beforeKey = getLastKey(getState());
|
||||
|
||||
this.updateOptionsResults.next({ identifier, state: LoadingState.Loading });
|
||||
|
||||
const variable = getVariable<QueryVariableModel>(identifier.id, getState());
|
||||
const variable = getVariable<QueryVariableModel>(identifier, getState());
|
||||
const timeSrv = getTimeSrv();
|
||||
const runnerArgs = { variable, datasource, searchFilter, timeSrv, runRequest };
|
||||
const runner = queryRunners.getRunnerForDatasource(datasource);
|
||||
@@ -115,9 +115,9 @@ export class VariableQueryRunner {
|
||||
.pipe(
|
||||
filter(() => {
|
||||
// Lets check if we started another batch during the execution of the observable. If so we just want to abort the rest.
|
||||
const afterUid = getState().templating.transaction.uid;
|
||||
const afterKey = getLastKey(getState());
|
||||
|
||||
return beforeUid === afterUid;
|
||||
return beforeKey === afterKey;
|
||||
}),
|
||||
filter((data) => data.state === LoadingState.Done || data.state === LoadingState.Error), // we only care about done or error for now
|
||||
take(1), // take the first result, using first caused a bug where it in some situations throw an uncaught error because of no results had been received yet
|
||||
|
||||
@@ -4,9 +4,8 @@ import { DataSourceRef, getDefaultTimeRange, LoadingState } from '@grafana/data'
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { createQueryVariableAdapter } from './adapter';
|
||||
import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||
import { getRootReducer, RootReducerType } from '../state/helpers';
|
||||
import { getPreloadedState, getRootReducer, RootReducerType } from '../state/helpers';
|
||||
import { QueryVariableModel, VariableHide, VariableQueryEditorProps, VariableRefresh, VariableSort } from '../types';
|
||||
import { toVariablePayload } from '../state/types';
|
||||
import {
|
||||
addVariable,
|
||||
changeVariableProp,
|
||||
@@ -27,6 +26,7 @@ import { updateVariableOptions } from './reducer';
|
||||
import {
|
||||
addVariableEditorError,
|
||||
changeVariableEditorExtended,
|
||||
initialVariableEditorState,
|
||||
removeVariableEditorError,
|
||||
setIdInEditor,
|
||||
} from '../editor/reducer';
|
||||
@@ -40,6 +40,8 @@ import { setVariableQueryRunner, VariableQueryRunner } from './VariableQueryRunn
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import { variablesInitTransaction } from '../state/transactionReducer';
|
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
|
||||
const mocks: Record<string, any> = {
|
||||
datasource: {
|
||||
@@ -93,8 +95,10 @@ describe('query actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toKeyedVariableIdentifier(variable)), true);
|
||||
|
||||
tester.thenNoActionsWhereDispatched();
|
||||
});
|
||||
@@ -109,16 +113,18 @@ describe('query actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toKeyedVariableIdentifier(variable)), true);
|
||||
|
||||
const option = createOption('A');
|
||||
const update = { results: optionsMetrics, templatedRegex: '' };
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
updateVariableOptions(toVariablePayload(variable, update)),
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option }))
|
||||
toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -132,21 +138,19 @@ describe('query actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toKeyedVariableIdentifier(variable)), true);
|
||||
|
||||
const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
|
||||
const update = { results: optionsMetrics, templatedRegex: '' };
|
||||
|
||||
tester.thenDispatchedActionsPredicateShouldEqual((actions) => {
|
||||
const [updateOptions, setCurrentAction] = actions;
|
||||
const expectedNumberOfActions = 2;
|
||||
|
||||
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, update)));
|
||||
expect(setCurrentAction).toEqual(setCurrentVariableValue(toVariablePayload(variable, { option })));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -159,23 +163,21 @@ describe('query actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenActionIsDispatched(setIdInEditor({ id: variable.id }))
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable)), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenActionIsDispatched(toKeyedAction('key', setIdInEditor({ id: variable.id })))
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toKeyedVariableIdentifier(variable)), true);
|
||||
|
||||
const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
|
||||
const update = { results: optionsMetrics, templatedRegex: '' };
|
||||
|
||||
tester.thenDispatchedActionsPredicateShouldEqual((actions) => {
|
||||
const [clearErrors, updateOptions, setCurrentAction] = actions;
|
||||
const expectedNumberOfActions = 3;
|
||||
|
||||
expect(clearErrors).toEqual(removeVariableEditorError({ errorProp: 'update' }));
|
||||
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, update)));
|
||||
expect(setCurrentAction).toEqual(setCurrentVariableValue(toVariablePayload(variable, { option })));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction('key', removeVariableEditorError({ errorProp: 'update' })),
|
||||
toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option })))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -188,21 +190,19 @@ describe('query actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenActionIsDispatched(setIdInEditor({ id: variable.id }))
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toVariablePayload(variable), 'search'), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenActionIsDispatched(toKeyedAction('key', setIdInEditor({ id: variable.id })))
|
||||
.whenAsyncActionIsDispatched(updateQueryVariableOptions(toKeyedVariableIdentifier(variable), 'search'), true);
|
||||
|
||||
const update = { results: optionsMetrics, templatedRegex: '' };
|
||||
|
||||
tester.thenDispatchedActionsPredicateShouldEqual((actions) => {
|
||||
const [clearErrors, updateOptions] = actions;
|
||||
const expectedNumberOfActions = 2;
|
||||
|
||||
expect(clearErrors).toEqual(removeVariableEditorError({ errorProp: 'update' }));
|
||||
expect(updateOptions).toEqual(updateVariableOptions(toVariablePayload(variable, update)));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction('key', removeVariableEditorError({ errorProp: 'update' })),
|
||||
toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update)))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -216,19 +216,26 @@ describe('query actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenActionIsDispatched(setIdInEditor({ id: variable.id }))
|
||||
.whenAsyncActionIsDispatched(updateOptions(toVariablePayload(variable)), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenActionIsDispatched(toKeyedAction('key', setIdInEditor({ id: variable.id })))
|
||||
.whenAsyncActionIsDispatched(updateOptions(toKeyedVariableIdentifier(variable)), true);
|
||||
|
||||
tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
|
||||
const expectedNumberOfActions = 5;
|
||||
|
||||
expect(dispatchedActions[0]).toEqual(variableStateFetching(toVariablePayload(variable)));
|
||||
expect(dispatchedActions[1]).toEqual(removeVariableEditorError({ errorProp: 'update' }));
|
||||
expect(dispatchedActions[2]).toEqual(addVariableEditorError({ errorProp: 'update', errorText: error.message }));
|
||||
expect(dispatchedActions[0]).toEqual(toKeyedAction('key', variableStateFetching(toVariablePayload(variable))));
|
||||
expect(dispatchedActions[1]).toEqual(toKeyedAction('key', removeVariableEditorError({ errorProp: 'update' })));
|
||||
expect(dispatchedActions[2]).toEqual(
|
||||
toKeyedAction('key', addVariableEditorError({ errorProp: 'update', errorText: error.message }))
|
||||
);
|
||||
expect(dispatchedActions[3]).toEqual(
|
||||
variableStateFailed(toVariablePayload(variable, { error: { message: 'failed to fetch metrics' } }))
|
||||
toKeyedAction(
|
||||
'key',
|
||||
variableStateFailed(toVariablePayload(variable, { error: { message: 'failed to fetch metrics' } }))
|
||||
)
|
||||
);
|
||||
expect(dispatchedActions[4].type).toEqual(notifyApp.type);
|
||||
expect(dispatchedActions[4].payload.title).toEqual('Templating [0]');
|
||||
@@ -253,12 +260,17 @@ describe('query actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenAsyncActionIsDispatched(initQueryVariableEditor(toKeyedVariableIdentifier(variable)), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -276,12 +288,17 @@ describe('query actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenAsyncActionIsDispatched(initQueryVariableEditor(toKeyedVariableIdentifier(variable)), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -298,12 +315,17 @@ describe('query actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenAsyncActionIsDispatched(initQueryVariableEditor(toVariablePayload(variable)), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenAsyncActionIsDispatched(initQueryVariableEditor(toKeyedVariableIdentifier(variable)), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -319,15 +341,20 @@ describe('query actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenAsyncActionIsDispatched(
|
||||
changeQueryVariableDataSource(toVariablePayload(variable), { uid: 'datasource' }),
|
||||
changeQueryVariableDataSource(toKeyedVariableIdentifier(variable), { uid: 'datasource' }),
|
||||
true
|
||||
);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -335,7 +362,14 @@ describe('query actions', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const variable = createVariable({ datasource: { uid: 'other' } });
|
||||
const editor = mocks.VariableQueryEditor;
|
||||
const preloadedState: any = { templating: { editor: { extended: { dataSource: { type: 'previous' } } } } };
|
||||
const previousDataSource: any = { type: 'previous' };
|
||||
const templatingState = {
|
||||
editor: {
|
||||
...initialVariableEditorState,
|
||||
extended: { dataSource: previousDataSource, VariableQueryEditor: editor },
|
||||
},
|
||||
};
|
||||
const preloadedState = getPreloadedState('key', templatingState);
|
||||
|
||||
mocks.pluginLoader.importDataSourcePlugin = jest.fn().mockResolvedValue({
|
||||
components: { VariableQueryEditor: editor },
|
||||
@@ -344,17 +378,20 @@ describe('query actions', () => {
|
||||
const tester = await reduxTester<RootReducerType>({ preloadedState })
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(
|
||||
addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable }))
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenAsyncActionIsDispatched(
|
||||
changeQueryVariableDataSource(toVariablePayload(variable), { uid: 'datasource' }),
|
||||
changeQueryVariableDataSource(toKeyedVariableIdentifier(variable), { uid: 'datasource' }),
|
||||
true
|
||||
);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: '' })),
|
||||
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
|
||||
toKeyedAction('key', changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: '' }))),
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -371,15 +408,20 @@ describe('query actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenAsyncActionIsDispatched(
|
||||
changeQueryVariableDataSource(toVariablePayload(variable), { uid: 'datasource' }),
|
||||
changeQueryVariableDataSource(toKeyedVariableIdentifier(variable), { uid: 'datasource' }),
|
||||
true
|
||||
);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableEditorExtended({ dataSource: mocks.datasource, VariableQueryEditor: editor })
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -396,21 +438,29 @@ describe('query actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenAsyncActionIsDispatched(changeQueryVariableQuery(toVariablePayload(variable), query, definition), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenAsyncActionIsDispatched(
|
||||
changeQueryVariableQuery(toKeyedVariableIdentifier(variable), query, definition),
|
||||
true
|
||||
);
|
||||
|
||||
const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
|
||||
const update = { results: optionsMetrics, templatedRegex: '' };
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
removeVariableEditorError({ errorProp: 'query' }),
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query })),
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition })),
|
||||
variableStateFetching(toVariablePayload(variable)),
|
||||
updateVariableOptions(toVariablePayload(variable, update)),
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option })),
|
||||
variableStateCompleted(toVariablePayload(variable))
|
||||
toKeyedAction('key', removeVariableEditorError({ errorProp: 'query' })),
|
||||
toKeyedAction('key', changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query }))),
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition }))
|
||||
),
|
||||
toKeyedAction('key', variableStateFetching(toVariablePayload(variable))),
|
||||
toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
|
||||
toKeyedAction('key', variableStateCompleted(toVariablePayload(variable)))
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -427,21 +477,29 @@ describe('query actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenAsyncActionIsDispatched(changeQueryVariableQuery(toVariablePayload(variable), query, definition), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenAsyncActionIsDispatched(
|
||||
changeQueryVariableQuery(toKeyedVariableIdentifier(variable), query, definition),
|
||||
true
|
||||
);
|
||||
|
||||
const option = createOption(ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE);
|
||||
const update = { results: optionsMetrics, templatedRegex: '' };
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
removeVariableEditorError({ errorProp: 'query' }),
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query })),
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition })),
|
||||
variableStateFetching(toVariablePayload(variable)),
|
||||
updateVariableOptions(toVariablePayload(variable, update)),
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option })),
|
||||
variableStateCompleted(toVariablePayload(variable))
|
||||
toKeyedAction('key', removeVariableEditorError({ errorProp: 'query' })),
|
||||
toKeyedAction('key', changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query }))),
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition }))
|
||||
),
|
||||
toKeyedAction('key', variableStateFetching(toVariablePayload(variable))),
|
||||
toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
|
||||
toKeyedAction('key', variableStateCompleted(toVariablePayload(variable)))
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -457,21 +515,29 @@ describe('query actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenAsyncActionIsDispatched(changeQueryVariableQuery(toVariablePayload(variable), query, definition), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenAsyncActionIsDispatched(
|
||||
changeQueryVariableQuery(toKeyedVariableIdentifier(variable), query, definition),
|
||||
true
|
||||
);
|
||||
|
||||
const option = createOption('A');
|
||||
const update = { results: optionsMetrics, templatedRegex: '' };
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
removeVariableEditorError({ errorProp: 'query' }),
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query })),
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition })),
|
||||
variableStateFetching(toVariablePayload(variable)),
|
||||
updateVariableOptions(toVariablePayload(variable, update)),
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option })),
|
||||
variableStateCompleted(toVariablePayload(variable))
|
||||
toKeyedAction('key', removeVariableEditorError({ errorProp: 'query' })),
|
||||
toKeyedAction('key', changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: query }))),
|
||||
toKeyedAction(
|
||||
'key',
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'definition', propValue: definition }))
|
||||
),
|
||||
toKeyedAction('key', variableStateFetching(toVariablePayload(variable))),
|
||||
toKeyedAction('key', updateVariableOptions(toVariablePayload(variable, update))),
|
||||
toKeyedAction('key', setCurrentVariableValue(toVariablePayload(variable, { option }))),
|
||||
toKeyedAction('key', variableStateCompleted(toVariablePayload(variable)))
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -484,19 +550,20 @@ describe('query actions', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenAsyncActionIsDispatched(changeQueryVariableQuery(toVariablePayload(variable), query, definition), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction('key', addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenActionIsDispatched(toKeyedAction('key', variablesInitTransaction({ uid: 'key' })))
|
||||
.whenAsyncActionIsDispatched(
|
||||
changeQueryVariableQuery(toKeyedVariableIdentifier(variable), query, definition),
|
||||
true
|
||||
);
|
||||
|
||||
const errorText = 'Query cannot contain a reference to itself. Variable: $' + variable.name;
|
||||
|
||||
tester.thenDispatchedActionsPredicateShouldEqual((actions) => {
|
||||
const [editorError] = actions;
|
||||
const expectedNumberOfActions = 1;
|
||||
|
||||
expect(editorError).toEqual(addVariableEditorError({ errorProp: 'query', errorText }));
|
||||
return actions.length === expectedNumberOfActions;
|
||||
});
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction('key', addVariableEditorError({ errorProp: 'query', errorText }))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -672,6 +739,7 @@ function createVariable(extend?: Partial<QueryVariableModel>): QueryVariableMode
|
||||
return {
|
||||
type: 'query',
|
||||
id: '0',
|
||||
rootStateKey: 'key',
|
||||
global: false,
|
||||
current: createOption(''),
|
||||
options: [],
|
||||
|
||||
@@ -5,30 +5,32 @@ import { DataSourceRef } from '@grafana/data';
|
||||
import { updateOptions } from '../state/actions';
|
||||
import { QueryVariableModel } from '../types';
|
||||
import { ThunkResult } from '../../../types';
|
||||
import { getVariable } from '../state/selectors';
|
||||
import { getVariable, getVariablesState } from '../state/selectors';
|
||||
import { addVariableEditorError, changeVariableEditorExtended, removeVariableEditorError } from '../editor/reducer';
|
||||
import { changeVariableProp } from '../state/sharedReducer';
|
||||
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
|
||||
import { KeyedVariableIdentifier } from '../state/types';
|
||||
import { getVariableQueryEditor } from '../editor/getVariableQueryEditor';
|
||||
import { getVariableQueryRunner } from './VariableQueryRunner';
|
||||
import { variableQueryObserver } from './variableQueryObserver';
|
||||
import { hasOngoingTransaction } from '../utils';
|
||||
import { hasOngoingTransaction, toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { getQueryVariableEditorState } from '../editor/selectors';
|
||||
|
||||
export const updateQueryVariableOptions = (
|
||||
identifier: VariableIdentifier,
|
||||
identifier: KeyedVariableIdentifier,
|
||||
searchFilter?: string
|
||||
): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
if (!hasOngoingTransaction(getState())) {
|
||||
const { rootStateKey } = identifier;
|
||||
if (!hasOngoingTransaction(rootStateKey, getState())) {
|
||||
// we might have cancelled a batch so then variable state is removed
|
||||
return;
|
||||
}
|
||||
|
||||
const variableInState = getVariable<QueryVariableModel>(identifier.id, getState());
|
||||
if (getState().templating.editor.id === variableInState.id) {
|
||||
dispatch(removeVariableEditorError({ errorProp: 'update' }));
|
||||
const variableInState = getVariable<QueryVariableModel>(identifier, getState());
|
||||
if (getVariablesState(rootStateKey, getState()).editor.id === variableInState.id) {
|
||||
dispatch(toKeyedAction(rootStateKey, removeVariableEditorError({ errorProp: 'update' })));
|
||||
}
|
||||
const datasource = await getDataSourceSrv().get(variableInState.datasource ?? '');
|
||||
|
||||
@@ -44,8 +46,11 @@ export const updateQueryVariableOptions = (
|
||||
});
|
||||
} catch (err) {
|
||||
const error = toDataQueryError(err);
|
||||
if (getState().templating.editor.id === identifier.id) {
|
||||
dispatch(addVariableEditorError({ errorProp: 'update', errorText: error.message }));
|
||||
const { rootStateKey } = identifier;
|
||||
if (getVariablesState(rootStateKey, getState()).editor.id === identifier.id) {
|
||||
dispatch(
|
||||
toKeyedAction(rootStateKey, addVariableEditorError({ errorProp: 'update', errorText: error.message }))
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
@@ -54,33 +59,43 @@ export const updateQueryVariableOptions = (
|
||||
};
|
||||
|
||||
export const initQueryVariableEditor =
|
||||
(identifier: VariableIdentifier): ThunkResult<void> =>
|
||||
(identifier: KeyedVariableIdentifier): ThunkResult<void> =>
|
||||
async (dispatch, getState) => {
|
||||
const variable = getVariable<QueryVariableModel>(identifier.id, getState());
|
||||
await dispatch(changeQueryVariableDataSource(toVariableIdentifier(variable), variable.datasource));
|
||||
const variable = getVariable<QueryVariableModel>(identifier, getState());
|
||||
await dispatch(changeQueryVariableDataSource(toKeyedVariableIdentifier(variable), variable.datasource));
|
||||
};
|
||||
|
||||
export const changeQueryVariableDataSource = (
|
||||
identifier: VariableIdentifier,
|
||||
identifier: KeyedVariableIdentifier,
|
||||
name: DataSourceRef | null
|
||||
): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
const extendedEditorState = getQueryVariableEditorState(getState().templating.editor);
|
||||
const { rootStateKey } = identifier;
|
||||
const { editor } = getVariablesState(rootStateKey, getState());
|
||||
const extendedEditorState = getQueryVariableEditorState(editor);
|
||||
const previousDatasource = extendedEditorState?.dataSource;
|
||||
const dataSource = await getDataSourceSrv().get(name ?? '');
|
||||
|
||||
if (previousDatasource && previousDatasource.type !== dataSource?.type) {
|
||||
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: '' })));
|
||||
dispatch(
|
||||
toKeyedAction(
|
||||
rootStateKey,
|
||||
changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: '' }))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const VariableQueryEditor = await getVariableQueryEditor(dataSource);
|
||||
|
||||
dispatch(
|
||||
changeVariableEditorExtended({
|
||||
dataSource: dataSource,
|
||||
VariableQueryEditor: VariableQueryEditor,
|
||||
})
|
||||
toKeyedAction(
|
||||
rootStateKey,
|
||||
changeVariableEditorExtended({
|
||||
dataSource,
|
||||
VariableQueryEditor,
|
||||
})
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -89,22 +104,38 @@ export const changeQueryVariableDataSource = (
|
||||
};
|
||||
|
||||
export const changeQueryVariableQuery =
|
||||
(identifier: VariableIdentifier, query: any, definition?: string): ThunkResult<void> =>
|
||||
(identifier: KeyedVariableIdentifier, query: any, definition?: string): ThunkResult<void> =>
|
||||
async (dispatch, getState) => {
|
||||
const variableInState = getVariable<QueryVariableModel>(identifier.id, getState());
|
||||
const { rootStateKey } = identifier;
|
||||
const variableInState = getVariable<QueryVariableModel>(identifier, getState());
|
||||
if (hasSelfReferencingQuery(variableInState.name, query)) {
|
||||
const errorText = 'Query cannot contain a reference to itself. Variable: $' + variableInState.name;
|
||||
dispatch(addVariableEditorError({ errorProp: 'query', errorText }));
|
||||
dispatch(toKeyedAction(rootStateKey, addVariableEditorError({ errorProp: 'query', errorText })));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(removeVariableEditorError({ errorProp: 'query' }));
|
||||
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: query })));
|
||||
dispatch(toKeyedAction(rootStateKey, removeVariableEditorError({ errorProp: 'query' })));
|
||||
dispatch(
|
||||
toKeyedAction(
|
||||
rootStateKey,
|
||||
changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: query }))
|
||||
)
|
||||
);
|
||||
|
||||
if (definition) {
|
||||
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: definition })));
|
||||
dispatch(
|
||||
toKeyedAction(
|
||||
rootStateKey,
|
||||
changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: definition }))
|
||||
)
|
||||
);
|
||||
} else if (typeof query === 'string') {
|
||||
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: query })));
|
||||
dispatch(
|
||||
toKeyedAction(
|
||||
rootStateKey,
|
||||
changeVariableProp(toVariablePayload(identifier, { propName: 'definition', propValue: query }))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await dispatch(updateOptions(identifier));
|
||||
|
||||
@@ -7,8 +7,7 @@ import { setOptionAsCurrent, setOptionFromUrl } from '../state/actions';
|
||||
import { VariableAdapter } from '../adapters';
|
||||
import { QueryVariableEditor } from './QueryVariableEditor';
|
||||
import { updateQueryVariableOptions } from './actions';
|
||||
import { toVariableIdentifier } from '../state/types';
|
||||
import { containsVariable, isAllVariable } from '../utils';
|
||||
import { containsVariable, isAllVariable, toKeyedVariableIdentifier } from '../utils';
|
||||
import { optionPickerFactory } from '../pickers';
|
||||
import { ALL_VARIABLE_TEXT } from '../constants';
|
||||
|
||||
@@ -25,16 +24,16 @@ export const createQueryVariableAdapter = (): VariableAdapter<QueryVariableModel
|
||||
return containsVariable(variable.query, variable.datasource?.uid, variable.regex, variableToTest.name);
|
||||
},
|
||||
setValue: async (variable, option, emitChanges = false) => {
|
||||
await dispatch(setOptionAsCurrent(toVariableIdentifier(variable), option, emitChanges));
|
||||
await dispatch(setOptionAsCurrent(toKeyedVariableIdentifier(variable), option, emitChanges));
|
||||
},
|
||||
setValueFromUrl: async (variable, urlValue) => {
|
||||
await dispatch(setOptionFromUrl(toVariableIdentifier(variable), urlValue));
|
||||
await dispatch(setOptionFromUrl(toKeyedVariableIdentifier(variable), urlValue));
|
||||
},
|
||||
updateOptions: async (variable, searchFilter) => {
|
||||
await dispatch(updateQueryVariableOptions(toVariableIdentifier(variable), searchFilter));
|
||||
await dispatch(updateQueryVariableOptions(toKeyedVariableIdentifier(variable), searchFilter));
|
||||
},
|
||||
getSaveModel: (variable) => {
|
||||
const { index, id, state, global, queryValue, ...rest } = cloneDeep(variable);
|
||||
const { index, id, state, global, queryValue, rootStateKey, ...rest } = cloneDeep(variable);
|
||||
// remove options
|
||||
if (variable.refresh !== VariableRefresh.never) {
|
||||
return { ...rest, options: [] };
|
||||
|
||||
@@ -3,6 +3,7 @@ import { queryBuilder } from '../shared/testing/builders';
|
||||
import { FieldType, toDataFrame } from '@grafana/data';
|
||||
import { updateVariableOptions } from './reducer';
|
||||
import { areMetricFindValues, toMetricFindValues, updateOptionsState, validateVariableSelection } from './operators';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
|
||||
describe('operators', () => {
|
||||
beforeEach(() => {
|
||||
@@ -12,7 +13,7 @@ describe('operators', () => {
|
||||
describe('validateVariableSelection', () => {
|
||||
describe('when called', () => {
|
||||
it('then the correct observable should be created', async () => {
|
||||
const variable = queryBuilder().withId('query').build();
|
||||
const variable = queryBuilder().withId('query').withRootStateKey('key').build();
|
||||
const dispatch = jest.fn().mockResolvedValue({});
|
||||
const observable = of(undefined).pipe(validateVariableSelection({ variable, dispatch }));
|
||||
|
||||
@@ -27,7 +28,7 @@ describe('operators', () => {
|
||||
describe('updateOptionsState', () => {
|
||||
describe('when called', () => {
|
||||
it('then the correct observable should be created', async () => {
|
||||
const variable = queryBuilder().withId('query').build();
|
||||
const variable = queryBuilder().withId('query').withRootStateKey('key').build();
|
||||
const dispatch = jest.fn();
|
||||
const getTemplatedRegexFunc = jest.fn().mockReturnValue('getTemplatedRegexFunc result');
|
||||
|
||||
@@ -39,11 +40,14 @@ describe('operators', () => {
|
||||
expect(getTemplatedRegexFunc).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
updateVariableOptions({
|
||||
id: 'query',
|
||||
type: 'query',
|
||||
data: { results: [{ text: 'A' }], templatedRegex: 'getTemplatedRegexFunc result' },
|
||||
})
|
||||
toKeyedAction(
|
||||
'key',
|
||||
updateVariableOptions({
|
||||
id: 'query',
|
||||
type: 'query',
|
||||
data: { results: [{ text: 'A' }], templatedRegex: 'getTemplatedRegexFunc result' },
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,12 +3,12 @@ import { map, mergeMap } from 'rxjs/operators';
|
||||
|
||||
import { QueryVariableModel } from '../types';
|
||||
import { ThunkDispatch } from '../../../types';
|
||||
import { toVariableIdentifier, toVariablePayload } from '../state/types';
|
||||
import { validateVariableSelectionState } from '../state/actions';
|
||||
import { FieldType, getFieldDisplayName, isDataFrame, MetricFindValue, PanelData } from '@grafana/data';
|
||||
import { updateVariableOptions } from './reducer';
|
||||
import { getTemplatedRegex } from '../utils';
|
||||
import { getTemplatedRegex, toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
import { getProcessedDataFrames } from 'app/features/query/state/runRequest';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
|
||||
export function toMetricFindValues(): OperatorFunction<PanelData, MetricFindValue[]> {
|
||||
return (source) =>
|
||||
@@ -102,9 +102,13 @@ export function updateOptionsState(args: {
|
||||
source.pipe(
|
||||
map((results) => {
|
||||
const { variable, dispatch, getTemplatedRegexFunc } = args;
|
||||
if (!variable.rootStateKey) {
|
||||
console.error('updateOptionsState: variable.rootStateKey is not defined');
|
||||
return;
|
||||
}
|
||||
const templatedRegex = getTemplatedRegexFunc(variable);
|
||||
const payload = toVariablePayload(variable, { results, templatedRegex });
|
||||
dispatch(updateVariableOptions(payload));
|
||||
dispatch(toKeyedAction(variable.rootStateKey, updateVariableOptions(payload)));
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -124,7 +128,7 @@ export function validateVariableSelection(args: {
|
||||
// So after search and selection the current value is already update so no setValue, refresh and URL update is performed
|
||||
// The if statement below fixes https://github.com/grafana/grafana/issues/25671
|
||||
if (!searchFilter) {
|
||||
return from(dispatch(validateVariableSelectionState(toVariableIdentifier(variable))));
|
||||
return from(dispatch(validateVariableSelectionState(toKeyedVariableIdentifier(variable))));
|
||||
}
|
||||
|
||||
return of<void>();
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { MetricFindValue } from '@grafana/data';
|
||||
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import {
|
||||
metricNamesToVariableValues,
|
||||
@@ -6,11 +9,10 @@ import {
|
||||
updateVariableOptions,
|
||||
} from './reducer';
|
||||
import { QueryVariableModel, VariableSort } from '../types';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { getVariableTestContext } from '../state/helpers';
|
||||
import { toVariablePayload, VariablesState } from '../state/types';
|
||||
import { VariablesState } from '../state/types';
|
||||
import { createQueryVariableAdapter } from './adapter';
|
||||
import { MetricFindValue } from '@grafana/data';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
describe('queryVariableReducer', () => {
|
||||
const adapter = createQueryVariableAdapter();
|
||||
|
||||
@@ -4,8 +4,9 @@ import { MetricFindValue, stringToJsRegex } from '@grafana/data';
|
||||
|
||||
import { initialVariableModelState, QueryVariableModel, VariableOption, VariableRefresh, VariableSort } from '../types';
|
||||
|
||||
import { getInstanceState, initialVariablesState, VariablePayload, VariablesState } from '../state/types';
|
||||
import { initialVariablesState, VariablePayload, VariablesState } from '../state/types';
|
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE, NONE_VARIABLE_TEXT, NONE_VARIABLE_VALUE } from '../constants';
|
||||
import { getInstanceState } from '../state/selectors';
|
||||
|
||||
interface VariableOptionsUpdate {
|
||||
templatedRegex: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { variableQueryObserver } from './variableQueryObserver';
|
||||
import { LoadingState } from '@grafana/data';
|
||||
import { VariableIdentifier } from '../state/types';
|
||||
import { KeyedVariableIdentifier } from '../state/types';
|
||||
import { UpdateOptionsResults } from './VariableQueryRunner';
|
||||
|
||||
function getTestContext(args: { next?: UpdateOptionsResults; error?: any; complete?: boolean }) {
|
||||
@@ -27,7 +27,7 @@ function getTestContext(args: { next?: UpdateOptionsResults; error?: any; comple
|
||||
return { resolve, reject, subscription, observer };
|
||||
}
|
||||
|
||||
const identifier: VariableIdentifier = { id: 'id', type: 'query' };
|
||||
const identifier: KeyedVariableIdentifier = { id: 'id', type: 'query', rootStateKey: 'uid' };
|
||||
|
||||
describe('variableQueryObserver', () => {
|
||||
describe('when receiving a Done state', () => {
|
||||
|
||||
@@ -19,6 +19,11 @@ export class VariableBuilder<T extends VariableModel> {
|
||||
return this;
|
||||
}
|
||||
|
||||
withRootStateKey(key: string) {
|
||||
this.variable.rootStateKey = key;
|
||||
return this;
|
||||
}
|
||||
|
||||
build(): T {
|
||||
return this.variable;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { getTemplatingRootReducer, TemplatingReducerType } from './helpers';
|
||||
import { getPreloadedState, getTemplatingRootReducer, TemplatingReducerType } from './helpers';
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { createQueryVariableAdapter } from '../query/adapter';
|
||||
import { createCustomVariableAdapter } from '../custom/adapter';
|
||||
import { createTextBoxVariableAdapter } from '../textbox/adapter';
|
||||
import { createConstantVariableAdapter } from '../constant/adapter';
|
||||
import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||
import { TemplatingState } from 'app/features/variables/state/reducers';
|
||||
import {
|
||||
cancelVariables,
|
||||
changeVariableMultiValue,
|
||||
@@ -27,7 +26,6 @@ import {
|
||||
variableStateFetching,
|
||||
variableStateNotStarted,
|
||||
} from './sharedReducer';
|
||||
import { toVariableIdentifier, toVariablePayload } from './types';
|
||||
import {
|
||||
constantBuilder,
|
||||
customBuilder,
|
||||
@@ -52,6 +50,8 @@ import * as runtime from '@grafana/runtime';
|
||||
import { LoadingState } from '@grafana/data';
|
||||
import { toAsyncOfResult } from '../../query/state/DashboardQueryRunner/testHelpers';
|
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE, NEW_VARIABLE_ID } from '../constants';
|
||||
import { toKeyedAction } from './keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
|
||||
variableAdapters.setInit(() => [
|
||||
createQueryVariableAdapter(),
|
||||
@@ -81,44 +81,66 @@ runtime.setDataSourceSrv({
|
||||
describe('shared actions', () => {
|
||||
describe('when initDashboardTemplating is dispatched', () => {
|
||||
it('then correct actions are dispatched', () => {
|
||||
const key = 'key';
|
||||
const query = queryBuilder().build();
|
||||
const constant = constantBuilder().build();
|
||||
const datasource = datasourceBuilder().build();
|
||||
const custom = customBuilder().build();
|
||||
const textbox = textboxBuilder().build();
|
||||
const list = [query, constant, datasource, custom, textbox];
|
||||
const dashboard: any = { templating: { list } };
|
||||
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(initDashboardTemplating(list))
|
||||
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
|
||||
.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
|
||||
expect(dispatchedActions.length).toEqual(8);
|
||||
expect(dispatchedActions[0]).toEqual(
|
||||
addVariable(toVariablePayload(query, { global: false, index: 0, model: query }))
|
||||
toKeyedAction(key, addVariable(toVariablePayload(query, { global: false, index: 0, model: query })))
|
||||
);
|
||||
expect(dispatchedActions[1]).toEqual(
|
||||
addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant }))
|
||||
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
|
||||
);
|
||||
expect(dispatchedActions[2]).toEqual(
|
||||
addVariable(toVariablePayload(custom, { global: false, index: 2, model: custom }))
|
||||
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 2, model: custom })))
|
||||
);
|
||||
expect(dispatchedActions[3]).toEqual(
|
||||
addVariable(toVariablePayload(textbox, { global: false, index: 3, model: textbox }))
|
||||
toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 3, model: textbox })))
|
||||
);
|
||||
|
||||
// because uuid are dynamic we need to get the uuid from the resulting state
|
||||
// an alternative would be to add our own uuids in the model above instead
|
||||
expect(dispatchedActions[4]).toEqual(
|
||||
variableStateNotStarted(toVariablePayload({ ...query, id: dispatchedActions[4].payload.id }))
|
||||
toKeyedAction(
|
||||
key,
|
||||
variableStateNotStarted(
|
||||
toVariablePayload({ ...query, id: dispatchedActions[4].payload.action.payload.id })
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(dispatchedActions[5]).toEqual(
|
||||
variableStateNotStarted(toVariablePayload({ ...constant, id: dispatchedActions[5].payload.id }))
|
||||
toKeyedAction(
|
||||
key,
|
||||
variableStateNotStarted(
|
||||
toVariablePayload({ ...constant, id: dispatchedActions[5].payload.action.payload.id })
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(dispatchedActions[6]).toEqual(
|
||||
variableStateNotStarted(toVariablePayload({ ...custom, id: dispatchedActions[6].payload.id }))
|
||||
toKeyedAction(
|
||||
key,
|
||||
variableStateNotStarted(
|
||||
toVariablePayload({ ...custom, id: dispatchedActions[6].payload.action.payload.id })
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(dispatchedActions[7]).toEqual(
|
||||
variableStateNotStarted(toVariablePayload({ ...textbox, id: dispatchedActions[7].payload.id }))
|
||||
toKeyedAction(
|
||||
key,
|
||||
variableStateNotStarted(
|
||||
toVariablePayload({ ...textbox, id: dispatchedActions[7].payload.action.payload.id })
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return true;
|
||||
@@ -128,52 +150,71 @@ describe('shared actions', () => {
|
||||
|
||||
describe('when processVariables is dispatched', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const key = 'key';
|
||||
const query = queryBuilder().build();
|
||||
const constant = constantBuilder().build();
|
||||
const datasource = datasourceBuilder().build();
|
||||
const custom = customBuilder().build();
|
||||
const textbox = textboxBuilder().build();
|
||||
const list = [query, constant, datasource, custom, textbox];
|
||||
const preloadedState = {
|
||||
templating: {} as unknown as TemplatingState,
|
||||
};
|
||||
const dashboard: any = { templating: { list } };
|
||||
const preloadedState = getPreloadedState(key, {});
|
||||
const locationService: any = { getSearchObject: () => ({}) };
|
||||
runtime.setLocationService(locationService);
|
||||
const variableQueryRunner: any = {
|
||||
cancelRequest: jest.fn(),
|
||||
queueRequest: jest.fn(),
|
||||
getResponse: () => toAsyncOfResult({ state: LoadingState.Done, identifier: toVariableIdentifier(query) }),
|
||||
getResponse: () => toAsyncOfResult({ state: LoadingState.Done, identifier: toKeyedVariableIdentifier(query) }),
|
||||
destroy: jest.fn(),
|
||||
};
|
||||
setVariableQueryRunner(variableQueryRunner);
|
||||
|
||||
const tester = await reduxTester<TemplatingReducerType>({ preloadedState })
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
|
||||
.whenActionIsDispatched(initDashboardTemplating(list))
|
||||
.whenAsyncActionIsDispatched(processVariables(), true);
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
|
||||
.whenAsyncActionIsDispatched(processVariables(key), true);
|
||||
|
||||
await tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
|
||||
expect(dispatchedActions.length).toEqual(5);
|
||||
|
||||
expect(dispatchedActions[0]).toEqual(
|
||||
variableStateFetching(toVariablePayload({ ...query, id: dispatchedActions[0].payload.id }))
|
||||
toKeyedAction(
|
||||
key,
|
||||
variableStateFetching(toVariablePayload({ ...query, id: dispatchedActions[0].payload.action.payload.id }))
|
||||
)
|
||||
);
|
||||
|
||||
expect(dispatchedActions[1]).toEqual(
|
||||
variableStateCompleted(toVariablePayload({ ...constant, id: dispatchedActions[1].payload.id }))
|
||||
toKeyedAction(
|
||||
key,
|
||||
variableStateCompleted(
|
||||
toVariablePayload({ ...constant, id: dispatchedActions[1].payload.action.payload.id })
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
expect(dispatchedActions[2]).toEqual(
|
||||
variableStateCompleted(toVariablePayload({ ...custom, id: dispatchedActions[2].payload.id }))
|
||||
toKeyedAction(
|
||||
key,
|
||||
variableStateCompleted(toVariablePayload({ ...custom, id: dispatchedActions[2].payload.action.payload.id }))
|
||||
)
|
||||
);
|
||||
|
||||
expect(dispatchedActions[3]).toEqual(
|
||||
variableStateCompleted(toVariablePayload({ ...textbox, id: dispatchedActions[3].payload.id }))
|
||||
toKeyedAction(
|
||||
key,
|
||||
variableStateCompleted(
|
||||
toVariablePayload({ ...textbox, id: dispatchedActions[3].payload.action.payload.id })
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
expect(dispatchedActions[4]).toEqual(
|
||||
variableStateCompleted(toVariablePayload({ ...query, id: dispatchedActions[4].payload.id }))
|
||||
toKeyedAction(
|
||||
key,
|
||||
variableStateCompleted(toVariablePayload({ ...query, id: dispatchedActions[4].payload.action.payload.id }))
|
||||
)
|
||||
);
|
||||
|
||||
return true;
|
||||
@@ -183,8 +224,10 @@ describe('shared actions', () => {
|
||||
// Fix for https://github.com/grafana/grafana/issues/28791
|
||||
it('fix for https://github.com/grafana/grafana/issues/28791', async () => {
|
||||
setVariableQueryRunner(new VariableQueryRunner());
|
||||
const key = 'key';
|
||||
const stats = queryBuilder()
|
||||
.withId('stats')
|
||||
.withRootStateKey(key)
|
||||
.withName('stats')
|
||||
.withQuery('stats.*')
|
||||
.withRefresh(VariableRefresh.onDashboardLoad)
|
||||
@@ -195,6 +238,7 @@ describe('shared actions', () => {
|
||||
|
||||
const substats = queryBuilder()
|
||||
.withId('substats')
|
||||
.withRootStateKey(key)
|
||||
.withName('substats')
|
||||
.withQuery('stats.$stats.*')
|
||||
.withRefresh(VariableRefresh.onDashboardLoad)
|
||||
@@ -204,45 +248,64 @@ describe('shared actions', () => {
|
||||
.build();
|
||||
|
||||
const list = [stats, substats];
|
||||
const dashboard: any = { templating: { list } };
|
||||
const query = { orgId: '1', 'var-stats': 'response', 'var-substats': ALL_VARIABLE_TEXT };
|
||||
const locationService: any = { getSearchObject: () => query };
|
||||
runtime.setLocationService(locationService);
|
||||
const preloadedState = {
|
||||
templating: {} as unknown as TemplatingState,
|
||||
};
|
||||
const preloadedState = getPreloadedState(key, {});
|
||||
|
||||
const tester = await reduxTester<TemplatingReducerType>({ preloadedState })
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
|
||||
.whenActionIsDispatched(initDashboardTemplating(list))
|
||||
.whenAsyncActionIsDispatched(processVariables(), true);
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
|
||||
.whenAsyncActionIsDispatched(processVariables(key), true);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
variableStateFetching(toVariablePayload(stats)),
|
||||
updateVariableOptions(
|
||||
toVariablePayload(stats, { results: [{ text: 'responses' }, { text: 'timers' }], templatedRegex: '' })
|
||||
toKeyedAction(key, variableStateFetching(toVariablePayload(stats))),
|
||||
toKeyedAction(
|
||||
key,
|
||||
updateVariableOptions(
|
||||
toVariablePayload(stats, { results: [{ text: 'responses' }, { text: 'timers' }], templatedRegex: '' })
|
||||
)
|
||||
),
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(stats, { option: { text: ALL_VARIABLE_TEXT, value: ALL_VARIABLE_VALUE, selected: false } })
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(stats, {
|
||||
option: { text: ALL_VARIABLE_TEXT, value: ALL_VARIABLE_VALUE, selected: false },
|
||||
})
|
||||
)
|
||||
),
|
||||
variableStateCompleted(toVariablePayload(stats)),
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(stats, { option: { text: ['response'], value: ['response'], selected: false } })
|
||||
toKeyedAction(key, variableStateCompleted(toVariablePayload(stats))),
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(stats, { option: { text: ['response'], value: ['response'], selected: false } })
|
||||
)
|
||||
),
|
||||
variableStateFetching(toVariablePayload(substats)),
|
||||
updateVariableOptions(
|
||||
toVariablePayload(substats, { results: [{ text: '200' }, { text: '500' }], templatedRegex: '' })
|
||||
toKeyedAction(key, variableStateFetching(toVariablePayload(substats))),
|
||||
toKeyedAction(
|
||||
key,
|
||||
updateVariableOptions(
|
||||
toVariablePayload(substats, { results: [{ text: '200' }, { text: '500' }], templatedRegex: '' })
|
||||
)
|
||||
),
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(substats, {
|
||||
option: { text: [ALL_VARIABLE_TEXT], value: [ALL_VARIABLE_VALUE], selected: true },
|
||||
})
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(substats, {
|
||||
option: { text: [ALL_VARIABLE_TEXT], value: [ALL_VARIABLE_VALUE], selected: true },
|
||||
})
|
||||
)
|
||||
),
|
||||
variableStateCompleted(toVariablePayload(substats)),
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(substats, {
|
||||
option: { text: [ALL_VARIABLE_TEXT], value: [ALL_VARIABLE_VALUE], selected: false },
|
||||
})
|
||||
toKeyedAction(key, variableStateCompleted(toVariablePayload(substats))),
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(substats, {
|
||||
option: { text: [ALL_VARIABLE_TEXT], value: [ALL_VARIABLE_VALUE], selected: false },
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -260,36 +323,42 @@ describe('shared actions', () => {
|
||||
${undefined} | ${'B'} | ${undefined} | ${'should not dispatch setCurrentVariableValue'}
|
||||
`('then correct actions are dispatched', async ({ withOptions, withCurrent, defaultValue, expected }) => {
|
||||
let custom;
|
||||
|
||||
const key = 'key';
|
||||
if (!withOptions) {
|
||||
custom = customBuilder().withId('0').withCurrent(withCurrent).withoutOptions().build();
|
||||
custom = customBuilder().withId('0').withRootStateKey(key).withCurrent(withCurrent).withoutOptions().build();
|
||||
} else {
|
||||
custom = customBuilder()
|
||||
.withId('0')
|
||||
.withRootStateKey(key)
|
||||
.withOptions(...withOptions)
|
||||
.withCurrent(withCurrent)
|
||||
.build();
|
||||
}
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
const tester = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(
|
||||
validateVariableSelectionState(toVariableIdentifier(custom), defaultValue),
|
||||
validateVariableSelectionState(toKeyedVariableIdentifier(custom), defaultValue),
|
||||
true
|
||||
);
|
||||
|
||||
await tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
|
||||
const expectedActions: AnyAction[] = !withOptions
|
||||
? []
|
||||
: [
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'custom', id: '0' },
|
||||
{ option: { text: expected, value: expected, selected: false } }
|
||||
const expectedActions: AnyAction[] = withOptions
|
||||
? [
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'custom', id: '0' },
|
||||
{ option: { text: expected, value: expected, selected: false } }
|
||||
)
|
||||
)
|
||||
),
|
||||
];
|
||||
]
|
||||
: [];
|
||||
expect(dispatchedActions).toEqual(expectedActions);
|
||||
return true;
|
||||
});
|
||||
@@ -309,37 +378,49 @@ describe('shared actions', () => {
|
||||
'then correct actions are dispatched',
|
||||
async ({ withOptions, withCurrent, defaultValue, expectedText, expectedSelected }) => {
|
||||
let custom;
|
||||
|
||||
const key = 'key';
|
||||
if (!withOptions) {
|
||||
custom = customBuilder().withId('0').withMulti().withCurrent(withCurrent).withoutOptions().build();
|
||||
custom = customBuilder()
|
||||
.withId('0')
|
||||
.withRootStateKey(key)
|
||||
.withMulti()
|
||||
.withCurrent(withCurrent)
|
||||
.withoutOptions()
|
||||
.build();
|
||||
} else {
|
||||
custom = customBuilder()
|
||||
.withId('0')
|
||||
.withRootStateKey(key)
|
||||
.withMulti()
|
||||
.withOptions(...withOptions)
|
||||
.withCurrent(withCurrent)
|
||||
.build();
|
||||
}
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
const tester = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(
|
||||
validateVariableSelectionState(toVariableIdentifier(custom), defaultValue),
|
||||
validateVariableSelectionState(toKeyedVariableIdentifier(custom), defaultValue),
|
||||
true
|
||||
);
|
||||
|
||||
await tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
|
||||
const expectedActions: AnyAction[] = !withOptions
|
||||
? []
|
||||
: [
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'custom', id: '0' },
|
||||
{ option: { text: expectedText, value: expectedText, selected: expectedSelected } }
|
||||
const expectedActions: AnyAction[] = withOptions
|
||||
? [
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'custom', id: '0' },
|
||||
{ option: { text: expectedText, value: expectedText, selected: expectedSelected } }
|
||||
)
|
||||
)
|
||||
),
|
||||
];
|
||||
]
|
||||
: [];
|
||||
expect(dispatchedActions).toEqual(expectedActions);
|
||||
return true;
|
||||
});
|
||||
@@ -351,154 +432,196 @@ describe('shared actions', () => {
|
||||
describe('changeVariableName', () => {
|
||||
describe('when changeVariableName is dispatched with the same name', () => {
|
||||
it('then the correct actions are dispatched', () => {
|
||||
const textbox = textboxBuilder().withId('textbox').withName('textbox').build();
|
||||
const constant = constantBuilder().withId('constant').withName('constant').build();
|
||||
const key = 'key';
|
||||
const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
|
||||
const constant = constantBuilder().withId('constant').withRootStateKey(key).withName('constant').build();
|
||||
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
.whenActionIsDispatched(
|
||||
addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant }))
|
||||
toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
)
|
||||
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), constant.name), true)
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
|
||||
)
|
||||
.whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), constant.name), true)
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
changeVariableNameSucceeded({ type: 'constant', id: 'constant', data: { newName: 'constant' } })
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableNameSucceeded({ type: 'constant', id: 'constant', data: { newName: 'constant' } })
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('when changeVariableName is dispatched with an unique name', () => {
|
||||
it('then the correct actions are dispatched', () => {
|
||||
const textbox = textboxBuilder().withId('textbox').withName('textbox').build();
|
||||
const constant = constantBuilder().withId('constant').withName('constant').build();
|
||||
const key = 'key';
|
||||
const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
|
||||
const constant = constantBuilder().withId('constant').withRootStateKey(key).withName('constant').build();
|
||||
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
.whenActionIsDispatched(
|
||||
addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant }))
|
||||
toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
)
|
||||
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), 'constant1'), true)
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
|
||||
)
|
||||
.whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), 'constant1'), true)
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
addVariable({
|
||||
type: 'constant',
|
||||
id: 'constant1',
|
||||
data: {
|
||||
global: false,
|
||||
index: 1,
|
||||
model: {
|
||||
...constant,
|
||||
name: 'constant1',
|
||||
id: 'constant1',
|
||||
toKeyedAction(
|
||||
key,
|
||||
addVariable({
|
||||
type: 'constant',
|
||||
id: 'constant1',
|
||||
data: {
|
||||
global: false,
|
||||
index: 1,
|
||||
current: { selected: true, text: '', value: '' },
|
||||
options: [{ selected: true, text: '', value: '' }],
|
||||
} as ConstantVariableModel,
|
||||
},
|
||||
}),
|
||||
changeVariableNameSucceeded({ type: 'constant', id: 'constant1', data: { newName: 'constant1' } }),
|
||||
setIdInEditor({ id: 'constant1' }),
|
||||
removeVariable({ type: 'constant', id: 'constant', data: { reIndex: false } })
|
||||
model: {
|
||||
...constant,
|
||||
name: 'constant1',
|
||||
id: 'constant1',
|
||||
global: false,
|
||||
index: 1,
|
||||
current: { selected: true, text: '', value: '' },
|
||||
options: [{ selected: true, text: '', value: '' }],
|
||||
} as ConstantVariableModel,
|
||||
},
|
||||
})
|
||||
),
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableNameSucceeded({ type: 'constant', id: 'constant1', data: { newName: 'constant1' } })
|
||||
),
|
||||
toKeyedAction(key, setIdInEditor({ id: 'constant1' })),
|
||||
toKeyedAction(key, removeVariable({ type: 'constant', id: 'constant', data: { reIndex: false } }))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeVariableName is dispatched with an unique name for a new variable', () => {
|
||||
it('then the correct actions are dispatched', () => {
|
||||
const textbox = textboxBuilder().withId('textbox').withName('textbox').build();
|
||||
const constant = constantBuilder().withId(NEW_VARIABLE_ID).withName('constant').build();
|
||||
const key = 'key';
|
||||
const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
|
||||
const constant = constantBuilder().withId(NEW_VARIABLE_ID).withRootStateKey(key).withName('constant').build();
|
||||
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
.whenActionIsDispatched(
|
||||
addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant }))
|
||||
toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
)
|
||||
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), 'constant1'), true)
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
|
||||
)
|
||||
.whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), 'constant1'), true)
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
addVariable({
|
||||
type: 'constant',
|
||||
id: 'constant1',
|
||||
data: {
|
||||
global: false,
|
||||
index: 1,
|
||||
model: {
|
||||
...constant,
|
||||
name: 'constant1',
|
||||
id: 'constant1',
|
||||
toKeyedAction(
|
||||
key,
|
||||
addVariable({
|
||||
type: 'constant',
|
||||
id: 'constant1',
|
||||
data: {
|
||||
global: false,
|
||||
index: 1,
|
||||
current: { selected: true, text: '', value: '' },
|
||||
options: [{ selected: true, text: '', value: '' }],
|
||||
} as ConstantVariableModel,
|
||||
},
|
||||
}),
|
||||
changeVariableNameSucceeded({ type: 'constant', id: 'constant1', data: { newName: 'constant1' } }),
|
||||
setIdInEditor({ id: 'constant1' }),
|
||||
removeVariable({ type: 'constant', id: NEW_VARIABLE_ID, data: { reIndex: false } })
|
||||
model: {
|
||||
...constant,
|
||||
name: 'constant1',
|
||||
id: 'constant1',
|
||||
global: false,
|
||||
index: 1,
|
||||
current: { selected: true, text: '', value: '' },
|
||||
options: [{ selected: true, text: '', value: '' }],
|
||||
} as ConstantVariableModel,
|
||||
},
|
||||
})
|
||||
),
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableNameSucceeded({ type: 'constant', id: 'constant1', data: { newName: 'constant1' } })
|
||||
),
|
||||
toKeyedAction(key, setIdInEditor({ id: 'constant1' })),
|
||||
toKeyedAction(key, removeVariable({ type: 'constant', id: NEW_VARIABLE_ID, data: { reIndex: false } }))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeVariableName is dispatched with __newName', () => {
|
||||
it('then the correct actions are dispatched', () => {
|
||||
const textbox = textboxBuilder().withId('textbox').withName('textbox').build();
|
||||
const constant = constantBuilder().withId('constant').withName('constant').build();
|
||||
const key = 'key';
|
||||
const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
|
||||
const constant = constantBuilder().withId('constant').withRootStateKey(key).withName('constant').build();
|
||||
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
.whenActionIsDispatched(
|
||||
addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant }))
|
||||
toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
)
|
||||
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), '__newName'), true)
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
|
||||
)
|
||||
.whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), '__newName'), true)
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
changeVariableNameFailed({
|
||||
newName: '__newName',
|
||||
errorText: "Template names cannot begin with '__', that's reserved for Grafana's global variables",
|
||||
})
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableNameFailed({
|
||||
newName: '__newName',
|
||||
errorText: "Template names cannot begin with '__', that's reserved for Grafana's global variables",
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeVariableName is dispatched with illegal characters', () => {
|
||||
it('then the correct actions are dispatched', () => {
|
||||
const textbox = textboxBuilder().withId('textbox').withName('textbox').build();
|
||||
const constant = constantBuilder().withId('constant').withName('constant').build();
|
||||
const key = 'key';
|
||||
const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
|
||||
const constant = constantBuilder().withId('constant').withRootStateKey(key).withName('constant').build();
|
||||
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
.whenActionIsDispatched(
|
||||
addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant }))
|
||||
toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
)
|
||||
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), '#constant!'), true)
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
|
||||
)
|
||||
.whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), '#constant!'), true)
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
changeVariableNameFailed({
|
||||
newName: '#constant!',
|
||||
errorText: 'Only word and digit characters are allowed in variable names',
|
||||
})
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableNameFailed({
|
||||
newName: '#constant!',
|
||||
errorText: 'Only word and digit characters are allowed in variable names',
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changeVariableName is dispatched with a name that is already used', () => {
|
||||
it('then the correct actions are dispatched', () => {
|
||||
const textbox = textboxBuilder().withId('textbox').withName('textbox').build();
|
||||
const constant = constantBuilder().withId('constant').withName('constant').build();
|
||||
const key = 'key';
|
||||
const textbox = textboxBuilder().withId('textbox').withRootStateKey(key).withName('textbox').build();
|
||||
const constant = constantBuilder().withId('constant').withRootStateKey(key).withName('constant').build();
|
||||
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
.whenActionIsDispatched(
|
||||
addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant }))
|
||||
toKeyedAction(key, addVariable(toVariablePayload(textbox, { global: false, index: 0, model: textbox })))
|
||||
)
|
||||
.whenActionIsDispatched(changeVariableName(toVariableIdentifier(constant), 'textbox'), true)
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 1, model: constant })))
|
||||
)
|
||||
.whenActionIsDispatched(changeVariableName(toKeyedVariableIdentifier(constant), 'textbox'), true)
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
changeVariableNameFailed({
|
||||
newName: 'textbox',
|
||||
errorText: 'Variable with the same name already exists',
|
||||
})
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableNameFailed({
|
||||
newName: 'textbox',
|
||||
errorText: 'Variable with the same name already exists',
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -507,28 +630,42 @@ describe('shared actions', () => {
|
||||
describe('changeVariableMultiValue', () => {
|
||||
describe('when changeVariableMultiValue is dispatched for variable with multi enabled', () => {
|
||||
it('then correct actions are dispatched', () => {
|
||||
const custom = customBuilder().withId('custom').withMulti(true).withCurrent(['A'], ['A']).build();
|
||||
const key = 'key';
|
||||
const custom = customBuilder()
|
||||
.withId('custom')
|
||||
.withRootStateKey(key)
|
||||
.withMulti(true)
|
||||
.withCurrent(['A'], ['A'])
|
||||
.build();
|
||||
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
.whenActionIsDispatched(changeVariableMultiValue(toVariableIdentifier(custom), false), true)
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
)
|
||||
.whenActionIsDispatched(changeVariableMultiValue(toKeyedVariableIdentifier(custom), false), true)
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
changeVariableProp(
|
||||
toVariablePayload(custom, {
|
||||
propName: 'multi',
|
||||
propValue: false,
|
||||
})
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableProp(
|
||||
toVariablePayload(custom, {
|
||||
propName: 'multi',
|
||||
propValue: false,
|
||||
})
|
||||
)
|
||||
),
|
||||
changeVariableProp(
|
||||
toVariablePayload(custom, {
|
||||
propName: 'current',
|
||||
propValue: {
|
||||
value: 'A',
|
||||
text: 'A',
|
||||
selected: true,
|
||||
},
|
||||
})
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableProp(
|
||||
toVariablePayload(custom, {
|
||||
propName: 'current',
|
||||
propValue: {
|
||||
value: 'A',
|
||||
text: 'A',
|
||||
selected: true,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -536,28 +673,42 @@ describe('shared actions', () => {
|
||||
|
||||
describe('when changeVariableMultiValue is dispatched for variable with multi disabled', () => {
|
||||
it('then correct actions are dispatched', () => {
|
||||
const custom = customBuilder().withId('custom').withMulti(false).withCurrent(['A'], ['A']).build();
|
||||
const key = 'key';
|
||||
const custom = customBuilder()
|
||||
.withId('custom')
|
||||
.withRootStateKey(key)
|
||||
.withMulti(false)
|
||||
.withCurrent(['A'], ['A'])
|
||||
.build();
|
||||
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
.whenActionIsDispatched(changeVariableMultiValue(toVariableIdentifier(custom), true), true)
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
)
|
||||
.whenActionIsDispatched(changeVariableMultiValue(toKeyedVariableIdentifier(custom), true), true)
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
changeVariableProp(
|
||||
toVariablePayload(custom, {
|
||||
propName: 'multi',
|
||||
propValue: true,
|
||||
})
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableProp(
|
||||
toVariablePayload(custom, {
|
||||
propName: 'multi',
|
||||
propValue: true,
|
||||
})
|
||||
)
|
||||
),
|
||||
changeVariableProp(
|
||||
toVariablePayload(custom, {
|
||||
propName: 'current',
|
||||
propValue: {
|
||||
value: ['A'],
|
||||
text: ['A'],
|
||||
selected: true,
|
||||
},
|
||||
})
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableProp(
|
||||
toVariablePayload(custom, {
|
||||
propName: 'current',
|
||||
propValue: {
|
||||
value: ['A'],
|
||||
text: ['A'],
|
||||
selected: true,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -567,14 +718,15 @@ describe('shared actions', () => {
|
||||
describe('cleanUpVariables', () => {
|
||||
describe('when called', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
const key = 'key';
|
||||
reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(cleanUpVariables())
|
||||
.whenActionIsDispatched(cleanUpVariables(key))
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
cleanVariables(),
|
||||
cleanEditorState(),
|
||||
cleanPickerState(),
|
||||
variablesClearTransaction()
|
||||
toKeyedAction(key, cleanVariables()),
|
||||
toKeyedAction(key, cleanEditorState()),
|
||||
toKeyedAction(key, cleanPickerState()),
|
||||
toKeyedAction(key, variablesClearTransaction())
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -588,14 +740,15 @@ describe('shared actions', () => {
|
||||
|
||||
describe('when called', () => {
|
||||
it('then cancelAllInFlightRequests should be called and correct actions are dispatched', async () => {
|
||||
reduxTester<{ templating: TemplatingState }>()
|
||||
const key = 'key';
|
||||
reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(cancelVariables({ getBackendSrv: () => backendSrvMock }))
|
||||
.whenActionIsDispatched(cancelVariables(key, { getBackendSrv: () => backendSrvMock }))
|
||||
.thenDispatchedActionsShouldEqual(
|
||||
cleanVariables(),
|
||||
cleanEditorState(),
|
||||
cleanPickerState(),
|
||||
variablesClearTransaction()
|
||||
toKeyedAction(key, cleanVariables()),
|
||||
toKeyedAction(key, cleanEditorState()),
|
||||
toKeyedAction(key, cleanPickerState()),
|
||||
toKeyedAction(key, variablesClearTransaction())
|
||||
);
|
||||
|
||||
expect(cancelAllInFlightRequestsMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
VariableWithOptions,
|
||||
} from '../types';
|
||||
import { AppNotification, StoreState, ThunkResult } from '../../../types';
|
||||
import { getVariable, getVariables } from './selectors';
|
||||
import { getIfExistsLastKey, getVariable, getVariablesByKey, getVariablesState } from './selectors';
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { Graph } from '../../../core/utils/dag';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
variableStateFetching,
|
||||
variableStateNotStarted,
|
||||
} from './sharedReducer';
|
||||
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from './types';
|
||||
import { KeyedVariableIdentifier } from './types';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { getTemplateSrv, TemplateSrv } from '../../templating/template_srv';
|
||||
import { alignCurrentWithMulti } from '../shared/multiOptions';
|
||||
@@ -71,6 +71,9 @@ import {
|
||||
getCurrentText,
|
||||
getVariableRefresh,
|
||||
hasOngoingTransaction,
|
||||
toKeyedVariableIdentifier,
|
||||
toStateKey,
|
||||
toVariablePayload,
|
||||
} from '../utils';
|
||||
import { store } from 'app/store/store';
|
||||
import { getDatasourceSrv } from '../../plugins/datasource_srv';
|
||||
@@ -80,6 +83,7 @@ import { locationService } from '@grafana/runtime';
|
||||
import { appEvents } from '../../../core/core';
|
||||
import { getAllAffectedPanelIdsForVariableChange } from '../inspect/utils';
|
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
|
||||
import { toKeyedAction } from './keyedVariablesReducer';
|
||||
|
||||
// process flow queryVariable
|
||||
// thunk => processVariables
|
||||
@@ -111,24 +115,27 @@ import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
|
||||
// thunk => variableUpdated
|
||||
// adapter => updateOptions for dependent nodes
|
||||
|
||||
export const initDashboardTemplating = (list: VariableModel[]): ThunkResult<void> => {
|
||||
export const initDashboardTemplating = (key: string, dashboard: DashboardModel): ThunkResult<void> => {
|
||||
return (dispatch, getState) => {
|
||||
let orderIndex = 0;
|
||||
const list = dashboard.templating.list;
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
const model = fixSelectedInconsistency(list[index]);
|
||||
model.rootStateKey = key;
|
||||
if (!variableAdapters.getIfExists(model.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
dispatch(addVariable(toVariablePayload(model, { global: false, index: orderIndex++, model })));
|
||||
dispatch(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(model, { global: false, index: orderIndex++, model })))
|
||||
);
|
||||
}
|
||||
|
||||
getTemplateSrv().updateTimeRange(getTimeSrv().timeRange());
|
||||
|
||||
const variables = getVariables(getState());
|
||||
for (let index = 0; index < variables.length; index++) {
|
||||
const variable = variables[index];
|
||||
dispatch(variableStateNotStarted(toVariablePayload(variable)));
|
||||
const variables = getVariablesByKey(key, getState());
|
||||
for (const variable of variables) {
|
||||
dispatch(toKeyedAction(key, variableStateNotStarted(toVariablePayload(variable))));
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -159,7 +166,7 @@ export function fixSelectedInconsistency(model: VariableModel): VariableModel |
|
||||
return model;
|
||||
}
|
||||
|
||||
export const addSystemTemplateVariables = (dashboard: DashboardModel): ThunkResult<void> => {
|
||||
export const addSystemTemplateVariables = (key: string, dashboard: DashboardModel): ThunkResult<void> => {
|
||||
return (dispatch) => {
|
||||
const dashboardModel: DashboardVariableModel = {
|
||||
...initialVariableModelState,
|
||||
@@ -179,12 +186,15 @@ export const addSystemTemplateVariables = (dashboard: DashboardModel): ThunkResu
|
||||
};
|
||||
|
||||
dispatch(
|
||||
addVariable(
|
||||
toVariablePayload(dashboardModel, {
|
||||
global: dashboardModel.global,
|
||||
index: dashboardModel.index,
|
||||
model: dashboardModel,
|
||||
})
|
||||
toKeyedAction(
|
||||
key,
|
||||
addVariable(
|
||||
toVariablePayload(dashboardModel, {
|
||||
global: dashboardModel.global,
|
||||
index: dashboardModel.index,
|
||||
model: dashboardModel,
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -206,7 +216,10 @@ export const addSystemTemplateVariables = (dashboard: DashboardModel): ThunkResu
|
||||
};
|
||||
|
||||
dispatch(
|
||||
addVariable(toVariablePayload(orgModel, { global: orgModel.global, index: orgModel.index, model: orgModel }))
|
||||
toKeyedAction(
|
||||
key,
|
||||
addVariable(toVariablePayload(orgModel, { global: orgModel.global, index: orgModel.index, model: orgModel }))
|
||||
)
|
||||
);
|
||||
|
||||
const userModel: UserVariableModel = {
|
||||
@@ -228,25 +241,39 @@ export const addSystemTemplateVariables = (dashboard: DashboardModel): ThunkResu
|
||||
};
|
||||
|
||||
dispatch(
|
||||
addVariable(toVariablePayload(userModel, { global: userModel.global, index: userModel.index, model: userModel }))
|
||||
toKeyedAction(
|
||||
key,
|
||||
addVariable(
|
||||
toVariablePayload(userModel, { global: userModel.global, index: userModel.index, model: userModel })
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const changeVariableMultiValue = (identifier: VariableIdentifier, multi: boolean): ThunkResult<void> => {
|
||||
export const changeVariableMultiValue = (identifier: KeyedVariableIdentifier, multi: boolean): ThunkResult<void> => {
|
||||
return (dispatch, getState) => {
|
||||
const variable = getVariable<VariableWithMultiSupport>(identifier.id, getState());
|
||||
const { rootStateKey: key } = identifier;
|
||||
const variable = getVariable<VariableWithMultiSupport>(identifier, getState());
|
||||
const current = alignCurrentWithMulti(variable.current, multi);
|
||||
|
||||
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'multi', propValue: multi })));
|
||||
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'current', propValue: current })));
|
||||
dispatch(
|
||||
toKeyedAction(key, changeVariableProp(toVariablePayload(identifier, { propName: 'multi', propValue: multi })))
|
||||
);
|
||||
dispatch(
|
||||
toKeyedAction(key, changeVariableProp(toVariablePayload(identifier, { propName: 'current', propValue: current })))
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const processVariableDependencies = async (variable: VariableModel, state: StoreState) => {
|
||||
if (!variable.rootStateKey) {
|
||||
throw new Error(`rootStateKey not found for variable with id:${variable.id}`);
|
||||
}
|
||||
|
||||
const dependencies: VariableModel[] = [];
|
||||
|
||||
for (const otherVariable of getVariables(state)) {
|
||||
for (const otherVariable of getVariablesByKey(variable.rootStateKey, state)) {
|
||||
if (variable === otherVariable) {
|
||||
continue;
|
||||
}
|
||||
@@ -258,13 +285,17 @@ export const processVariableDependencies = async (variable: VariableModel, state
|
||||
}
|
||||
}
|
||||
|
||||
if (!isWaitingForDependencies(dependencies, state)) {
|
||||
if (!isWaitingForDependencies(variable.rootStateKey, dependencies, state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
const unsubscribe = store.subscribe(() => {
|
||||
if (!isWaitingForDependencies(dependencies, store.getState())) {
|
||||
if (!variable.rootStateKey) {
|
||||
throw new Error(`rootStateKey not found for variable with id:${variable.id}`);
|
||||
}
|
||||
|
||||
if (!isWaitingForDependencies(variable.rootStateKey, dependencies, store.getState())) {
|
||||
unsubscribe();
|
||||
resolve();
|
||||
}
|
||||
@@ -272,12 +303,12 @@ export const processVariableDependencies = async (variable: VariableModel, state
|
||||
});
|
||||
};
|
||||
|
||||
const isWaitingForDependencies = (dependencies: VariableModel[], state: StoreState): boolean => {
|
||||
const isWaitingForDependencies = (key: string, dependencies: VariableModel[], state: StoreState): boolean => {
|
||||
if (dependencies.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const variables = getVariables(state);
|
||||
const variables = getVariablesByKey(key, state);
|
||||
const notCompletedDependencies = dependencies.filter((d) =>
|
||||
variables.some((v) => v.id === d.id && (v.state === LoadingState.NotStarted || v.state === LoadingState.Loading))
|
||||
);
|
||||
@@ -286,11 +317,11 @@ const isWaitingForDependencies = (dependencies: VariableModel[], state: StoreSta
|
||||
};
|
||||
|
||||
export const processVariable = (
|
||||
identifier: VariableIdentifier,
|
||||
identifier: KeyedVariableIdentifier,
|
||||
queryParams: UrlQueryMap
|
||||
): ThunkResult<Promise<void>> => {
|
||||
return async (dispatch, getState) => {
|
||||
const variable = getVariable(identifier.id, getState());
|
||||
const variable = getVariable(identifier, getState());
|
||||
await processVariableDependencies(variable, getState());
|
||||
|
||||
const urlValue = queryParams['var-' + variable.name];
|
||||
@@ -306,7 +337,7 @@ export const processVariable = (
|
||||
refreshableVariable.refresh === VariableRefresh.onDashboardLoad ||
|
||||
refreshableVariable.refresh === VariableRefresh.onTimeRangeChanged
|
||||
) {
|
||||
await dispatch(updateOptions(toVariableIdentifier(refreshableVariable)));
|
||||
await dispatch(updateOptions(toKeyedVariableIdentifier(refreshableVariable)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -316,11 +347,12 @@ export const processVariable = (
|
||||
};
|
||||
};
|
||||
|
||||
export const processVariables = (): ThunkResult<Promise<void>> => {
|
||||
export const processVariables = (key: string): ThunkResult<Promise<void>> => {
|
||||
return async (dispatch, getState) => {
|
||||
const queryParams = locationService.getSearchObject();
|
||||
const promises = getVariables(getState()).map(
|
||||
async (variable: VariableModel) => await dispatch(processVariable(toVariableIdentifier(variable), queryParams))
|
||||
const promises = getVariablesByKey(key, getState()).map(
|
||||
async (variable: VariableModel) =>
|
||||
await dispatch(processVariable(toKeyedVariableIdentifier(variable), queryParams))
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
@@ -328,19 +360,19 @@ export const processVariables = (): ThunkResult<Promise<void>> => {
|
||||
};
|
||||
|
||||
export const setOptionFromUrl = (
|
||||
identifier: VariableIdentifier,
|
||||
identifier: KeyedVariableIdentifier,
|
||||
urlValue: UrlQueryValue
|
||||
): ThunkResult<Promise<void>> => {
|
||||
return async (dispatch, getState) => {
|
||||
const stringUrlValue = ensureStringValues(urlValue);
|
||||
const variable = getVariable(identifier.id, getState());
|
||||
const variable = getVariable(identifier, getState());
|
||||
if (getVariableRefresh(variable) !== VariableRefresh.never) {
|
||||
// updates options
|
||||
await dispatch(updateOptions(toVariableIdentifier(variable)));
|
||||
await dispatch(updateOptions(toKeyedVariableIdentifier(variable)));
|
||||
}
|
||||
|
||||
// get variable from state
|
||||
const variableFromState = getVariable<VariableWithOptions>(variable.id, getState());
|
||||
const variableFromState = getVariable<VariableWithOptions>(toKeyedVariableIdentifier(variable), getState());
|
||||
if (!variableFromState) {
|
||||
throw new Error(`Couldn't find variable with name: ${variable.name}`);
|
||||
}
|
||||
@@ -418,11 +450,11 @@ export const selectOptionsForCurrentValue = (variable: VariableWithOptions): Var
|
||||
};
|
||||
|
||||
export const validateVariableSelectionState = (
|
||||
identifier: VariableIdentifier,
|
||||
identifier: KeyedVariableIdentifier,
|
||||
defaultValue?: string
|
||||
): ThunkResult<Promise<void>> => {
|
||||
return (dispatch, getState) => {
|
||||
const variableInState = getVariable<VariableWithOptions>(identifier.id, getState());
|
||||
const variableInState = getVariable<VariableWithOptions>(identifier, getState());
|
||||
const current = variableInState.current || ({} as unknown as VariableOption);
|
||||
const setValue = variableAdapters.get(variableInState.type).setValue;
|
||||
|
||||
@@ -474,12 +506,13 @@ export const validateVariableSelectionState = (
|
||||
};
|
||||
|
||||
export const setOptionAsCurrent = (
|
||||
identifier: VariableIdentifier,
|
||||
identifier: KeyedVariableIdentifier,
|
||||
current: VariableOption,
|
||||
emitChanges: boolean
|
||||
): ThunkResult<Promise<void>> => {
|
||||
return async (dispatch) => {
|
||||
dispatch(setCurrentVariableValue(toVariablePayload(identifier, { option: current })));
|
||||
const { rootStateKey: key } = identifier;
|
||||
dispatch(toKeyedAction(key, setCurrentVariableValue(toVariablePayload(identifier, { option: current }))));
|
||||
return await dispatch(variableUpdated(identifier, emitChanges));
|
||||
};
|
||||
};
|
||||
@@ -507,25 +540,26 @@ const createGraph = (variables: VariableModel[]) => {
|
||||
};
|
||||
|
||||
export const variableUpdated = (
|
||||
identifier: VariableIdentifier,
|
||||
identifier: KeyedVariableIdentifier,
|
||||
emitChangeEvents: boolean,
|
||||
events: typeof appEvents = appEvents
|
||||
): ThunkResult<Promise<void>> => {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const variableInState = getVariable(identifier.id, state);
|
||||
const { rootStateKey } = identifier;
|
||||
const variableInState = getVariable(identifier, state);
|
||||
|
||||
// if we're initializing variables ignore cascading update because we are in a boot up scenario
|
||||
if (state.templating.transaction.status === TransactionStatus.Fetching) {
|
||||
if (getVariablesState(rootStateKey, state).transaction.status === TransactionStatus.Fetching) {
|
||||
if (getVariableRefresh(variableInState) === VariableRefresh.never) {
|
||||
// for variable types with updates that go the setValueFromUrl path in the update let's make sure their state is set to Done.
|
||||
await dispatch(upgradeLegacyQueries(toVariableIdentifier(variableInState)));
|
||||
await dispatch(upgradeLegacyQueries(toKeyedVariableIdentifier(variableInState)));
|
||||
dispatch(completeVariableLoading(identifier));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const variables = getVariables(state);
|
||||
const variables = getVariablesByKey(rootStateKey, state);
|
||||
const g = createGraph(variables);
|
||||
const panels = state.dashboard?.getModel()?.panels ?? [];
|
||||
const event: VariablesChangedEvent = isAdHoc(variableInState)
|
||||
@@ -541,14 +575,14 @@ export const variableUpdated = (
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return dispatch(updateOptions(toVariableIdentifier(variable)));
|
||||
return dispatch(updateOptions(toKeyedVariableIdentifier(variable)));
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
if (emitChangeEvents) {
|
||||
events.publish(new VariablesChanged(event));
|
||||
locationService.partial(getQueryWithVariables(getState));
|
||||
locationService.partial(getQueryWithVariables(rootStateKey, getState));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -561,12 +595,13 @@ export interface OnTimeRangeUpdatedDependencies {
|
||||
|
||||
export const onTimeRangeUpdated =
|
||||
(
|
||||
key: string,
|
||||
timeRange: TimeRange,
|
||||
dependencies: OnTimeRangeUpdatedDependencies = { templateSrv: getTemplateSrv(), events: appEvents }
|
||||
): ThunkResult<Promise<void>> =>
|
||||
async (dispatch, getState) => {
|
||||
dependencies.templateSrv.updateTimeRange(timeRange);
|
||||
const variablesThatNeedRefresh = getVariables(getState()).filter((variable) => {
|
||||
const variablesThatNeedRefresh = getVariablesByKey(key, getState()).filter((variable) => {
|
||||
if (variable.hasOwnProperty('refresh') && variable.hasOwnProperty('options')) {
|
||||
const variableWithRefresh = variable as unknown as QueryVariableModel;
|
||||
return variableWithRefresh.refresh === VariableRefresh.onTimeRangeChanged;
|
||||
@@ -577,7 +612,7 @@ export const onTimeRangeUpdated =
|
||||
|
||||
const variableIds = variablesThatNeedRefresh.map((variable) => variable.id);
|
||||
const promises = variablesThatNeedRefresh.map((variable: VariableWithOptions) =>
|
||||
dispatch(timeRangeUpdated(toVariableIdentifier(variable)))
|
||||
dispatch(timeRangeUpdated(toKeyedVariableIdentifier(variable)))
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -590,14 +625,14 @@ export const onTimeRangeUpdated =
|
||||
};
|
||||
|
||||
const timeRangeUpdated =
|
||||
(identifier: VariableIdentifier): ThunkResult<Promise<void>> =>
|
||||
(identifier: KeyedVariableIdentifier): ThunkResult<Promise<void>> =>
|
||||
async (dispatch, getState) => {
|
||||
const variableInState = getVariable<VariableWithOptions>(identifier.id);
|
||||
const variableInState = getVariable<VariableWithOptions>(identifier, getState());
|
||||
const previousOptions = variableInState.options.slice();
|
||||
|
||||
await dispatch(updateOptions(toVariableIdentifier(variableInState), true));
|
||||
await dispatch(updateOptions(toKeyedVariableIdentifier(variableInState), true));
|
||||
|
||||
const updatedVariable = getVariable<VariableWithOptions>(identifier.id, getState());
|
||||
const updatedVariable = getVariable<VariableWithOptions>(identifier, getState());
|
||||
const updatedOptions = updatedVariable.options;
|
||||
|
||||
if (JSON.stringify(previousOptions) !== JSON.stringify(updatedOptions)) {
|
||||
@@ -607,11 +642,11 @@ const timeRangeUpdated =
|
||||
};
|
||||
|
||||
export const templateVarsChangedInUrl =
|
||||
(vars: ExtendedUrlQueryMap, events: typeof appEvents = appEvents): ThunkResult<void> =>
|
||||
(key: string, vars: ExtendedUrlQueryMap, events: typeof appEvents = appEvents): ThunkResult<void> =>
|
||||
async (dispatch, getState) => {
|
||||
const update: Array<Promise<any>> = [];
|
||||
const dashboard = getState().dashboard.getModel();
|
||||
for (const variable of getVariables(getState())) {
|
||||
for (const variable of getVariablesByKey(key, getState())) {
|
||||
const key = `var-${variable.name}`;
|
||||
if (!vars.hasOwnProperty(key)) {
|
||||
// key not found quick exit
|
||||
@@ -657,7 +692,7 @@ export function isVariableUrlValueDifferentFromCurrent(variable: VariableModel,
|
||||
return !isEqual(variableValue, stringUrlValue);
|
||||
}
|
||||
|
||||
const getQueryWithVariables = (getState: () => StoreState): UrlQueryMap => {
|
||||
const getQueryWithVariables = (key: string, getState: () => StoreState): UrlQueryMap => {
|
||||
const queryParams = locationService.getSearchObject();
|
||||
|
||||
const queryParamsNew = Object.keys(queryParams)
|
||||
@@ -667,7 +702,7 @@ const getQueryWithVariables = (getState: () => StoreState): UrlQueryMap => {
|
||||
return obj;
|
||||
}, {} as UrlQueryMap);
|
||||
|
||||
for (const variable of getVariables(getState())) {
|
||||
for (const variable of getVariablesByKey(key, getState())) {
|
||||
if (variable.skipUrlSync) {
|
||||
continue;
|
||||
}
|
||||
@@ -680,27 +715,32 @@ const getQueryWithVariables = (getState: () => StoreState): UrlQueryMap => {
|
||||
};
|
||||
|
||||
export const initVariablesTransaction =
|
||||
(dashboardUid: string, dashboard: DashboardModel): ThunkResult<void> =>
|
||||
(urlUid: string, dashboard: DashboardModel): ThunkResult<Promise<void>> =>
|
||||
async (dispatch, getState) => {
|
||||
try {
|
||||
const transactionState = getState().templating.transaction;
|
||||
if (transactionState.status === TransactionStatus.Fetching) {
|
||||
// previous dashboard is still fetching variables, cancel all requests
|
||||
dispatch(cancelVariables());
|
||||
const uid = toStateKey(urlUid);
|
||||
const state = getState();
|
||||
const lastKey = getIfExistsLastKey(state);
|
||||
if (lastKey) {
|
||||
const transactionState = getVariablesState(lastKey, state).transaction;
|
||||
if (transactionState.status === TransactionStatus.Fetching) {
|
||||
// previous dashboard is still fetching variables, cancel all requests
|
||||
dispatch(cancelVariables(lastKey));
|
||||
}
|
||||
}
|
||||
|
||||
// Start init transaction
|
||||
dispatch(variablesInitTransaction({ uid: dashboardUid }));
|
||||
dispatch(toKeyedAction(uid, variablesInitTransaction({ uid })));
|
||||
// Add system variables like __dashboard and __user
|
||||
dispatch(addSystemTemplateVariables(dashboard));
|
||||
dispatch(addSystemTemplateVariables(uid, dashboard));
|
||||
// Load all variables into redux store
|
||||
dispatch(initDashboardTemplating(dashboard.templating.list));
|
||||
dispatch(initDashboardTemplating(uid, dashboard));
|
||||
// Migrate data source name to ref
|
||||
dispatch(migrateVariablesDatasourceNameToRef());
|
||||
dispatch(migrateVariablesDatasourceNameToRef(uid));
|
||||
// Process all variable updates
|
||||
await dispatch(processVariables());
|
||||
// Mark update as complete
|
||||
dispatch(variablesCompleteTransaction({ uid: dashboardUid }));
|
||||
await dispatch(processVariables(uid));
|
||||
// Set transaction as complete
|
||||
dispatch(toKeyedAction(uid, variablesCompleteTransaction({ uid })));
|
||||
} catch (err) {
|
||||
dispatch(notifyApp(createVariableErrorNotification('Templating init failed', err)));
|
||||
console.error(err);
|
||||
@@ -708,10 +748,11 @@ export const initVariablesTransaction =
|
||||
};
|
||||
|
||||
export function migrateVariablesDatasourceNameToRef(
|
||||
getDatasourceSrvFunc: typeof getDatasourceSrv = getDatasourceSrv
|
||||
key: string,
|
||||
getDatasourceSrvFunc = getDatasourceSrv
|
||||
): ThunkResult<void> {
|
||||
return function (dispatch, getState) {
|
||||
const variables = getVariables(getState());
|
||||
return (dispatch, getState) => {
|
||||
const variables = getVariablesByKey(key, getState());
|
||||
for (const variable of variables) {
|
||||
if (!isAdHoc(variable) && !isQuery(variable)) {
|
||||
continue;
|
||||
@@ -726,43 +767,51 @@ export function migrateVariablesDatasourceNameToRef(
|
||||
// the call to getInstanceSettings needs to be done after initDashboardTemplating because we might have
|
||||
// datasource variables that need to be resolved
|
||||
const ds = getDatasourceSrvFunc().getInstanceSettings(nameOrRef);
|
||||
const dsRef = !ds ? { uid: nameOrRef } : getDataSourceRef(ds);
|
||||
dispatch(changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: dsRef })));
|
||||
const dsRef = ds ? getDataSourceRef(ds) : { uid: nameOrRef };
|
||||
dispatch(
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'datasource', propValue: dsRef }))
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const cleanUpVariables = (): ThunkResult<void> => (dispatch) => {
|
||||
dispatch(cleanVariables());
|
||||
dispatch(cleanEditorState());
|
||||
dispatch(cleanPickerState());
|
||||
dispatch(variablesClearTransaction());
|
||||
};
|
||||
export const cleanUpVariables =
|
||||
(key: string): ThunkResult<void> =>
|
||||
(dispatch) => {
|
||||
dispatch(toKeyedAction(key, cleanVariables()));
|
||||
dispatch(toKeyedAction(key, cleanEditorState()));
|
||||
dispatch(toKeyedAction(key, cleanPickerState()));
|
||||
dispatch(toKeyedAction(key, variablesClearTransaction()));
|
||||
};
|
||||
|
||||
type CancelVariablesDependencies = { getBackendSrv: typeof getBackendSrv };
|
||||
export const cancelVariables =
|
||||
(dependencies: CancelVariablesDependencies = { getBackendSrv: getBackendSrv }): ThunkResult<void> =>
|
||||
(key: string, dependencies: CancelVariablesDependencies = { getBackendSrv: getBackendSrv }): ThunkResult<void> =>
|
||||
(dispatch) => {
|
||||
dependencies.getBackendSrv().cancelAllInFlightRequests();
|
||||
dispatch(cleanUpVariables());
|
||||
dispatch(cleanUpVariables(key));
|
||||
};
|
||||
|
||||
export const updateOptions =
|
||||
(identifier: VariableIdentifier, rethrow = false): ThunkResult<Promise<void>> =>
|
||||
(identifier: KeyedVariableIdentifier, rethrow = false): ThunkResult<Promise<void>> =>
|
||||
async (dispatch, getState) => {
|
||||
const { rootStateKey } = identifier;
|
||||
try {
|
||||
if (!hasOngoingTransaction(getState())) {
|
||||
if (!hasOngoingTransaction(rootStateKey, getState())) {
|
||||
// we might have cancelled a batch so then variable state is removed
|
||||
return;
|
||||
}
|
||||
|
||||
const variableInState = getVariable(identifier.id, getState());
|
||||
dispatch(variableStateFetching(toVariablePayload(variableInState)));
|
||||
await dispatch(upgradeLegacyQueries(toVariableIdentifier(variableInState)));
|
||||
const variableInState = getVariable(identifier, getState());
|
||||
dispatch(toKeyedAction(rootStateKey, variableStateFetching(toVariablePayload(variableInState))));
|
||||
await dispatch(upgradeLegacyQueries(toKeyedVariableIdentifier(variableInState)));
|
||||
await variableAdapters.get(variableInState.type).updateOptions(variableInState);
|
||||
dispatch(completeVariableLoading(identifier));
|
||||
} catch (error) {
|
||||
dispatch(variableStateFailed(toVariablePayload(identifier, { error })));
|
||||
dispatch(toKeyedAction(rootStateKey, variableStateFailed(toVariablePayload(identifier, { error }))));
|
||||
|
||||
if (!rethrow) {
|
||||
console.error(error);
|
||||
@@ -778,7 +827,7 @@ export const updateOptions =
|
||||
export const createVariableErrorNotification = (
|
||||
message: string,
|
||||
error: any,
|
||||
identifier?: VariableIdentifier
|
||||
identifier?: KeyedVariableIdentifier
|
||||
): AppNotification =>
|
||||
createErrorNotification(
|
||||
`${identifier ? `Templating [${identifier.id}]` : 'Templating'}`,
|
||||
@@ -786,31 +835,33 @@ export const createVariableErrorNotification = (
|
||||
);
|
||||
|
||||
export const completeVariableLoading =
|
||||
(identifier: VariableIdentifier): ThunkResult<void> =>
|
||||
(identifier: KeyedVariableIdentifier): ThunkResult<void> =>
|
||||
(dispatch, getState) => {
|
||||
if (!hasOngoingTransaction(getState())) {
|
||||
const { rootStateKey } = identifier;
|
||||
if (!hasOngoingTransaction(rootStateKey, getState())) {
|
||||
// we might have cancelled a batch so then variable state is removed
|
||||
return;
|
||||
}
|
||||
|
||||
const variableInState = getVariable(identifier.id, getState());
|
||||
const variableInState = getVariable(identifier, getState());
|
||||
|
||||
if (variableInState.state !== LoadingState.Done) {
|
||||
dispatch(variableStateCompleted(toVariablePayload(variableInState)));
|
||||
dispatch(toKeyedAction(identifier.rootStateKey, variableStateCompleted(toVariablePayload(variableInState))));
|
||||
}
|
||||
};
|
||||
|
||||
export function upgradeLegacyQueries(
|
||||
identifier: VariableIdentifier,
|
||||
identifier: KeyedVariableIdentifier,
|
||||
getDatasourceSrvFunc: typeof getDatasourceSrv = getDatasourceSrv
|
||||
): ThunkResult<void> {
|
||||
return async function (dispatch, getState) {
|
||||
if (!hasOngoingTransaction(getState())) {
|
||||
const { id, rootStateKey } = identifier;
|
||||
if (!hasOngoingTransaction(rootStateKey, getState())) {
|
||||
// we might have cancelled a batch so then variable state is removed
|
||||
return;
|
||||
}
|
||||
|
||||
const variable = getVariable<QueryVariableModel>(identifier.id, getState());
|
||||
const variable = getVariable<QueryVariableModel>(identifier, getState());
|
||||
|
||||
if (!isQuery(variable)) {
|
||||
return;
|
||||
@@ -832,11 +883,16 @@ export function upgradeLegacyQueries(
|
||||
}
|
||||
|
||||
const query = {
|
||||
refId: `${datasource.name}-${identifier.id}-Variable-Query`,
|
||||
refId: `${datasource.name}-${id}-Variable-Query`,
|
||||
query: variable.query,
|
||||
};
|
||||
|
||||
dispatch(changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: query })));
|
||||
dispatch(
|
||||
toKeyedAction(
|
||||
rootStateKey,
|
||||
changeVariableProp(toVariablePayload(identifier, { propName: 'query', propValue: query }))
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
dispatch(notifyApp(createVariableErrorNotification('Failed to upgrade legacy queries', err)));
|
||||
console.error(err);
|
||||
|
||||
@@ -13,9 +13,10 @@ import {
|
||||
|
||||
import { VariableAdapter } from '../adapters';
|
||||
import { dashboardReducer } from 'app/features/dashboard/state/reducers';
|
||||
import { templatingReducers, TemplatingState } from './reducers';
|
||||
import { DashboardState } from '../../../types';
|
||||
import { DashboardState, StoreState } from '../../../types';
|
||||
import { NEW_VARIABLE_ID } from '../constants';
|
||||
import { keyedVariablesReducer, KeyedVariablesState } from './keyedVariablesReducer';
|
||||
import { getInitialTemplatingState, TemplatingState } from './reducers';
|
||||
|
||||
export const getVariableState = (
|
||||
noOfVariables: number,
|
||||
@@ -86,6 +87,7 @@ export const getVariableState = (
|
||||
for (let index = 0; index < noOfVariables; index++) {
|
||||
variables[index] = {
|
||||
id: index.toString(),
|
||||
rootStateKey: 'key',
|
||||
type: 'query',
|
||||
name: `Name-${index}`,
|
||||
hide: VariableHide.dontHide,
|
||||
@@ -102,6 +104,7 @@ export const getVariableState = (
|
||||
if (includeEmpty) {
|
||||
variables[NEW_VARIABLE_ID] = {
|
||||
id: NEW_VARIABLE_ID,
|
||||
rootStateKey: 'key',
|
||||
type: 'query',
|
||||
name: `Name-${NEW_VARIABLE_ID}`,
|
||||
hide: VariableHide.dontHide,
|
||||
@@ -122,12 +125,16 @@ export const getVariableTestContext = <Model extends VariableModel>(
|
||||
adapter: VariableAdapter<Model>,
|
||||
variableOverrides: Partial<Model> = {}
|
||||
) => {
|
||||
const defaultVariable = {
|
||||
...adapter.initialState,
|
||||
const defaults: Partial<VariableModel> = {
|
||||
id: '0',
|
||||
rootStateKey: 'key',
|
||||
index: 0,
|
||||
name: '0',
|
||||
};
|
||||
const defaultVariable = {
|
||||
...adapter.initialState,
|
||||
...defaults,
|
||||
};
|
||||
|
||||
const initialState: VariablesState = {
|
||||
'0': { ...defaultVariable, ...variableOverrides },
|
||||
@@ -139,14 +146,31 @@ export const getVariableTestContext = <Model extends VariableModel>(
|
||||
export const getRootReducer = () =>
|
||||
combineReducers({
|
||||
dashboard: dashboardReducer,
|
||||
templating: templatingReducers,
|
||||
templating: keyedVariablesReducer,
|
||||
});
|
||||
|
||||
export type RootReducerType = { dashboard: DashboardState; templating: TemplatingState };
|
||||
export type RootReducerType = { dashboard: DashboardState; templating: KeyedVariablesState };
|
||||
|
||||
export const getTemplatingRootReducer = () =>
|
||||
combineReducers({
|
||||
templating: templatingReducers,
|
||||
templating: keyedVariablesReducer,
|
||||
});
|
||||
|
||||
export type TemplatingReducerType = { templating: TemplatingState };
|
||||
export type TemplatingReducerType = { templating: KeyedVariablesState };
|
||||
|
||||
export function getPreloadedState(
|
||||
key: string,
|
||||
templatingState: Partial<TemplatingState>
|
||||
): Pick<StoreState, 'templating'> {
|
||||
return {
|
||||
templating: {
|
||||
lastKey: key,
|
||||
keys: {
|
||||
[key]: {
|
||||
...getInitialTemplatingState(),
|
||||
...templatingState,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getRootReducer, RootReducerType } from './helpers';
|
||||
import { getPreloadedState, getRootReducer, RootReducerType } from './helpers';
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { createQueryVariableAdapter } from '../query/adapter';
|
||||
import { createConstantVariableAdapter } from '../constant/adapter';
|
||||
@@ -11,15 +11,15 @@ import {
|
||||
variableStateFetching,
|
||||
variableStateNotStarted,
|
||||
} from './sharedReducer';
|
||||
import { toVariablePayload } from './types';
|
||||
import { adHocBuilder, constantBuilder, datasourceBuilder, queryBuilder } from '../shared/testing/builders';
|
||||
import { cleanEditorState, initialVariableEditorState } from '../editor/reducer';
|
||||
import { cleanEditorState } from '../editor/reducer';
|
||||
import {
|
||||
initialTransactionState,
|
||||
variablesClearTransaction,
|
||||
variablesCompleteTransaction,
|
||||
variablesInitTransaction,
|
||||
} from './transactionReducer';
|
||||
import { cleanPickerState, initialState } from '../pickers/OptionsPicker/reducer';
|
||||
import { cleanPickerState } from '../pickers/OptionsPicker/reducer';
|
||||
import { cleanVariables } from './variablesReducer';
|
||||
import { createAdHocVariableAdapter } from '../adhoc/adapter';
|
||||
import { createDataSourceVariableAdapter } from '../datasource/adapter';
|
||||
@@ -30,6 +30,8 @@ import { toAsyncOfResult } from '../../query/state/DashboardQueryRunner/testHelp
|
||||
import { setVariableQueryRunner } from '../query/VariableQueryRunner';
|
||||
import { createDataSourceOptions } from '../datasource/reducer';
|
||||
import { initVariablesTransaction } from './actions';
|
||||
import { toKeyedAction } from './keyedVariablesReducer';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
variableAdapters.setInit(() => [
|
||||
createQueryVariableAdapter(),
|
||||
@@ -39,7 +41,7 @@ variableAdapters.setInit(() => [
|
||||
]);
|
||||
|
||||
function getTestContext(variables?: VariableModel[]) {
|
||||
const uid = 'uid';
|
||||
const key = 'key';
|
||||
const constant = constantBuilder().withId('constant').withName('constant').build();
|
||||
const templating = { list: variables ?? [constant] };
|
||||
const getInstanceSettingsMock = jest.fn().mockReturnValue(undefined);
|
||||
@@ -57,34 +59,34 @@ function getTestContext(variables?: VariableModel[]) {
|
||||
};
|
||||
setVariableQueryRunner(variableQueryRunner);
|
||||
|
||||
const dashboard: any = { title: 'Some dash', uid, templating };
|
||||
const dashboard: any = { title: 'Some dash', uid: key, templating };
|
||||
|
||||
return { constant, getInstanceSettingsMock, templating, uid, dashboard };
|
||||
return { constant, getInstanceSettingsMock, templating, key, dashboard };
|
||||
}
|
||||
|
||||
describe('initVariablesTransaction', () => {
|
||||
describe('when called and the previous dashboard has completed', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const { constant, uid, dashboard } = getTestContext();
|
||||
const { constant, key, dashboard } = getTestContext();
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenAsyncActionIsDispatched(initVariablesTransaction(uid, dashboard));
|
||||
.whenAsyncActionIsDispatched(initVariablesTransaction(key, dashboard));
|
||||
|
||||
tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
|
||||
expect(dispatchedActions[0]).toEqual(variablesInitTransaction({ uid }));
|
||||
expect(dispatchedActions[1].type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[1].payload.id).toEqual('__dashboard');
|
||||
expect(dispatchedActions[2].type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[2].payload.id).toEqual('__org');
|
||||
expect(dispatchedActions[3].type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[3].payload.id).toEqual('__user');
|
||||
expect(dispatchedActions[0]).toEqual(toKeyedAction(key, variablesInitTransaction({ uid: key })));
|
||||
expect(dispatchedActions[1].payload.action.type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[1].payload.action.payload.id).toEqual('__dashboard');
|
||||
expect(dispatchedActions[2].payload.action.type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[2].payload.action.payload.id).toEqual('__org');
|
||||
expect(dispatchedActions[3].payload.action.type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[3].payload.action.payload.id).toEqual('__user');
|
||||
expect(dispatchedActions[4]).toEqual(
|
||||
addVariable(toVariablePayload(constant, { global: false, index: 0, model: constant }))
|
||||
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 0, model: constant })))
|
||||
);
|
||||
expect(dispatchedActions[5]).toEqual(variableStateNotStarted(toVariablePayload(constant)));
|
||||
expect(dispatchedActions[6]).toEqual(variableStateCompleted(toVariablePayload(constant)));
|
||||
expect(dispatchedActions[5]).toEqual(toKeyedAction(key, variableStateNotStarted(toVariablePayload(constant))));
|
||||
expect(dispatchedActions[6]).toEqual(toKeyedAction(key, variableStateCompleted(toVariablePayload(constant))));
|
||||
|
||||
expect(dispatchedActions[7]).toEqual(variablesCompleteTransaction({ uid }));
|
||||
expect(dispatchedActions[7]).toEqual(toKeyedAction(key, variablesCompleteTransaction({ uid: key })));
|
||||
return dispatchedActions.length === 8;
|
||||
});
|
||||
});
|
||||
@@ -92,54 +94,73 @@ describe('initVariablesTransaction', () => {
|
||||
describe('and there are variables that have data source that need to be migrated', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const legacyDs = '${ds}' as unknown as DataSourceRef;
|
||||
const ds = datasourceBuilder().withId('ds').withName('ds').withQuery('prom').build();
|
||||
const query = queryBuilder().withId('query').withName('query').withDatasource(legacyDs).build();
|
||||
const adhoc = adHocBuilder().withId('adhoc').withName('adhoc').withDatasource(legacyDs).build();
|
||||
const { uid, dashboard } = getTestContext([ds, query, adhoc]);
|
||||
const ds = datasourceBuilder().withId('ds').withRootStateKey('key').withName('ds').withQuery('prom').build();
|
||||
const query = queryBuilder()
|
||||
.withId('query')
|
||||
.withRootStateKey('key')
|
||||
.withName('query')
|
||||
.withDatasource(legacyDs)
|
||||
.build();
|
||||
const adhoc = adHocBuilder()
|
||||
.withId('adhoc')
|
||||
.withRootStateKey('key')
|
||||
.withName('adhoc')
|
||||
.withDatasource(legacyDs)
|
||||
.build();
|
||||
const { key, dashboard } = getTestContext([ds, query, adhoc]);
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenAsyncActionIsDispatched(initVariablesTransaction(uid, dashboard));
|
||||
.whenAsyncActionIsDispatched(initVariablesTransaction(key, dashboard));
|
||||
|
||||
tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
|
||||
expect(dispatchedActions[0]).toEqual(variablesInitTransaction({ uid }));
|
||||
expect(dispatchedActions[1].type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[1].payload.id).toEqual('__dashboard');
|
||||
expect(dispatchedActions[2].type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[2].payload.id).toEqual('__org');
|
||||
expect(dispatchedActions[3].type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[3].payload.id).toEqual('__user');
|
||||
expect(dispatchedActions[0]).toEqual(toKeyedAction(key, variablesInitTransaction({ uid: key })));
|
||||
expect(dispatchedActions[1].payload.action.type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[1].payload.action.payload.id).toEqual('__dashboard');
|
||||
expect(dispatchedActions[2].payload.action.type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[2].payload.action.payload.id).toEqual('__org');
|
||||
expect(dispatchedActions[3].payload.action.type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[3].payload.action.payload.id).toEqual('__user');
|
||||
expect(dispatchedActions[4]).toEqual(
|
||||
addVariable(toVariablePayload(ds, { global: false, index: 0, model: ds }))
|
||||
toKeyedAction(key, addVariable(toVariablePayload(ds, { global: false, index: 0, model: ds })))
|
||||
);
|
||||
expect(dispatchedActions[5]).toEqual(
|
||||
addVariable(toVariablePayload(query, { global: false, index: 1, model: query }))
|
||||
toKeyedAction(key, addVariable(toVariablePayload(query, { global: false, index: 1, model: query })))
|
||||
);
|
||||
expect(dispatchedActions[6]).toEqual(
|
||||
addVariable(toVariablePayload(adhoc, { global: false, index: 2, model: adhoc }))
|
||||
toKeyedAction(key, addVariable(toVariablePayload(adhoc, { global: false, index: 2, model: adhoc })))
|
||||
);
|
||||
expect(dispatchedActions[7]).toEqual(variableStateNotStarted(toVariablePayload(ds)));
|
||||
expect(dispatchedActions[8]).toEqual(variableStateNotStarted(toVariablePayload(query)));
|
||||
expect(dispatchedActions[9]).toEqual(variableStateNotStarted(toVariablePayload(adhoc)));
|
||||
expect(dispatchedActions[7]).toEqual(toKeyedAction(key, variableStateNotStarted(toVariablePayload(ds))));
|
||||
expect(dispatchedActions[8]).toEqual(toKeyedAction(key, variableStateNotStarted(toVariablePayload(query))));
|
||||
expect(dispatchedActions[9]).toEqual(toKeyedAction(key, variableStateNotStarted(toVariablePayload(adhoc))));
|
||||
expect(dispatchedActions[10]).toEqual(
|
||||
changeVariableProp(toVariablePayload(query, { propName: 'datasource', propValue: { uid: '${ds}' } }))
|
||||
);
|
||||
expect(dispatchedActions[11]).toEqual(
|
||||
changeVariableProp(toVariablePayload(adhoc, { propName: 'datasource', propValue: { uid: '${ds}' } }))
|
||||
);
|
||||
expect(dispatchedActions[12]).toEqual(variableStateFetching(toVariablePayload(ds)));
|
||||
expect(dispatchedActions[13]).toEqual(variableStateCompleted(toVariablePayload(adhoc)));
|
||||
expect(dispatchedActions[14]).toEqual(
|
||||
createDataSourceOptions(toVariablePayload(ds, { sources: [], regex: undefined }))
|
||||
);
|
||||
expect(dispatchedActions[15]).toEqual(
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(ds, { option: { selected: false, text: 'No data sources found', value: '' } })
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableProp(toVariablePayload(query, { propName: 'datasource', propValue: { uid: '${ds}' } }))
|
||||
)
|
||||
);
|
||||
expect(dispatchedActions[16]).toEqual(variableStateCompleted(toVariablePayload(ds)));
|
||||
expect(dispatchedActions[17]).toEqual(variableStateFetching(toVariablePayload(query)));
|
||||
expect(dispatchedActions[18]).toEqual(variableStateCompleted(toVariablePayload(query)));
|
||||
expect(dispatchedActions[19]).toEqual(variablesCompleteTransaction({ uid }));
|
||||
expect(dispatchedActions[11]).toEqual(
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableProp(toVariablePayload(adhoc, { propName: 'datasource', propValue: { uid: '${ds}' } }))
|
||||
)
|
||||
);
|
||||
expect(dispatchedActions[12]).toEqual(toKeyedAction(key, variableStateFetching(toVariablePayload(ds))));
|
||||
expect(dispatchedActions[13]).toEqual(toKeyedAction(key, variableStateCompleted(toVariablePayload(adhoc))));
|
||||
expect(dispatchedActions[14]).toEqual(
|
||||
toKeyedAction(key, createDataSourceOptions(toVariablePayload(ds, { sources: [], regex: undefined })))
|
||||
);
|
||||
expect(dispatchedActions[15]).toEqual(
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(ds, { option: { selected: false, text: 'No data sources found', value: '' } })
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(dispatchedActions[16]).toEqual(toKeyedAction(key, variableStateCompleted(toVariablePayload(ds))));
|
||||
expect(dispatchedActions[17]).toEqual(toKeyedAction(key, variableStateFetching(toVariablePayload(query))));
|
||||
expect(dispatchedActions[18]).toEqual(toKeyedAction(key, variableStateCompleted(toVariablePayload(query))));
|
||||
expect(dispatchedActions[19]).toEqual(toKeyedAction(key, variablesCompleteTransaction({ uid: key })));
|
||||
|
||||
return dispatchedActions.length === 20;
|
||||
});
|
||||
@@ -149,40 +170,32 @@ describe('initVariablesTransaction', () => {
|
||||
|
||||
describe('when called and the previous dashboard is still processing variables', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const { constant, uid, dashboard } = getTestContext();
|
||||
const transactionState = { uid: 'previous-uid', status: TransactionStatus.Fetching };
|
||||
const { constant, key, dashboard } = getTestContext();
|
||||
const transactionState = { ...initialTransactionState, uid: 'previous-uid', status: TransactionStatus.Fetching };
|
||||
const preloadedState = getPreloadedState(key, { transaction: transactionState });
|
||||
|
||||
const tester = await reduxTester<RootReducerType>({
|
||||
preloadedState: {
|
||||
templating: {
|
||||
transaction: transactionState,
|
||||
variables: {},
|
||||
optionsPicker: { ...initialState },
|
||||
editor: { ...initialVariableEditorState },
|
||||
},
|
||||
} as unknown as RootReducerType,
|
||||
})
|
||||
const tester = await reduxTester<RootReducerType>({ preloadedState })
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenAsyncActionIsDispatched(initVariablesTransaction(uid, dashboard));
|
||||
.whenAsyncActionIsDispatched(initVariablesTransaction(key, dashboard));
|
||||
|
||||
tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
|
||||
expect(dispatchedActions[0]).toEqual(cleanVariables());
|
||||
expect(dispatchedActions[1]).toEqual(cleanEditorState());
|
||||
expect(dispatchedActions[2]).toEqual(cleanPickerState());
|
||||
expect(dispatchedActions[3]).toEqual(variablesClearTransaction());
|
||||
expect(dispatchedActions[4]).toEqual(variablesInitTransaction({ uid }));
|
||||
expect(dispatchedActions[5].type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[5].payload.id).toEqual('__dashboard');
|
||||
expect(dispatchedActions[6].type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[6].payload.id).toEqual('__org');
|
||||
expect(dispatchedActions[7].type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[7].payload.id).toEqual('__user');
|
||||
expect(dispatchedActions[0]).toEqual(toKeyedAction(key, cleanVariables()));
|
||||
expect(dispatchedActions[1]).toEqual(toKeyedAction(key, cleanEditorState()));
|
||||
expect(dispatchedActions[2]).toEqual(toKeyedAction(key, cleanPickerState()));
|
||||
expect(dispatchedActions[3]).toEqual(toKeyedAction(key, variablesClearTransaction()));
|
||||
expect(dispatchedActions[4]).toEqual(toKeyedAction(key, variablesInitTransaction({ uid: key })));
|
||||
expect(dispatchedActions[5].payload.action.type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[5].payload.action.payload.id).toEqual('__dashboard');
|
||||
expect(dispatchedActions[6].payload.action.type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[6].payload.action.payload.id).toEqual('__org');
|
||||
expect(dispatchedActions[7].payload.action.type).toEqual(addVariable.type);
|
||||
expect(dispatchedActions[7].payload.action.payload.id).toEqual('__user');
|
||||
expect(dispatchedActions[8]).toEqual(
|
||||
addVariable(toVariablePayload(constant, { global: false, index: 0, model: constant }))
|
||||
toKeyedAction(key, addVariable(toVariablePayload(constant, { global: false, index: 0, model: constant })))
|
||||
);
|
||||
expect(dispatchedActions[9]).toEqual(variableStateNotStarted(toVariablePayload(constant)));
|
||||
expect(dispatchedActions[10]).toEqual(variableStateCompleted(toVariablePayload(constant)));
|
||||
expect(dispatchedActions[11]).toEqual(variablesCompleteTransaction({ uid }));
|
||||
expect(dispatchedActions[9]).toEqual(toKeyedAction(key, variableStateNotStarted(toVariablePayload(constant))));
|
||||
expect(dispatchedActions[10]).toEqual(toKeyedAction(key, variableStateCompleted(toVariablePayload(constant))));
|
||||
expect(dispatchedActions[11]).toEqual(toKeyedAction(key, variablesCompleteTransaction({ uid: key })));
|
||||
return dispatchedActions.length === 12;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
initialKeyedVariablesState,
|
||||
keyedVariablesReducer,
|
||||
KeyedVariablesState,
|
||||
toKeyedAction,
|
||||
} from './keyedVariablesReducer';
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { initialTransactionState, variablesCompleteTransaction, variablesInitTransaction } from './transactionReducer';
|
||||
import { TransactionStatus } from '../types';
|
||||
import { getInitialTemplatingState } from './reducers';
|
||||
|
||||
describe('dashboardVariablesReducer', () => {
|
||||
describe('when an toUidAction is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
const key = 'key';
|
||||
reducerTester<KeyedVariablesState>()
|
||||
.givenReducer(keyedVariablesReducer, {
|
||||
...initialKeyedVariablesState,
|
||||
lastKey: key,
|
||||
keys: {
|
||||
[key]: {
|
||||
...getInitialTemplatingState(),
|
||||
transaction: {
|
||||
...initialTransactionState,
|
||||
uid: key,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesCompleteTransaction({ uid: key })))
|
||||
.thenStateShouldEqual({
|
||||
...initialKeyedVariablesState,
|
||||
lastKey: key,
|
||||
keys: {
|
||||
[key]: {
|
||||
...getInitialTemplatingState(),
|
||||
transaction: {
|
||||
...initialTransactionState,
|
||||
uid: key,
|
||||
status: TransactionStatus.Completed,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when an toUidAction with variablesInitTransaction is dispatched', () => {
|
||||
it('then lastUid property should be correct', () => {
|
||||
const lastUid = 'lastUid';
|
||||
const key = 'key';
|
||||
reducerTester<KeyedVariablesState>()
|
||||
.givenReducer(keyedVariablesReducer, {
|
||||
...initialKeyedVariablesState,
|
||||
lastKey: lastUid,
|
||||
keys: {
|
||||
[lastUid]: {
|
||||
...getInitialTemplatingState(),
|
||||
transaction: {
|
||||
...initialTransactionState,
|
||||
uid: lastUid,
|
||||
status: TransactionStatus.Completed,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.thenStateShouldEqual({
|
||||
...initialKeyedVariablesState,
|
||||
lastKey: key,
|
||||
keys: {
|
||||
[key]: {
|
||||
...getInitialTemplatingState(),
|
||||
transaction: {
|
||||
...initialTransactionState,
|
||||
uid: key,
|
||||
status: TransactionStatus.Fetching,
|
||||
},
|
||||
},
|
||||
[lastUid]: {
|
||||
...getInitialTemplatingState(),
|
||||
transaction: {
|
||||
...initialTransactionState,
|
||||
uid: lastUid,
|
||||
status: TransactionStatus.Completed,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when action other than toUidAction is dispatched', () => {
|
||||
it('then state should not be affected', () => {
|
||||
const key = 'key';
|
||||
reducerTester<KeyedVariablesState>()
|
||||
.givenReducer(keyedVariablesReducer, {
|
||||
...initialKeyedVariablesState,
|
||||
lastKey: key,
|
||||
keys: {
|
||||
[key]: {
|
||||
...getInitialTemplatingState(),
|
||||
transaction: {
|
||||
...initialTransactionState,
|
||||
uid: key,
|
||||
status: TransactionStatus.Completed,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'newUid' }))
|
||||
.thenStateShouldEqual({
|
||||
...initialKeyedVariablesState,
|
||||
lastKey: key,
|
||||
keys: {
|
||||
[key]: {
|
||||
...getInitialTemplatingState(),
|
||||
transaction: {
|
||||
...initialTransactionState,
|
||||
uid: key,
|
||||
status: TransactionStatus.Completed,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { AnyAction } from 'redux';
|
||||
import { createAction, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { getTemplatingReducers, TemplatingState } from './reducers';
|
||||
import { variablesInitTransaction } from './transactionReducer';
|
||||
import { toStateKey } from '../utils';
|
||||
|
||||
export interface KeyedVariablesState {
|
||||
lastKey?: string;
|
||||
keys: Record<string, TemplatingState>;
|
||||
}
|
||||
|
||||
export const initialKeyedVariablesState: KeyedVariablesState = { keys: {} };
|
||||
|
||||
export interface KeyedAction {
|
||||
key: string;
|
||||
action: PayloadAction<any>;
|
||||
}
|
||||
|
||||
const keyedAction = createAction<KeyedAction>('templating/keyedAction');
|
||||
|
||||
export function toKeyedAction(key: string, action: PayloadAction<any>): PayloadAction<KeyedAction> {
|
||||
const keyAsString = toStateKey(key);
|
||||
return keyedAction({ key: keyAsString, action });
|
||||
}
|
||||
|
||||
export function keyedVariablesReducer(state = initialKeyedVariablesState, outerAction: AnyAction): KeyedVariablesState {
|
||||
if (keyedAction.match(outerAction)) {
|
||||
const { key, action } = outerAction.payload;
|
||||
const stringKey = toStateKey(key);
|
||||
const lastKey = variablesInitTransaction.match(action) ? stringKey : state.lastKey;
|
||||
const templatingReducers = getTemplatingReducers();
|
||||
const prevKeyState = state.keys[stringKey];
|
||||
const nextKeyState = templatingReducers(prevKeyState, action);
|
||||
|
||||
return {
|
||||
...state,
|
||||
lastKey,
|
||||
keys: {
|
||||
...state.keys,
|
||||
[stringKey]: nextKeyState,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export default {
|
||||
templating: keyedVariablesReducer,
|
||||
};
|
||||
@@ -2,13 +2,17 @@ import { migrateVariablesDatasourceNameToRef } from './actions';
|
||||
import { adHocBuilder, queryBuilder } from '../shared/testing/builders';
|
||||
import { DataSourceRef } from '@grafana/data/src';
|
||||
import { changeVariableProp } from './sharedReducer';
|
||||
import { toVariablePayload } from './types';
|
||||
import { toKeyedAction } from './keyedVariablesReducer';
|
||||
import { getPreloadedState } from './helpers';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
function getTestContext(ds: DataSourceRef, dsInstance?: { uid: string; type: string }) {
|
||||
jest.clearAllMocks();
|
||||
const query = queryBuilder().withId('query').withName('query').withDatasource(ds).build();
|
||||
const adhoc = adHocBuilder().withId('adhoc').withName('adhoc').withDatasource(ds).build();
|
||||
const state = { templating: { variables: [query, adhoc] } };
|
||||
const key = 'key';
|
||||
const query = queryBuilder().withId('query').withRootStateKey(key).withName('query').withDatasource(ds).build();
|
||||
const adhoc = adHocBuilder().withId('adhoc').withRootStateKey(key).withName('adhoc').withDatasource(ds).build();
|
||||
const templatingState = { variables: { query, adhoc } };
|
||||
const state = getPreloadedState(key, templatingState);
|
||||
const dispatch = jest.fn();
|
||||
const getState = jest.fn().mockReturnValue(state);
|
||||
const getInstanceSettingsMock = jest.fn().mockReturnValue(dsInstance);
|
||||
@@ -18,7 +22,7 @@ function getTestContext(ds: DataSourceRef, dsInstance?: { uid: string; type: str
|
||||
getInstanceSettings: getInstanceSettingsMock,
|
||||
});
|
||||
|
||||
return { query, adhoc, dispatch, getState, getDatasourceSrvFunc };
|
||||
return { key, query, adhoc, dispatch, getState, getDatasourceSrvFunc };
|
||||
}
|
||||
|
||||
describe('migrateVariablesDatasourceNameToRef', () => {
|
||||
@@ -26,22 +30,34 @@ describe('migrateVariablesDatasourceNameToRef', () => {
|
||||
describe('and data source exists', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const legacyDs = '${ds}' as unknown as DataSourceRef;
|
||||
const { query, adhoc, dispatch, getState, getDatasourceSrvFunc } = getTestContext(legacyDs, {
|
||||
const { query, adhoc, dispatch, getState, getDatasourceSrvFunc, key } = getTestContext(legacyDs, {
|
||||
uid: 'a random uid',
|
||||
type: 'prometheus',
|
||||
});
|
||||
|
||||
migrateVariablesDatasourceNameToRef(getDatasourceSrvFunc)(dispatch, getState, undefined);
|
||||
migrateVariablesDatasourceNameToRef(key, getDatasourceSrvFunc)(dispatch, getState, undefined);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch.mock.calls[0][0]).toEqual(
|
||||
changeVariableProp(
|
||||
toVariablePayload(query, { propName: 'datasource', propValue: { uid: 'a random uid', type: 'prometheus' } })
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableProp(
|
||||
toVariablePayload(query, {
|
||||
propName: 'datasource',
|
||||
propValue: { uid: 'a random uid', type: 'prometheus' },
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(dispatch.mock.calls[1][0]).toEqual(
|
||||
changeVariableProp(
|
||||
toVariablePayload(adhoc, { propName: 'datasource', propValue: { uid: 'a random uid', type: 'prometheus' } })
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableProp(
|
||||
toVariablePayload(adhoc, {
|
||||
propName: 'datasource',
|
||||
propValue: { uid: 'a random uid', type: 'prometheus' },
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -50,16 +66,22 @@ describe('migrateVariablesDatasourceNameToRef', () => {
|
||||
describe('and data source does not exist', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const legacyDs = '${ds}' as unknown as DataSourceRef;
|
||||
const { query, adhoc, dispatch, getState, getDatasourceSrvFunc } = getTestContext(legacyDs, undefined);
|
||||
const { query, adhoc, dispatch, getState, getDatasourceSrvFunc, key } = getTestContext(legacyDs, undefined);
|
||||
|
||||
migrateVariablesDatasourceNameToRef(getDatasourceSrvFunc)(dispatch, getState, undefined);
|
||||
migrateVariablesDatasourceNameToRef(key, getDatasourceSrvFunc)(dispatch, getState, undefined);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch.mock.calls[0][0]).toEqual(
|
||||
changeVariableProp(toVariablePayload(query, { propName: 'datasource', propValue: { uid: '${ds}' } }))
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableProp(toVariablePayload(query, { propName: 'datasource', propValue: { uid: '${ds}' } }))
|
||||
)
|
||||
);
|
||||
expect(dispatch.mock.calls[1][0]).toEqual(
|
||||
changeVariableProp(toVariablePayload(adhoc, { propName: 'datasource', propValue: { uid: '${ds}' } }))
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableProp(toVariablePayload(adhoc, { propName: 'datasource', propValue: { uid: '${ds}' } }))
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -68,9 +90,9 @@ describe('migrateVariablesDatasourceNameToRef', () => {
|
||||
describe('when called and variables have dataSourceRef', () => {
|
||||
it('then no actions are dispatched', async () => {
|
||||
const legacyDs = { uid: '${ds}', type: 'prometheus' };
|
||||
const { dispatch, getState, getDatasourceSrvFunc } = getTestContext(legacyDs, undefined);
|
||||
const { dispatch, getState, getDatasourceSrvFunc, key } = getTestContext(legacyDs, undefined);
|
||||
|
||||
migrateVariablesDatasourceNameToRef(getDatasourceSrvFunc)(dispatch, getState, undefined);
|
||||
migrateVariablesDatasourceNameToRef(key, getDatasourceSrvFunc)(dispatch, getState, undefined);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
@@ -10,8 +10,7 @@ import { createConstantVariableAdapter } from '../constant/adapter';
|
||||
import { VariableRefresh } from '../types';
|
||||
import { constantBuilder, intervalBuilder } from '../shared/testing/builders';
|
||||
import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||
import { getRootReducer, RootReducerType } from './helpers';
|
||||
import { toVariableIdentifier, toVariablePayload } from './types';
|
||||
import { getPreloadedState, getRootReducer, RootReducerType } from './helpers';
|
||||
import {
|
||||
setCurrentVariableValue,
|
||||
variableStateCompleted,
|
||||
@@ -22,17 +21,20 @@ import { createIntervalOptions } from '../interval/reducer';
|
||||
import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput';
|
||||
import { notifyApp } from '../../../core/reducers/appNotification';
|
||||
import { expect } from '../../../../test/lib/common';
|
||||
import { TemplatingState } from './reducers';
|
||||
import { appEvents } from '../../../core/core';
|
||||
import { variablesInitTransaction } from './transactionReducer';
|
||||
import { toKeyedAction } from './keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
|
||||
variableAdapters.setInit(() => [createIntervalVariableAdapter(), createConstantVariableAdapter()]);
|
||||
|
||||
const getTestContext = (dashboard: DashboardModel) => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const key = 'key';
|
||||
const interval = intervalBuilder()
|
||||
.withId('interval-0')
|
||||
.withRootStateKey(key)
|
||||
.withName('interval-0')
|
||||
.withOptions('1m', '10m', '30m', '1h', '6h', '12h', '1d', '7d', '14d', '30d')
|
||||
.withCurrent('1m')
|
||||
@@ -41,6 +43,7 @@ const getTestContext = (dashboard: DashboardModel) => {
|
||||
|
||||
const constant = constantBuilder()
|
||||
.withId('constant-1')
|
||||
.withRootStateKey(key)
|
||||
.withName('constant-1')
|
||||
.withOptions('a constant')
|
||||
.withCurrent('a constant')
|
||||
@@ -65,17 +68,19 @@ const getTestContext = (dashboard: DashboardModel) => {
|
||||
getModel: () => dashboard,
|
||||
} as unknown as DashboardState;
|
||||
const adapter = variableAdapters.get('interval');
|
||||
const templatingState = {
|
||||
variables: {
|
||||
'interval-0': { ...interval },
|
||||
'constant-1': { ...constant },
|
||||
},
|
||||
};
|
||||
const preloadedState = {
|
||||
dashboard: dashboardState,
|
||||
templating: {
|
||||
variables: {
|
||||
'interval-0': { ...interval },
|
||||
'constant-1': { ...constant },
|
||||
},
|
||||
} as unknown as TemplatingState,
|
||||
...getPreloadedState(key, templatingState),
|
||||
} as unknown as RootReducerType;
|
||||
|
||||
return {
|
||||
key,
|
||||
interval,
|
||||
range,
|
||||
dependencies,
|
||||
@@ -91,6 +96,7 @@ describe('when onTimeRangeUpdated is dispatched', () => {
|
||||
describe('and options are changed by update', () => {
|
||||
it('then correct actions are dispatched and correct dependencies are called', async () => {
|
||||
const {
|
||||
key,
|
||||
preloadedState,
|
||||
range,
|
||||
dependencies,
|
||||
@@ -101,20 +107,23 @@ describe('when onTimeRangeUpdated is dispatched', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>({ preloadedState })
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenAsyncActionIsDispatched(onTimeRangeUpdated(range, dependencies));
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.whenAsyncActionIsDispatched(onTimeRangeUpdated(key, range, dependencies));
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
variablesInitTransaction({ uid: 'a uid' }),
|
||||
variableStateFetching(toVariablePayload({ type: 'interval', id: 'interval-0' })),
|
||||
createIntervalOptions(toVariablePayload({ type: 'interval', id: 'interval-0' })),
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'interval', id: 'interval-0' },
|
||||
{ option: { text: '1m', value: '1m', selected: false } }
|
||||
toKeyedAction(key, variablesInitTransaction({ uid: key })),
|
||||
toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'interval', id: 'interval-0' }))),
|
||||
toKeyedAction(key, createIntervalOptions(toVariablePayload({ type: 'interval', id: 'interval-0' }))),
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'interval', id: 'interval-0' },
|
||||
{ option: { text: '1m', value: '1m', selected: false } }
|
||||
)
|
||||
)
|
||||
),
|
||||
variableStateCompleted(toVariablePayload({ type: 'interval', id: 'interval-0' }))
|
||||
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'interval', id: 'interval-0' })))
|
||||
);
|
||||
|
||||
expect(updateTimeRangeMock).toHaveBeenCalledTimes(1);
|
||||
@@ -127,6 +136,7 @@ describe('when onTimeRangeUpdated is dispatched', () => {
|
||||
describe('and options are not changed by update', () => {
|
||||
it('then correct actions are dispatched and correct dependencies are called', async () => {
|
||||
const {
|
||||
key,
|
||||
interval,
|
||||
preloadedState,
|
||||
range,
|
||||
@@ -138,21 +148,26 @@ describe('when onTimeRangeUpdated is dispatched', () => {
|
||||
|
||||
const base = await reduxTester<RootReducerType>({ preloadedState })
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenAsyncActionIsDispatched(setOptionAsCurrent(toVariableIdentifier(interval), interval.options[0], false));
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.whenAsyncActionIsDispatched(
|
||||
setOptionAsCurrent(toKeyedVariableIdentifier(interval), interval.options[0], false)
|
||||
);
|
||||
|
||||
const tester = await base.whenAsyncActionIsDispatched(onTimeRangeUpdated(range, dependencies), true);
|
||||
const tester = await base.whenAsyncActionIsDispatched(onTimeRangeUpdated(key, range, dependencies), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
variableStateFetching(toVariablePayload({ type: 'interval', id: 'interval-0' })),
|
||||
createIntervalOptions(toVariablePayload({ type: 'interval', id: 'interval-0' })),
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'interval', id: 'interval-0' },
|
||||
{ option: { text: '1m', value: '1m', selected: false } }
|
||||
toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'interval', id: 'interval-0' }))),
|
||||
toKeyedAction(key, createIntervalOptions(toVariablePayload({ type: 'interval', id: 'interval-0' }))),
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'interval', id: 'interval-0' },
|
||||
{ option: { text: '1m', value: '1m', selected: false } }
|
||||
)
|
||||
)
|
||||
),
|
||||
variableStateCompleted(toVariablePayload({ type: 'interval', id: 'interval-0' }))
|
||||
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'interval', id: 'interval-0' })))
|
||||
);
|
||||
|
||||
expect(updateTimeRangeMock).toHaveBeenCalledTimes(1);
|
||||
@@ -166,6 +181,7 @@ describe('when onTimeRangeUpdated is dispatched', () => {
|
||||
silenceConsoleOutput();
|
||||
it('then correct actions are dispatched and correct dependencies are called', async () => {
|
||||
const {
|
||||
key,
|
||||
adapter,
|
||||
preloadedState,
|
||||
range,
|
||||
@@ -179,16 +195,19 @@ describe('when onTimeRangeUpdated is dispatched', () => {
|
||||
|
||||
const tester = await reduxTester<RootReducerType>({ preloadedState, debug: true })
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: 'a uid' }))
|
||||
.whenAsyncActionIsDispatched(onTimeRangeUpdated(range, dependencies), true);
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.whenAsyncActionIsDispatched(onTimeRangeUpdated(key, range, dependencies), true);
|
||||
|
||||
tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
|
||||
expect(dispatchedActions[0]).toEqual(
|
||||
variableStateFetching(toVariablePayload({ type: 'interval', id: 'interval-0' }))
|
||||
toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'interval', id: 'interval-0' })))
|
||||
);
|
||||
expect(dispatchedActions[1]).toEqual(
|
||||
variableStateFailed(
|
||||
toVariablePayload({ type: 'interval', id: 'interval-0' }, { error: new Error('Something broke') })
|
||||
toKeyedAction(
|
||||
key,
|
||||
variableStateFailed(
|
||||
toVariablePayload({ type: 'interval', id: 'interval-0' }, { error: new Error('Something broke') })
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(dispatchedActions[2].type).toEqual(notifyApp.type);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { UrlQueryMap } from '@grafana/data';
|
||||
|
||||
import { getTemplatingRootReducer } from './helpers';
|
||||
import { getTemplatingRootReducer, TemplatingReducerType } from './helpers';
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { createQueryVariableAdapter } from '../query/adapter';
|
||||
import { createCustomVariableAdapter } from '../custom/adapter';
|
||||
import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||
import { TemplatingState } from 'app/features/variables/state/reducers';
|
||||
import { initDashboardTemplating, processVariable } from './actions';
|
||||
import { setCurrentVariableValue, variableStateCompleted, variableStateFetching } from './sharedReducer';
|
||||
import { toVariableIdentifier, toVariablePayload } from './types';
|
||||
import { VariableRefresh } from '../types';
|
||||
import { updateVariableOptions } from '../query/reducer';
|
||||
import { customBuilder, queryBuilder } from '../shared/testing/builders';
|
||||
import { variablesInitTransaction } from './transactionReducer';
|
||||
import { setVariableQueryRunner, VariableQueryRunner } from '../query/VariableQueryRunner';
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import { toKeyedAction } from './keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
|
||||
jest.mock('app/features/dashboard/services/TimeSrv', () => ({
|
||||
getTimeSrv: jest.fn().mockReturnValue({
|
||||
@@ -68,9 +68,11 @@ describe('processVariable', () => {
|
||||
// custom doesn't depend on any other variable
|
||||
// queryDependsOnCustom depends on custom
|
||||
// queryNoDepends doesn't depend on any other variable
|
||||
const getAndSetupProcessVariableContext = () => {
|
||||
const key = 'key';
|
||||
const getTestContext = () => {
|
||||
const custom = customBuilder()
|
||||
.withId('custom')
|
||||
.withRootStateKey(key)
|
||||
.withName('custom')
|
||||
.withQuery('A,B,C')
|
||||
.withOptions('A', 'B', 'C')
|
||||
@@ -79,6 +81,7 @@ describe('processVariable', () => {
|
||||
|
||||
const queryDependsOnCustom = queryBuilder()
|
||||
.withId('queryDependsOnCustom')
|
||||
.withRootStateKey(key)
|
||||
.withName('queryDependsOnCustom')
|
||||
.withQuery('$custom.*')
|
||||
.withOptions('AA', 'AB', 'AC')
|
||||
@@ -87,6 +90,7 @@ describe('processVariable', () => {
|
||||
|
||||
const queryNoDepends = queryBuilder()
|
||||
.withId('queryNoDepends')
|
||||
.withRootStateKey(key)
|
||||
.withName('queryNoDepends')
|
||||
.withQuery('*')
|
||||
.withOptions('A', 'B', 'C')
|
||||
@@ -94,13 +98,15 @@ describe('processVariable', () => {
|
||||
.build();
|
||||
|
||||
const list = [custom, queryDependsOnCustom, queryNoDepends];
|
||||
const dashboard: any = { templating: { list } };
|
||||
setVariableQueryRunner(new VariableQueryRunner());
|
||||
|
||||
return {
|
||||
key,
|
||||
custom,
|
||||
queryDependsOnCustom,
|
||||
queryNoDepends,
|
||||
list,
|
||||
dashboard,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -108,33 +114,41 @@ describe('processVariable', () => {
|
||||
describe('when processVariable is dispatched for a custom variable without dependencies', () => {
|
||||
describe('and queryParams does not match variable', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const { list, custom } = getAndSetupProcessVariableContext();
|
||||
const { key, dashboard, custom } = getTestContext();
|
||||
const queryParams: UrlQueryMap = {};
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
const tester = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
|
||||
.whenActionIsDispatched(initDashboardTemplating(list))
|
||||
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(custom), queryParams), true);
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
|
||||
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams), true);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(variableStateCompleted(toVariablePayload(custom)));
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction(key, variableStateCompleted(toVariablePayload(custom)))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and queryParams does match variable', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const { list, custom } = getAndSetupProcessVariableContext();
|
||||
const { key, dashboard, custom } = getTestContext();
|
||||
const queryParams: UrlQueryMap = { 'var-custom': 'B' };
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
const tester = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
|
||||
.whenActionIsDispatched(initDashboardTemplating(list))
|
||||
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(custom), queryParams), true);
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
|
||||
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams), true);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload({ type: 'custom', id: 'custom' }, { option: { text: 'B', value: 'B', selected: false } })
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'custom', id: 'custom' },
|
||||
{ option: { text: 'B', value: 'B', selected: false } }
|
||||
)
|
||||
)
|
||||
),
|
||||
variableStateCompleted(toVariablePayload(custom))
|
||||
toKeyedAction(key, variableStateCompleted(toVariablePayload(custom)))
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -148,15 +162,17 @@ describe('processVariable', () => {
|
||||
describe('and refresh is VariableRefresh.never', () => {
|
||||
const refresh = VariableRefresh.never;
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const { list, queryNoDepends } = getAndSetupProcessVariableContext();
|
||||
const { dashboard, key, queryNoDepends } = getTestContext();
|
||||
queryNoDepends.refresh = refresh;
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
const tester = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
|
||||
.whenActionIsDispatched(initDashboardTemplating(list))
|
||||
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(queryNoDepends), queryParams), true);
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
|
||||
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(queryNoDepends), queryParams), true);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(variableStateCompleted(toVariablePayload(queryNoDepends)));
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
toKeyedAction(key, variableStateCompleted(toVariablePayload(queryNoDepends)))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -165,36 +181,42 @@ describe('processVariable', () => {
|
||||
${VariableRefresh.onDashboardLoad}
|
||||
${VariableRefresh.onTimeRangeChanged}
|
||||
`('and refresh is $refresh then correct actions are dispatched', async ({ refresh }) => {
|
||||
const { list, queryNoDepends } = getAndSetupProcessVariableContext();
|
||||
const { dashboard, key, queryNoDepends } = getTestContext();
|
||||
queryNoDepends.refresh = refresh;
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
const tester = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
|
||||
.whenActionIsDispatched(initDashboardTemplating(list))
|
||||
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(queryNoDepends), queryParams), true);
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
|
||||
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(queryNoDepends), queryParams), true);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
variableStateFetching(toVariablePayload({ type: 'query', id: 'queryNoDepends' })),
|
||||
updateVariableOptions(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryNoDepends' },
|
||||
{
|
||||
results: [
|
||||
{ value: 'A', text: 'A' },
|
||||
{ value: 'B', text: 'B' },
|
||||
{ value: 'C', text: 'C' },
|
||||
],
|
||||
templatedRegex: '',
|
||||
}
|
||||
toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))),
|
||||
toKeyedAction(
|
||||
key,
|
||||
updateVariableOptions(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryNoDepends' },
|
||||
{
|
||||
results: [
|
||||
{ value: 'A', text: 'A' },
|
||||
{ value: 'B', text: 'B' },
|
||||
{ value: 'C', text: 'C' },
|
||||
],
|
||||
templatedRegex: '',
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryNoDepends' },
|
||||
{ option: { text: 'A', value: 'A', selected: false } }
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryNoDepends' },
|
||||
{ option: { text: 'A', value: 'A', selected: false } }
|
||||
)
|
||||
)
|
||||
),
|
||||
variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))
|
||||
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryNoDepends' })))
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -205,22 +227,25 @@ describe('processVariable', () => {
|
||||
describe('and refresh is VariableRefresh.never', () => {
|
||||
const refresh = VariableRefresh.never;
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const { list, queryNoDepends } = getAndSetupProcessVariableContext();
|
||||
const { dashboard, key, queryNoDepends } = getTestContext();
|
||||
queryNoDepends.refresh = refresh;
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
const tester = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
|
||||
.whenActionIsDispatched(initDashboardTemplating(list))
|
||||
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(queryNoDepends), queryParams), true);
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
|
||||
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(queryNoDepends), queryParams), true);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryNoDepends' },
|
||||
{ option: { text: 'B', value: 'B', selected: false } }
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryNoDepends' },
|
||||
{ option: { text: 'B', value: 'B', selected: false } }
|
||||
)
|
||||
)
|
||||
),
|
||||
variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))
|
||||
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryNoDepends' })))
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -230,40 +255,49 @@ describe('processVariable', () => {
|
||||
${VariableRefresh.onDashboardLoad}
|
||||
${VariableRefresh.onTimeRangeChanged}
|
||||
`('and refresh is $refresh then correct actions are dispatched', async ({ refresh }) => {
|
||||
const { list, queryNoDepends } = getAndSetupProcessVariableContext();
|
||||
const { dashboard, key, queryNoDepends } = getTestContext();
|
||||
queryNoDepends.refresh = refresh;
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
const tester = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
|
||||
.whenActionIsDispatched(initDashboardTemplating(list))
|
||||
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(queryNoDepends), queryParams), true);
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
|
||||
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(queryNoDepends), queryParams), true);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
variableStateFetching(toVariablePayload({ type: 'query', id: 'queryNoDepends' })),
|
||||
updateVariableOptions(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryNoDepends' },
|
||||
{
|
||||
results: [
|
||||
{ value: 'A', text: 'A' },
|
||||
{ value: 'B', text: 'B' },
|
||||
{ value: 'C', text: 'C' },
|
||||
],
|
||||
templatedRegex: '',
|
||||
}
|
||||
toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))),
|
||||
toKeyedAction(
|
||||
key,
|
||||
updateVariableOptions(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryNoDepends' },
|
||||
{
|
||||
results: [
|
||||
{ value: 'A', text: 'A' },
|
||||
{ value: 'B', text: 'B' },
|
||||
{ value: 'C', text: 'C' },
|
||||
],
|
||||
templatedRegex: '',
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryNoDepends' },
|
||||
{ option: { text: 'A', value: 'A', selected: false } }
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryNoDepends' },
|
||||
{ option: { text: 'A', value: 'A', selected: false } }
|
||||
)
|
||||
)
|
||||
),
|
||||
variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryNoDepends' })),
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryNoDepends' },
|
||||
{ option: { text: 'B', value: 'B', selected: false } }
|
||||
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))),
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryNoDepends' },
|
||||
{ option: { text: 'B', value: 'B', selected: false } }
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -279,21 +313,21 @@ describe('processVariable', () => {
|
||||
describe('and refresh is VariableRefresh.never', () => {
|
||||
const refresh = VariableRefresh.never;
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const { list, custom, queryDependsOnCustom } = getAndSetupProcessVariableContext();
|
||||
const { key, dashboard, custom, queryDependsOnCustom } = getTestContext();
|
||||
queryDependsOnCustom.refresh = refresh;
|
||||
const customProcessed = await reduxTester<{ templating: TemplatingState }>()
|
||||
const customProcessed = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
|
||||
.whenActionIsDispatched(initDashboardTemplating(list))
|
||||
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
|
||||
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
|
||||
|
||||
const tester = await customProcessed.whenAsyncActionIsDispatched(
|
||||
processVariable(toVariableIdentifier(queryDependsOnCustom), queryParams),
|
||||
processVariable(toKeyedVariableIdentifier(queryDependsOnCustom), queryParams),
|
||||
true
|
||||
);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))
|
||||
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' })))
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -303,41 +337,47 @@ describe('processVariable', () => {
|
||||
${VariableRefresh.onDashboardLoad}
|
||||
${VariableRefresh.onTimeRangeChanged}
|
||||
`('and refresh is $refresh then correct actions are dispatched', async ({ refresh }) => {
|
||||
const { list, custom, queryDependsOnCustom } = getAndSetupProcessVariableContext();
|
||||
const { key, dashboard, custom, queryDependsOnCustom } = getTestContext();
|
||||
queryDependsOnCustom.refresh = refresh;
|
||||
const customProcessed = await reduxTester<{ templating: TemplatingState }>()
|
||||
const customProcessed = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
|
||||
.whenActionIsDispatched(initDashboardTemplating(list))
|
||||
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
|
||||
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
|
||||
|
||||
const tester = await customProcessed.whenAsyncActionIsDispatched(
|
||||
processVariable(toVariableIdentifier(queryDependsOnCustom), queryParams),
|
||||
processVariable(toKeyedVariableIdentifier(queryDependsOnCustom), queryParams),
|
||||
true
|
||||
);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
variableStateFetching(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' })),
|
||||
updateVariableOptions(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryDependsOnCustom' },
|
||||
{
|
||||
results: [
|
||||
{ value: 'AA', text: 'AA' },
|
||||
{ value: 'AB', text: 'AB' },
|
||||
{ value: 'AC', text: 'AC' },
|
||||
],
|
||||
templatedRegex: '',
|
||||
}
|
||||
toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))),
|
||||
toKeyedAction(
|
||||
key,
|
||||
updateVariableOptions(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryDependsOnCustom' },
|
||||
{
|
||||
results: [
|
||||
{ value: 'AA', text: 'AA' },
|
||||
{ value: 'AB', text: 'AB' },
|
||||
{ value: 'AC', text: 'AC' },
|
||||
],
|
||||
templatedRegex: '',
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryDependsOnCustom' },
|
||||
{ option: { text: 'AA', value: 'AA', selected: false } }
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryDependsOnCustom' },
|
||||
{ option: { text: 'AA', value: 'AA', selected: false } }
|
||||
)
|
||||
)
|
||||
),
|
||||
variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))
|
||||
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' })))
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -348,27 +388,30 @@ describe('processVariable', () => {
|
||||
describe('and refresh is VariableRefresh.never', () => {
|
||||
const refresh = VariableRefresh.never;
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const { list, custom, queryDependsOnCustom } = getAndSetupProcessVariableContext();
|
||||
const { key, dashboard, custom, queryDependsOnCustom } = getTestContext();
|
||||
queryDependsOnCustom.refresh = refresh;
|
||||
const customProcessed = await reduxTester<{ templating: TemplatingState }>()
|
||||
const customProcessed = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
|
||||
.whenActionIsDispatched(initDashboardTemplating(list))
|
||||
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
|
||||
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
|
||||
|
||||
const tester = await customProcessed.whenAsyncActionIsDispatched(
|
||||
processVariable(toVariableIdentifier(queryDependsOnCustom), queryParams),
|
||||
processVariable(toKeyedVariableIdentifier(queryDependsOnCustom), queryParams),
|
||||
true
|
||||
);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryDependsOnCustom' },
|
||||
{ option: { text: 'AB', value: 'AB', selected: false } }
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryDependsOnCustom' },
|
||||
{ option: { text: 'AB', value: 'AB', selected: false } }
|
||||
)
|
||||
)
|
||||
),
|
||||
variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))
|
||||
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' })))
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -378,45 +421,54 @@ describe('processVariable', () => {
|
||||
${VariableRefresh.onDashboardLoad}
|
||||
${VariableRefresh.onTimeRangeChanged}
|
||||
`('and refresh is $refresh then correct actions are dispatched', async ({ refresh }) => {
|
||||
const { list, custom, queryDependsOnCustom } = getAndSetupProcessVariableContext();
|
||||
const { key, dashboard, custom, queryDependsOnCustom } = getTestContext();
|
||||
queryDependsOnCustom.refresh = refresh;
|
||||
const customProcessed = await reduxTester<{ templating: TemplatingState }>()
|
||||
const customProcessed = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(variablesInitTransaction({ uid: '' }))
|
||||
.whenActionIsDispatched(initDashboardTemplating(list))
|
||||
.whenAsyncActionIsDispatched(processVariable(toVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
|
||||
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
|
||||
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
|
||||
.whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
|
||||
|
||||
const tester = await customProcessed.whenAsyncActionIsDispatched(
|
||||
processVariable(toVariableIdentifier(queryDependsOnCustom), queryParams),
|
||||
processVariable(toKeyedVariableIdentifier(queryDependsOnCustom), queryParams),
|
||||
true
|
||||
);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
variableStateFetching(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' })),
|
||||
updateVariableOptions(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryDependsOnCustom' },
|
||||
{
|
||||
results: [
|
||||
{ value: 'AA', text: 'AA' },
|
||||
{ value: 'AB', text: 'AB' },
|
||||
{ value: 'AC', text: 'AC' },
|
||||
],
|
||||
templatedRegex: '',
|
||||
}
|
||||
toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))),
|
||||
toKeyedAction(
|
||||
key,
|
||||
updateVariableOptions(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryDependsOnCustom' },
|
||||
{
|
||||
results: [
|
||||
{ value: 'AA', text: 'AA' },
|
||||
{ value: 'AB', text: 'AB' },
|
||||
{ value: 'AC', text: 'AC' },
|
||||
],
|
||||
templatedRegex: '',
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryDependsOnCustom' },
|
||||
{ option: { text: 'AA', value: 'AA', selected: false } }
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryDependsOnCustom' },
|
||||
{ option: { text: 'AA', value: 'AA', selected: false } }
|
||||
)
|
||||
)
|
||||
),
|
||||
variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' })),
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryDependsOnCustom' },
|
||||
{ option: { text: 'AB', value: 'AB', selected: false } }
|
||||
toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))),
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'query', id: 'queryDependsOnCustom' },
|
||||
{ option: { text: 'AB', value: 'AB', selected: false } }
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { VariableType } from '@grafana/data';
|
||||
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { initialVariableModelState, QueryVariableModel } from '../types';
|
||||
import { VariableAdapter, variableAdapters } from '../adapters';
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { cleanVariables, variablesReducer } from './variablesReducer';
|
||||
import { VariablesState, toVariablePayload, VariablePayload } from './types';
|
||||
import { VariableType } from '@grafana/data';
|
||||
import { VariablePayload, VariablesState } from './types';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
const variableAdapter: VariableAdapter<QueryVariableModel> = {
|
||||
id: 'mock' as unknown as VariableType,
|
||||
|
||||
@@ -1,20 +1,41 @@
|
||||
import { combineReducers } from '@reduxjs/toolkit';
|
||||
import { optionsPickerReducer } from '../pickers/OptionsPicker/reducer';
|
||||
import { variableEditorReducer } from '../editor/reducer';
|
||||
import { CombinedState, combineReducers, Reducer } from 'redux';
|
||||
import { initialOptionPickerState, optionsPickerReducer, OptionsPickerState } from '../pickers/OptionsPicker/reducer';
|
||||
import { initialVariableEditorState, variableEditorReducer, VariableEditorState } from '../editor/reducer';
|
||||
import { variablesReducer } from './variablesReducer';
|
||||
import { transactionReducer } from './transactionReducer';
|
||||
import { variableInspectReducer } from '../inspect/reducer';
|
||||
import { initialTransactionState, transactionReducer, TransactionState } from './transactionReducer';
|
||||
import { initialVariableInspectState, variableInspectReducer, VariableInspectState } from '../inspect/reducer';
|
||||
import { initialVariablesState, VariablesState } from './types';
|
||||
|
||||
export const templatingReducers = combineReducers({
|
||||
editor: variableEditorReducer,
|
||||
variables: variablesReducer,
|
||||
optionsPicker: optionsPickerReducer,
|
||||
transaction: transactionReducer,
|
||||
inspect: variableInspectReducer,
|
||||
});
|
||||
export interface TemplatingState {
|
||||
editor: VariableEditorState;
|
||||
variables: VariablesState;
|
||||
optionsPicker: OptionsPickerState;
|
||||
transaction: TransactionState;
|
||||
inspect: VariableInspectState;
|
||||
}
|
||||
|
||||
export type TemplatingState = ReturnType<typeof templatingReducers>;
|
||||
let templatingReducers: Reducer<CombinedState<TemplatingState>>;
|
||||
|
||||
export default {
|
||||
templating: templatingReducers,
|
||||
};
|
||||
export function getTemplatingReducers() {
|
||||
if (!templatingReducers) {
|
||||
templatingReducers = combineReducers({
|
||||
editor: variableEditorReducer,
|
||||
variables: variablesReducer,
|
||||
optionsPicker: optionsPickerReducer,
|
||||
transaction: transactionReducer,
|
||||
inspect: variableInspectReducer,
|
||||
});
|
||||
}
|
||||
|
||||
return templatingReducers;
|
||||
}
|
||||
|
||||
export function getInitialTemplatingState() {
|
||||
return {
|
||||
editor: initialVariableEditorState,
|
||||
variables: initialVariablesState,
|
||||
optionsPicker: initialOptionPickerState,
|
||||
transaction: initialTransactionState,
|
||||
inspect: initialVariableInspectState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,52 +2,63 @@ import { StoreState } from '../../../types';
|
||||
import { VariableModel } from '../types';
|
||||
import { getState } from '../../../store/store';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { getInitialTemplatingState, TemplatingState } from './reducers';
|
||||
import { toStateKey } from '../utils';
|
||||
import { KeyedVariableIdentifier, VariablesState } from './types';
|
||||
|
||||
export const getVariable = <T extends VariableModel = VariableModel>(
|
||||
id: string,
|
||||
identifier: KeyedVariableIdentifier,
|
||||
state: StoreState = getState(),
|
||||
throwWhenMissing = true
|
||||
): T => {
|
||||
if (!state.templating.variables[id]) {
|
||||
const { id, rootStateKey } = identifier;
|
||||
const variablesState = getVariablesState(rootStateKey, state);
|
||||
if (!variablesState.variables[id]) {
|
||||
if (throwWhenMissing) {
|
||||
throw new Error(`Couldn't find variable with id:${id}`);
|
||||
}
|
||||
return undefined as unknown as T;
|
||||
}
|
||||
|
||||
return state.templating.variables[id] as T;
|
||||
return variablesState.variables[id] as T;
|
||||
};
|
||||
|
||||
export const getFilteredVariables = (filter: (model: VariableModel) => boolean, state: StoreState = getState()) => {
|
||||
return Object.values(state.templating.variables)
|
||||
function getFilteredVariablesByKey(
|
||||
filter: (model: VariableModel) => boolean,
|
||||
key: string,
|
||||
state: StoreState = getState()
|
||||
) {
|
||||
return Object.values(getVariablesState(key, state).variables)
|
||||
.filter(filter)
|
||||
.sort((s1, s2) => s1.index - s2.index);
|
||||
};
|
||||
}
|
||||
|
||||
export const getVariableWithName = (name: string, state: StoreState = getState()) => {
|
||||
return getVariable(name, state, false);
|
||||
};
|
||||
export function getVariablesState(key: string, state: StoreState = getState()): TemplatingState {
|
||||
return state.templating.keys[toStateKey(key)] ?? getInitialTemplatingState();
|
||||
}
|
||||
|
||||
export const getVariables = (state: StoreState = getState()): VariableModel[] => {
|
||||
return getFilteredVariables(defaultVariablesFilter, state);
|
||||
};
|
||||
export function getVariablesByKey(key: string, state: StoreState = getState()): VariableModel[] {
|
||||
return getFilteredVariablesByKey(defaultVariablesFilter, key, state);
|
||||
}
|
||||
|
||||
export function defaultVariablesFilter(variable: VariableModel): boolean {
|
||||
function defaultVariablesFilter(variable: VariableModel): boolean {
|
||||
return variable.type !== 'system';
|
||||
}
|
||||
|
||||
export const getSubMenuVariables = memoizeOne((variables: Record<string, VariableModel>): VariableModel[] => {
|
||||
return getVariables(getState());
|
||||
});
|
||||
export const getSubMenuVariables = memoizeOne(
|
||||
(key: string, variables: Record<string, VariableModel>): VariableModel[] => {
|
||||
return getVariablesByKey(key, getState());
|
||||
}
|
||||
);
|
||||
|
||||
export const getEditorVariables = (state: StoreState): VariableModel[] => {
|
||||
return getVariables(state);
|
||||
export const getEditorVariables = (key: string, state: StoreState): VariableModel[] => {
|
||||
return getVariablesByKey(key, state);
|
||||
};
|
||||
|
||||
export type GetVariables = typeof getVariables;
|
||||
export type GetVariables = typeof getVariablesByKey;
|
||||
|
||||
export function getNewVariableIndex(state: StoreState = getState()): number {
|
||||
return getNextVariableIndex(Object.values(state.templating.variables));
|
||||
export function getNewVariableIndex(key: string, state: StoreState = getState()): number {
|
||||
return getNextVariableIndex(Object.values(getVariablesState(key, state).variables));
|
||||
}
|
||||
|
||||
export function getNextVariableIndex(variables: VariableModel[]): number {
|
||||
@@ -55,6 +66,47 @@ export function getNextVariableIndex(variables: VariableModel[]): number {
|
||||
return sorted.length > 0 ? sorted[sorted.length - 1].index + 1 : 0;
|
||||
}
|
||||
|
||||
export function getVariablesIsDirty(state: StoreState = getState()): boolean {
|
||||
return state.templating.transaction.isDirty;
|
||||
export function getVariablesIsDirty(key: string, state: StoreState = getState()): boolean {
|
||||
return getVariablesState(key, state).transaction.isDirty;
|
||||
}
|
||||
|
||||
export function getIfExistsLastKey(state: StoreState = getState()): string | undefined {
|
||||
return state.templating?.lastKey;
|
||||
}
|
||||
|
||||
export function getLastKey(state: StoreState = getState()): string {
|
||||
if (!state.templating?.lastKey) {
|
||||
throw new Error('Accessing lastKey without initializing it variables');
|
||||
}
|
||||
|
||||
return state.templating.lastKey;
|
||||
}
|
||||
|
||||
// selectors used by template srv, assumes that lastKey is in state. Needs to change when/if dashboard redux state becomes keyed too.
|
||||
export function getFilteredVariables(filter: (model: VariableModel) => boolean, state: StoreState = getState()) {
|
||||
const lastKey = getIfExistsLastKey(state);
|
||||
if (!lastKey) {
|
||||
return [];
|
||||
}
|
||||
return getFilteredVariablesByKey(filter, lastKey, state);
|
||||
}
|
||||
|
||||
export function getVariables(state: StoreState = getState()) {
|
||||
const lastKey = getIfExistsLastKey(state);
|
||||
if (!lastKey) {
|
||||
return [];
|
||||
}
|
||||
return getVariablesByKey(lastKey, state);
|
||||
}
|
||||
|
||||
export function getVariableWithName(name: string, state: StoreState = getState()) {
|
||||
const lastKey = getIfExistsLastKey(state);
|
||||
if (!lastKey) {
|
||||
return;
|
||||
}
|
||||
return getVariable({ id: name, rootStateKey: lastKey, type: 'query' }, state, false);
|
||||
}
|
||||
|
||||
export function getInstanceState<Model extends VariableModel = VariableModel>(state: VariablesState, id: string) {
|
||||
return state[id] as Model;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { variableAdapters } from '../adapters';
|
||||
import { createCustomVariableAdapter } from '../custom/adapter';
|
||||
import { customBuilder } from '../shared/testing/builders';
|
||||
import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||
import { TemplatingState } from './reducers';
|
||||
import { getTemplatingRootReducer } from './helpers';
|
||||
import { getTemplatingRootReducer, TemplatingReducerType } from './helpers';
|
||||
import { addVariable, setCurrentVariableValue } from './sharedReducer';
|
||||
import { toVariableIdentifier, toVariablePayload } from './types';
|
||||
import { setOptionFromUrl } from './actions';
|
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
|
||||
import { toKeyedAction } from './keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
|
||||
variableAdapters.setInit(() => [createCustomVariableAdapter()]);
|
||||
|
||||
@@ -28,16 +28,31 @@ describe('when setOptionFromUrl is dispatched with a custom variable (no refresh
|
||||
${null} | ${true} | ${['']}
|
||||
${undefined} | ${true} | ${['']}
|
||||
`('and urlValue is $urlValue then correct actions are dispatched', async ({ urlValue, expected, isMulti }) => {
|
||||
const custom = customBuilder().withId('0').withMulti(isMulti).withOptions('A', 'B', 'C').withCurrent('A').build();
|
||||
const key = 'key';
|
||||
const custom = customBuilder()
|
||||
.withId('0')
|
||||
.withRootStateKey(key)
|
||||
.withMulti(isMulti)
|
||||
.withOptions('A', 'B', 'C')
|
||||
.withCurrent('A')
|
||||
.build();
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
const tester = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
.whenAsyncActionIsDispatched(setOptionFromUrl(toVariableIdentifier(custom), urlValue), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(setOptionFromUrl(toKeyedVariableIdentifier(custom), urlValue), true);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload({ type: 'custom', id: '0' }, { option: { text: expected, value: expected, selected: false } })
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'custom', id: '0' },
|
||||
{ option: { text: expected, value: expected, selected: false } }
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -47,8 +62,10 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
|
||||
it('and urlValue contains same all value then correct actions are dispatched', async () => {
|
||||
const allValue = '.*';
|
||||
const urlValue = allValue;
|
||||
const key = 'key';
|
||||
const custom = customBuilder()
|
||||
.withId('0')
|
||||
.withRootStateKey(key)
|
||||
.withMulti(false)
|
||||
.withIncludeAll()
|
||||
.withAllValue(allValue)
|
||||
@@ -56,16 +73,21 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
|
||||
.withCurrent('A')
|
||||
.build();
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
const tester = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
.whenAsyncActionIsDispatched(setOptionFromUrl(toVariableIdentifier(custom), urlValue), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(setOptionFromUrl(toKeyedVariableIdentifier(custom), urlValue), true);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'custom', id: '0' },
|
||||
{ option: { text: ALL_VARIABLE_TEXT, value: ALL_VARIABLE_VALUE, selected: false } }
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'custom', id: '0' },
|
||||
{ option: { text: ALL_VARIABLE_TEXT, value: ALL_VARIABLE_VALUE, selected: false } }
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -74,8 +96,10 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
|
||||
it('and urlValue differs from all value then correct actions are dispatched', async () => {
|
||||
const allValue = '.*';
|
||||
const urlValue = 'X';
|
||||
const key = 'key';
|
||||
const custom = customBuilder()
|
||||
.withId('0')
|
||||
.withRootStateKey(key)
|
||||
.withMulti(false)
|
||||
.withIncludeAll()
|
||||
.withAllValue(allValue)
|
||||
@@ -83,14 +107,19 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
|
||||
.withCurrent('A')
|
||||
.build();
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
const tester = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
.whenAsyncActionIsDispatched(setOptionFromUrl(toVariableIdentifier(custom), urlValue), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(setOptionFromUrl(toKeyedVariableIdentifier(custom), urlValue), true);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload({ type: 'custom', id: '0' }, { option: { text: 'X', value: 'X', selected: false } })
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload({ type: 'custom', id: '0' }, { option: { text: 'X', value: 'X', selected: false } })
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -98,8 +127,10 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
|
||||
it('and urlValue differs but matches an option then correct actions are dispatched', async () => {
|
||||
const allValue = '.*';
|
||||
const urlValue = 'B';
|
||||
const key = 'key';
|
||||
const custom = customBuilder()
|
||||
.withId('0')
|
||||
.withRootStateKey(key)
|
||||
.withMulti(false)
|
||||
.withIncludeAll()
|
||||
.withAllValue(allValue)
|
||||
@@ -107,14 +138,19 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
|
||||
.withCurrent('A')
|
||||
.build();
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
const tester = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
.whenAsyncActionIsDispatched(setOptionFromUrl(toVariableIdentifier(custom), urlValue), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(setOptionFromUrl(toKeyedVariableIdentifier(custom), urlValue), true);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload({ type: 'custom', id: '0' }, { option: { text: 'B', value: 'B', selected: false } })
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload({ type: 'custom', id: '0' }, { option: { text: 'B', value: 'B', selected: false } })
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -122,8 +158,10 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
|
||||
it('and custom all value matches an option', async () => {
|
||||
const allValue = '.*';
|
||||
const urlValue = allValue;
|
||||
const key = 'key';
|
||||
const custom = customBuilder()
|
||||
.withId('0')
|
||||
.withRootStateKey(key)
|
||||
.withMulti(false)
|
||||
.withIncludeAll()
|
||||
.withAllValue(allValue)
|
||||
@@ -133,16 +171,21 @@ describe('when setOptionFromUrl is dispatched for a variable with a custom all v
|
||||
|
||||
custom.options[2].value = 'special value for .*';
|
||||
|
||||
const tester = await reduxTester<{ templating: TemplatingState }>()
|
||||
const tester = await reduxTester<TemplatingReducerType>()
|
||||
.givenRootReducer(getTemplatingRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
.whenAsyncActionIsDispatched(setOptionFromUrl(toVariableIdentifier(custom), urlValue), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(setOptionFromUrl(toKeyedVariableIdentifier(custom), urlValue), true);
|
||||
|
||||
await tester.thenDispatchedActionsShouldEqual(
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'custom', id: '0' },
|
||||
{ option: { text: '.*', value: 'special value for .*', selected: false } }
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(
|
||||
toVariablePayload(
|
||||
{ type: 'custom', id: '0' },
|
||||
{ option: { text: '.*', value: 'special value for .*', selected: false } }
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
variableStateNotStarted,
|
||||
} from './sharedReducer';
|
||||
import { ConstantVariableModel, QueryVariableModel, VariableHide, VariableOption } from '../types';
|
||||
import { initialVariablesState, toVariablePayload, VariableIdentifier, VariablesState } from './types';
|
||||
import { initialVariablesState, KeyedVariableIdentifier, VariablesState } from './types';
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { createQueryVariableAdapter } from '../query/adapter';
|
||||
import { initialQueryVariableModelState } from '../query/reducer';
|
||||
@@ -26,6 +26,7 @@ import { changeVariableNameSucceeded } from '../editor/reducer';
|
||||
import { createConstantVariableAdapter } from '../constant/adapter';
|
||||
import { initialConstantVariableModelState } from '../constant/reducer';
|
||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../constants';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
variableAdapters.setInit(() => [createQueryVariableAdapter(), createConstantVariableAdapter()]);
|
||||
|
||||
@@ -102,6 +103,7 @@ describe('sharedReducer', () => {
|
||||
.thenStateShouldEqual({
|
||||
'0': {
|
||||
id: '0',
|
||||
rootStateKey: 'key',
|
||||
type: 'query',
|
||||
name: 'Name-0',
|
||||
hide: VariableHide.dontHide,
|
||||
@@ -115,6 +117,7 @@ describe('sharedReducer', () => {
|
||||
},
|
||||
'2': {
|
||||
id: '2',
|
||||
rootStateKey: 'key',
|
||||
type: 'query',
|
||||
name: 'Name-2',
|
||||
hide: VariableHide.dontHide,
|
||||
@@ -140,6 +143,7 @@ describe('sharedReducer', () => {
|
||||
.thenStateShouldEqual({
|
||||
'0': {
|
||||
id: '0',
|
||||
rootStateKey: 'key',
|
||||
type: 'query',
|
||||
name: 'Name-0',
|
||||
hide: VariableHide.dontHide,
|
||||
@@ -153,6 +157,7 @@ describe('sharedReducer', () => {
|
||||
},
|
||||
'2': {
|
||||
id: '2',
|
||||
rootStateKey: 'key',
|
||||
type: 'query',
|
||||
name: 'Name-2',
|
||||
hide: VariableHide.dontHide,
|
||||
@@ -179,6 +184,7 @@ describe('sharedReducer', () => {
|
||||
...initialState,
|
||||
'0': {
|
||||
id: '0',
|
||||
rootStateKey: 'key',
|
||||
type: 'query',
|
||||
name: 'Name-0',
|
||||
hide: VariableHide.dontHide,
|
||||
@@ -192,6 +198,7 @@ describe('sharedReducer', () => {
|
||||
},
|
||||
'1': {
|
||||
id: '1',
|
||||
rootStateKey: 'key',
|
||||
type: 'query',
|
||||
name: 'Name-1',
|
||||
hide: VariableHide.dontHide,
|
||||
@@ -205,6 +212,7 @@ describe('sharedReducer', () => {
|
||||
},
|
||||
'2': {
|
||||
id: '2',
|
||||
rootStateKey: 'key',
|
||||
type: 'query',
|
||||
name: 'Name-2',
|
||||
hide: VariableHide.dontHide,
|
||||
@@ -219,6 +227,7 @@ describe('sharedReducer', () => {
|
||||
'11': {
|
||||
...initialQueryVariableModelState,
|
||||
id: '11',
|
||||
rootStateKey: 'key',
|
||||
name: 'copy_of_Name-1',
|
||||
index: 3,
|
||||
label: 'Label-1',
|
||||
@@ -237,6 +246,7 @@ describe('sharedReducer', () => {
|
||||
.thenStateShouldEqual({
|
||||
'0': {
|
||||
id: '0',
|
||||
rootStateKey: 'key',
|
||||
type: 'query',
|
||||
name: 'Name-0',
|
||||
hide: VariableHide.dontHide,
|
||||
@@ -250,6 +260,7 @@ describe('sharedReducer', () => {
|
||||
},
|
||||
'1': {
|
||||
id: '1',
|
||||
rootStateKey: 'key',
|
||||
type: 'query',
|
||||
name: 'Name-1',
|
||||
hide: VariableHide.dontHide,
|
||||
@@ -263,6 +274,7 @@ describe('sharedReducer', () => {
|
||||
},
|
||||
'2': {
|
||||
id: '2',
|
||||
rootStateKey: 'key',
|
||||
type: 'query',
|
||||
name: 'Name-2',
|
||||
hide: VariableHide.dontHide,
|
||||
@@ -286,6 +298,7 @@ describe('sharedReducer', () => {
|
||||
.thenStateShouldEqual({
|
||||
'0': {
|
||||
id: '0',
|
||||
rootStateKey: 'key',
|
||||
type: 'query',
|
||||
name: 'Name-0',
|
||||
hide: VariableHide.dontHide,
|
||||
@@ -299,6 +312,7 @@ describe('sharedReducer', () => {
|
||||
},
|
||||
'1': {
|
||||
id: '1',
|
||||
rootStateKey: 'key',
|
||||
type: 'query',
|
||||
name: 'Name-1',
|
||||
hide: VariableHide.dontHide,
|
||||
@@ -312,6 +326,7 @@ describe('sharedReducer', () => {
|
||||
},
|
||||
'2': {
|
||||
id: '2',
|
||||
rootStateKey: 'key',
|
||||
type: 'query',
|
||||
name: 'Name-2',
|
||||
hide: VariableHide.dontHide,
|
||||
@@ -548,7 +563,7 @@ describe('sharedReducer', () => {
|
||||
const constantAdapter = createConstantVariableAdapter();
|
||||
const { initialState: constantAdapterState } = getVariableTestContext(constantAdapter);
|
||||
const newType = 'constant' as VariableType;
|
||||
const identifier: VariableIdentifier = { id: '0', type: 'query' };
|
||||
const identifier: KeyedVariableIdentifier = { id: '0', type: 'query', rootStateKey: 'key' };
|
||||
const payload = toVariablePayload(identifier, { newType });
|
||||
reducerTester<VariablesState>()
|
||||
.givenReducer(sharedReducer, cloneDeep(queryAdapterState))
|
||||
@@ -564,6 +579,7 @@ describe('sharedReducer', () => {
|
||||
...constantAdapterState,
|
||||
'0': {
|
||||
...constantAdapterState[0],
|
||||
rootStateKey: 'key',
|
||||
name: 'test',
|
||||
description: 'new description',
|
||||
label: 'new label',
|
||||
|
||||
@@ -2,11 +2,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { cloneDeep, defaults as lodashDefaults } from 'lodash';
|
||||
import { LoadingState, VariableType } from '@grafana/data';
|
||||
import { VariableModel, VariableOption, VariableWithOptions } from '../types';
|
||||
import { AddVariable, getInstanceState, initialVariablesState, VariablePayload, VariablesState } from './types';
|
||||
import { AddVariable, initialVariablesState, VariablePayload, VariablesState } from './types';
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { changeVariableNameSucceeded } from '../editor/reducer';
|
||||
import { ensureStringValues } from '../utils';
|
||||
import { getNextVariableIndex } from './selectors';
|
||||
import { getInstanceState, getNextVariableIndex } from './selectors';
|
||||
|
||||
const sharedReducerSlice = createSlice({
|
||||
name: 'templating/shared',
|
||||
@@ -100,11 +100,12 @@ const sharedReducerSlice = createSlice({
|
||||
},
|
||||
changeVariableType: (state: VariablesState, action: PayloadAction<VariablePayload<{ newType: VariableType }>>) => {
|
||||
const { id } = action.payload;
|
||||
const { label, name, index, description } = state[id];
|
||||
const { label, name, index, description, rootStateKey } = state[id];
|
||||
|
||||
state[id] = {
|
||||
...cloneDeep(variableAdapters.get(action.payload.data.newType).initialState),
|
||||
id: id,
|
||||
id,
|
||||
rootStateKey: rootStateKey,
|
||||
label,
|
||||
name,
|
||||
index,
|
||||
|
||||
@@ -2,12 +2,12 @@ import { variableAdapters } from '../adapters';
|
||||
import { constantBuilder, customBuilder } from '../shared/testing/builders';
|
||||
import { DashboardState, StoreState } from '../../../types';
|
||||
import { initialState } from '../../dashboard/state/reducers';
|
||||
import { TemplatingState } from './reducers';
|
||||
import { ExtendedUrlQueryMap } from '../utils';
|
||||
import { templateVarsChangedInUrl } from './actions';
|
||||
import { createCustomVariableAdapter } from '../custom/adapter';
|
||||
import { VariablesState } from './types';
|
||||
import { DashboardModel } from '../../dashboard/state';
|
||||
import { getPreloadedState } from './helpers';
|
||||
import { createConstantVariableAdapter } from '../constant/adapter';
|
||||
import { VariableModel } from '../types';
|
||||
|
||||
@@ -18,9 +18,11 @@ variableAdapters.setInit(() => [createCustomVariableAdapter(), createConstantVar
|
||||
async function getTestContext(urlQueryMap: ExtendedUrlQueryMap = {}, variable: VariableModel | undefined = undefined) {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const key = 'key';
|
||||
if (!variable) {
|
||||
variable = customBuilder()
|
||||
.withId('variable')
|
||||
.withRootStateKey(key)
|
||||
.withName('variable')
|
||||
.withCurrent(['A', 'C'])
|
||||
.withOptions('A', 'B', 'C')
|
||||
@@ -43,15 +45,14 @@ async function getTestContext(urlQueryMap: ExtendedUrlQueryMap = {}, variable: V
|
||||
};
|
||||
|
||||
const variables: VariablesState = { variable };
|
||||
const templating = { variables } as unknown as TemplatingState;
|
||||
const state: Partial<StoreState> = {
|
||||
dashboard,
|
||||
templating,
|
||||
...getPreloadedState(key, { variables }),
|
||||
};
|
||||
const getState = () => state as unknown as StoreState;
|
||||
|
||||
const dispatch = jest.fn();
|
||||
const thunk = templateVarsChangedInUrl(urlQueryMap);
|
||||
const thunk = templateVarsChangedInUrl(key, urlQueryMap);
|
||||
|
||||
await thunk(dispatch, getState, undefined);
|
||||
|
||||
@@ -125,6 +126,7 @@ describe('templateVarsChangedInUrl', () => {
|
||||
it('then the value should change to the value in dashboard json and dashboard should be refreshed', async () => {
|
||||
const constant = constantBuilder()
|
||||
.withId('variable')
|
||||
.withRootStateKey('key')
|
||||
.withName('variable')
|
||||
.withQuery('default value in dash.json')
|
||||
.build();
|
||||
|
||||
@@ -52,14 +52,14 @@ const transactionSlice = createSlice({
|
||||
});
|
||||
|
||||
function actionAffectsDirtyState(action: AnyAction): boolean {
|
||||
return [
|
||||
removeVariable.type,
|
||||
addVariable.type,
|
||||
changeVariableProp.type,
|
||||
changeVariableOrder.type,
|
||||
duplicateVariable.type,
|
||||
changeVariableType.type,
|
||||
].includes(action.type);
|
||||
return (
|
||||
removeVariable.match(action) ||
|
||||
addVariable.match(action) ||
|
||||
changeVariableProp.match(action) ||
|
||||
changeVariableOrder.match(action) ||
|
||||
duplicateVariable.match(action) ||
|
||||
changeVariableType.match(action)
|
||||
);
|
||||
}
|
||||
|
||||
export const { variablesInitTransaction, variablesClearTransaction, variablesCompleteTransaction } =
|
||||
|
||||
@@ -6,15 +6,17 @@ export interface VariablesState extends Record<string, VariableModel> {}
|
||||
|
||||
export const initialVariablesState: VariablesState = {};
|
||||
|
||||
export const getInstanceState = <Model extends VariableModel = VariableModel>(state: VariablesState, id: string) => {
|
||||
return state[id] as Model;
|
||||
};
|
||||
|
||||
export interface VariableIdentifier {
|
||||
type: VariableType;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface KeyedVariableIdentifier {
|
||||
type: VariableType;
|
||||
id: string;
|
||||
rootStateKey: string;
|
||||
}
|
||||
|
||||
export interface VariablePayload<T extends any = undefined> extends VariableIdentifier {
|
||||
data: T;
|
||||
}
|
||||
@@ -24,21 +26,3 @@ export interface AddVariable<T extends VariableModel = VariableModel> {
|
||||
index: number; // the order in variables list
|
||||
model: T;
|
||||
}
|
||||
|
||||
export const toVariableIdentifier = (variable: VariableModel): VariableIdentifier => {
|
||||
return { type: variable.type, id: variable.id };
|
||||
};
|
||||
|
||||
export function toVariablePayload<T extends any = undefined>(
|
||||
identifier: VariableIdentifier,
|
||||
data?: T
|
||||
): VariablePayload<T>;
|
||||
// eslint-disable-next-line
|
||||
export function toVariablePayload<T extends any = undefined>(model: VariableModel, data?: T): VariablePayload<T>;
|
||||
// eslint-disable-next-line
|
||||
export function toVariablePayload<T extends any = undefined>(
|
||||
obj: VariableIdentifier | VariableModel,
|
||||
data?: T
|
||||
): VariablePayload<T> {
|
||||
return { type: obj.type, id: obj.id, data: data as T };
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { customBuilder, queryBuilder } from '../shared/testing/builders';
|
||||
import { VariableSupportType } from '@grafana/data';
|
||||
import { toVariableIdentifier } from './types';
|
||||
import { upgradeLegacyQueries } from './actions';
|
||||
import { changeVariableProp } from './sharedReducer';
|
||||
import { thunkTester } from '../../../../test/core/thunk/thunkTester';
|
||||
import { TransactionStatus, VariableModel } from '../types';
|
||||
import { toKeyedAction } from './keyedVariablesReducer';
|
||||
import { getPreloadedState } from './helpers';
|
||||
import { toKeyedVariableIdentifier } from '../utils';
|
||||
|
||||
interface Args {
|
||||
query?: any;
|
||||
@@ -18,22 +20,23 @@ function getTestContext({
|
||||
datasource,
|
||||
transactionStatus = TransactionStatus.Fetching,
|
||||
}: Args = {}) {
|
||||
const key = 'key';
|
||||
variable =
|
||||
variable ??
|
||||
queryBuilder()
|
||||
.withId('query')
|
||||
.withRootStateKey(key)
|
||||
.withName('query')
|
||||
.withQuery(query)
|
||||
.withDatasource({ uid: 'test-data', type: 'test-data' })
|
||||
.build();
|
||||
const state = {
|
||||
templating: {
|
||||
transaction: { status: transactionStatus },
|
||||
variables: {
|
||||
[variable.id]: variable,
|
||||
},
|
||||
const templatingState = {
|
||||
transaction: { status: transactionStatus, uid: key, isDirty: false },
|
||||
variables: {
|
||||
[variable.id]: variable,
|
||||
},
|
||||
};
|
||||
const state = getPreloadedState(key, templatingState);
|
||||
datasource = datasource ?? {
|
||||
name: 'TestData',
|
||||
metricFindQuery: () => undefined,
|
||||
@@ -41,32 +44,35 @@ function getTestContext({
|
||||
};
|
||||
const get = jest.fn().mockResolvedValue(datasource);
|
||||
const getDatasourceSrv = jest.fn().mockReturnValue({ get });
|
||||
const identifier = toVariableIdentifier(variable);
|
||||
const identifier = toKeyedVariableIdentifier(variable);
|
||||
|
||||
return { state, get, getDatasourceSrv, identifier };
|
||||
return { key, state, get, getDatasourceSrv, identifier };
|
||||
}
|
||||
|
||||
describe('upgradeLegacyQueries', () => {
|
||||
describe('when called with a query variable for a standard variable supported data source that has not been upgraded', () => {
|
||||
it('then it should dispatch changeVariableProp', async () => {
|
||||
const { state, identifier, get, getDatasourceSrv } = getTestContext({ query: '*' });
|
||||
const { key, state, identifier, get, getDatasourceSrv } = getTestContext({ query: '*' });
|
||||
|
||||
const dispatchedActions = await thunkTester(state)
|
||||
.givenThunk(upgradeLegacyQueries)
|
||||
.whenThunkIsDispatched(identifier, getDatasourceSrv);
|
||||
|
||||
expect(dispatchedActions).toEqual([
|
||||
changeVariableProp({
|
||||
type: 'query',
|
||||
id: 'query',
|
||||
data: {
|
||||
propName: 'query',
|
||||
propValue: {
|
||||
refId: 'TestData-query-Variable-Query',
|
||||
query: '*',
|
||||
toKeyedAction(
|
||||
key,
|
||||
changeVariableProp({
|
||||
type: 'query',
|
||||
id: 'query',
|
||||
data: {
|
||||
propName: 'query',
|
||||
propValue: {
|
||||
refId: 'TestData-query-Variable-Query',
|
||||
query: '*',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
),
|
||||
]);
|
||||
expect(get).toHaveBeenCalledTimes(1);
|
||||
expect(get).toHaveBeenCalledWith({ uid: 'test-data', type: 'test-data' });
|
||||
@@ -161,7 +167,7 @@ describe('upgradeLegacyQueries', () => {
|
||||
|
||||
describe('when called with a custom variable', () => {
|
||||
it('then it should not dispatch any actions', async () => {
|
||||
const variable = customBuilder().withId('custom').withName('custom').build();
|
||||
const variable = customBuilder().withId('custom').withRootStateKey('key').withName('custom').build();
|
||||
const { state, identifier, get, getDatasourceSrv } = getTestContext({ variable });
|
||||
|
||||
const dispatchedActions = await thunkTester(state)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { ChangeEvent, FocusEvent, KeyboardEvent, ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { TextBoxVariableModel } from '../types';
|
||||
import { toVariablePayload } from '../state/types';
|
||||
import { changeVariableProp } from '../state/sharedReducer';
|
||||
import { VariablePickerProps } from '../pickers/types';
|
||||
import { Input } from '@grafana/ui';
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
export interface Props extends VariablePickerProps<TextBoxVariableModel> {}
|
||||
|
||||
@@ -18,13 +19,21 @@ export function TextBoxVariablePicker({ variable, onVariableChange }: Props): Re
|
||||
}, [variable]);
|
||||
|
||||
const updateVariable = useCallback(() => {
|
||||
if (!variable.rootStateKey) {
|
||||
console.error('Cannot update variable without rootStateKey');
|
||||
return;
|
||||
}
|
||||
|
||||
if (variable.current.value === updatedValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
changeVariableProp(
|
||||
toVariablePayload({ id: variable.id, type: variable.type }, { propName: 'query', propValue: updatedValue })
|
||||
toKeyedAction(
|
||||
variable.rootStateKey,
|
||||
changeVariableProp(
|
||||
toVariablePayload({ id: variable.id, type: variable.type }, { propName: 'query', propValue: updatedValue })
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ import { reduxTester } from '../../../../test/core/redux/reduxTester';
|
||||
import { setTextBoxVariableOptionsFromUrl, updateTextBoxVariableOptions } from './actions';
|
||||
import { getRootReducer, RootReducerType } from '../state/helpers';
|
||||
import { VariableOption } from '../types';
|
||||
import { toVariablePayload } from '../state/types';
|
||||
import { createTextBoxOptions } from './reducer';
|
||||
import { addVariable, changeVariableProp, setCurrentVariableValue } from '../state/sharedReducer';
|
||||
import { textboxBuilder } from '../shared/testing/builders';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
|
||||
jest.mock('@grafana/runtime', () => {
|
||||
const original = jest.requireActual('@grafana/runtime');
|
||||
@@ -32,16 +33,25 @@ describe('textbox actions', () => {
|
||||
selected: false,
|
||||
};
|
||||
|
||||
const variable = textboxBuilder().withId('textbox').withName('textbox').withCurrent('A').withQuery('A').build();
|
||||
const key = 'key';
|
||||
const variable = textboxBuilder()
|
||||
.withId('textbox')
|
||||
.withRootStateKey(key)
|
||||
.withName('textbox')
|
||||
.withCurrent('A')
|
||||
.withQuery('A')
|
||||
.build();
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenAsyncActionIsDispatched(updateTextBoxVariableOptions(toVariablePayload(variable)), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(updateTextBoxVariableOptions(toKeyedVariableIdentifier(variable)), true);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
createTextBoxOptions(toVariablePayload(variable)),
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option }))
|
||||
toKeyedAction(key, createTextBoxOptions(toVariablePayload(variable))),
|
||||
toKeyedAction(key, setCurrentVariableValue(toVariablePayload(variable, { option })))
|
||||
);
|
||||
expect(locationService.partial).toHaveBeenLastCalledWith({ 'var-textbox': 'A' });
|
||||
});
|
||||
@@ -50,16 +60,31 @@ describe('textbox actions', () => {
|
||||
describe('when setTextBoxVariableOptionsFromUrl is dispatched', () => {
|
||||
it('then correct actions are dispatched', async () => {
|
||||
const urlValue = 'bB';
|
||||
const variable = textboxBuilder().withId('textbox').withName('textbox').withCurrent('A').withQuery('A').build();
|
||||
const key = 'key';
|
||||
const variable = textboxBuilder()
|
||||
.withId('textbox')
|
||||
.withRootStateKey(key)
|
||||
.withName('textbox')
|
||||
.withCurrent('A')
|
||||
.withQuery('A')
|
||||
.build();
|
||||
|
||||
const tester = await reduxTester<RootReducerType>()
|
||||
.givenRootReducer(getRootReducer())
|
||||
.whenActionIsDispatched(addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
.whenAsyncActionIsDispatched(setTextBoxVariableOptionsFromUrl(toVariablePayload(variable), urlValue), true);
|
||||
.whenActionIsDispatched(
|
||||
toKeyedAction(key, addVariable(toVariablePayload(variable, { global: false, index: 0, model: variable })))
|
||||
)
|
||||
.whenAsyncActionIsDispatched(
|
||||
setTextBoxVariableOptionsFromUrl(toKeyedVariableIdentifier(variable), urlValue),
|
||||
true
|
||||
);
|
||||
|
||||
tester.thenDispatchedActionsShouldEqual(
|
||||
changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: 'bB' })),
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option: { text: 'bB', value: 'bB', selected: false } }))
|
||||
toKeyedAction(key, changeVariableProp(toVariablePayload(variable, { propName: 'query', propValue: 'bB' }))),
|
||||
toKeyedAction(
|
||||
key,
|
||||
setCurrentVariableValue(toVariablePayload(variable, { option: { text: 'bB', value: 'bB', selected: false } }))
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,28 +3,36 @@ import { ThunkResult } from '../../../types';
|
||||
import { getVariable } from '../state/selectors';
|
||||
import { variableAdapters } from '../adapters';
|
||||
import { createTextBoxOptions } from './reducer';
|
||||
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
|
||||
import { KeyedVariableIdentifier } from '../state/types';
|
||||
import { setOptionFromUrl } from '../state/actions';
|
||||
import { UrlQueryValue } from '@grafana/data';
|
||||
import { changeVariableProp } from '../state/sharedReducer';
|
||||
import { ensureStringValues } from '../utils';
|
||||
import { ensureStringValues, toKeyedVariableIdentifier, toVariablePayload } from '../utils';
|
||||
import { toKeyedAction } from '../state/keyedVariablesReducer';
|
||||
|
||||
export const updateTextBoxVariableOptions = (identifier: VariableIdentifier): ThunkResult<void> => {
|
||||
export const updateTextBoxVariableOptions = (identifier: KeyedVariableIdentifier): ThunkResult<void> => {
|
||||
return async (dispatch, getState) => {
|
||||
await dispatch(createTextBoxOptions(toVariablePayload(identifier)));
|
||||
const { rootStateKey, type } = identifier;
|
||||
dispatch(toKeyedAction(rootStateKey, createTextBoxOptions(toVariablePayload(identifier))));
|
||||
|
||||
const variableInState = getVariable<TextBoxVariableModel>(identifier.id, getState());
|
||||
await variableAdapters.get(identifier.type).setValue(variableInState, variableInState.options[0], true);
|
||||
const variableInState = getVariable<TextBoxVariableModel>(identifier, getState());
|
||||
await variableAdapters.get(type).setValue(variableInState, variableInState.options[0], true);
|
||||
};
|
||||
};
|
||||
|
||||
export const setTextBoxVariableOptionsFromUrl =
|
||||
(identifier: VariableIdentifier, urlValue: UrlQueryValue): ThunkResult<void> =>
|
||||
(identifier: KeyedVariableIdentifier, urlValue: UrlQueryValue): ThunkResult<void> =>
|
||||
async (dispatch, getState) => {
|
||||
const variableInState = getVariable<TextBoxVariableModel>(identifier.id, getState());
|
||||
const { rootStateKey } = identifier;
|
||||
const variableInState = getVariable<TextBoxVariableModel>(identifier, getState());
|
||||
|
||||
const stringUrlValue = ensureStringValues(urlValue);
|
||||
dispatch(changeVariableProp(toVariablePayload(variableInState, { propName: 'query', propValue: stringUrlValue })));
|
||||
dispatch(
|
||||
toKeyedAction(
|
||||
rootStateKey,
|
||||
changeVariableProp(toVariablePayload(variableInState, { propName: 'query', propValue: stringUrlValue }))
|
||||
)
|
||||
);
|
||||
|
||||
await dispatch(setOptionFromUrl(toVariableIdentifier(variableInState), stringUrlValue));
|
||||
await dispatch(setOptionFromUrl(toKeyedVariableIdentifier(variableInState), stringUrlValue));
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ describe('createTextBoxVariableAdapter', () => {
|
||||
it('then the model should be correct', () => {
|
||||
const text = textboxBuilder()
|
||||
.withId('text')
|
||||
.withRootStateKey('key')
|
||||
.withName('text')
|
||||
.withQuery('query')
|
||||
.withOriginalQuery('original')
|
||||
@@ -41,6 +42,7 @@ describe('createTextBoxVariableAdapter', () => {
|
||||
it('then the model should be correct', () => {
|
||||
const text = textboxBuilder()
|
||||
.withId('text')
|
||||
.withRootStateKey('key')
|
||||
.withName('text')
|
||||
.withQuery('query')
|
||||
.withOriginalQuery('original')
|
||||
|
||||
@@ -8,7 +8,7 @@ import { VariableAdapter } from '../adapters';
|
||||
import { TextBoxVariablePicker } from './TextBoxVariablePicker';
|
||||
import { TextBoxVariableEditor } from './TextBoxVariableEditor';
|
||||
import { setTextBoxVariableOptionsFromUrl, updateTextBoxVariableOptions } from './actions';
|
||||
import { toVariableIdentifier } from '../state/types';
|
||||
import { toKeyedVariableIdentifier } from '../utils';
|
||||
|
||||
export const createTextBoxVariableAdapter = (): VariableAdapter<TextBoxVariableModel> => {
|
||||
return {
|
||||
@@ -23,16 +23,16 @@ export const createTextBoxVariableAdapter = (): VariableAdapter<TextBoxVariableM
|
||||
return false;
|
||||
},
|
||||
setValue: async (variable, option, emitChanges = false) => {
|
||||
await dispatch(setOptionAsCurrent(toVariableIdentifier(variable), option, emitChanges));
|
||||
await dispatch(setOptionAsCurrent(toKeyedVariableIdentifier(variable), option, emitChanges));
|
||||
},
|
||||
setValueFromUrl: async (variable, urlValue) => {
|
||||
await dispatch(setTextBoxVariableOptionsFromUrl(toVariableIdentifier(variable), urlValue));
|
||||
await dispatch(setTextBoxVariableOptionsFromUrl(toKeyedVariableIdentifier(variable), urlValue));
|
||||
},
|
||||
updateOptions: async (variable) => {
|
||||
await dispatch(updateTextBoxVariableOptions(toVariableIdentifier(variable)));
|
||||
await dispatch(updateTextBoxVariableOptions(toKeyedVariableIdentifier(variable)));
|
||||
},
|
||||
getSaveModel: (variable, saveCurrentAsDefault) => {
|
||||
const { index, id, state, global, originalQuery, ...rest } = cloneDeep(variable);
|
||||
const { index, id, state, global, originalQuery, rootStateKey, ...rest } = cloneDeep(variable);
|
||||
|
||||
if (variable.query !== originalQuery && !saveCurrentAsDefault) {
|
||||
const origQuery = originalQuery ?? '';
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { reducerTester } from '../../../../test/core/redux/reducerTester';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { getVariableTestContext } from '../state/helpers';
|
||||
import { toVariablePayload, VariablesState } from '../state/types';
|
||||
import { VariablesState } from '../state/types';
|
||||
import { createTextBoxOptions, textBoxVariableReducer } from './reducer';
|
||||
import { TextBoxVariableModel } from '../types';
|
||||
import { createTextBoxVariableAdapter } from './adapter';
|
||||
import { toVariablePayload } from '../utils';
|
||||
|
||||
describe('textBoxVariableReducer', () => {
|
||||
const adapter = createTextBoxVariableAdapter();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { initialVariableModelState, TextBoxVariableModel, VariableOption } from '../types';
|
||||
import { getInstanceState, VariablePayload, initialVariablesState, VariablesState } from '../state/types';
|
||||
import { initialVariablesState, VariablePayload, VariablesState } from '../state/types';
|
||||
import { getInstanceState } from '../state/selectors';
|
||||
|
||||
export const initialTextBoxVariableModelState: TextBoxVariableModel = {
|
||||
...initialVariableModelState,
|
||||
|
||||
@@ -131,6 +131,7 @@ export interface SystemVariable<TProps extends { toString: () => string }> exten
|
||||
|
||||
export interface VariableModel extends BaseVariableModel {
|
||||
id: string;
|
||||
rootStateKey: string | null;
|
||||
global: boolean;
|
||||
hide: VariableHide;
|
||||
skipUrlSync: boolean;
|
||||
@@ -142,6 +143,7 @@ export interface VariableModel extends BaseVariableModel {
|
||||
|
||||
export const initialVariableModelState: VariableModel = {
|
||||
id: NEW_VARIABLE_ID,
|
||||
rootStateKey: null,
|
||||
name: '',
|
||||
label: null,
|
||||
type: '' as unknown as VariableType,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user