[release-12.2.1] Table: Fix logic to calculate footer height (#110954) (#111107)

Table: Fix logic to calculate footer height (#110954)

* Table: Fix logic to calculate footer height

* add non-numeric footer case to gdev

* Update packages/grafana-ui/src/components/Table/TableNG/utils.ts



* Update packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx



---------


(cherry picked from commit cb37539ed7)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Paul Marbach 2025-09-15 10:37:08 -04:00 committed by GitHub
parent 987573a17c
commit 8ce2c2d3eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 108 additions and 53 deletions

View File

@ -1442,6 +1442,67 @@
}
],
"type": "table"
},
{
"datasource": {
"type": "grafana-testdata-datasource"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"footer": {
"reducers": ["lastNotNull", "countAll"]
},
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 6,
"w": 12,
"x": 0,
"y": 24
},
"id": 9,
"options": {
"cellHeight": "sm",
"showHeader": true
},
"pluginVersion": "12.2.0-pre",
"targets": [
{
"csvContent": "a,b\nfoo,bar\nbaz,bim\nbop,boop",
"datasource": {
"type": "grafana-testdata-datasource"
},
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "No numeric fields",
"type": "table"
}
],
"preload": false,

View File

@ -108,7 +108,6 @@ export function TableNG(props: TableNGProps) {
enablePagination = false,
enableSharedCrosshair = false,
enableVirtualization,
fieldConfig,
frozenColumns = 0,
getActions = () => [],
height,
@ -125,12 +124,6 @@ export function TableNG(props: TableNGProps) {
width,
} = props;
const hasFooter = useMemo(
() => data.fields.some((field) => field.config?.custom?.footer?.reducers?.length ?? false),
[data.fields]
);
const footerHeight = hasFooter ? calculateFooterHeight(data, fieldConfig) : 0;
const theme = useTheme2();
const styles = useStyles2(getGridStyles, enablePagination, transparent);
const panelContext = usePanelContext();
@ -146,7 +139,16 @@ export function TableNG(props: TableNGProps) {
[getActions, data, userCanExecuteActions]
);
const visibleFields = useMemo(() => getVisibleFields(data.fields), [data.fields]);
const hasHeader = !noHeader;
const hasFooter = useMemo(
() => visibleFields.some((field) => Boolean(field.config.custom?.footer?.reducers?.length)),
[visibleFields]
);
const footerHeight = useMemo(
() => (hasFooter ? calculateFooterHeight(visibleFields) : 0),
[hasFooter, visibleFields]
);
const resizeHandler = useColumnResize(onColumnResize);
@ -173,7 +175,7 @@ export function TableNG(props: TableNGProps) {
const [expandedRows, setExpandedRows] = useState(() => new Set<number>());
// vt scrollbar accounting for column auto-sizing
const visibleFields = useMemo(() => getVisibleFields(data.fields), [data.fields]);
const defaultRowHeight = useMemo(
() => getDefaultRowHeight(theme, visibleFields, cellHeight),
[theme, visibleFields, cellHeight]

View File

@ -46,6 +46,7 @@ import {
getDefaultRowHeight,
getDisplayName,
predicateByName,
calculateFooterHeight,
} from './utils';
describe('TableNG utils', () => {
@ -1380,6 +1381,35 @@ describe('TableNG utils', () => {
});
});
describe('calculateFooterHeight', () => {
it('should return 0 if no footer is present', () => {
const frame = createDataFrame({
fields: [
{ name: 'time', values: [1, 1, 2], nanos: [100, 99, 0] },
{ name: 'value', values: [10, 20, 30] },
],
});
expect(calculateFooterHeight(frame.fields)).toBe(0);
});
it('should return the height in pixels for the max reducers on a given field', () => {
const frame = createDataFrame({
fields: [
{
name: 'time',
values: [1, 1, 2],
nanos: [100, 99, 0],
config: { custom: { footer: { reducers: ['min', 'max', 'count'] } } },
},
{ name: 'value', values: [10, 20, 30], config: { custom: { footer: { reducers: ['min'] } } } },
],
});
expect(calculateFooterHeight(frame.fields)).toBe(78); // 3 reducers * 22px line height + 12px padding
});
});
describe('getDisplayName', () => {
it('should return the display name if set', () => {
const field: Field = {

View File

@ -8,7 +8,6 @@ import { Count, varPreLine } from 'uwrap';
import {
FieldType,
Field,
FieldConfigSource,
formattedValueToString,
GrafanaTheme2,
DisplayValue,
@ -842,55 +841,18 @@ export const processNestedTableRows = (
return result;
};
/**
* @internal
* Get the maximum number of reducers across all fields
*/
const getMaxReducerCount = (dataFrame: DataFrame, fieldConfig?: FieldConfigSource): number => {
// Filter to only numeric fields that can have reducers
const numericFields = dataFrame.fields.filter(({ type }) => type === FieldType.number);
// If there are no numeric fields, return 0
if (numericFields.length === 0) {
return 0;
}
// Map each field to its reducer count (direct config or override)
const reducerCounts = numericFields.map((field) => {
// Get the direct reducer count from the field config
const directReducers = field.config?.custom?.footer?.reducers ?? [];
let reducerCount = directReducers.length;
// Check for overrides if field config is available
if (fieldConfig?.overrides) {
// Find override that matches this field
const override = fieldConfig.overrides.find(
({ matcher: { id, options } }) => id === 'byName' && options === getDisplayName(field)
);
// Check if there's a footer reducer property in the override
const footerProperty = override?.properties?.find(({ id }) => id === 'custom.footer.reducers');
if (footerProperty?.value && Array.isArray(footerProperty.value)) {
// If override exists, it takes precedence over direct config
reducerCount = footerProperty.value.length;
}
}
return reducerCount;
});
// Return the maximum count or 0 if no reducers found
return reducerCounts.length > 0 ? Math.max(...reducerCounts) : 0;
};
/**
* @internal
* Calculate the footer height based on the maximum reducer count
*/
export const calculateFooterHeight = (dataFrame: DataFrame, fieldConfig?: FieldConfigSource) => {
const maxReducerCount = getMaxReducerCount(dataFrame, fieldConfig);
export const calculateFooterHeight = (fields: Field[]): number => {
let maxReducerCount = 0;
for (const field of fields) {
maxReducerCount = Math.max(maxReducerCount, field.config.custom?.footer?.reducers?.length ?? 0);
}
// Base height (+ padding) + height per reducer
return maxReducerCount * TABLE.LINE_HEIGHT + TABLE.CELL_PADDING * 2;
return maxReducerCount > 0 ? maxReducerCount * TABLE.LINE_HEIGHT + TABLE.CELL_PADDING * 2 : 0;
};
/**