Templating: Replace __data , __field and __cell_n with macros (#65324)

* Templating: __data __field and __series macros

* filter out datacontext from json serialization

* Fix condition

* Update

* Added test cases for formatting data, and field macros
This commit is contained in:
Torkel Ödegaard 2023-04-05 11:10:33 +02:00 committed by GitHub
parent caac9838d8
commit 507c6e7d97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 315 additions and 136 deletions

View File

@ -200,9 +200,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "6"], [0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"] [0, 0, 0, "Unexpected any. Specify a different type.", "7"]
], ],
"packages/grafana-data/src/field/templateProxies.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-data/src/geo/layer.ts:5381": [ "packages/grafana-data/src/geo/layer.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"] [0, 0, 0, "Unexpected any. Specify a different type.", "0"]
], ],
@ -3352,6 +3349,9 @@ exports[`better eslint`] = {
"public/app/features/teams/state/selectors.ts:5381": [ "public/app/features/teams/state/selectors.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"] [0, 0, 0, "Unexpected any. Specify a different type.", "0"]
], ],
"public/app/features/templating/fieldAccessorCache.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/templating/formatVariableValue.ts:5381": [ "public/app/features/templating/formatVariableValue.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@ -3360,6 +3360,9 @@ exports[`better eslint`] = {
"public/app/features/templating/macroRegistry.test.ts:5381": [ "public/app/features/templating/macroRegistry.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"] [0, 0, 0, "Unexpected any. Specify a different type.", "0"]
], ],
"public/app/features/templating/templateProxies.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/templating/template_srv.mock.ts:5381": [ "public/app/features/templating/template_srv.mock.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"] [0, 0, 0, "Do not use any type assertions.", "0"]
], ],
@ -3376,11 +3379,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"], [0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"], [0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"], [0, 0, 0, "Do not use any type assertions.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"], [0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Do not use any type assertions.", "14"], [0, 0, 0, "Do not use any type assertions.", "14"]
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Do not use any type assertions.", "16"]
], ],
"public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx:5381": [ "public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -6,7 +6,7 @@ import { ReducerID } from '../transformations/fieldReducer';
import { FieldConfigPropertyItem, MappingType, SpecialValueMatch, ValueMapping } from '../types'; import { FieldConfigPropertyItem, MappingType, SpecialValueMatch, ValueMapping } from '../types';
import { getDisplayProcessor } from './displayProcessor'; import { getDisplayProcessor } from './displayProcessor';
import { getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay'; import { fixCellTemplateExpressions, getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry'; import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
describe('FieldDisplay', () => { describe('FieldDisplay', () => {
@ -534,3 +534,11 @@ function createDisplayOptions(extend: Partial<GetFieldDisplayValuesOptions> = {}
return merge(options, extend); return merge(options, extend);
} }
describe('fixCellTemplateExpressions', () => {
it('Should replace __cell_x correctly', () => {
expect(fixCellTemplateExpressions('$__cell_10 asd ${__cell_15} asd [[__cell_20]]')).toEqual(
'${__data.fields[10]} asd ${__data.fields[15]} asd ${__data.fields[20]}'
);
});
});

View File

@ -1,4 +1,4 @@
import { toString, isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { DataFrameView } from '../dataframe/DataFrameView'; import { DataFrameView } from '../dataframe/DataFrameView';
import { getTimeField } from '../dataframe/processDataFrame'; import { getTimeField } from '../dataframe/processDataFrame';
@ -96,7 +96,6 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
const data = options.data ?? []; const data = options.data ?? [];
const limit = reduceOptions.limit ? reduceOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT; const limit = reduceOptions.limit ? reduceOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
const scopedVars: ScopedVars = {};
let hitLimit = false; let hitLimit = false;
@ -125,7 +124,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
}; };
} }
const displayName = field.config.displayName ?? ''; let displayName = field.config.displayName ?? '';
const display = const display =
field.display ?? field.display ??
@ -137,23 +136,10 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
// Show all rows // Show all rows
if (reduceOptions.values) { if (reduceOptions.values) {
const usesCellValues = displayName.indexOf(VAR_CELL_PREFIX) >= 0;
for (let j = 0; j < field.values.length; j++) { for (let j = 0; j < field.values.length; j++) {
// Add all the row variables
if (usesCellValues) {
for (let k = 0; k < dataFrame.fields.length; k++) {
const f = dataFrame.fields[k];
const v = f.values.get(j);
scopedVars[VAR_CELL_PREFIX + k] = {
value: v,
text: toString(v),
};
}
}
field.state = setIndexForPaletteColor(field, values.length); field.state = setIndexForPaletteColor(field, values.length);
const scopedVars = getFieldScopedVarsWithDataContexAndRowIndex(field, j);
const displayValue = display(field.values.get(j)); const displayValue = display(field.values.get(j));
const rowName = getSmartDisplayNameForRow(dataFrame, field, j, replaceVariables, scopedVars); const rowName = getSmartDisplayNameForRow(dataFrame, field, j, replaceVariables, scopedVars);
const overrideColor = lookupRowColorFromOverride(rowName, options.fieldConfig, theme); const overrideColor = lookupRowColorFromOverride(rowName, options.fieldConfig, theme);
@ -190,14 +176,13 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
}); });
for (const calc of calcs) { for (const calc of calcs) {
const scopedVars = field.state?.scopedVars ?? {};
scopedVars[VAR_CALC] = { value: calc, text: calc }; scopedVars[VAR_CALC] = { value: calc, text: calc };
const displayValue = display(results[calc]); const displayValue = display(results[calc]);
if (displayName !== '') { if (displayName !== '') {
displayValue.title = replaceVariables(displayName, { displayValue.title = replaceVariables(displayName, scopedVars);
...field.state?.scopedVars, // series and field scoped vars
...scopedVars,
});
} else { } else {
displayValue.title = getFieldDisplayName(field, dataFrame, data); displayValue.title = getFieldDisplayName(field, dataFrame, data);
} }
@ -247,18 +232,22 @@ function getSmartDisplayNameForRow(
field: Field, field: Field,
rowIndex: number, rowIndex: number,
replaceVariables: InterpolateFunction, replaceVariables: InterpolateFunction,
scopedVars: ScopedVars scopedVars: ScopedVars | undefined
): string { ): string {
const displayName = field.config.displayName;
if (displayName) {
// Handle old __cell_n syntax
if (displayName.indexOf(VAR_CELL_PREFIX)) {
return replaceVariables(fixCellTemplateExpressions(displayName), scopedVars);
}
return replaceVariables(displayName, scopedVars);
}
let parts: string[] = []; let parts: string[] = [];
let otherNumericFields = 0; let otherNumericFields = 0;
if (field.config.displayName) {
return replaceVariables(field.config.displayName, {
...field.state?.scopedVars, // series and field scoped vars
...scopedVars,
});
}
for (const otherField of frame.fields) { for (const otherField of frame.fields) {
if (otherField === field) { if (otherField === field) {
continue; continue;
@ -382,3 +371,28 @@ function getDisplayText(display: DisplayValue, fallback: string): string {
} }
return display.text; return display.text;
} }
export function fixCellTemplateExpressions(str: string) {
return str.replace(/\${__cell_(\d+)}|\[\[__cell_(\d+)\]\]|\$__cell_(\d+)/g, (match, fmt1, fmt2, fmt3) => {
return `\${__data.fields[${fmt1 ?? fmt2 ?? fmt3}]}`;
});
}
/**
* Clones the existing dataContext and adds rowIndex to it
*/
function getFieldScopedVarsWithDataContexAndRowIndex(field: Field, rowIndex: number): ScopedVars | undefined {
if (field.state?.scopedVars?.__dataContext) {
return {
...field.state?.scopedVars,
__dataContext: {
value: {
...field.state?.scopedVars?.__dataContext.value,
rowIndex,
},
},
};
}
return field.state?.scopedVars;
}

View File

@ -199,35 +199,9 @@ describe('applyFieldOverrides', () => {
fieldConfigRegistry: new FieldConfigOptionsRegistry(), fieldConfigRegistry: new FieldConfigOptionsRegistry(),
}); });
expect(withOverrides[0].fields[0].state!.scopedVars).toMatchInlineSnapshot(` expect(withOverrides[0].fields[0].state!.scopedVars?.__dataContext?.value.frame).toBe(withOverrides[0]);
{ expect(withOverrides[0].fields[0].state!.scopedVars?.__dataContext?.value.frameIndex).toBe(0);
"__field": { expect(withOverrides[0].fields[0].state!.scopedVars?.__dataContext?.value.field).toBe(withOverrides[0].fields[0]);
"text": "Field",
"value": {},
},
"__series": {
"text": "Series",
"value": {
"name": "A",
},
},
}
`);
expect(withOverrides[1].fields[0].state!.scopedVars).toMatchInlineSnapshot(`
{
"__field": {
"text": "Field",
"value": {},
},
"__series": {
"text": "Series",
"value": {
"name": "B",
},
},
}
`);
}); });
}); });

