From 8ce2c2d3eb884ac7b35a6cf3746b7018f895b280 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Mon, 15 Sep 2025 10:37:08 -0400 Subject: [PATCH] [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 cb37539ed7ca14230fda6e61012ab519eabccab7) Co-authored-by: Leon Sorokin --- .../panel-table/table_footer.json | 61 +++++++++++++++++++ .../src/components/Table/TableNG/TableNG.tsx | 18 +++--- .../components/Table/TableNG/utils.test.ts | 30 +++++++++ .../src/components/Table/TableNG/utils.ts | 52 +++------------- 4 files changed, 108 insertions(+), 53 deletions(-) diff --git a/devenv/dev-dashboards/panel-table/table_footer.json b/devenv/dev-dashboards/panel-table/table_footer.json index fddbeaa7747..6d7ca427f7b 100644 --- a/devenv/dev-dashboards/panel-table/table_footer.json +++ b/devenv/dev-dashboards/panel-table/table_footer.json @@ -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, diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index 3720be3d2bf..726a9a3ccf0 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -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()); // 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] diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts index 7331c048a92..946ee3fd799 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts @@ -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 = { diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.ts index 7da729539a3..162c8e6df91 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.ts @@ -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; }; /**