From e5db6687bf046984ca419fa665a5d178f6095772 Mon Sep 17 00:00:00 2001 From: rmawatson Date: Thu, 24 Jul 2025 01:16:24 +0000 Subject: [PATCH] Added Template Expression to 'Add field from calculation' --- .betterer.results | 9 +-- .../transform-data/index.md | 1 - .../transformers/calculateField.ts | 36 +++++++++++ .../transformers/formatString.ts | 24 ++++++-- .../app/features/transformers/docs/content.ts | 2 + .../CalculateFieldTransformerEditor.tsx | 13 ++++ .../TemplateExpressionOptionsEditor.tsx | 59 +++++++++++++++++++ public/locales/en-US/grafana.json | 7 ++- 8 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 public/app/features/transformers/editors/CalculateFieldTransformerEditor/TemplateExpressionOptionsEditor.tsx diff --git a/.betterer.results b/.betterer.results index f4cd2232eef..f87310233eb 100644 --- a/.betterer.results +++ b/.betterer.results @@ -167,6 +167,9 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"] ], + "packages/grafana-data/src/transformations/transformers/calculateField.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], "packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -3810,12 +3813,6 @@ exports[`better eslint`] = { "public/app/plugins/panel/geomap/layers/basemaps/esri.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "public/app/plugins/panel/geomap/layers/data/geojsonDynamic.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "public/app/plugins/panel/geomap/layers/data/routeLayer.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], "public/app/plugins/panel/geomap/layers/registry.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] diff --git a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md index 48beaa8e77f..033115c665c 100644 --- a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md +++ b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md @@ -578,7 +578,6 @@ Use this transformation to customize the output of a string field. This transfor - **Kebab case** - Formats all characters in the string in lowercase and uses dashes instead of spaces between words. - **Trim** - Removes all leading and trailing spaces from the string. - **Substring** - Returns a substring of the string, using the specified start and end positions. -- **Affix** - Add a prefix or suffix string to the string. This transformation provides a convenient way to standardize and tailor the presentation of string data for better visualization and analysis. diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index 1e9baf3f872..2e36ce7a193 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -23,6 +23,7 @@ export enum CalculateFieldMode { BinaryOperation = 'binary', UnaryOperation = 'unary', Index = 'index', + TemplateExpression = 'templateExpression', } export enum WindowSizeMode { @@ -72,6 +73,10 @@ interface IndexOptions { asPercentile: boolean; } +interface TemplateExpressionOptions { + expression: string; +} + const defaultReduceOptions: ReduceOptions = { reducer: ReducerID.sum, }; @@ -106,6 +111,7 @@ export interface CalculateFieldTransformerOptions { binary?: BinaryOptions; unary?: UnaryOptions; index?: IndexOptions; + template?: TemplateExpressionOptions; // Remove other fields replaceFields?: boolean; @@ -249,6 +255,36 @@ export const calculateFieldTransformer: DataTransformerInfo match[1])), + ]; + type FieldData = { fieldName: string; values: any[] }; + + creator = (frame: DataFrame) => { + if (!options.template) { + return undefined; + } + const exprFieldValues: FieldData[] = exprFieldNames.reduce((values: FieldData[], fieldName: string) => { + const field = frame.fields.find((f) => f.name === fieldName); + if (field) { + values.push({ fieldName, values: field.values }); + } + return values; + }, []); + + const outValues = []; + for (let i = 0; i < frame.length; i++) { + let rowValue = expression; + exprFieldValues.map(({ fieldName, values }) => { + rowValue = rowValue.replaceAll(`{${fieldName}}`, values[i]); + }); + outValues.push(rowValue); + } + return outValues; + }; + break; } // Nothing configured diff --git a/packages/grafana-data/src/transformations/transformers/formatString.ts b/packages/grafana-data/src/transformations/transformers/formatString.ts index 17011ff1eee..ddd302e9d5e 100644 --- a/packages/grafana-data/src/transformations/transformers/formatString.ts +++ b/packages/grafana-data/src/transformations/transformers/formatString.ts @@ -18,7 +18,7 @@ export enum FormatStringOutput { KebabCase = 'Kebab Case', Trim = 'Trim', Substring = 'Substring', - Affix = 'Affix' + Affix = 'Affix', } export interface FormatStringTransformerOptions { @@ -39,8 +39,8 @@ const splitToCapitalWords = (input: string) => { }; export const getFormatStringFunction = (options: FormatStringTransformerOptions) => { - return (field: Field) => - field.values.map((value: string) => { + return (field: Field, allFields: Field[]) => + field.values.map((value: string, index: number) => { switch (options.outputFormat) { case FormatStringOutput.UpperCase: return value.toUpperCase(); @@ -64,7 +64,19 @@ export const getFormatStringFunction = (options: FormatStringTransformerOptions) case FormatStringOutput.Substring: return value.substring(options.substringStart, options.substringEnd); case FormatStringOutput.Affix: - return (options.stringPrefix ?? "") + value + (options.stringSuffix ?? ""); + let prefix = { value: options.stringPrefix ?? '' }; + let suffix = { value: options.stringSuffix ?? '' }; + [prefix, suffix].map((affix) => { + const matches = [...affix.value.matchAll(/\{([\w\d\._-]+)\}/g)]; + matches.forEach((match) => { + const fieldName = match[1]; + const matchingField = allFields.find((field) => field.name === fieldName); + if (matchingField) { + affix.value = affix.value.replace(`{${fieldName}}`, matchingField.values[index]); + } + }); + }); + return prefix.value + value + suffix.value; } }); }; @@ -111,12 +123,12 @@ export const formatStringTransformer: DataTransformerInfo string[]) => + (fieldMatches: FieldMatcher, formatStringFunction: (field: Field, allFields: Field[]) => string[]) => (frame: DataFrame, allFrames: DataFrame[]) => { return frame.fields.map((field) => { // Find the configured field if (fieldMatches(field, frame, allFrames)) { - const newVals = formatStringFunction(field); + const newVals = formatStringFunction(field, frame.fields); return { ...field, diff --git a/public/app/features/transformers/docs/content.ts b/public/app/features/transformers/docs/content.ts index a441149469d..afc0688179c 100644 --- a/public/app/features/transformers/docs/content.ts +++ b/public/app/features/transformers/docs/content.ts @@ -61,6 +61,7 @@ Use this transformation to add a new field calculated from two other fields. Eac - **Stddev** - Calculates the moving standard deviation. - **Variance** - Calculates the moving variance. - **Row index** - Insert a field with the row index. + - **Template expression** - Insert a field with the value generated from a template expression. The expression can use the values of other fields in the calculation by using the {field} syntax. - **Field name** - Select the names of fields you want to use in the calculation for the new field. - **Calculation** - If you select **Reduce row** mode, then the **Calculation** field appears. Click in the field to see a list of calculation choices you can use to create the new field. For information about available calculations, refer to [Calculation types][]. - **Operation** - If you select **Binary operation** or **Unary operation** mode, then the **Operation** fields appear. These fields allow you to apply basic math operations on values in a single row from selected fields. You can also use numerical values for binary operations. @@ -515,6 +516,7 @@ Use this transformation to customize the output of a string field. This transfor - **Kebab case** - Formats all characters in the string in lowercase and uses dashes instead of spaces between words. - **Trim** - Removes all leading and trailing spaces from the string. - **Substring** - Returns a substring of the string, using the specified start and end positions. +- **Affix** - Add a prefix or suffix string to the string. Allows pasting the value of other fields when provided {field} as the affix expression. This transformation provides a convenient way to standardize and tailor the presentation of string data for better visualization and analysis.`; }, diff --git a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx b/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx index 64d1fffba80..411ba8a0d31 100644 --- a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx +++ b/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx @@ -34,6 +34,7 @@ import { BinaryOperationOptionsEditor } from './BinaryOperationOptionsEditor'; import { CumulativeOptionsEditor } from './CumulativeOptionsEditor'; import { IndexOptionsEditor } from './IndexOptionsEditor'; import { ReduceRowOptionsEditor } from './ReduceRowOptionsEditor'; +import { TemplateExpressionOptionsEditor } from './TemplateExpressionOptionsEditor'; import { UnaryOperationEditor } from './UnaryOperationEditor'; import { WindowOptionsEditor } from './WindowOptionsEditor'; import { LABEL_WIDTH } from './constants'; @@ -89,6 +90,11 @@ export const CalculateFieldTransformerEditor = (props: CalculateFieldTransformer ); } + calculationModes.push({ + value: CalculateFieldMode.TemplateExpression, + label: t('transformers.calculate-field-transformer-editor.label.template-expression', 'Template expression'), + }); + useEffect(() => { const ctx = { interpolate: (v: string) => v }; const subscription = of(input) @@ -244,6 +250,13 @@ export const CalculateFieldTransformerEditor = (props: CalculateFieldTransformer {mode === CalculateFieldMode.Index && ( )} + {mode === CalculateFieldMode.TemplateExpression && ( + + )} ) => { + const onTemplateExpressionChanged = useCallback( + (value?: string) => { + onChange({ + ...options, + mode: CalculateFieldMode.TemplateExpression, + template: { + expression: value ?? '', + }, + }); + }, + [onChange, options] + ); + + const dummyStringSettings: StandardEditorsRegistryItem = { + id: '', + name: '', + description: '', + editor: StringValueEditor, + settings: {}, + }; + + return ( + <> + + + + + + + ); +}; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index e2032905f35..4f6ad991fe0 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -12834,13 +12834,18 @@ "placeholder-field-or-number": "Field or number", "placeholder-fields-or-number": "Field(s) or number" }, + "template-expression-options-editor": { + "label-expression:": "Expression", + "tooltip-transform-template-expression": "Transform a template expression into a field value" + }, "calculate-field-transformer-editor": { "calculation-modes": { "label": { "binary-operation": "Binary operation", "reduce-row": "Reduce row", "row-index": "Row index", - "unary-operation": "Unary operation" + "unary-operation": "Unary operation", + "template-expression": "Template expression" } }, "label": {