[Feature] Add 'Template expression' option to 'Add field from calculation'

This commit is contained in:
rmawatson
2025-07-24 01:24:41 +00:00
7 changed files with 119 additions and 8 deletions
+3 -6
View File
@@ -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/layersgeojsonDynamic.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/panel/geomap/layersrouteLayer.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"]
@@ -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. 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.
@@ -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<CalculateFieldTransf
fields: options.replaceFields ? [f] : [...frame.fields, f],
};
});
case CalculateFieldMode.TemplateExpression:
const expression = options.template?.expression ?? '';
const exprFieldNames = [
...new Set([...expression.matchAll(/\{([\w\d\._-]+)\}/g)].map((match) => 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
@@ -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.`;
},
@@ -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 && (
<IndexOptionsEditor options={options} onChange={props.onChange}></IndexOptionsEditor>
)}
{mode === CalculateFieldMode.TemplateExpression && (
<TemplateExpressionOptionsEditor
input={input}
options={options}
onChange={props.onChange}
></TemplateExpressionOptionsEditor>
)}
<InlineField
labelWidth={LABEL_WIDTH}
label={t('transformers.calculate-field-transformer-editor.label-alias', 'Alias')}
@@ -0,0 +1,59 @@
import { useCallback } from 'react';
import { TransformerUIProps, StringFieldConfigSettings, StandardEditorsRegistryItem } from '@grafana/data';
import { CalculateFieldMode, CalculateFieldTransformerOptions } from '@grafana/data/internal';
import { t } from '@grafana/i18n';
import { InlineField, InlineFieldRow } from '@grafana/ui';
import { StringValueEditor } from 'app/core/components/OptionsUI/string';
import { LABEL_WIDTH } from './constants';
export const TemplateExpressionOptionsEditor = ({
input,
options,
onChange,
}: TransformerUIProps<CalculateFieldTransformerOptions>) => {
const onTemplateExpressionChanged = useCallback(
(value?: string) => {
onChange({
...options,
mode: CalculateFieldMode.TemplateExpression,
template: {
expression: value ?? '',
},
});
},
[onChange, options]
);
const dummyStringSettings: StandardEditorsRegistryItem<string, StringFieldConfigSettings> = {
id: '',
name: '',
description: '',
editor: StringValueEditor,
settings: {},
};
return (
<>
<InlineFieldRow>
<InlineField
labelWidth={LABEL_WIDTH}
label={t('transformers.template-expression-options-editor.label-expression', 'Expression')}
tooltip={t(
'transformers.template-expression-options-editor.tooltip-transform-template-expression',
'Transform a template expression into a field value'
)}
>
<StringValueEditor
context={{ data: input }}
value={options.template?.expression ?? ''}
onChange={onTemplateExpressionChanged}
item={dummyStringSettings}
preserveWhitespace={true}
/>
</InlineField>
</InlineFieldRow>
</>
);
};
+6 -1
View File
@@ -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": {