From 576326d852b5f1526381f9312102d058c83dbca5 Mon Sep 17 00:00:00 2001 From: Tanvi Kuchanur Date: Thu, 23 Jul 2026 11:24:37 +0530 Subject: [PATCH] [BUGFIX] Table: align filter rows with table columns Signed-off-by: Tanvi Kuchanur --- table/src/components/TablePanel.tsx | 252 +++++++++++++++++++--------- 1 file changed, 174 insertions(+), 78 deletions(-) diff --git a/table/src/components/TablePanel.tsx b/table/src/components/TablePanel.tsx index 75106f32f..3b943db2d 100644 --- a/table/src/components/TablePanel.tsx +++ b/table/src/components/TablePanel.tsx @@ -37,6 +37,7 @@ import { QueryDataType, TimeSeriesData } from '@perses-dev/spec'; import { CellSettings, ColumnSettings, evaluateConditionalFormatting, TableOptions } from '../models'; import { buildRawTableData, getTablePanelQueryMode } from '../table-data-utils'; import { EmbeddedPanel } from './EmbeddedPanel'; +import { createPortal } from 'react-dom'; type FilterValuesType = Array<{ original: T; formatted: T }>; @@ -401,6 +402,10 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps }); const filteredDataRef = useRef>>([]); + // Refs used to keep the filter row in sync with the table's horizontal + const panelContainerRef = useRef(null); + const filterRowInnerRef = useRef(null); + const filterCellRefs = useRef>([]); // Convert selectionMap to TanStack's RowSelectionState format const rowSelection = useMemo((): RowSelectionState => { @@ -728,6 +733,77 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps } }, [spec.pagination, pagination]); + // Sync the filter row's horizontal position with the table scroll. + useEffect(() => { + if (!spec.enableFiltering) { + return; + } + + const scrollContainer = panelContainerRef.current?.querySelector('.MuiTableContainer-root'); + const filterRowInner = filterRowInnerRef.current; + + if (!scrollContainer || !filterRowInner) { + return; + } + + const syncFilterRowScroll = (): void => { + filterRowInner.style.transform = `translateX(-${scrollContainer.scrollLeft}px)`; + + setOpenFilterColumn((current) => (current === null ? current : null)); + setFilterAnchorEl({}); + }; + + syncFilterRowScroll(); + + scrollContainer.addEventListener('scroll', syncFilterRowScroll, { passive: true }); + return (): void => { + scrollContainer.removeEventListener('scroll', syncFilterRowScroll); + }; + }, [spec.enableFiltering, columns, contentDimensions]); + +// Sync filter cell widths with the actual rendered table column widths to keep them aligned. + useEffect(() => { + if (!spec.enableFiltering) { + return; + } + + const scrollContainer = panelContainerRef.current?.querySelector('.MuiTableContainer-root'); + if (!scrollContainer) { + return; + } + + const syncColumnWidths = (): void => { + const headerCells = scrollContainer.querySelectorAll('thead tr th'); + + columns.forEach((_, idx) => { + const headerCell = headerCells[idx]; + const filterCell = filterCellRefs.current[idx]; + if (!headerCell || !filterCell) { + return; + } + + const width = `${headerCell.getBoundingClientRect().width}px`; + filterCell.style.width = width; + filterCell.style.minWidth = width; + filterCell.style.maxWidth = width; + }); + }; + + syncColumnWidths(); + + // Re-sync whenever the header row's size changes + const headerRow = scrollContainer.querySelector('thead tr'); + const resizeObserver = new ResizeObserver(syncColumnWidths); + if (headerRow) { + resizeObserver.observe(headerRow); + } + + return (): void => { + resizeObserver.disconnect(); + }; + }, [spec.enableFiltering, columns, contentDimensions, selectionEnabled, actionButtons]); + + if (contentDimensions === undefined) { return null; } @@ -748,99 +824,119 @@ export function TablePanel({ contentDimensions, spec, queryResults }: TableProps } return ( - <> +
{confirmDialog} {spec.enableFiltering && (
- {columns.map((column, idx) => { - const filters = getSelectedFilterValues(column.accessorKey as string); - const columnWidth = column.width || spec.defaultColumnWidth; - return ( -
- - {filters.length ? `${filters.length} items` : 'All'} - - - - {openFilterColumn === column.accessorKey && ( -
+ {filters.length ? `${filters.length} items` : 'All'} + +
- )} -
- ); - })} + ▼ + + + {openFilterColumn === column.accessorKey && + anchorEl && + createPortal( +
+ updateColumnFilter(column.accessorKey as string, values)} + theme={theme} + /> +
, + document.body + )} +
+ ); + })} +
)} getItemActionButtons({ id, data: data as Record })} hasItemActions={actionButtons && actionButtons.length > 0} /> - + ); -} +} \ No newline at end of file