From f7d39204cd4791f39ba74232b803e8ee5df903bc Mon Sep 17 00:00:00 2001 From: Ihor Yeromin Date: Wed, 20 Aug 2025 19:13:36 +0200 Subject: [PATCH] Transformation: Add missing binary operator (#109695) * fix(transformation): add binary operator --- .../transformers/calculateField.test.ts | 139 ++++++++++++++++++ .../transformers/calculateField.ts | 12 +- 2 files changed, 147 insertions(+), 4 deletions(-) diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts index 62e4dd174bf..5f7cf6c8db1 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts @@ -31,6 +31,7 @@ import { ReduceOptions, WindowSizeMode, WindowAlignment, + getNameFromOptions, } from './calculateField'; import { DataTransformerID } from './ids'; @@ -1459,6 +1460,144 @@ describe('calculateField transformer w/ timeseries', () => { }); }); +describe('getNameFromOptions', () => { + it('returns alias when provided', () => { + const options = { + mode: CalculateFieldMode.ReduceRow, + alias: 'My Custom Name', + reduce: { reducer: ReducerID.sum }, + }; + expect(getNameFromOptions(options)).toBe('My Custom Name'); + }); + + it('returns cumulative function name', () => { + const options = { + mode: CalculateFieldMode.CumulativeFunctions, + cumulative: { reducer: ReducerID.sum, field: 'Value' }, + }; + expect(getNameFromOptions(options)).toBe('cumulative sum(Value)'); + }); + + it('returns cumulative function name without field', () => { + const options = { + mode: CalculateFieldMode.CumulativeFunctions, + cumulative: { reducer: ReducerID.mean }, + }; + expect(getNameFromOptions(options)).toBe('cumulative mean'); + }); + + it('returns window function name', () => { + const options = { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Trailing, + reducer: ReducerID.mean, + field: 'Temperature', + }, + }; + expect(getNameFromOptions(options)).toBe('trailing moving mean(Temperature)'); + }); + + it('returns window function name without field', () => { + const options = { + mode: CalculateFieldMode.WindowFunctions, + window: { + windowAlignment: WindowAlignment.Centered, + reducer: ReducerID.sum, + }, + }; + expect(getNameFromOptions(options)).toBe('centered moving sum'); + }); + + it('returns unary operation name', () => { + const options = { + mode: CalculateFieldMode.UnaryOperation, + unary: { + operator: UnaryOperationID.Abs, + fieldName: 'Value', + }, + }; + expect(getNameFromOptions(options)).toBe('abs(Value)'); + }); + + it('returns unary operation name without field', () => { + const options = { + mode: CalculateFieldMode.UnaryOperation, + unary: { + operator: UnaryOperationID.Abs, + fieldName: '', + }, + }; + expect(getNameFromOptions(options)).toBe('abs'); + }); + + it('returns binary operation name with field matcher', () => { + const options = { + mode: CalculateFieldMode.BinaryOperation, + binary: { + left: { matcher: { id: FieldMatcherID.byName, options: 'FieldA' } }, + operator: BinaryOperationID.Add, + right: { matcher: { id: FieldMatcherID.byName, options: 'FieldB' } }, + }, + }; + expect(getNameFromOptions(options)).toBe('FieldA + FieldB'); + }); + + it('returns binary operation name with fixed values', () => { + const options = { + mode: CalculateFieldMode.BinaryOperation, + binary: { + left: { fixed: '10' }, + operator: BinaryOperationID.Multiply, + right: { fixed: '5' }, + }, + }; + expect(getNameFromOptions(options)).toBe('10 * 5'); + }); + + it('returns empty string for binary operation with variables', () => { + const options = { + mode: CalculateFieldMode.BinaryOperation, + binary: { + left: { fixed: '$variable1' }, + operator: BinaryOperationID.Add, + right: { fixed: '10' }, + }, + }; + expect(getNameFromOptions(options)).toBe(''); + }); + + it('returns empty string when binary field is not provided', () => { + const options = { + mode: CalculateFieldMode.BinaryOperation, + // No binary field provided at all + }; + expect(getNameFromOptions(options)).toBe(''); + }); + + it('returns reducer name for reduce row mode', () => { + const options = { + mode: CalculateFieldMode.ReduceRow, + reduce: { reducer: ReducerID.mean }, + }; + expect(getNameFromOptions(options)).toBe('Mean'); + }); + + it('returns "Row" for index mode', () => { + const options = { + mode: CalculateFieldMode.Index, + }; + expect(getNameFromOptions(options)).toBe('Row'); + }); + + it('returns "math" as default', () => { + const options = { + mode: 'invalid-mode' as CalculateFieldMode, + }; + expect(getNameFromOptions(options)).toBe('math'); + }); +}); + function activateFullSceneTree(scene: SceneObject): SceneDeactivationHandler { const deactivationHandlers: SceneDeactivationHandler[] = []; diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index 1e9baf3f872..733155a3f10 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -693,11 +693,15 @@ export function getNameFromOptions(options: CalculateFieldTransformerOptions) { } case CalculateFieldMode.BinaryOperation: { const { binary } = options; - const alias = `${binary?.left?.matcher?.options ?? binary?.left?.fixed ?? ''} ${binary?.operator ?? ''} ${binary?.right?.matcher?.options ?? binary?.right?.fixed ?? ''}`; - + const left = binary?.left?.matcher?.options ?? binary?.left?.fixed ?? ''; + const right = binary?.right?.matcher?.options ?? binary?.right?.fixed ?? ''; // binary calculations with variables will be interpolated on the visualization but we don't want to do that here, so just give a blank placeholder - const variableFound = /\$/g.test(alias); - return variableFound ? '' : alias; + if (/\$/.test(left) || /\$/.test(right)) { + return ''; + } + + const operator = binary?.operator ?? BinaryOperationID.Add; + return left && right ? `${left} ${operator} ${right}` : ''; } case CalculateFieldMode.ReduceRow: {