mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
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);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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;
|
||||
|
||||
48
public/app/features/variables/system/adapter.ts
Normal file
48
public/app/features/variables/system/adapter.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user