From cd2abce91493e1f6d100bcb9630a504bdc25b53d Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Tue, 23 Jan 2024 14:59:22 -0700 Subject: [PATCH] Table: Fix case where undefined data crashes the visualization (#80498) --- .betterer.results | 3 + .../src/field/fieldOverrides.test.ts | 2 +- .../src/transformations/fieldReducer.test.ts | 12 +++- .../src/transformations/fieldReducer.ts | 56 +++++++++++-------- 4 files changed, 47 insertions(+), 26 deletions(-) diff --git a/.betterer.results b/.betterer.results index 6ccf70dd6ec..3a1a3eb80f0 100644 --- a/.betterer.results +++ b/.betterer.results @@ -179,6 +179,9 @@ exports[`better eslint`] = { "packages/grafana-data/src/themes/createColors.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "packages/grafana-data/src/transformations/fieldReducer.test.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], "packages/grafana-data/src/transformations/matchers/valueMatchers/types.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/packages/grafana-data/src/field/fieldOverrides.test.ts b/packages/grafana-data/src/field/fieldOverrides.test.ts index 8d358a80320..67426e6f538 100644 --- a/packages/grafana-data/src/field/fieldOverrides.test.ts +++ b/packages/grafana-data/src/field/fieldOverrides.test.ts @@ -124,7 +124,7 @@ describe('Global MinMax', () => { }); }); - describe('when value values are zeo', () => { + describe('when values are zero', () => { it('then global min max should be correct', () => { const frame = toDataFrame({ fields: [ diff --git a/packages/grafana-data/src/transformations/fieldReducer.test.ts b/packages/grafana-data/src/transformations/fieldReducer.test.ts index ebe4fd2a592..c1164e96126 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.test.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.test.ts @@ -3,7 +3,7 @@ import { difference } from 'lodash'; import { createDataFrame, guessFieldTypeFromValue } from '../dataframe/processDataFrame'; import { Field, FieldType, NullValueMode } from '../types/index'; -import { fieldReducers, ReducerID, reduceField } from './fieldReducer'; +import { fieldReducers, ReducerID, reduceField, defaultCalcs } from './fieldReducer'; /** * Run a reducer and get back the value @@ -63,6 +63,16 @@ describe('Stats Calculators', () => { expect(stats.count).toEqual(2); }); + it('should handle undefined field data without crashing', () => { + // eslint-ignore @typescript-eslint/no-explicit-any + const stats = reduceField({ + field: { name: 'a', values: undefined as any, config: {}, type: FieldType.number }, + reducers: [ReducerID.first, ReducerID.last, ReducerID.mean, ReducerID.count], + }); + + expect(stats).toEqual(defaultCalcs); + }); + it('should support a single stat also', () => { basicTable.fields[0].state = undefined; // clear the cache const stats = reduceField({ diff --git a/packages/grafana-data/src/transformations/fieldReducer.ts b/packages/grafana-data/src/transformations/fieldReducer.ts index dd8b1f98acf..e43697d15cc 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.ts @@ -84,7 +84,7 @@ export function reduceField(options: ReduceFieldOptions): FieldCalcs { // Return early for empty series // This lets the concrete implementations assume at least one row const data = field.values; - if (data.length < 1) { + if (data && data.length < 1) { const calcs: FieldCalcs = { ...field.state.calcs }; for (const reducer of queue) { calcs[reducer.id] = reducer.emptyInputResult !== null ? reducer.emptyInputResult : null; @@ -271,33 +271,41 @@ export const fieldReducers = new Registry(() => [ }, ]); -export function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs { - const calcs: FieldCalcs = { - sum: 0, - max: -Number.MAX_VALUE, - min: Number.MAX_VALUE, - logmin: Number.MAX_VALUE, - mean: null, - last: null, - first: null, - lastNotNull: null, - firstNotNull: null, - count: 0, - nonNullCount: 0, - allIsNull: true, - allIsZero: true, - range: null, - diff: null, - delta: 0, - step: Number.MAX_VALUE, - diffperc: 0, +// Used for test cases +export const defaultCalcs: FieldCalcs = { + sum: 0, + max: -Number.MAX_VALUE, + min: Number.MAX_VALUE, + logmin: Number.MAX_VALUE, + mean: null, + last: null, + first: null, + lastNotNull: null, + firstNotNull: null, + count: 0, + nonNullCount: 0, + allIsNull: true, + allIsZero: true, + range: null, + diff: null, + delta: 0, + step: Number.MAX_VALUE, + diffperc: 0, - // Just used for calculations -- not exposed as a stat - previousDeltaUp: true, - }; + // Just used for calculations -- not exposed as a stat + previousDeltaUp: true, +}; + +export function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs { + const calcs: FieldCalcs = { ...defaultCalcs }; const data = field.values; + // early return for undefined / empty series + if (!data) { + return calcs; + } + const isNumberField = field.type === FieldType.number || field.type === FieldType.time; for (let i = 0; i < data.length; i++) {