refactor(widgets): let the UniFi OS chart fill the tile

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
zacharyandClaude Opus 5 committed 2026-10-08 00:24:40 +02:00
1 parent fa945e947e
commit 35095276fa
2 files changed
+62 -26

No files matched your search

+24 -6
View File
@@ -11,7 +11,7 @@ import { useIntegrationConnected } from "@homarr/common";
import { useI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
import { ThroughputChart } from "./throughput-chart";
import { ThroughputChart, ThroughputLegend } from "./throughput-chart";
import type { ThroughputPoint } from "./throughput-history";
import { appendThroughputPoint } from "./throughput-history";
@@ -94,8 +94,11 @@ export default function UnifiOsWidget({ integrationIds, options }: WidgetCompone
const wanColor = summary.wan.state === "online" ? "green" : summary.wan.state === "offline" ? "red" : "gray";
const isp = summary.wan.isp;
// No `justify="space-between"` on the outer stack: the Internet card flexes to
// take the leftover height, so there is no slack left to distribute —
// distributing it is exactly what piled empty space between the bottom rows.
return (
<Stack h="100%" p="sm" gap="xs" justify="space-between">
<Stack h="100%" p="sm" gap="xs">
{/* The site name is not decorative: a console can host several sites and
the widget only shows one. Naming it makes that choice visible. */}
<Group gap={6} wrap="nowrap">
@@ -116,11 +119,23 @@ export default function UnifiOsWidget({ integrationIds, options }: WidgetCompone
)}
</Group>
{/* The one block that grows; the header, the stats rows and the footer keep
their natural size. No `minHeight: 0` here on purpose: a flex item's
automatic minimum is its content, so on a tile too short for everything
the card stops shrinking at its own content and the tile scrolls, as it
already did, rather than the card being squeezed and spilling the graph
over the rows below it. */}
<Stack
gap={4}
p="xs"
style={{ borderRadius: "var(--mantine-radius-md)", background: "var(--mantine-color-default-hover)" }}
style={{
flex: 1,
borderRadius: "var(--mantine-radius-md)",
background: "var(--mantine-color-default-hover)",
}}
>
{/* The rates ride the header row, right-aligned opposite the title, so
they read as a heading and detach from the area below them. */}
<Group justify="space-between" wrap="nowrap" gap="xs">
<Group gap={6} wrap="nowrap">
<IconCircleFilled size={10} color={`var(--mantine-color-${wanColor}-6)`} />
@@ -128,10 +143,15 @@ export default function UnifiOsWidget({ integrationIds, options }: WidgetCompone
{t("widget.unifiOs.card.internet")}
</Text>
</Group>
{summary.wan.throughput === null && (
{summary.wan.throughput === null ? (
<Text size="sm" ff="monospace">
{EMPTY_VALUE}
</Text>
) : (
<ThroughputLegend
upLabel={formatBitsPerSecond(summary.wan.throughput.txBps)}
downLabel={formatBitsPerSecond(summary.wan.throughput.rxBps)}
/>
)}
</Group>
@@ -141,8 +161,6 @@ export default function UnifiOsWidget({ integrationIds, options }: WidgetCompone
{summary.wan.throughput !== null && (
<ThroughputChart
history={throughputHistory}
upLabel={formatBitsPerSecond(summary.wan.throughput.txBps)}
downLabel={formatBitsPerSecond(summary.wan.throughput.rxBps)}
formatRate={formatBitsPerSecond}
upName={t("widget.unifiOs.card.up")}
downName={t("widget.unifiOs.card.down")}
@@ -1,7 +1,7 @@
"use client";
import { AreaChart } from "@mantine/charts";
import { Group, Paper, Stack, Text } from "@mantine/core";
import { Box, Group, Paper, Stack, Text } from "@mantine/core";
import type { ThroughputPoint } from "./throughput-history";
@@ -19,7 +19,13 @@ import type { ThroughputPoint } from "./throughput-history";
const DOWN_COLOR = "blue.7";
const UP_COLOR = "orange.8";
const CHART_HEIGHT = 44;
/**
* A floor, not a height: the chart now takes the height the card has left over.
* `ResponsiveContainer` measures its parent and renders nothing at all when that
* parent collapses to zero, so the wrapper keeps a small non-zero minimum — on a
* very short tile the area stays legible instead of disappearing.
*/
const CHART_MIN_HEIGHT = 32;
/**
* A dot carries the series identity so the figure beside it can stay in muted
@@ -69,34 +75,46 @@ const ThroughputTooltip = ({
);
};
export interface ThroughputChartProps {
history: ThroughputPoint[];
export interface ThroughputLegendProps {
/** Rendered beside the up marker, already formatted. */
upLabel: string;
/** Rendered beside the down marker, already formatted. */
downLabel: string;
}
/**
* Sits on the card's "Internet" header row rather than above the graph, so the
* figures read as a heading and detach from the area they summarise. Exported
* from here instead of being rewritten there: the dots take the very constants
* the chart's series use, so the two can never drift apart.
*/
export const ThroughputLegend = ({ upLabel, downLabel }: ThroughputLegendProps) => (
<Group gap="sm" wrap="nowrap" justify="flex-end">
<SeriesKey color={DOWN_COLOR} label={`↓ ${downLabel}`} />
<SeriesKey color={UP_COLOR} label={`↑ ${upLabel}`} />
</Group>
);
export interface ThroughputChartProps {
history: ThroughputPoint[];
/** Formats a raw bits-per-second value for the tooltip. */
formatRate: (bitsPerSecond: number) => string;
upName: string;
downName: string;
}
export const ThroughputChart = ({
history,
upLabel,
downLabel,
formatRate,
upName,
downName,
}: ThroughputChartProps) => (
<Stack gap={2}>
<Group gap="sm" wrap="nowrap" justify="flex-end">
<SeriesKey color={DOWN_COLOR} label={`↓ ${downLabel}`} />
<SeriesKey color={UP_COLOR} label={`↑ ${upLabel}`} />
</Group>
/**
* The wrapper is the flex child that claims the card's leftover height, and the
* chart fills it with `h="100%"`. Mantine turns that into an inline
* `height: 100%` on the chart root, which recharts' `ResponsiveContainer` then
* measures; a percentage resolves here because flex layout gives this wrapper a
* definite used height, the card above it being a flex item of a `h="100%"`
* stack inside a tile of fixed height.
*/
export const ThroughputChart = ({ history, formatRate, upName, downName }: ThroughputChartProps) => (
<Box style={{ flex: 1, minHeight: CHART_MIN_HEIGHT }}>
<AreaChart
h={CHART_HEIGHT}
h="100%"
data={history}
dataKey="index"
series={[
@@ -115,5 +133,5 @@ export const ThroughputChart = ({
content: <ThroughputTooltip formatRate={formatRate} upName={upName} downName={downName} />,
}}
/>
</Stack>
</Box>
);