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:
parent
caac9838d8
commit
507c6e7d97
@ -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"],
|
||||||
|
@ -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]}'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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] } };
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
16
public/app/features/templating/fieldAccessorCache.ts
Normal file
16
public/app/features/templating/fieldAccessorCache.ts
Normal 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));
|
||||||
|
}
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { toDataFrame } from '../dataframe';
|
import { toDataFrame } from '@grafana/data';
|
||||||
|
|
||||||
import { getTemplateProxyForField } from './templateProxies';
|
import { getTemplateProxyForField } from './templateProxies';
|
||||||
|
|
@ -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
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user