From ce75171a7cf5d1c39d9e67eb6a411230d8c91f1f Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Fri, 31 Jul 2026 06:03:45 +0200 Subject: [PATCH] fix(widgets): stabilize table layout hydration --- .../src/beszel-system-table/component.tsx | 16 +++- .../src/common/use-persisted-table-layout.ts | 74 +++++++++++++------ 2 files changed, 67 insertions(+), 23 deletions(-) diff --git a/packages/widgets/src/beszel-system-table/component.tsx b/packages/widgets/src/beszel-system-table/component.tsx index c155eda37..a5ad9c123 100644 --- a/packages/widgets/src/beszel-system-table/component.tsx +++ b/packages/widgets/src/beszel-system-table/component.tsx @@ -338,7 +338,21 @@ export default function BeszelSystemTableWidget({ ]; return cols.filter(Boolean) as DataTableColumn[]; - }, [options, t, size]); + }, [ + options.showAgent, + options.showBattery, + options.showCpu, + options.showDisk, + options.showGpu, + options.showLoadAvg, + options.showMemory, + options.showNet, + options.showServices, + options.showTemp, + options.showUptime, + size, + t, + ]); const { effectiveColumns, storeKey } = usePersistedTableLayout({ columns, diff --git a/packages/widgets/src/common/use-persisted-table-layout.ts b/packages/widgets/src/common/use-persisted-table-layout.ts index fe7882a46..bfee3e0c9 100644 --- a/packages/widgets/src/common/use-persisted-table-layout.ts +++ b/packages/widgets/src/common/use-persisted-table-layout.ts @@ -21,6 +21,29 @@ interface UsePersistedTableLayoutProps { const layoutSaveDelay = 350; +const toPixelWidth = (width: string | number): number | undefined => { + if (typeof width === "number" && Number.isFinite(width) && width > 0) return width; + if (typeof width !== "string" || !width.endsWith("px")) return undefined; + + const parsedWidth = Number.parseInt(width, 10); + return Number.isFinite(parsedWidth) && parsedWidth > 0 ? parsedWidth : undefined; +}; + +const getColumnWidthMap = ( + columnsWidth: Record[], + visibleAccessorSet: ReadonlySet, +): Record => { + const widths: Record = {}; + for (const entry of columnsWidth) { + for (const [accessor, width] of Object.entries(entry)) { + if (!visibleAccessorSet.has(accessor)) continue; + const pixelWidth = toPixelWidth(width); + if (pixelWidth !== undefined) widths[accessor] = pixelWidth; + } + } + return widths; +}; + export const parseColumnOrder = (value: string, columnAccessors: readonly string[]): string[] => { if (!value) return []; try { @@ -84,6 +107,8 @@ export const usePersistedTableLayout = ({ const lastStoreKey = useRef(storeKey); const hydrated = useRef(false); + const previousOrder = useRef(columnsOrder); + const previousWidths = useRef(columnsWidth); useEffect(() => { if (lastStoreKey.current !== storeKey) { lastStoreKey.current = storeKey; @@ -91,20 +116,35 @@ export const usePersistedTableLayout = ({ } if (hydrated.current) return; - if (restoredOrder.length > 0) setColumnsOrder(restoredOrder); - const visibleWidths = Object.entries(savedWidths) - .filter(([accessor]) => visibleAccessorSet.has(accessor)) - .map(([accessor, width]) => ({ accessor, width })); - if (visibleWidths.length > 0) setMultipleColumnWidths(visibleWidths); + const orderMatches = + columnsOrder.length === restoredOrder.length && + columnsOrder.every((accessor, index) => accessor === restoredOrder[index]); + const currentWidths = getColumnWidthMap(columnsWidth, visibleAccessorSet); + const visibleWidths = Object.entries(savedWidths).filter(([accessor]) => visibleAccessorSet.has(accessor)); + const widthsMatch = visibleWidths.every(([accessor, width]) => currentWidths[accessor] === width); - const animationFrame = requestAnimationFrame(() => { + if (orderMatches && widthsMatch) { + previousOrder.current = columnsOrder; + previousWidths.current = columnsWidth; hydrated.current = true; - }); - return () => cancelAnimationFrame(animationFrame); - }, [restoredOrder, savedWidths, setColumnsOrder, setMultipleColumnWidths, storeKey, visibleAccessorSet]); + return; + } + + if (!orderMatches) setColumnsOrder(restoredOrder); + if (!widthsMatch) { + setMultipleColumnWidths(visibleWidths.map(([accessor, width]) => ({ accessor, width }))); + } + }, [ + columnsOrder, + columnsWidth, + restoredOrder, + savedWidths, + setColumnsOrder, + setMultipleColumnWidths, + storeKey, + visibleAccessorSet, + ]); - const previousOrder = useRef(columnsOrder); - const previousWidths = useRef(columnsWidth); const pendingLayout = useRef({}); const saveTimeout = useRef(undefined); useEffect( @@ -133,17 +173,7 @@ export const usePersistedTableLayout = ({ } if (widthsChanged) { const widthMap: Record = { ...savedWidths }; - for (const entry of columnsWidth) { - const accessor = Object.keys(entry)[0]; - if (!accessor || !visibleAccessorSet.has(accessor)) continue; - const width = entry[accessor]; - if (typeof width === "number" && Number.isFinite(width) && width > 0) { - widthMap[accessor] = width; - } else if (typeof width === "string" && width.endsWith("px")) { - const parsedWidth = Number.parseInt(width, 10); - if (Number.isFinite(parsedWidth) && parsedWidth > 0) widthMap[accessor] = parsedWidth; - } - } + Object.assign(widthMap, getColumnWidthMap(columnsWidth, visibleAccessorSet)); newLayout.columnWidths = JSON.stringify(widthMap); }