View File

@ -39,10 +39,7 @@ import { mapInternalLinkToExplore } from '../utils/dataLinks';
import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry'; import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
import { getDisplayProcessor, getRawDisplayProcessor } from './displayProcessor'; import { getDisplayProcessor, getRawDisplayProcessor } from './displayProcessor';
import { getFrameDisplayName } from './fieldState';
import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry'; import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
import { getTemplateProxyForField } from './templateProxies';
interface OverrideProps { interface OverrideProps {
match: FieldMatcher; match: FieldMatcher;
@ -122,18 +119,17 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
}; };
}); });
const scopedVars: ScopedVars = {
__series: { text: 'Series', value: { name: getFrameDisplayName(newFrame, index) } }, // might be missing
};
for (const field of newFrame.fields) { for (const field of newFrame.fields) {
const config = field.config; const config = field.config;
field.state!.scopedVars = { field.state!.scopedVars = {
...scopedVars, __dataContext: {
__field: { value: {
text: 'Field', data: options.data!,
value: getTemplateProxyForField(field, newFrame, options.data), frame: newFrame,
frameIndex: index,
field: field,
},
}, },
}; };
@ -371,49 +367,29 @@ export const getLinksSupplier =
} }
return field.config.links.map((link: DataLink) => { return field.config.links.map((link: DataLink) => {
let dataFrameVars = {}; const dataContext: DataContextScopedVar = getFieldDataContextClone(frame, field, fieldScopedVars);
let dataContext: DataContextScopedVar = { value: { frame, field } }; const dataLinkScopedVars = {
...fieldScopedVars,
__dataContext: dataContext,
};
// We are not displaying reduction result // We are not displaying reduction result
if (config.valueRowIndex !== undefined && !isNaN(config.valueRowIndex)) { if (config.valueRowIndex !== undefined && !isNaN(config.valueRowIndex)) {
dataContext.value.rowIndex = config.valueRowIndex; dataContext.value.rowIndex = config.valueRowIndex;
const fieldsProxy = getFieldDisplayValuesProxy({
frame,
rowIndex: config.valueRowIndex,
timeZone: timeZone,
});
dataFrameVars = {
__data: {
value: {
name: frame.name,
refId: frame.refId,
fields: fieldsProxy,
},
text: 'Data',
},
};
} else { } else {
dataContext.value.calculatedValue = config.calculatedValue; dataContext.value.calculatedValue = config.calculatedValue;
} }
const variables: ScopedVars = {
...fieldScopedVars,
...dataFrameVars,
__dataContext: dataContext,
};
if (link.onClick) { if (link.onClick) {
return { return {
href: link.url, href: link.url,
title: replaceVariables(link.title || '', variables), title: replaceVariables(link.title || '', dataLinkScopedVars),
target: link.targetBlank ? '_blank' : undefined, target: link.targetBlank ? '_blank' : undefined,
onClick: (evt, origin) => { onClick: (evt, origin) => {
link.onClick!({ link.onClick!({
origin: origin ?? field, origin: origin ?? field,
e: evt, e: evt,
replaceVariables: (v) => replaceVariables(v, variables), replaceVariables: (v) => replaceVariables(v, dataLinkScopedVars),
}); });
}, },
origin: field, origin: field,
@ -425,7 +401,7 @@ export const getLinksSupplier =
return mapInternalLinkToExplore({ return mapInternalLinkToExplore({
link, link,
internalLink: link.internal, internalLink: link.internal,
scopedVars: variables, scopedVars: dataLinkScopedVars,
field, field,
range: link.internal.range ?? ({} as any), range: link.internal.range ?? ({} as any),
replaceVariables, replaceVariables,
@ -440,13 +416,13 @@ export const getLinksSupplier =
if (href) { if (href) {
href = locationUtil.assureBaseUrl(href.replace(/\n/g, '')); href = locationUtil.assureBaseUrl(href.replace(/\n/g, ''));
href = replaceVariables(href, variables, VariableFormatID.PercentEncode); href = replaceVariables(href, dataLinkScopedVars, VariableFormatID.PercentEncode);
href = locationUtil.processUrl(href); href = locationUtil.processUrl(href);
} }
const info: LinkModel<Field> = { const info: LinkModel<Field> = {
href, href,
title: replaceVariables(link.title || '', variables), title: replaceVariables(link.title || '', dataLinkScopedVars),
target: link.targetBlank ? '_blank' : undefined, target: link.targetBlank ? '_blank' : undefined,
origin: field, origin: field,
}; };
@ -530,3 +506,18 @@ export function useFieldOverrides(
}; };
}, [fieldConfigRegistry, fieldConfig, data, prevSeries, timeZone, theme, replace]); }, [fieldConfigRegistry, fieldConfig, data, prevSeries, timeZone, theme, replace]);
} }
/**
* Clones the existing dataContext or creates a new one
*/
function getFieldDataContextClone(frame: DataFrame, field: Field, fieldScopedVars: ScopedVars) {
if (fieldScopedVars?.__dataContext) {
return {
value: {
...fieldScopedVars.__dataContext.value,
},
};
}
return { value: { frame, field, data: [frame] } };
}

View File

@ -16,9 +16,11 @@ export interface ScopedVars {
*/ */
export interface DataContextScopedVar { export interface DataContextScopedVar {
value: { value: {
data: DataFrame[];
frame: DataFrame; frame: DataFrame;
field: Field; field: Field;
rowIndex?: number; rowIndex?: number;
frameIndex?: number;
calculatedValue?: DisplayValue; calculatedValue?: DisplayValue;
}; };
} }

View File

@ -191,7 +191,7 @@ async function getJSONObject(show: ShowContent, panel?: PanelModel, data?: Panel
function getPrettyJSON(obj: any): string { function getPrettyJSON(obj: any): string {
let r = ''; let r = '';
try { try {
r = JSON.stringify(obj, null, 2); r = JSON.stringify(obj, getCircularReplacer(), 2);
} catch (e) { } catch (e) {
if ( if (
e instanceof Error && e instanceof Error &&
@ -204,3 +204,21 @@ function getPrettyJSON(obj: any): string {
} }
return r; return r;
} }
function getCircularReplacer() {
const seen = new WeakSet();
return (key: string, value: unknown) => {
if (key === '__dataContext') {
return 'Filtered out in JSON serialization';
}
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
}

View File

@ -3,7 +3,7 @@ import { initTemplateSrv } from 'test/helpers/initTemplateSrv';
import { DataContextScopedVar, FieldType, toDataFrame } from '@grafana/data'; import { DataContextScopedVar, FieldType, toDataFrame } from '@grafana/data';
import { TemplateSrv } from '@grafana/runtime'; import { TemplateSrv } from '@grafana/runtime';
describe('templateSrv', () => { describe('dataMacros', () => {
let _templateSrv: TemplateSrv; let _templateSrv: TemplateSrv;
beforeEach(() => { beforeEach(() => {
@ -11,12 +11,14 @@ describe('templateSrv', () => {
}); });
const data = toDataFrame({ const data = toDataFrame({
name: 'A', name: 'frameName',
refId: 'refIdA',
fields: [ fields: [
{ {
name: 'number', name: 'CoolNumber',
type: FieldType.number, type: FieldType.number,
values: [5, 10], values: [5, 10],
labels: { cluster: 'US', region: 'west=1' },
display: (value: number) => { display: (value: number) => {
return { text: value.toString(), numeric: value, suffix: '%' }; return { text: value.toString(), numeric: value, suffix: '%' };
}, },
@ -32,6 +34,7 @@ describe('templateSrv', () => {
it('Should interpolate __value.* expressions with dataContext in scopedVars', () => { it('Should interpolate __value.* expressions with dataContext in scopedVars', () => {
const dataContext: DataContextScopedVar = { const dataContext: DataContextScopedVar = {
value: { value: {
data: [data],
frame: data, frame: data,
field: data.fields[0], field: data.fields[0],
rowIndex: 1, rowIndex: 1,
@ -52,6 +55,7 @@ describe('templateSrv', () => {
it('Should interpolate __value.* with calculatedValue', () => { it('Should interpolate __value.* with calculatedValue', () => {
const dataContext: DataContextScopedVar = { const dataContext: DataContextScopedVar = {
value: { value: {
data: [data],
frame: data, frame: data,
field: data.fields[0], field: data.fields[0],
calculatedValue: { calculatedValue: {
@ -74,6 +78,7 @@ describe('templateSrv', () => {
it('Should return match when ${__value.*} is used and no dataContext or rowIndex is found', () => { it('Should return match when ${__value.*} is used and no dataContext or rowIndex is found', () => {
const dataContext: DataContextScopedVar = { const dataContext: DataContextScopedVar = {
value: { value: {
data: [data],
frame: data, frame: data,
field: data.fields[0], field: data.fields[0],
}, },
@ -83,4 +88,71 @@ describe('templateSrv', () => {
expect(_templateSrv.replace('${__value.raw}', scopedVars)).toBe('${__value.raw}'); expect(_templateSrv.replace('${__value.raw}', scopedVars)).toBe('${__value.raw}');
}); });
it('Should interpolate __data.* correctly', () => {
const dataContext: DataContextScopedVar = {
value: {
data: [data],
frame: data,
field: data.fields[0],
rowIndex: 1,
},
};
const scopedVars = { __dataContext: dataContext };
expect(_templateSrv.replace('${__data.fields[1]}', scopedVars)).toBe('10000');
expect(_templateSrv.replace('${__data.fields[0]}', scopedVars)).toBe('10%');
expect(_templateSrv.replace('${__data.fields[0].text}', scopedVars)).toBe('10');
expect(_templateSrv.replace('${__data.fields["CoolNumber"].text}', scopedVars)).toBe('10');
expect(_templateSrv.replace('${__data.name}', scopedVars)).toBe('frameName');
expect(_templateSrv.replace('${__data.refId}', scopedVars)).toBe('refIdA');
expect(_templateSrv.replace('${__data.fields[0]:percentencode}', scopedVars)).toBe('10%25');
});
it('${__data.*} should return match when the rowIndex is missing dataContext is not there', () => {
const dataContext: DataContextScopedVar = {
value: {
data: [data],
frame: data,
field: data.fields[0],
},
};
const scopedVars = { __dataContext: dataContext };
expect(_templateSrv.replace('${__data.name}', scopedVars)).toBe('${__data.name}');
});
it('Should interpolate ${__series} to frame display name', () => {
const dataContext: DataContextScopedVar = {
value: {
data: [data],
frame: data,
field: data.fields[0],
frameIndex: 0,
},
};
const scopedVars = { __dataContext: dataContext };
expect(_templateSrv.replace('${__series.name}', scopedVars)).toBe('frameName');
});
it('Should interpolate ${__field.*} correctly', () => {
const dataContext: DataContextScopedVar = {
value: {
data: [data],
frame: data,
field: data.fields[0],
frameIndex: 0,
},
};
const scopedVars = { __dataContext: dataContext };
expect(_templateSrv.replace('${__field.name}', scopedVars)).toBe('CoolNumber');
expect(_templateSrv.replace('${__field.labels.cluster}', scopedVars)).toBe('US');
expect(_templateSrv.replace('${__field.labels.region:percentencode}', scopedVars)).toBe('west%3D1');
});
}); });

View File

@ -1,7 +1,17 @@
import { DisplayProcessor, FieldType, formattedValueToString, getDisplayProcessor, ScopedVars } from '@grafana/data'; import {
DisplayProcessor,
FieldType,
formattedValueToString,
getDisplayProcessor,
getFieldDisplayValuesProxy,
getFrameDisplayName,
ScopedVars,
} from '@grafana/data';
import { VariableCustomFormatterFn } from '@grafana/scenes'; import { VariableCustomFormatterFn } from '@grafana/scenes';
import { getFieldAccessor } from './fieldAccessorCache';
import { formatVariableValue } from './formatVariableValue'; import { formatVariableValue } from './formatVariableValue';
import { getTemplateProxyForField } from './templateProxies';
/** /**
* ${__value.raw/nummeric/text/time} macro * ${__value.raw/nummeric/text/time} macro
@ -65,6 +75,41 @@ function getValueForValueMacro(match: string, fieldPath?: string, scopedVars?: S
} }
} }
/**
* Macro support doing things like.
* ${__data.name}
* ${__data.fields[0].name}
* ${__data.fields["Value"].labels.cluster}
*
* Requires rowIndex on dataContext
*/
export function dataMacro(
match: string,
fieldPath?: string,
scopedVars?: ScopedVars,
format?: string | VariableCustomFormatterFn
) {
const dataContext = scopedVars?.__dataContext;
if (!dataContext || !fieldPath) {
return match;
}
const { frame, rowIndex } = dataContext.value;
if (rowIndex === undefined || fieldPath === undefined) {
return match;
}
const obj = {
name: frame.name,
refId: frame.refId,
fields: getFieldDisplayValuesProxy({ frame, rowIndex }),
};
const value = getFieldAccessor(fieldPath)(obj) ?? '';
return formatVariableValue(value, format);
}
let fallbackDisplayProcessor: DisplayProcessor | undefined; let fallbackDisplayProcessor: DisplayProcessor | undefined;
function getFallbackDisplayProcessor() { function getFallbackDisplayProcessor() {
@ -74,3 +119,53 @@ function getFallbackDisplayProcessor() {
return fallbackDisplayProcessor; return fallbackDisplayProcessor;
} }
/**
* ${__series} = frame display name
*/
export function seriesNameMacro(
match: string,
fieldPath?: string,
scopedVars?: ScopedVars,
format?: string | VariableCustomFormatterFn
) {
const dataContext = scopedVars?.__dataContext;
if (!dataContext) {
return match;
}
if (fieldPath !== 'name') {
return match;
}
const { frame, frameIndex } = dataContext.value;
const value = getFrameDisplayName(frame, frameIndex);
return formatVariableValue(value, format);
}
/**
* Handles expressions like
* ${__field.name}
* ${__field.labels.cluster}
*/
export function fieldMacro(
match: string,
fieldPath?: string,
scopedVars?: ScopedVars,
format?: string | VariableCustomFormatterFn
) {
const dataContext = scopedVars?.__dataContext;
if (!dataContext) {
return match;
}
if (fieldPath === undefined || fieldPath === '') {
return match;
}
const { frame, field, data } = dataContext.value;
const obj = getTemplateProxyForField(field, frame, data);
const value = getFieldAccessor(fieldPath)(obj) ?? '';
return formatVariableValue(value, format);
}

View File

@ -0,0 +1,16 @@
import { property } from 'lodash';
interface FieldAccessorCache {
[key: string]: (obj: object) => any;
}
let fieldAccessorCache: FieldAccessorCache = {};
export function getFieldAccessor(fieldPath: string) {
const accessor = fieldAccessorCache[fieldPath];
if (accessor) {
return accessor;
}
return (fieldAccessorCache[fieldPath] = property(fieldPath));
}

View File

@ -3,11 +3,14 @@ import { DataLinkBuiltInVars, ScopedVars, urlUtil } from '@grafana/data';
import { getTimeSrv } from '../dashboard/services/TimeSrv'; import { getTimeSrv } from '../dashboard/services/TimeSrv';
import { getVariablesUrlParams } from '../variables/getAllVariableValuesForUrl'; import { getVariablesUrlParams } from '../variables/getAllVariableValuesForUrl';
import { valueMacro } from './dataMacros'; import { dataMacro, fieldMacro, seriesNameMacro, valueMacro } from './dataMacros';
import { MacroHandler } from './types'; import { MacroHandler } from './types';
export const macroRegistry: Record<string, MacroHandler> = { export const macroRegistry: Record<string, MacroHandler> = {
['__value']: valueMacro, ['__value']: valueMacro,
['__data']: dataMacro,
['__series']: seriesNameMacro,
['__field']: fieldMacro,
[DataLinkBuiltInVars.includeVars]: includeVarsMacro, [DataLinkBuiltInVars.includeVars]: includeVarsMacro,
[DataLinkBuiltInVars.keepTime]: urlTimeRangeMacro, [DataLinkBuiltInVars.keepTime]: urlTimeRangeMacro,
}; };

View File

@ -1,4 +1,4 @@
import { toDataFrame } from '../dataframe'; import { toDataFrame } from '@grafana/data';
import { getTemplateProxyForField } from './templateProxies'; import { getTemplateProxyForField } from './templateProxies';

View File

@ -1,7 +1,4 @@
import { DataFrame, Field } from '../types'; import { Field, DataFrame, getFieldDisplayName, formatLabels } from '@grafana/data';
import { formatLabels } from '../utils/labels';
import { getFieldDisplayName } from './fieldState';
/** /**
* This object is created often, and only used when tmplates exist. Using a proxy lets us delay * This object is created often, and only used when tmplates exist. Using a proxy lets us delay

View File

@ -1,4 +1,4 @@
import { escape, isString, property } from 'lodash'; import { escape, isString } from 'lodash';
import { import {
deprecationWarning, deprecationWarning,
@ -24,13 +24,10 @@ import { isAdHoc } from '../variables/guard';
import { getFilteredVariables, getVariables, getVariableWithName } from '../variables/state/selectors'; import { getFilteredVariables, getVariables, getVariableWithName } from '../variables/state/selectors';
import { variableRegex } from '../variables/utils'; import { variableRegex } from '../variables/utils';
import { getFieldAccessor } from './fieldAccessorCache';
import { formatVariableValue } from './formatVariableValue'; import { formatVariableValue } from './formatVariableValue';
import { macroRegistry } from './macroRegistry'; import { macroRegistry } from './macroRegistry';
interface FieldAccessorCache {
[key: string]: (obj: any) => any;
}
/** /**
* Internal regex replace function * Internal regex replace function
*/ */
@ -54,7 +51,6 @@ export class TemplateSrv implements BaseTemplateSrv {
private index: any = {}; private index: any = {};
private grafanaVariables = new Map<string, any>(); private grafanaVariables = new Map<string, any>();
private timeRange?: TimeRange | null = null; private timeRange?: TimeRange | null = null;
private fieldAccessorCache: FieldAccessorCache = {};
constructor(private dependencies: TemplateSrvDependencies = runtimeDependencies) { constructor(private dependencies: TemplateSrvDependencies = runtimeDependencies) {
this._variables = []; this._variables = [];
@ -206,18 +202,9 @@ export class TemplateSrv implements BaseTemplateSrv {
return values; return values;
} }
private getFieldAccessor(fieldPath: string) {
const accessor = this.fieldAccessorCache[fieldPath];
if (accessor) {
return accessor;
}
return (this.fieldAccessorCache[fieldPath] = property(fieldPath));
}
private getVariableValue(scopedVar: ScopedVar, fieldPath: string | undefined) { private getVariableValue(scopedVar: ScopedVar, fieldPath: string | undefined) {
if (fieldPath) { if (fieldPath) {
return this.getFieldAccessor(fieldPath)(scopedVar.value); return getFieldAccessor(fieldPath)(scopedVar.value);
} }
return scopedVar.value; return scopedVar.value;
@ -284,8 +271,9 @@ export class TemplateSrv implements BaseTemplateSrv {
} }
if (!variable) { if (!variable) {
if (macroRegistry[variableName]) { const macro = macroRegistry[variableName];
return macroRegistry[variableName](match, fieldPath, scopedVars, format); if (macro) {
return macro(match, fieldPath, scopedVars, format);
} }
return match; return match;