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.
This commit is contained in:
1 parent
7855278ed1
commit
10ba298233
4 files changed
+70
-16
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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 = <T,>(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 (
|
||||
<SimpleGrid cols={columns} spacing="md">
|
||||
<SimpleGrid cols={effectiveColumns} spacing="md">
|
||||
{visibility.cpu && cpuData.length > 0 && (
|
||||
<BeszelChartPanel
|
||||
title={t("chart.cpu.title")}
|
||||
subtitle={t("chart.cpu.subtitle")}
|
||||
chartProps={{
|
||||
h: CHART_HEIGHT,
|
||||
h: chartHeight,
|
||||
data: cpuData,
|
||||
series: series.cpu,
|
||||
yAxisFormatter: chartAxisFormatters.percent,
|
||||
@@ -269,7 +296,7 @@ export function BeszelStatsView({
|
||||
title={t("chart.memory.title")}
|
||||
subtitle={t("chart.memory.subtitle")}
|
||||
chartProps={{
|
||||
h: CHART_HEIGHT,
|
||||
h: chartHeight,
|
||||
data: memoryData,
|
||||
type: "stacked",
|
||||
series: series.memory,
|
||||
@@ -283,7 +310,7 @@ export function BeszelStatsView({
|
||||
title={t("chart.disk.title")}
|
||||
subtitle={t("chart.disk.subtitle")}
|
||||
chartProps={{
|
||||
h: CHART_HEIGHT,
|
||||
h: chartHeight,
|
||||
data: diskData,
|
||||
type: "stacked",
|
||||
series: diskSeries,
|
||||
@@ -297,7 +324,7 @@ export function BeszelStatsView({
|
||||
title={t("chart.diskIO.title")}
|
||||
subtitle={t("chart.diskIO.subtitle")}
|
||||
chartProps={{
|
||||
h: CHART_HEIGHT,
|
||||
h: chartHeight,
|
||||
data: diskIOData,
|
||||
series: series.diskIO,
|
||||
yAxisFormatter: chartAxisFormatters.rate,
|
||||
@@ -310,7 +337,7 @@ export function BeszelStatsView({
|
||||
title={t("chart.network.title")}
|
||||
subtitle={t("chart.network.subtitle")}
|
||||
chartProps={{
|
||||
h: CHART_HEIGHT,
|
||||
h: chartHeight,
|
||||
data: networkData,
|
||||
series: series.network,
|
||||
yAxisFormatter: chartAxisFormatters.rate,
|
||||
@@ -325,7 +352,7 @@ export function BeszelStatsView({
|
||||
title={t("chart.dockerCpu.title")}
|
||||
subtitle={t("chart.dockerCpu.subtitle")}
|
||||
chartProps={{
|
||||
h: CHART_HEIGHT,
|
||||
h: chartHeight,
|
||||
data: dockerCpuData,
|
||||
type: "stacked",
|
||||
series: containerSeries,
|
||||
@@ -339,7 +366,7 @@ export function BeszelStatsView({
|
||||
title={t("chart.dockerMemory.title")}
|
||||
subtitle={t("chart.dockerMemory.subtitle")}
|
||||
chartProps={{
|
||||
h: CHART_HEIGHT,
|
||||
h: chartHeight,
|
||||
data: dockerMemoryData,
|
||||
type: "stacked",
|
||||
series: containerSeries,
|
||||
@@ -353,7 +380,7 @@ export function BeszelStatsView({
|
||||
title={t("chart.dockerNetwork.title")}
|
||||
subtitle={t("chart.dockerNetwork.subtitle")}
|
||||
chartProps={{
|
||||
h: CHART_HEIGHT,
|
||||
h: chartHeight,
|
||||
data: dockerNetworkData,
|
||||
series: containerSeries,
|
||||
yAxisFormatter: chartAxisFormatters.rate,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { Box, Container, Flex, HoverCard, Text, useMantineTheme } from "@mantine/core";
|
||||
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
@@ -13,15 +14,25 @@ interface CalendarDayProps {
|
||||
rootHeight: number;
|
||||
}
|
||||
|
||||
const fallbackDayScale = { textSize: "xs" as const, indicatorSize: 3, indicatorMarginTop: 2 };
|
||||
const dayScaleTiers = [
|
||||
{ minAxisSize: 700, textSize: "lg" as const, indicatorSize: 6, indicatorMarginTop: 6 },
|
||||
{ minAxisSize: 450, textSize: "md" as const, indicatorSize: 5, indicatorMarginTop: 4 },
|
||||
{ minAxisSize: 250, textSize: "sm" as const, indicatorSize: 4, indicatorMarginTop: 3 },
|
||||
{ minAxisSize: 0, ...fallbackDayScale },
|
||||
];
|
||||
|
||||
const getDayScale = (minAxisSize: number) =>
|
||||
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 = (
|
||||
<Container
|
||||
@@ -39,10 +50,15 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: C
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Text ta={"center"} size={shouldScaleDown ? "xs" : "md"} lh={1}>
|
||||
<Text ta={"center"} size={textSize} lh={1}>
|
||||
{date.getDate()}
|
||||
</Text>
|
||||
<NotificationIndicator events={events} size={indicatorSize} visible={shouldShowIndicators} />
|
||||
<NotificationIndicator
|
||||
events={events}
|
||||
size={indicatorSize}
|
||||
marginTop={indicatorMarginTop}
|
||||
visible={shouldShowIndicators}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Flex
|
||||
mt={3}
|
||||
mt={marginTop}
|
||||
w="fit-content"
|
||||
maw="75%"
|
||||
h={size}
|
||||
|
||||
@@ -212,6 +212,7 @@ const CalendarBase = ({
|
||||
monthCell: {
|
||||
textAlign: "center",
|
||||
position: "relative",
|
||||
padding: 3,
|
||||
},
|
||||
day: {
|
||||
borderRadius: actualItemRadius,
|
||||
|
||||
Reference in new issue
Block a user