Commit 6e3e9b7c by Hugo Häggmark Committed by GitHub

Templating: fixes variables not being interpolated after dashboard refresh (#25698)

* Templating: Moves global variables from TemplateSrv to Redux

* Refactor: renamed to meta

* Tests: fixed broken tests

* Chore: reduces strict null errors

* renamed meta variabel to system variable.

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>
parent 69eb5afd
export type VariableType = 'query' | 'adhoc' | 'constant' | 'datasource' | 'interval' | 'textbox' | 'custom';
export type VariableType = 'query' | 'adhoc' | 'constant' | 'datasource' | 'interval' | 'textbox' | 'custom' | 'system';
export interface VariableModel {
type: VariableType;
......
import React, { PureComponent } from 'react';
import { connect, MapStateToProps } from 'react-redux';
import { StoreState } from '../../../../types';
import { getVariables } from '../../../variables/state/selectors';
import { getSubMenuVariables } from '../../../variables/state/selectors';
import { VariableHide, VariableModel } from '../../../variables/types';
import { DashboardModel } from '../../state';
import { DashboardLinks } from './DashboardLinks';
......@@ -67,7 +67,7 @@ class SubMenuUnConnected extends PureComponent<Props> {
}
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = state => ({
variables: getVariables(state, false),
variables: getSubMenuVariables(state),
});
export const SubMenu = connect(mapStateToProps)(SubMenuUnConnected);
......
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { initDashboard, InitDashboardArgs } from './initDashboard';
import { DashboardRouteInfo, DashboardInitPhase } from 'app/types';
import { DashboardInitPhase, DashboardRouteInfo } from 'app/types';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { dashboardInitCompleted, dashboardInitFetching, dashboardInitServices } from './reducers';
import { updateLocation } from '../../../core/actions';
......@@ -184,8 +184,8 @@ describeInitScenario('Initializing new dashboard', ctx => {
});
it('Should send action dashboardInitCompleted', () => {
expect(ctx.actions[5].type).toBe(dashboardInitCompleted.type);
expect(ctx.actions[5].payload.title).toBe('New dashboard');
expect(ctx.actions[8].type).toBe(dashboardInitCompleted.type);
expect(ctx.actions[8].payload.title).toBe('New dashboard');
});
it('Should initialize services', () => {
......@@ -257,8 +257,8 @@ describeInitScenario('Initializing existing dashboard', ctx => {
});
it('Should send action dashboardInitCompleted', () => {
expect(ctx.actions[6].type).toBe(dashboardInitCompleted.type);
expect(ctx.actions[6].payload.title).toBe('My cool dashboard');
expect(ctx.actions[9].type).toBe(dashboardInitCompleted.type);
expect(ctx.actions[9].payload.title).toBe('My cool dashboard');
});
it('Should initialize services', () => {
......
......@@ -240,7 +240,13 @@ export class TemplateSrv implements BaseTemplateSrv {
this.grafanaVariables[name] = value;
}
/**
* @deprecated: setGlobalVariable function should not be used and will be removed in future releases
*
* Use addVariable action to add variables to Redux instead
*/
setGlobalVariable(name: string, variable: any) {
deprecationWarning('template_srv.ts', 'setGlobalVariable', '');
this.index = {
...this.index,
[name]: {
......
......@@ -23,6 +23,7 @@ import { createConstantVariableAdapter } from './constant/adapter';
import { createDataSourceVariableAdapter } from './datasource/adapter';
import { createIntervalVariableAdapter } from './interval/adapter';
import { createAdHocVariableAdapter } from './adhoc/adapter';
import { createSystemVariableAdapter } from './system/adapter';
export interface VariableAdapter<Model extends VariableModel> {
id: VariableType;
......@@ -58,6 +59,7 @@ export const getDefaultVariableAdapters = () => [
createDataSourceVariableAdapter(),
createIntervalVariableAdapter(),
createAdHocVariableAdapter(),
createSystemVariableAdapter(),
];
export const variableAdapters: VariableTypeRegistry = new Registry<VariableAdapter<VariableModels>>();
......@@ -8,7 +8,7 @@ import { VariableEditorList } from './VariableEditorList';
import { VariableEditorEditor } from './VariableEditorEditor';
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
import { getVariables } from '../state/selectors';
import { getEditorVariables } from '../state/selectors';
import { VariableModel } from '../types';
import { switchToEditMode, switchToListMode, switchToNewMode } from './actions';
import { changeVariableOrder, duplicateVariable, removeVariable } from '../state/sharedReducer';
......@@ -124,7 +124,7 @@ class VariableEditorContainerUnconnected extends PureComponent<Props> {
}
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = state => ({
variables: getVariables(state, true),
variables: getEditorVariables(state),
idInEditor: state.templating.editor.id,
});
......
......@@ -566,14 +566,23 @@ describe('shared actions', () => {
.givenRootReducer(getRootReducer())
.whenAsyncActionIsDispatched(initVariablesTransaction(uid, dashboard));
tester.thenDispatchedActionsShouldEqual(
variablesInitTransaction({ uid }),
addVariable(toVariablePayload(constant, { global: false, index: 0, model: constant })),
addInitLock(toVariablePayload(constant)),
resolveInitLock(toVariablePayload(constant)),
removeInitLock(toVariablePayload(constant)),
variablesCompleteTransaction({ uid })
);
tester.thenDispatchedActionsPredicateShouldEqual(dispatchedActions => {
expect(dispatchedActions[0]).toEqual(variablesInitTransaction({ uid }));
expect(dispatchedActions[1]).toEqual(
addVariable(toVariablePayload(constant, { global: false, index: 0, model: constant }))
);
expect(dispatchedActions[2]).toEqual(addInitLock(toVariablePayload(constant)));
expect(dispatchedActions[3]).toEqual(resolveInitLock(toVariablePayload(constant)));
expect(dispatchedActions[4]).toEqual(removeInitLock(toVariablePayload(constant)));
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[8]).toEqual(variablesCompleteTransaction({ uid }));
return dispatchedActions.length === 9;
});
});
});
......@@ -594,16 +603,25 @@ describe('shared actions', () => {
.givenRootReducer(getRootReducer())
.whenAsyncActionIsDispatched(initVariablesTransaction(uid, dashboard));
tester.thenDispatchedActionsShouldEqual(
cleanVariables(),
variablesClearTransaction(),
variablesInitTransaction({ uid }),
addVariable(toVariablePayload(constant, { global: false, index: 0, model: constant })),
addInitLock(toVariablePayload(constant)),
resolveInitLock(toVariablePayload(constant)),
removeInitLock(toVariablePayload(constant)),
variablesCompleteTransaction({ uid })
);
tester.thenDispatchedActionsPredicateShouldEqual(dispatchedActions => {
expect(dispatchedActions[0]).toEqual(cleanVariables());
expect(dispatchedActions[1]).toEqual(variablesClearTransaction());
expect(dispatchedActions[2]).toEqual(variablesInitTransaction({ uid }));
expect(dispatchedActions[3]).toEqual(
addVariable(toVariablePayload(constant, { global: false, index: 0, model: constant }))
);
expect(dispatchedActions[4]).toEqual(addInitLock(toVariablePayload(constant)));
expect(dispatchedActions[5]).toEqual(resolveInitLock(toVariablePayload(constant)));
expect(dispatchedActions[6]).toEqual(removeInitLock(toVariablePayload(constant)));
expect(dispatchedActions[7].type).toEqual(addVariable.type);
expect(dispatchedActions[7].payload.id).toEqual('__dashboard');
expect(dispatchedActions[8].type).toEqual(addVariable.type);
expect(dispatchedActions[8].payload.id).toEqual('__org');
expect(dispatchedActions[9].type).toEqual(addVariable.type);
expect(dispatchedActions[9].payload.id).toEqual('__user');
expect(dispatchedActions[10]).toEqual(variablesCompleteTransaction({ uid }));
return dispatchedActions.length === 11;
});
});
});
});
......
......@@ -3,7 +3,11 @@ import { AppEvents, TimeRange, UrlQueryMap, UrlQueryValue } from '@grafana/data'
import angular from 'angular';
import {
DashboardVariableModel,
OrgVariableModel,
QueryVariableModel,
UserVariableModel,
VariableHide,
VariableModel,
VariableOption,
VariableRefresh,
......@@ -94,33 +98,77 @@ export const initDashboardTemplating = (list: VariableModel[]): ThunkResult<void
export const completeDashboardTemplating = (dashboard: DashboardModel): ThunkResult<void> => {
return (dispatch, getState) => {
templateSrv.setGlobalVariable('__dashboard', {
value: {
name: dashboard.title,
uid: dashboard.uid,
toString: function() {
return this.uid;
const dashboardModel: DashboardVariableModel = {
id: '__dashboard',
name: '__dashboard',
label: null,
type: 'system',
index: -3,
skipUrlSync: true,
hide: VariableHide.hideVariable,
global: false,
current: {
value: {
name: dashboard.title,
uid: dashboard.uid,
toString: () => dashboard.title,
},
},
});
templateSrv.setGlobalVariable('__org', {
value: {
name: contextSrv.user.orgName,
id: contextSrv.user.orgId,
toString: function() {
return this.id;
};
dispatch(
addVariable(
toVariablePayload(dashboardModel, {
global: dashboardModel.global,
index: dashboardModel.index,
model: dashboardModel,
})
)
);
const orgModel: OrgVariableModel = {
id: '__org',
name: '__org',
label: null,
type: 'system',
index: -2,
skipUrlSync: true,
hide: VariableHide.hideVariable,
global: false,
current: {
value: {
name: contextSrv.user.orgName,
id: contextSrv.user.orgId,
toString: () => contextSrv.user.orgId.toString(),
},
},
});
templateSrv.setGlobalVariable('__user', {
value: {
login: contextSrv.user.login,
id: contextSrv.user.id,
toString: function() {
return this.id;
};
dispatch(
addVariable(toVariablePayload(orgModel, { global: orgModel.global, index: orgModel.index, model: orgModel }))
);
const userModel: UserVariableModel = {
id: '__user',
name: '__user',
label: null,
type: 'system',
index: -1,
skipUrlSync: true,
hide: VariableHide.hideVariable,
global: false,
current: {
value: {
login: contextSrv.user.login,
id: contextSrv.user.id,
toString: () => contextSrv.user.id.toString(),
},
},
});
};
dispatch(
addVariable(toVariablePayload(userModel, { global: userModel.global, index: userModel.index, model: userModel }))
);
};
};
......
......@@ -29,7 +29,25 @@ export const getVariableWithName = (name: string, state: StoreState = getState()
};
export const getVariables = (state: StoreState = getState(), includeNewVariable = false): VariableModel[] => {
return getFilteredVariables(variable => (includeNewVariable ? true : variable.id !== NEW_VARIABLE_ID), state);
const filter = (variable: VariableModel) => {
if (variable.type === 'system') {
return false;
}
if (includeNewVariable) {
return true;
}
return variable.id !== NEW_VARIABLE_ID;
};
return getFilteredVariables(filter, state);
};
export const getSubMenuVariables = (state: StoreState): VariableModel[] => {
return getVariables(state);
};
export const getEditorVariables = (state: StoreState): VariableModel[] => {
return getVariables(state, true);
};
export type GetVariables = typeof getVariables;
......
import { ComponentType } from 'react';
import { SystemVariable, VariableHide } from '../types';
import { VariableAdapter } from '../adapters';
import { NEW_VARIABLE_ID } from '../state/types';
import { Deferred } from '../../../core/utils/deferred';
import { VariablePickerProps } from '../pickers/types';
import { VariableEditorProps } from '../editor/types';
export const createSystemVariableAdapter = (): VariableAdapter<SystemVariable<any>> => {
return {
id: 'system',
description: '',
name: 'system',
initialState: {
id: NEW_VARIABLE_ID,
global: false,
type: 'system',
name: '',
label: (null as unknown) as string,
hide: VariableHide.hideVariable,
skipUrlSync: true,
current: { value: { toString: () => '' } },
index: -1,
initLock: (null as unknown) as Deferred,
},
reducer: (state: any, action: any) => state,
picker: (null as unknown) as ComponentType<VariablePickerProps>,
editor: (null as unknown) as ComponentType<VariableEditorProps>,
dependsOn: () => {
return false;
},
setValue: async (variable, option, emitChanges = false) => {
return;
},
setValueFromUrl: async (variable, urlValue) => {
return;
},
updateOptions: async variable => {
return;
},
getSaveModel: variable => {
return {};
},
getValueForUrl: variable => {
return '';
},
};
};
......@@ -91,6 +91,34 @@ export interface VariableWithOptions extends VariableModel {
query: string;
}
export interface DashboardProps {
name: string;
uid: string;
toString: () => string;
}
export interface DashboardVariableModel extends SystemVariable<DashboardProps> {}
export interface OrgProps {
name: string;
id: number;
toString: () => string;
}
export interface OrgVariableModel extends SystemVariable<OrgProps> {}
export interface UserProps {
login: string;
id: number;
toString: () => string;
}
export interface UserVariableModel extends SystemVariable<UserProps> {}
export interface SystemVariable<TProps extends { toString: () => string }> extends VariableModel {
current: { value: TProps };
}
export interface VariableModel extends BaseVariableModel {
id: string;
global: boolean;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment