TableNG: Footer fields declaration should work for displayName or name (#108482)

back to using our own getDisplayNam
This commit is contained in:
Paul Marbach
2025-07-23 14:49:10 -04:00
committed by GitHub
parent 60e0d2d136
commit d9ef08f824
4 changed files with 103 additions and 54 deletions
@@ -208,7 +208,7 @@ export function TableNG(props: TableNGProps) {
});
// Create a map of column key to text wrap
const footerCalcs = useFooterCalcs(sortedRows, data.fields, { enabled: hasFooter, footerOptions, isCountRowsSet });
const footerCalcs = useFooterCalcs(sortedRows, data, { enabled: hasFooter, footerOptions, isCountRowsSet });
const applyToRowBgFn = useMemo(() => getApplyToRowBgFn(data.fields, theme) ?? undefined, [data.fields, theme]);
const renderRow = useMemo(
@@ -1,7 +1,7 @@
import { act, renderHook } from '@testing-library/react';
import { varPreLine } from 'uwrap';
import { Field, FieldType } from '@grafana/data';
import { cacheFieldDisplayNames, createDataFrame, Field, FieldType } from '@grafana/data';
import {
useFilteredRows,
@@ -226,11 +226,11 @@ describe('TableNG hooks', () => {
describe('useFooterCalcs', () => {
const rows = [
{ Field1: 1, Text: 'a', __depth: 0, __index: 0 },
{ Field1: 2, Text: 'b', __depth: 0, __index: 1 },
{ Field1: 3, Text: 'c', __depth: 0, __index: 2 },
{ Field2: 3, Text: 'd', __depth: 0, __index: 3 },
{ Field2: 10, Text: 'e', __depth: 0, __index: 4 },
{ 'Field 1': 1, Text: 'a', __depth: 0, __index: 0 },
{ 'Field 1': 2, Text: 'b', __depth: 0, __index: 1 },
{ 'Field 1': 3, Text: 'c', __depth: 0, __index: 2 },
{ 'Field 2': 3, Text: 'd', __depth: 0, __index: 3 },
{ 'Field 2': 10, Text: 'e', __depth: 0, __index: 4 },
];
const numericField: Field = {
@@ -239,6 +239,7 @@ describe('TableNG hooks', () => {
values: [1, 2, 3],
config: {
custom: {},
displayName: 'Field 1',
},
display: (value: unknown) => ({
text: String(value),
@@ -247,7 +248,6 @@ describe('TableNG hooks', () => {
prefix: undefined,
suffix: undefined,
}),
state: {},
getLinks: undefined,
};
@@ -255,7 +255,10 @@ describe('TableNG hooks', () => {
name: 'Field2',
type: FieldType.number,
values: [3, 10],
config: { custom: {} },
config: {
custom: {},
displayName: 'Field 2',
},
display: (value: unknown) => ({
text: String(value),
numeric: Number(value),
@@ -263,7 +266,6 @@ describe('TableNG hooks', () => {
prefix: undefined,
suffix: undefined,
}),
state: {},
getLinks: undefined,
};
@@ -279,105 +281,135 @@ describe('TableNG hooks', () => {
prefix: undefined,
suffix: undefined,
}),
state: {},
getLinks: undefined,
};
it('should calculate sum for numeric fields', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, numericField], {
const { result } = renderHook(() => {
const data = createDataFrame({ fields: [textField, numericField] });
cacheFieldDisplayNames([data]);
return useFooterCalcs(rows, data, {
enabled: true,
footerOptions: { show: true, reducer: ['sum'] },
})
);
});
});
expect(result.current).toEqual(['Total', '6']); // 1 + 2 + 3
});
it('should calculate mean for numeric fields', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, numericField], {
const { result } = renderHook(() => {
const data = createDataFrame({ fields: [textField, numericField] });
cacheFieldDisplayNames([data]);
return useFooterCalcs(rows, data, {
enabled: true,
footerOptions: { show: true, reducer: ['mean'] },
})
);
});
});
expect(result.current).toEqual(['Mean', '2']); // (1 + 2 + 3) / 3
});
it('should return an empty string for non-numeric fields', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, textField], {
const { result } = renderHook(() => {
const data = createDataFrame({ fields: [textField, textField] });
cacheFieldDisplayNames([data]);
return useFooterCalcs(rows, data, {
enabled: true,
footerOptions: { show: true, reducer: ['sum'] },
})
);
});
});
expect(result.current).toEqual(['Total', '']);
});
it('should return empty array if no footerOptions are provided', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, textField], {
const { result } = renderHook(() => {
const data = createDataFrame({ fields: [textField, numericField, numericField2] });
cacheFieldDisplayNames([data]);
return useFooterCalcs(rows, data, {
enabled: true,
footerOptions: undefined,
})
);
});
});
expect(result.current).toEqual([]);
});
it('should return empty array when footer is disabled', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, textField], {
const { result } = renderHook(() => {
const data = createDataFrame({ fields: [textField, numericField, numericField2] });
cacheFieldDisplayNames([data]);
return useFooterCalcs(rows, data, {
enabled: false,
footerOptions: { show: true, reducer: ['sum'] },
})
);
});
});
expect(result.current).toEqual([]);
});
it('should return empty array when reducer is undefined', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, textField], {
const { result } = renderHook(() => {
const data = createDataFrame({ fields: [textField, textField] });
cacheFieldDisplayNames([data]);
return useFooterCalcs(rows, data, {
enabled: true,
footerOptions: { show: true, reducer: undefined },
})
);
});
});
expect(result.current).toEqual([]);
});
it('should return empty array when reducer is empty', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, textField], {
const { result } = renderHook(() => {
const data = createDataFrame({ fields: [textField, numericField, numericField2] });
cacheFieldDisplayNames([data]);
return useFooterCalcs(rows, data, {
enabled: true,
footerOptions: { show: true, reducer: [] },
})
);
});
});
expect(result.current).toEqual([]);
});
it('should return empty string if fields array doesnt include this field', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, numericField, numericField2], {
const { result } = renderHook(() => {
const data = createDataFrame({ fields: [textField, numericField, numericField2] });
cacheFieldDisplayNames([data]);
return useFooterCalcs(rows, data, {
enabled: true,
footerOptions: { show: true, reducer: ['sum'], fields: ['Field2', 'Field3'] },
})
);
});
});
expect(result.current).toEqual(['Total', '', '13']);
});
it('should return the calculation if fields array includes this field', () => {
const { result } = renderHook(() =>
useFooterCalcs(rows, [textField, numericField, numericField2], {
const { result } = renderHook(() => {
const data = createDataFrame({ fields: [textField, numericField, numericField2] });
cacheFieldDisplayNames([data]);
return useFooterCalcs(rows, data, {
enabled: true,
footerOptions: { show: true, reducer: ['sum'], fields: ['Field1', 'Field2', 'Field3'] },
})
);
});
});
expect(result.current).toEqual(['Total', '6', '13']);
});
it('should return the calculation if fields array includes this field by either name or display name', () => {
const { result } = renderHook(() => {
const data = createDataFrame({ fields: [textField, numericField, numericField2] });
cacheFieldDisplayNames([data]);
return useFooterCalcs(rows, data, {
enabled: true,
footerOptions: { show: true, reducer: ['sum'], fields: ['Field1', 'Field 2'] },
});
});
expect(result.current).toEqual(['Total', '6', '13']);
});
@@ -2,7 +2,16 @@ import { useState, useMemo, useEffect, useCallback, useRef, useLayoutEffect, Ref
import { Column, DataGridHandle, DataGridProps, SortColumn } from 'react-data-grid';
import { varPreLine } from 'uwrap';
import { Field, fieldReducers, FieldType, formattedValueToString, reduceField } from '@grafana/data';
import {
DataFrame,
Field,
FieldMatcherID,
fieldReducers,
FieldType,
formattedValueToString,
getFieldMatcher,
reduceField,
} from '@grafana/data';
import { useTheme2 } from '../../../themes/ThemeContext';
import { TableCellDisplayMode, TableColumnResizeActionCallback } from '../types';
@@ -255,7 +264,7 @@ export interface FooterCalcsOptions {
export function useFooterCalcs(
rows: TableRow[],
fields: Field[],
data: DataFrame,
{ enabled, footerOptions, isCountRowsSet }: FooterCalcsOptions
): string[] {
return useMemo(() => {
@@ -265,7 +274,11 @@ export function useFooterCalcs(
return [];
}
return fields.map((field, index) => {
const fieldNameMatcher = footerOptions.fields
? getFieldMatcher({ id: FieldMatcherID.byNames, options: { names: footerOptions.fields } })
: undefined;
return data.fields.map((field, index) => {
if (field.state?.calcs) {
delete field.state?.calcs;
}
@@ -289,8 +302,9 @@ export function useFooterCalcs(
return '';
}
// If fields array is specified, only show footer for fields included in that array
if (footerOptions.fields?.length && !footerOptions.fields?.includes(getDisplayName(field))) {
// If fields array is specified, only show footer for fields included in that array.
// the array can include either the display name or the field name.
if (fieldNameMatcher && !fieldNameMatcher(field, data, [data])) {
return '';
}
@@ -305,7 +319,7 @@ export function useFooterCalcs(
return formattedValueToString(displayFn(value));
});
}, [fields, enabled, footerOptions, isCountRowsSet, rows]);
}, [data, enabled, footerOptions, isCountRowsSet, rows]);
}
interface TypographyCtx {
@@ -537,7 +537,10 @@ export const processNestedTableRows = (
/**
* @internal
* returns the display name of a field
* returns the display name of a field.
* We intentionally do not want to use @grafana/data's getFieldDisplayName here,
* instead we have a call to cacheFieldDisplayNames up in TablePanel to handle this
* before we begin.
*/
export const getDisplayName = (field: Field): string => {
return field.state?.displayName ?? field.name;