From 507c6e7d97e8a9396fdbb26298269eababfc84e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 5 Apr 2023 11:10:33 +0200 Subject: [PATCH] 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 --- .betterer.results | 15 +-- .../src/field/fieldDisplay.test.ts | 10 +- .../grafana-data/src/field/fieldDisplay.ts | 72 ++++++++------ .../src/field/fieldOverrides.test.ts | 32 +----- .../grafana-data/src/field/fieldOverrides.ts | 73 ++++++-------- packages/grafana-data/src/types/ScopedVars.ts | 2 + .../app/features/inspector/InspectJSONTab.tsx | 20 +++- .../features/templating/dataMacros.test.ts | 78 ++++++++++++++- public/app/features/templating/dataMacros.ts | 97 ++++++++++++++++++- .../features/templating/fieldAccessorCache.ts | 16 +++ .../app/features/templating/macroRegistry.ts | 5 +- .../templating}/templateProxies.test.ts | 2 +- .../features/templating}/templateProxies.ts | 5 +- .../app/features/templating/template_srv.ts | 24 ++--- 14 files changed, 315 insertions(+), 136 deletions(-) create mode 100644 public/app/features/templating/fieldAccessorCache.ts rename {packages/grafana-data/src/field => public/app/features/templating}/templateProxies.test.ts (94%) rename {packages/grafana-data/src/field => public/app/features/templating}/templateProxies.ts (87%) diff --git a/.betterer.results b/.betterer.results index 91f06e90952..265b61e70ac 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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.", "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": [ [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": [ [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": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [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": [ [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": [ [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.", "10"], [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, "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"] + [0, 0, 0, "Do not use any type assertions.", "14"] ], "public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/packages/grafana-data/src/field/fieldDisplay.test.ts b/packages/grafana-data/src/field/fieldDisplay.test.ts index f7e293987c1..ebea46752bd 100644 --- a/packages/grafana-data/src/field/fieldDisplay.test.ts +++ b/packages/grafana-data/src/field/fieldDisplay.test.ts @@ -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 = {} 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]}' + ); + }); +}); diff --git a/packages/grafana-data/src/field/fieldDisplay.ts b/packages/grafana-data/src/field/fieldDisplay.ts index f21393f21d0..6bd10b415fb 100644 --- a/packages/grafana-data/src/field/fieldDisplay.ts +++ b/packages/grafana-data/src/field/fieldDisplay.ts @@ -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; +} diff --git a/packages/grafana-data/src/field/fieldOverrides.test.ts b/packages/grafana-data/src/field/fieldOverrides.test.ts index 59abb84e52f..1199f8118ff 100644 --- a/packages/grafana-data/src/field/fieldOverrides.test.ts +++ b/packages/grafana-data/src/field/fieldOverrides.test.ts @@ -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]); }); }); diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index ceceb40952d..08bc0c63d43 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -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 = { 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] } }; +} diff --git a/packages/grafana-data/src/types/ScopedVars.ts b/packages/grafana-data/src/types/ScopedVars.ts index b5e6b9e0ed7..725dd051a8d 100644 --- a/packages/grafana-data/src/types/ScopedVars.ts +++ b/packages/grafana-data/src/types/ScopedVars.ts @@ -16,9 +16,11 @@ export interface ScopedVars { */ export interface DataContextScopedVar { value: { + data: DataFrame[]; frame: DataFrame; field: Field; rowIndex?: number; + frameIndex?: number; calculatedValue?: DisplayValue; }; } diff --git a/public/app/features/inspector/InspectJSONTab.tsx b/public/app/features/inspector/InspectJSONTab.tsx index ff78fe81898..b59b46840d9 100644 --- a/public/app/features/inspector/InspectJSONTab.tsx +++ b/public/app/features/inspector/InspectJSONTab.tsx @@ -191,7 +191,7 @@ async function getJSONObject(show: ShowContent, panel?: PanelModel, data?: Panel function getPrettyJSON(obj: any): string { let r = ''; try { - r = JSON.stringify(obj, null, 2); + r = JSON.stringify(obj, getCircularReplacer(), 2); } catch (e) { if ( e instanceof Error && @@ -204,3 +204,21 @@ function getPrettyJSON(obj: any): string { } 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; + }; +} diff --git a/public/app/features/templating/dataMacros.test.ts b/public/app/features/templating/dataMacros.test.ts index 164a5c8b345..493643c2361 100644 --- a/public/app/features/templating/dataMacros.test.ts +++ b/public/app/features/templating/dataMacros.test.ts @@ -3,7 +3,7 @@ import { initTemplateSrv } from 'test/helpers/initTemplateSrv'; import { DataContextScopedVar, FieldType, toDataFrame } from '@grafana/data'; import { TemplateSrv } from '@grafana/runtime'; -describe('templateSrv', () => { +describe('dataMacros', () => { let _templateSrv: TemplateSrv; beforeEach(() => { @@ -11,12 +11,14 @@ describe('templateSrv', () => { }); const data = toDataFrame({ - name: 'A', + name: 'frameName', + refId: 'refIdA', fields: [ { - name: 'number', + name: 'CoolNumber', type: FieldType.number, values: [5, 10], + labels: { cluster: 'US', region: 'west=1' }, display: (value: number) => { return { text: value.toString(), numeric: value, suffix: '%' }; }, @@ -32,6 +34,7 @@ describe('templateSrv', () => { it('Should interpolate __value.* expressions with dataContext in scopedVars', () => { const dataContext: DataContextScopedVar = { value: { + data: [data], frame: data, field: data.fields[0], rowIndex: 1, @@ -52,6 +55,7 @@ describe('templateSrv', () => { it('Should interpolate __value.* with calculatedValue', () => { const dataContext: DataContextScopedVar = { value: { + data: [data], frame: data, field: data.fields[0], calculatedValue: { @@ -74,6 +78,7 @@ describe('templateSrv', () => { it('Should return match when ${__value.*} is used and no dataContext or rowIndex is found', () => { const dataContext: DataContextScopedVar = { value: { + data: [data], frame: data, field: data.fields[0], }, @@ -83,4 +88,71 @@ describe('templateSrv', () => { 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'); + }); }); diff --git a/public/app/features/templating/dataMacros.ts b/public/app/features/templating/dataMacros.ts index 354c1b58f42..5eac406a39f 100644 --- a/public/app/features/templating/dataMacros.ts +++ b/public/app/features/templating/dataMacros.ts @@ -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 { getFieldAccessor } from './fieldAccessorCache'; import { formatVariableValue } from './formatVariableValue'; +import { getTemplateProxyForField } from './templateProxies'; /** * ${__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; function getFallbackDisplayProcessor() { @@ -74,3 +119,53 @@ function getFallbackDisplayProcessor() { 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); +} diff --git a/public/app/features/templating/fieldAccessorCache.ts b/public/app/features/templating/fieldAccessorCache.ts new file mode 100644 index 00000000000..a14b281e6be --- /dev/null +++ b/public/app/features/templating/fieldAccessorCache.ts @@ -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)); +} diff --git a/public/app/features/templating/macroRegistry.ts b/public/app/features/templating/macroRegistry.ts index 8431ef6c154..65ede4f22b6 100644 --- a/public/app/features/templating/macroRegistry.ts +++ b/public/app/features/templating/macroRegistry.ts @@ -3,11 +3,14 @@ import { DataLinkBuiltInVars, ScopedVars, urlUtil } from '@grafana/data'; import { getTimeSrv } from '../dashboard/services/TimeSrv'; import { getVariablesUrlParams } from '../variables/getAllVariableValuesForUrl'; -import { valueMacro } from './dataMacros'; +import { dataMacro, fieldMacro, seriesNameMacro, valueMacro } from './dataMacros'; import { MacroHandler } from './types'; export const macroRegistry: Record = { ['__value']: valueMacro, + ['__data']: dataMacro, + ['__series']: seriesNameMacro, + ['__field']: fieldMacro, [DataLinkBuiltInVars.includeVars]: includeVarsMacro, [DataLinkBuiltInVars.keepTime]: urlTimeRangeMacro, }; diff --git a/packages/grafana-data/src/field/templateProxies.test.ts b/public/app/features/templating/templateProxies.test.ts similarity index 94% rename from packages/grafana-data/src/field/templateProxies.test.ts rename to public/app/features/templating/templateProxies.test.ts index 154bd77628d..95f48388fe6 100644 --- a/packages/grafana-data/src/field/templateProxies.test.ts +++ b/public/app/features/templating/templateProxies.test.ts @@ -1,4 +1,4 @@ -import { toDataFrame } from '../dataframe'; +import { toDataFrame } from '@grafana/data'; import { getTemplateProxyForField } from './templateProxies'; diff --git a/packages/grafana-data/src/field/templateProxies.ts b/public/app/features/templating/templateProxies.ts similarity index 87% rename from packages/grafana-data/src/field/templateProxies.ts rename to public/app/features/templating/templateProxies.ts index e547f1b689e..634ecfe5c23 100644 --- a/packages/grafana-data/src/field/templateProxies.ts +++ b/public/app/features/templating/templateProxies.ts @@ -1,7 +1,4 @@ -import { DataFrame, Field } from '../types'; -import { formatLabels } from '../utils/labels'; - -import { getFieldDisplayName } from './fieldState'; +import { Field, DataFrame, getFieldDisplayName, formatLabels } from '@grafana/data'; /** * This object is created often, and only used when tmplates exist. Using a proxy lets us delay diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 2b1445def8b..359a7c5af43 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -1,4 +1,4 @@ -import { escape, isString, property } from 'lodash'; +import { escape, isString } from 'lodash'; import { deprecationWarning, @@ -24,13 +24,10 @@ import { isAdHoc } from '../variables/guard'; import { getFilteredVariables, getVariables, getVariableWithName } from '../variables/state/selectors'; import { variableRegex } from '../variables/utils'; +import { getFieldAccessor } from './fieldAccessorCache'; import { formatVariableValue } from './formatVariableValue'; import { macroRegistry } from './macroRegistry'; -interface FieldAccessorCache { - [key: string]: (obj: any) => any; -} - /** * Internal regex replace function */ @@ -54,7 +51,6 @@ export class TemplateSrv implements BaseTemplateSrv { private index: any = {}; private grafanaVariables = new Map(); private timeRange?: TimeRange | null = null; - private fieldAccessorCache: FieldAccessorCache = {}; constructor(private dependencies: TemplateSrvDependencies = runtimeDependencies) { this._variables = []; @@ -206,18 +202,9 @@ export class TemplateSrv implements BaseTemplateSrv { 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) { if (fieldPath) { - return this.getFieldAccessor(fieldPath)(scopedVar.value); + return getFieldAccessor(fieldPath)(scopedVar.value); } return scopedVar.value; @@ -284,8 +271,9 @@ export class TemplateSrv implements BaseTemplateSrv { } if (!variable) { - if (macroRegistry[variableName]) { - return macroRegistry[variableName](match, fieldPath, scopedVars, format); + const macro = macroRegistry[variableName]; + if (macro) { + return macro(match, fieldPath, scopedVars, format); } return match;