From 10ba298233f2daf08089b2c84aac5aeec6aee16f Mon Sep 17 00:00:00 2001 From: Bergasha Date: Sat, 29 Aug 2026 11:02:10 +1000 Subject: [PATCH] fix(board): beszel & calendar scale fix (#6737) * fix(calendar): add cell spacing and scale day content with widget size * fix(beszel): stop chart panels from leaving blank grid columns and size charts to fit the widget * fix(widgets): address CodeRabbit review on calendar/beszel scaling PR - beszel: account for the SimpleGrid row gap and the Stack gap between the controls row and the grid in the available-height budget, so a two-row layout no longer overflows the widget. - calendar: memoize the day-cell scale lookup instead of recomputing it on every render. - calendar: don't render the notification indicator row when none of a day's events have an indicatorColor, which was shifting the date number up with no visible dot to show for it. Skipped one finding: the claim that .day's absolute inset:0 covers monthCell's padding. It doesn't - the containing block for an absolutely positioned element is the padding edge of its positioned ancestor per the CSS spec, so the padding correctly insets .day. This was also visually verified in the browser while building the fix. * fix(beszel): don't treat availableHeight=0 as omitted in computeChartHeight A falsy check on availableHeight also caught the legitimate value 0 (widget shorter than its own padding+controls), falling back to the full 180px height instead of clamping to MIN_CHART_HEIGHT - the exact overflow this height budget was meant to prevent. --- .../src/beszel-system-stats/component.tsx | 9 ++++ .../widgets/src/beszel/_shared/stats-view.tsx | 45 +++++++++++++++---- .../widgets/src/calendar/calender-day.tsx | 31 ++++++++++--- packages/widgets/src/calendar/component.tsx | 1 + 4 files changed, 70 insertions(+), 16 deletions(-) diff --git a/packages/widgets/src/beszel-system-stats/component.tsx b/packages/widgets/src/beszel-system-stats/component.tsx index 0aca4348f..2a209e85e 100644 --- a/packages/widgets/src/beszel-system-stats/component.tsx +++ b/packages/widgets/src/beszel-system-stats/component.tsx @@ -23,11 +23,16 @@ import { getUsableWidgetQueryData } from "../common/query-state"; import { BeszelStatsView } from "../beszel/_shared/stats-view"; import { createBeszelSystemChoices, resolveBeszelSystemChoice } from "./selection"; +const CONTROLS_ROW_HEIGHT = 36; +const CONTROLS_ROW_GAP = 16; +const STACK_PADDING = 24; + export default function BeszelSystemStatsWidget({ options, integrationIds, isEditMode, width, + height, boardId, itemId, setOptions, @@ -256,6 +261,10 @@ export default function BeszelSystemStatsWidget({ systemId={selectedSystem?.systemId ?? ""} timePeriod={options.timePeriod as BeszelTimePeriod} columns={width > 600 ? 2 : 1} + availableHeight={Math.max( + 0, + height - STACK_PADDING - (isEditMode ? 0 : CONTROLS_ROW_HEIGHT + CONTROLS_ROW_GAP), + )} visibility={{ cpu: options.showCpu, memory: options.showMemory, diff --git a/packages/widgets/src/beszel/_shared/stats-view.tsx b/packages/widgets/src/beszel/_shared/stats-view.tsx index 66a24f38f..ef67f40ee 100644 --- a/packages/widgets/src/beszel/_shared/stats-view.tsx +++ b/packages/widgets/src/beszel/_shared/stats-view.tsx @@ -62,9 +62,20 @@ interface BeszelStatsViewProps { timePeriod: BeszelTimePeriod; visibility: BeszelStatsVisibility; columns: 1 | 2; + availableHeight?: number; onSwitchToHistorical?: () => void; } +const MIN_CHART_HEIGHT = 100; +const PANEL_ROW_OVERHEAD = 28; +const GRID_ROW_GAP = 16; + +const computeChartHeight = (availableHeight: number | undefined, rowCount: number) => { + if (availableHeight === undefined || rowCount <= 0) return CHART_HEIGHT; + const perRow = (availableHeight - (rowCount - 1) * GRID_ROW_GAP) / rowCount - PANEL_ROW_OVERHEAD; + return Math.max(MIN_CHART_HEIGHT, Math.min(CHART_HEIGHT, Math.floor(perRow))); +}; + const whenVisible = (visible: boolean, data: T | undefined) => (visible ? data : undefined); export function BeszelStatsView({ @@ -73,6 +84,7 @@ export function BeszelStatsView({ timePeriod, visibility, columns, + availableHeight, onSwitchToHistorical, }: BeszelStatsViewProps) { const t = useI18n("widget.beszelSystemStats"); @@ -248,14 +260,29 @@ export function BeszelStatsView({ ); } + const renderedPanelCount = + (visibility.cpu && cpuData.length > 0 ? 1 : 0) + + (visibility.memory && memoryData.length > 0 ? 1 : 0) + + (visibility.disk && diskData.length > 0 ? 1 : 0) + + (visibility.diskIO && diskIOData.length > 0 ? 1 : 0) + + (visibility.network && networkData.length > 0 ? 1 : 0) + + (showDocker && containerSeries.length > 0 + ? (visibility.dockerCpu && dockerCpuData.length > 0 ? 1 : 0) + + (visibility.dockerMemory && dockerMemoryData.length > 0 ? 1 : 0) + + (visibility.dockerNetwork && dockerNetworkData.length > 0 ? 1 : 0) + : 0); + const effectiveColumns = Math.min(columns, Math.max(1, renderedPanelCount)) as 1 | 2; + const rowCount = Math.ceil(renderedPanelCount / effectiveColumns); + const chartHeight = computeChartHeight(availableHeight, rowCount); + return ( - + {visibility.cpu && cpuData.length > 0 && ( + dayScaleTiers.find((tier) => minAxisSize >= tier.minAxisSize) ?? fallbackDayScale; + export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: CalendarDayProps) => { const board = useRequiredBoard(); const mantineTheme = useMantineTheme(); const actualItemRadius = mantineTheme.radius[board.itemRadius]; const minAxisSize = Math.min(rootWidth, rootHeight); - const shouldScaleDown = minAxisSize < 350; const shouldShowIndicators = rootHeight >= 256; - const indicatorSize = shouldScaleDown ? 3 : 4; + const { textSize, indicatorSize, indicatorMarginTop } = useMemo(() => getDayScale(minAxisSize), [minAxisSize]); const cell = ( - + {date.getDate()} - + ); @@ -70,19 +86,20 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C interface NotificationIndicatorProps { events: CalendarEvent[]; size: number; + marginTop: number; visible: boolean; } -const NotificationIndicator = ({ events, size, visible }: NotificationIndicatorProps) => { +const NotificationIndicator = ({ events, size, marginTop, visible }: NotificationIndicatorProps) => { const notificationEvents = [...new Set(events.map((event) => event.indicatorColor))].filter( (color): color is string => Boolean(color), ); - if (!visible) return null; + if (!visible || notificationEvents.length === 0) return null; return (