mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -6,7 +6,7 @@ import { ReducerID } from '../transformations/fieldReducer';
|
||||
import { FieldConfigPropertyItem, MappingType, SpecialValueMatch, ValueMapping } from '../types';
|
||||
|
||||
import { getDisplayProcessor } from './displayProcessor';
|
||||
import { getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
|
||||
import { fixCellTemplateExpressions, getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
|
||||
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
|
||||
|
||||
describe('FieldDisplay', () => {
|
||||
@@ -534,3 +534,11 @@ function createDisplayOptions(extend: Partial<GetFieldDisplayValuesOptions> = {}
|
||||
|
||||
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]}'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { toString, isEmpty } from 'lodash';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { DataFrameView } from '../dataframe/DataFrameView';
|
||||
import { getTimeField } from '../dataframe/processDataFrame';
|
||||
@@ -96,7 +96,6 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||
|
||||
const data = options.data ?? [];
|
||||
const limit = reduceOptions.limit ? reduceOptions.limit : DEFAULT_FIELD_DISPLAY_VALUES_LIMIT;
|
||||
const scopedVars: ScopedVars = {};
|
||||
|
||||
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 =
|
||||
field.display ??
|
||||
@@ -137,23 +136,10 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||
|
||||
// Show all rows
|
||||
if (reduceOptions.values) {
|
||||
const usesCellValues = displayName.indexOf(VAR_CELL_PREFIX) >= 0;
|
||||
|
||||
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);
|
||||
|
||||
const scopedVars = getFieldScopedVarsWithDataContexAndRowIndex(field, j);
|
||||
const displayValue = display(field.values.get(j));
|
||||
const rowName = getSmartDisplayNameForRow(dataFrame, field, j, replaceVariables, scopedVars);
|
||||
const overrideColor = lookupRowColorFromOverride(rowName, options.fieldConfig, theme);
|
||||
@@ -190,14 +176,13 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
|
||||
});
|
||||
|
||||
for (const calc of calcs) {
|
||||
const scopedVars = field.state?.scopedVars ?? {};
|
||||
scopedVars[VAR_CALC] = { value: calc, text: calc };
|
||||
|
||||
const displayValue = display(results[calc]);
|
||||
|
||||
if (displayName !== '') {
|
||||
displayValue.title = replaceVariables(displayName, {
|
||||
...field.state?.scopedVars, // series and field scoped vars
|
||||
...scopedVars,
|
||||
});
|
||||
displayValue.title = replaceVariables(displayName, scopedVars);
|
||||
} else {
|
||||
displayValue.title = getFieldDisplayName(field, dataFrame, data);
|
||||
}
|
||||
@@ -247,18 +232,22 @@ function getSmartDisplayNameForRow(
|
||||
field: Field,
|
||||
rowIndex: number,
|
||||
replaceVariables: InterpolateFunction,
|
||||
scopedVars: ScopedVars
|
||||
scopedVars: ScopedVars | undefined
|
||||
): 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 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) {
|
||||
if (otherField === field) {
|
||||
continue;
|
||||
@@ -382,3 +371,28 @@ function getDisplayText(display: DisplayValue, fallback: string): string {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -199,35 +199,9 @@ describe('applyFieldOverrides', () => {
|
||||
fieldConfigRegistry: new FieldConfigOptionsRegistry(),
|
||||
});
|
||||
|
||||
expect(withOverrides[0].fields[0].state!.scopedVars).toMatchInlineSnapshot(`
|
||||
{
|
||||
"__field": {
|
||||
"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",
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
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);
|
||||
expect(withOverrides[0].fields[0].state!.scopedVars?.__dataContext?.value.field).toBe(withOverrides[0].fields[0]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -39,10 +39,7 @@ import { mapInternalLinkToExplore } from '../utils/dataLinks';
|
||||
|
||||
import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
|
||||
import { getDisplayProcessor, getRawDisplayProcessor } from './displayProcessor';
|
||||
import { getFrameDisplayName } from './fieldState';
|
||||
import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
|
||||
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
|
||||
import { getTemplateProxyForField } from './templateProxies';
|
||||
|
||||
interface OverrideProps {
|
||||
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) {
|
||||
const config = field.config;
|
||||
|
||||
field.state!.scopedVars = {
|
||||
...scopedVars,
|
||||
__field: {
|
||||
text: 'Field',
|
||||
value: getTemplateProxyForField(field, newFrame, options.data),
|
||||
__dataContext: {
|
||||
value: {
|
||||
data: options.data!,
|
||||
frame: newFrame,
|
||||
frameIndex: index,
|
||||
field: field,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -371,49 +367,29 @@ export const getLinksSupplier =
|
||||
}
|
||||
|
||||
return field.config.links.map((link: DataLink) => {
|
||||
let dataFrameVars = {};
|
||||
let dataContext: DataContextScopedVar = { value: { frame, field } };
|
||||
const dataContext: DataContextScopedVar = getFieldDataContextClone(frame, field, fieldScopedVars);
|
||||
const dataLinkScopedVars = {
|
||||
...fieldScopedVars,
|
||||
__dataContext: dataContext,
|
||||
};
|
||||
|
||||
// We are not displaying reduction result
|
||||
if (config.valueRowIndex !== undefined && !isNaN(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 {
|
||||
dataContext.value.calculatedValue = config.calculatedValue;
|
||||
}
|
||||
|
||||
const variables: ScopedVars = {
|
||||
...fieldScopedVars,
|
||||
...dataFrameVars,
|
||||
__dataContext: dataContext,
|
||||
};
|
||||
|
||||
if (link.onClick) {
|
||||
return {
|
||||
href: link.url,
|
||||
title: replaceVariables(link.title || '', variables),
|
||||
title: replaceVariables(link.title || '', dataLinkScopedVars),
|
||||
target: link.targetBlank ? '_blank' : undefined,
|
||||
onClick: (evt, origin) => {
|
||||
link.onClick!({
|
||||
origin: origin ?? field,
|
||||
e: evt,
|
||||
replaceVariables: (v) => replaceVariables(v, variables),
|
||||
replaceVariables: (v) => replaceVariables(v, dataLinkScopedVars),
|
||||
});
|
||||
},
|
||||
origin: field,
|
||||
@@ -425,7 +401,7 @@ export const getLinksSupplier =
|
||||
return mapInternalLinkToExplore({
|
||||
link,
|
||||
internalLink: link.internal,
|
||||
scopedVars: variables,
|
||||
scopedVars: dataLinkScopedVars,
|
||||
field,
|
||||
range: link.internal.range ?? ({} as any),
|
||||
replaceVariables,
|
||||
@@ -440,13 +416,13 @@ export const getLinksSupplier =
|
||||
|
||||
if (href) {
|
||||
href = locationUtil.assureBaseUrl(href.replace(/\n/g, ''));
|
||||
href = replaceVariables(href, variables, VariableFormatID.PercentEncode);
|
||||
href = replaceVariables(href, dataLinkScopedVars, VariableFormatID.PercentEncode);
|
||||
href = locationUtil.processUrl(href);
|
||||
}
|
||||
|
||||
const info: LinkModel<Field> = {
|
||||
href,
|
||||
title: replaceVariables(link.title || '', variables),
|
||||
title: replaceVariables(link.title || '', dataLinkScopedVars),
|
||||
target: link.targetBlank ? '_blank' : undefined,
|
||||
origin: field,
|
||||
};
|
||||
@@ -530,3 +506,18 @@ export function useFieldOverrides(
|
||||
};
|
||||
}, [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] } };
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { toDataFrame } from '../dataframe';
|
||||
|
||||
import { getTemplateProxyForField } from './templateProxies';
|
||||
|
||||
describe('Template proxies', () => {
|
||||
it('supports name and displayName', () => {
|
||||
const frames = [
|
||||
toDataFrame({
|
||||
fields: [
|
||||
{
|
||||
name: '🔥',
|
||||
config: { displayName: '✨' },
|
||||
labels: {
|
||||
b: 'BBB',
|
||||
a: 'AAA',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const f = getTemplateProxyForField(frames[0].fields[0], frames[0], frames);
|
||||
|
||||
expect(f.name).toEqual('🔥');
|
||||
expect(f.displayName).toEqual('✨');
|
||||
expect(`${f.labels}`).toEqual('a="AAA", b="BBB"');
|
||||
expect(f.labels.__values).toEqual('AAA, BBB');
|
||||
expect(f.labels.a).toEqual('AAA');
|
||||
|
||||
// Deprecated syntax
|
||||
expect(`${f.formattedLabels}`).toEqual('a="AAA", b="BBB"');
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
import { DataFrame, Field } from '../types';
|
||||
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
|
||||
* calculations of the more complex structures (label names) until they are actually used
|
||||
*/
|
||||
export function getTemplateProxyForField(field: Field, frame?: DataFrame, frames?: DataFrame[]): any {
|
||||
return new Proxy(
|
||||
{}, // This object shows up in test snapshots
|
||||
{
|
||||
get: (obj, key) => {
|
||||
if (key === 'name') {
|
||||
return field.name;
|
||||
}
|
||||
|
||||
if (key === 'displayName') {
|
||||
return getFieldDisplayName(field, frame, frames);
|
||||
}
|
||||
|
||||
if (key === 'labels' || key === 'formattedLabels') {
|
||||
// formattedLabels deprecated
|
||||
if (!field.labels) {
|
||||
return '';
|
||||
}
|
||||
return {
|
||||
...field.labels,
|
||||
__values: Object.values(field.labels).sort().join(', '),
|
||||
toString: () => {
|
||||
return formatLabels(field.labels!, '', true);
|
||||
},
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -16,9 +16,11 @@ export interface ScopedVars {
|
||||
*/
|
||||
export interface DataContextScopedVar {
|
||||
value: {
|
||||
data: DataFrame[];
|
||||
frame: DataFrame;
|
||||
field: Field;
|
||||
rowIndex?: number;
|
||||
frameIndex?: number;
|
||||
calculatedValue?: DisplayValue;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user