fix(widgets): restore compact v1 surfaces (#6710)

This commit is contained in:
Thomas authored and GitHub committed 2026-08-27 11:08:17 +02:00
1 parent 1b2f1bd720
commit 643ecf3df7
105 files changed
+1603 -1285

No files matched your search

@@ -1,7 +1,6 @@
"use client";
import { useMemo } from "react";
import { Badge, Card, Center, Group, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
import { Badge, Box, Card, Center, Group, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
@@ -13,8 +12,7 @@ import { getNetworkControllerStatusLayout } from "./layout";
import { WifiVariant } from "./variants/wifi-variant";
import { WiredVariant } from "./variants/wired-variant";
const neutralBorderColor =
"rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45))";
const neutralBorderColor = "rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45))";
export default function NetworkControllerNetworkStatusWidget({
options,
@@ -22,17 +20,18 @@ export default function NetworkControllerNetworkStatusWidget({
displayMode,
width,
height,
displayScale = 1,
}: WidgetComponentProps<"networkControllerStatus">) {
const summaryQuery = clientApi.widget.networkController.summary.useQuery({
integrationIds,
});
const results = getUsableWidgetQueryData(summaryQuery) ?? [];
const summaries = useMemo(
() =>
results.filter(
(result): result is typeof result & { summary: NonNullable<typeof result.summary> } => result.summary !== null,
),
[results],
const isAdvanced = displayMode === "advanced";
let results = summaryQuery.data ?? [];
if (isAdvanced) {
results = getUsableWidgetQueryData(summaryQuery) ?? [];
}
const summaries = results.filter(
(result): result is typeof result & { summary: NonNullable<typeof result.summary> } => result.summary !== null,
);
const tWidgetCommon = useI18n("widget.common");
const tCommon = useI18n("common");
@@ -42,14 +41,55 @@ export default function NetworkControllerNetworkStatusWidget({
const countWifiUsers = summaries.reduce((sum, { summary }) => sum + summary.wifi.users, 0);
const countLanGuests = summaries.reduce((sum, { summary }) => sum + summary.lan.guests, 0);
const countLanUsers = summaries.reduce((sum, { summary }) => sum + summary.lan.users, 0);
const layout = getNetworkControllerStatusLayout({ width, height, displayMode, content: options.content });
const isAdvanced = displayMode === "advanced";
let responsiveWidth = width;
let responsiveHeight = height;
if (!isAdvanced && Number.isFinite(displayScale) && displayScale > 0) {
responsiveWidth *= displayScale;
responsiveHeight *= displayScale;
}
const layout = getNetworkControllerStatusLayout({
width: responsiveWidth,
height: responsiveHeight,
displayMode,
content: options.content,
});
const indicatorResults =
results.length > 0 || !summaryQuery.error
? results
: integrationIds.map((integrationId) => ({ integrationId, error: "query" }));
const queryIndicators = (
<Group gap={0} justify="flex-end">
<IntegrationErrorIndicator results={results} />
<IntegrationErrorIndicator results={indicatorResults} />
</Group>
);
if (!isAdvanced) {
return (
<Box h="100%" p={layout.padding} pos="relative">
<Box pos="absolute" top={4} right={8} style={{ zIndex: 2 }}>
{queryIndicators}
</Box>
{options.content === "wifi" ? (
<WifiVariant
countGuests={countWifiGuests}
countUsers={countWifiUsers}
compact={layout.compact}
horizontal={layout.horizontalStats}
inline={layout.inlineStats}
/>
) : (
<WiredVariant
countGuests={countLanGuests}
countUsers={countLanUsers}
compact={layout.compact}
horizontal={layout.horizontalStats}
inline={layout.inlineStats}
/>
)}
</Box>
);
}
if (summaryQuery.isPending || summaries.length === 0) {
return (
<Stack h="100%" gap={0}>
@@ -63,92 +103,40 @@ export default function NetworkControllerNetworkStatusWidget({
);
}
if (isAdvanced) {
return (
<ScrollArea h="100%" p={layout.padding}>
{queryIndicators}
<SimpleGrid cols={summaries.length > 1 ? layout.sourceColumns : 1} spacing="sm">
{summaries.map(({ integration, summary, updatedAt }) => (
<Card key={integration.id} withBorder p="sm" bg="transparent" style={{ borderColor: neutralBorderColor }}>
<Stack gap="sm">
<Group justify="space-between" wrap="nowrap">
<Stack gap={0} miw={0}>
<Text fw={600} size="sm" truncate="end">
{integration.name}
</Text>
{updatedAt && (
<Text size="xs" c="dimmed">
{tWidgetCommon("updatedAt", { date: new Date(updatedAt).toLocaleString(locale) })}
</Text>
)}
</Stack>
<Badge size="xs" variant="light">
{integration.kind}
</Badge>
</Group>
<SimpleGrid cols={layout.columns} spacing="sm">
<Card
p={layout.cardPadding}
withBorder
bg="transparent"
style={{ borderColor: neutralBorderColor }}
>
<WifiVariant countGuests={summary.wifi.guests} countUsers={summary.wifi.users} />
</Card>
<Card
p={layout.cardPadding}
withBorder
bg="transparent"
style={{ borderColor: neutralBorderColor }}
>
<WiredVariant countGuests={summary.lan.guests} countUsers={summary.lan.users} />
</Card>
</SimpleGrid>
</Stack>
</Card>
))}
</SimpleGrid>
</ScrollArea>
);
}
return (
<Stack p={layout.padding} h="100%" gap={0}>
<ScrollArea h="100%" p={layout.padding}>
{queryIndicators}
<SimpleGrid cols={layout.columns} spacing="sm" style={{ flex: 1, alignItems: "stretch" }}>
{layout.showWifi && (
<Card
p={layout.cardPadding}
withBorder={layout.withBorder}
bg="transparent"
h="100%"
style={{ borderColor: neutralBorderColor }}
>
<WifiVariant
countGuests={countWifiGuests}
countUsers={countWifiUsers}
compact={layout.compact}
horizontal={layout.horizontalStats}
/>
<SimpleGrid cols={summaries.length > 1 ? layout.sourceColumns : 1} spacing="sm">
{summaries.map(({ integration, summary, updatedAt }) => (
<Card key={integration.id} withBorder p="sm" bg="transparent" style={{ borderColor: neutralBorderColor }}>
<Stack gap="sm">
<Group justify="space-between" wrap="nowrap">
<Stack gap={0} miw={0}>
<Text fw={600} size="sm" truncate="end">
{integration.name}
</Text>
{updatedAt && (
<Text size="xs" c="dimmed">
{tWidgetCommon("updatedAt", { date: new Date(updatedAt).toLocaleString(locale) })}
</Text>
)}
</Stack>
<Badge size="xs" variant="light">
{integration.kind}
</Badge>
</Group>
<SimpleGrid cols={layout.columns} spacing="sm">
<Card p={layout.cardPadding} withBorder bg="transparent" style={{ borderColor: neutralBorderColor }}>
<WifiVariant countGuests={summary.wifi.guests} countUsers={summary.wifi.users} advanced />
</Card>
<Card p={layout.cardPadding} withBorder bg="transparent" style={{ borderColor: neutralBorderColor }}>
<WiredVariant countGuests={summary.lan.guests} countUsers={summary.lan.users} advanced />
</Card>
</SimpleGrid>
</Stack>
</Card>
)}
{layout.showWired && (
<Card
p={layout.cardPadding}
withBorder={layout.withBorder}
bg="transparent"
h="100%"
style={{ borderColor: neutralBorderColor }}
>
<WiredVariant
countGuests={countLanGuests}
countUsers={countLanUsers}
compact={layout.compact}
horizontal={layout.horizontalStats}
/>
</Card>
)}
))}
</SimpleGrid>
</Stack>
</ScrollArea>
);
}
@@ -7,7 +7,7 @@ describe("network controller status layout", () => {
{
name: "minimum compact",
input: { width: 160, height: 90, displayMode: "compact" as const, content: "wifi" as const },
expected: { padding: "xs", columns: 1, showWifi: true, showWired: false, horizontalStats: false },
expected: { padding: 4, columns: 1, showWifi: true, showWired: false, horizontalStats: true },
},
{
name: "short compact",
@@ -58,7 +58,13 @@ describe("network controller status layout", () => {
).toMatchObject({ padding: "sm", horizontalStats: false });
expect(
getNetworkControllerStatusLayout({ width: 199, height: 119, displayMode: "compact", content: "wifi" }),
).toMatchObject({ padding: "xs", horizontalStats: false });
).toMatchObject({ padding: 4, horizontalStats: true });
expect(
getNetworkControllerStatusLayout({ width: 199, height: 80, displayMode: "compact", content: "wifi" }),
).toMatchObject({ inlineStats: false });
expect(
getNetworkControllerStatusLayout({ width: 199, height: 79, displayMode: "compact", content: "wifi" }),
).toMatchObject({ inlineStats: true });
expect(
getNetworkControllerStatusLayout({ width: 960, height: 320, displayMode: "advanced", content: "wifi" }),
).toMatchObject({ sourceColumns: 2 });
@@ -11,7 +11,7 @@ interface NetworkControllerStatusLayoutInput {
}
export interface NetworkControllerStatusLayout {
padding: "xs" | "sm" | "md";
padding: 4 | "xs" | "sm" | "md";
columns: 1 | 2;
sourceColumns: 1 | 2;
showWifi: boolean;
@@ -20,6 +20,7 @@ export interface NetworkControllerStatusLayout {
withBorder: boolean;
compact: boolean;
horizontalStats: boolean;
inlineStats: boolean;
}
export const getNetworkControllerStatusLayout = ({
@@ -29,16 +30,18 @@ export const getNetworkControllerStatusLayout = ({
content,
}: NetworkControllerStatusLayoutInput): NetworkControllerStatusLayout => {
const isAdvanced = displayMode === "advanced";
const compact = !isAdvanced && (width < 240 || height < 180);
return {
padding: isAdvanced ? "md" : height < 120 ? "xs" : "sm",
padding: isAdvanced ? "md" : height < 120 ? 4 : "sm",
columns: isAdvanced && width >= 560 ? 2 : 1,
sourceColumns: isAdvanced && width >= 960 ? 2 : 1,
showWifi: isAdvanced || content === "wifi",
showWired: isAdvanced || content === "wired",
cardPadding: isAdvanced ? "md" : 0,
withBorder: isAdvanced,
compact: !isAdvanced,
horizontalStats: !isAdvanced && height < 150 && width >= 200,
compact,
horizontalStats: compact && height < 150,
inlineStats: compact && height < 80,
};
};
@@ -1,14 +1,44 @@
import { Stack, Text } from "@mantine/core";
import { Group, Stack, Text } from "@mantine/core";
export const StatRow = ({
label,
value,
advanced = false,
compact = false,
inline = false,
}: {
label: string;
value: string | number;
advanced?: boolean;
compact?: boolean;
inline?: boolean;
}) => {
if (!advanced && compact && inline) {
return (
<Group gap={2} align="baseline" justify="center" wrap="nowrap" miw={0}>
<Text size="xl" fw={800} lh={1} style={{ flexShrink: 0 }}>
{value}
</Text>
<Text size="sm" c="dimmed" truncate>
{label}
</Text>
</Group>
);
}
if (!advanced) {
return (
<Stack gap={0}>
<Text size={compact ? "xl" : "2xl"} fw={compact ? 800 : 900} lh={1}>
{value}
</Text>
<Text size={compact ? "sm" : "md"} c="dimmed">
{label}
</Text>
</Stack>
);
}
return (
<Stack gap={2} align="center" justify="center">
<Text size={compact ? "xl" : "2xl"} fw={800} lh={1} ta="center">
@@ -2,22 +2,50 @@ import { Center, Group, SimpleGrid, Stack, Text } from "@mantine/core";
import { IconWifi } from "@tabler/icons-react";
import { useI18n } from "@homarr/translation/client";
import { iconSizes } from "@homarr/ui";
import { iconSizes, zoomCompensatedSize } from "@homarr/ui";
import { StatRow } from "./stat-row";
export const WifiVariant = ({
countGuests,
countUsers,
advanced = false,
compact = false,
horizontal = false,
inline = false,
}: {
countUsers: number;
countGuests: number;
advanced?: boolean;
compact?: boolean;
horizontal?: boolean;
inline?: boolean;
}) => {
const t = useI18n("widget.networkControllerStatus.card");
if (!advanced) {
const iconSize = horizontal ? 20 : 24;
let titleMargin: 0 | "xs" | "md" = "md";
if (compact) titleMargin = "xs";
if (horizontal) titleMargin = 0;
let statsSpacing: number | "xs" | "lg" = "lg";
if (compact) statsSpacing = "xs";
if (horizontal) statsSpacing = 4;
return (
<>
<Group gap="xs" wrap="nowrap" mb={titleMargin}>
<IconWifi style={zoomCompensatedSize(iconSize)} />
<Text size="md" fw="bold" style={{ whiteSpace: "nowrap", flexShrink: 0 }}>
{t("variants.wifi.name")}
</Text>
</Group>
<SimpleGrid cols={horizontal ? 2 : 1} spacing={statsSpacing}>
<StatRow label={t("users.label")} value={countUsers} compact={compact} inline={inline} />
<StatRow label={t("guests.label")} value={countGuests} compact={compact} inline={inline} />
</SimpleGrid>
</>
);
}
return (
<Stack h="100%" align="center" justify="center" gap={compact ? "sm" : "md"}>
<Group gap="xs" wrap="nowrap" justify="center">
@@ -29,8 +57,8 @@ export const WifiVariant = ({
</Text>
</Group>
<SimpleGrid cols={horizontal ? 2 : 1} spacing={compact ? "sm" : "lg"} w="100%" maw={horizontal ? 360 : 220}>
<StatRow label={t("users.label")} value={countUsers} compact={compact} />
<StatRow label={t("guests.label")} value={countGuests} compact={compact} />
<StatRow label={t("users.label")} value={countUsers} compact={compact} advanced />
<StatRow label={t("guests.label")} value={countGuests} compact={compact} advanced />
</SimpleGrid>
</Stack>
);
@@ -2,22 +2,50 @@ import { Center, Group, SimpleGrid, Stack, Text } from "@mantine/core";
import { IconNetwork } from "@tabler/icons-react";
import { useI18n } from "@homarr/translation/client";
import { iconSizes } from "@homarr/ui";
import { iconSizes, zoomCompensatedSize } from "@homarr/ui";
import { StatRow } from "./stat-row";
export const WiredVariant = ({
countGuests,
countUsers,
advanced = false,
compact = false,
horizontal = false,
inline = false,
}: {
countUsers: number;
countGuests: number;
advanced?: boolean;
compact?: boolean;
horizontal?: boolean;
inline?: boolean;
}) => {
const t = useI18n("widget.networkControllerStatus.card");
if (!advanced) {
const iconSize = horizontal ? 20 : 24;
let titleMargin: 0 | "xs" | "md" = "md";
if (compact) titleMargin = "xs";
if (horizontal) titleMargin = 0;
let statsSpacing: number | "xs" | "lg" = "lg";
if (compact) statsSpacing = "xs";
if (horizontal) statsSpacing = 4;
return (
<>
<Group gap="xs" wrap="nowrap" mb={titleMargin}>
<IconNetwork style={zoomCompensatedSize(iconSize)} />
<Text size="md" fw="bold" style={{ whiteSpace: "nowrap", flexShrink: 0 }}>
{t("variants.wired.name")}
</Text>
</Group>
<SimpleGrid cols={horizontal ? 2 : 1} spacing={statsSpacing}>
<StatRow label={t("users.label")} value={countUsers} compact={compact} inline={inline} />
<StatRow label={t("guests.label")} value={countGuests} compact={compact} inline={inline} />
</SimpleGrid>
</>
);
}
return (
<Stack h="100%" align="center" justify="center" gap={compact ? "sm" : "md"}>
<Group gap="xs" wrap="nowrap" justify="center">
@@ -29,8 +57,8 @@ export const WiredVariant = ({
</Text>
</Group>
<SimpleGrid cols={horizontal ? 2 : 1} spacing={compact ? "sm" : "lg"} w="100%" maw={horizontal ? 360 : 220}>
<StatRow label={t("users.label")} value={countUsers} compact={compact} />
<StatRow label={t("guests.label")} value={countGuests} compact={compact} />
<StatRow label={t("users.label")} value={countUsers} compact={compact} advanced />
<StatRow label={t("guests.label")} value={countGuests} compact={compact} advanced />
</SimpleGrid>
</Stack>
);
@@ -1,7 +1,7 @@
"use client";
import type { CSSProperties } from "react";
import { Badge, Card, Center, Group, List, ScrollArea, SimpleGrid, Stack, Text, useMantineTheme } from "@mantine/core";
import { Box, Card, Center, Group, List, ScrollArea, SimpleGrid, Stack, Text, useMantineTheme } from "@mantine/core";
import { IconCircleCheckFilled, IconCircleXFilled } from "@tabler/icons-react";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
@@ -27,13 +27,16 @@ dayjs.extend(duration);
export default function NetworkControllerSummaryWidget({
integrationIds,
width,
height,
displayMode,
}: WidgetComponentProps<"networkControllerSummary">) {
const summaryQuery = clientApi.widget.networkController.summary.useQuery({
integrationIds,
});
const results = getUsableWidgetQueryData(summaryQuery) ?? [];
const isAdvanced = displayMode === "advanced";
let results = summaryQuery.data ?? [];
if (isAdvanced) {
results = getUsableWidgetQueryData(summaryQuery) ?? [];
}
const summaries = results.filter(
(result): result is typeof result & { summary: NonNullable<typeof result.summary>; updatedAt: Date } =>
result.summary !== null && result.updatedAt !== undefined,
@@ -51,17 +54,18 @@ export default function NetworkControllerSummaryWidget({
unknown: tDns("status.unknown"),
};
const isDense = height < 160;
const showDetails = height >= 180 && width >= 240;
const columns = width >= 720 ? 2 : 1;
const isAdvanced = displayMode === "advanced";
const indicatorResults =
results.length > 0 || !summaryQuery.error
? results
: integrationIds.map((integrationId) => ({ integrationId, error: "query" }));
const queryIndicators = (
<Group gap={0} justify="flex-end">
<IntegrationErrorIndicator results={results} />
<IntegrationErrorIndicator results={indicatorResults} />
</Group>
);
const firstSummary = summaries[0];
if (isPending || summaries.length === 0) {
if (isPending || !firstSummary) {
return (
<Stack h="100%" gap={0}>
{queryIndicators}
@@ -74,190 +78,152 @@ export default function NetworkControllerSummaryWidget({
);
}
if (isAdvanced) {
const controllerColumns = width >= 960 ? 2 : 1;
const matrixColumns = width >= 720 ? 3 : width >= 440 ? 2 : 1;
if (!isAdvanced) {
const summary = firstSummary.summary;
return (
<ScrollArea h="100%" p="sm" pos="relative">
{queryIndicators}
<SimpleGrid cols={controllerColumns} spacing="sm">
{summaries.map(({ integration, summary, updatedAt }) => (
<Card
key={integration.id}
p="sm"
withBorder
bg="transparent"
style={{ borderColor: neutralSurfaceBorder }}
<Box h="100%" p="sm" pos="relative">
<Box pos="absolute" top={4} right={8} style={{ zIndex: 2 }}>
{queryIndicators}
</Box>
<Center h="100%">
<List spacing="xs" center>
<List.Item
icon={
<StatusIcon
status={summary.wanStatus}
label={statusLabels[getBinaryStatusKey(summary.wanStatus)]}
style={iconSizes.xl}
/>
}
>
<Stack gap="sm">
<Group justify="space-between" align="flex-start" wrap="nowrap">
<Stack gap={0} style={{ minWidth: 0 }}>
<Text fw={600} size="sm" truncate="end">
{integration.name}
</Text>
<Text size="xs" c="dimmed">
{integration.kind}
</Text>
</Stack>
<Stack gap={0} align="flex-end" style={{ flexShrink: 0 }}>
<Text size="10px" c="dimmed">
{t("advanced.updated")}
</Text>
<Text size="xs" c="dimmed" ta="right">
{new Date(updatedAt).toLocaleString(locale)}
</Text>
</Stack>
</Group>
<SimpleGrid cols={matrixColumns} spacing="xs">
{getNetworkControllerMatrix(summary).map((section) => (
<Card
key={section.key}
p="xs"
withBorder
bg="transparent"
style={{ borderColor: neutralSurfaceBorder }}
>
<Stack gap="xs">
<Group justify="space-between" wrap="nowrap">
<Group gap={6} wrap="nowrap">
<StatusIcon
status={section.status}
label={statusLabels[getBinaryStatusKey(section.status)]}
size="var(--mantine-font-size-md)"
/>
<Text size="sm" fw={600}>
{getMatrixSectionLabel(section.key, t)}
</Text>
</Group>
<Text size="xs" c="dimmed">
{statusLabels[getBinaryStatusKey(section.status)]}
</Text>
</Group>
{section.metrics.length > 0 && (
<SimpleGrid cols={2} spacing={4} verticalSpacing={2}>
{section.metrics.map((metric) => (
<Stack key={metric.key} gap={0}>
<Text size="10px" c="dimmed">
{getMatrixMetricLabel(metric.key, t)}
</Text>
<Text size="xs" fw={500}>
{formatMatrixMetric(metric.key, metric.value)}
</Text>
</Stack>
))}
</SimpleGrid>
)}
</Stack>
</Card>
))}
</SimpleGrid>
</Stack>
</Card>
))}
</SimpleGrid>
</ScrollArea>
{t("card.wan")}
</List.Item>
<List.Item
icon={
<StatusIcon
status={summary.www.status}
label={statusLabels[getBinaryStatusKey(summary.www.status)]}
style={iconSizes.xl}
/>
}
>
<Text>
WWW
<Text c="dimmed" size="md" ms="xs" span>
{summary.www.latency}ms
</Text>
</Text>
</List.Item>
<List.Item
icon={
<StatusIcon
status={summary.wifi.status}
label={statusLabels[getBinaryStatusKey(summary.wifi.status)]}
style={iconSizes.xl}
/>
}
>
{t("card.wifi")}
</List.Item>
<List.Item
icon={
<StatusIcon
status={summary.vpn.status}
label={statusLabels[getBinaryStatusKey(summary.vpn.status)]}
style={iconSizes.xl}
/>
}
>
<Text>
{t("card.vpn.label")}
<Text c="dimmed" size="md" ms="xs" span>
{t("card.vpn.countConnected", { count: summary.vpn.users })}
</Text>
</Text>
</List.Item>
</List>
</Center>
</Box>
);
}
const controllerColumns = width >= 960 ? 2 : 1;
const matrixColumns = width >= 720 ? 3 : width >= 440 ? 2 : 1;
return (
<Stack h="100%" p={isDense ? "xs" : "sm"} gap={0} style={{ overflow: "hidden" }}>
<ScrollArea h="100%" p="sm" pos="relative">
{queryIndicators}
<SimpleGrid cols={columns} spacing="sm" style={{ flex: 1, minHeight: 0, alignContent: "center" }}>
{summaries.map(({ integration, summary }) => (
<Card
key={integration.id}
p={isDense ? "xs" : "sm"}
withBorder={summaries.length > 1}
bg="transparent"
style={{ borderColor: neutralSurfaceBorder }}
>
<Stack h="100%" gap={isDense ? 4 : 8} justify="center" align="center">
{summaries.length > 1 && (
<Group justify="space-between" wrap="nowrap">
<SimpleGrid cols={controllerColumns} spacing="sm">
{summaries.map(({ integration, summary, updatedAt }) => (
<Card key={integration.id} p="sm" withBorder bg="transparent" style={{ borderColor: neutralSurfaceBorder }}>
<Stack gap="sm">
<Group justify="space-between" align="flex-start" wrap="nowrap">
<Stack gap={0} style={{ minWidth: 0 }}>
<Text fw={600} size="sm" truncate="end">
{integration.name}
</Text>
<Badge size="xs" variant="light">
<Text size="xs" c="dimmed">
{integration.kind}
</Badge>
</Group>
)}
<List spacing={isDense ? 2 : 4} center size={isDense ? "xs" : "sm"} w="fit-content" mx="auto">
<List.Item
icon={
<StatusIcon
status={summary.wanStatus}
label={statusLabels[getBinaryStatusKey(summary.wanStatus)]}
style={isDense ? iconSizes.md : iconSizes.xl}
/>
}
>
{t("card.wan")}
</List.Item>
<List.Item
icon={
<StatusIcon
status={summary.www.status}
label={statusLabels[getBinaryStatusKey(summary.www.status)]}
style={isDense ? iconSizes.md : iconSizes.xl}
/>
}
>
<Text>
{t("card.web")}
<Text c="dimmed" size="sm" ms="xs" span>
{summary.www.latency}ms{showDetails ? ` · ${summary.www.ping}ms` : ""}
</Text>
</Text>
</List.Item>
<List.Item
icon={
<StatusIcon
status={summary.wifi.status}
label={statusLabels[getBinaryStatusKey(summary.wifi.status)]}
style={isDense ? iconSizes.md : iconSizes.xl}
/>
}
>
{t("card.wifi")}
{showDetails ? ` · ${summary.wifi.users + summary.wifi.guests}` : ""}
</List.Item>
{showDetails && (
<List.Item
icon={
<StatusIcon
status={summary.lan.status}
label={statusLabels[getBinaryStatusKey(summary.lan.status)]}
style={isDense ? iconSizes.md : iconSizes.xl}
/>
}
</Stack>
<Stack gap={0} align="flex-end" style={{ flexShrink: 0 }}>
<Text size="10px" c="dimmed">
{t("advanced.updated")}
</Text>
<Text size="xs" c="dimmed" ta="right">
{new Date(updatedAt).toLocaleString(locale)}
</Text>
</Stack>
</Group>
<SimpleGrid cols={matrixColumns} spacing="xs">
{getNetworkControllerMatrix(summary).map((section) => (
<Card
key={section.key}
p="xs"
withBorder
bg="transparent"
style={{ borderColor: neutralSurfaceBorder }}
>
{t("card.lan")} · {summary.lan.users + summary.lan.guests}
</List.Item>
)}
<List.Item
icon={
<StatusIcon
status={summary.vpn.status}
label={statusLabels[getBinaryStatusKey(summary.vpn.status)]}
style={isDense ? iconSizes.md : iconSizes.xl}
/>
}
>
<Text>
{t("card.vpn.label")}
<Text c="dimmed" size="sm" ms="xs" span>
{t("card.vpn.countConnected", { count: summary.vpn.users })}
</Text>
</Text>
</List.Item>
</List>
<Stack gap="xs">
<Group justify="space-between" wrap="nowrap">
<Group gap={6} wrap="nowrap">
<StatusIcon
status={section.status}
label={statusLabels[getBinaryStatusKey(section.status)]}
style={iconSizes.md}
/>
<Text size="sm" fw={600}>
{getMatrixSectionLabel(section.key, t)}
</Text>
</Group>
<Text size="xs" c="dimmed">
{statusLabels[getBinaryStatusKey(section.status)]}
</Text>
</Group>
{section.metrics.length > 0 && (
<SimpleGrid cols={2} spacing={4} verticalSpacing={2}>
{section.metrics.map((metric) => (
<Stack key={metric.key} gap={0}>
<Text size="10px" c="dimmed">
{getMatrixMetricLabel(metric.key, t)}
</Text>
<Text size="xs" fw={500}>
{formatMatrixMetric(metric.key, metric.value)}
</Text>
</Stack>
))}
</SimpleGrid>
)}
</Stack>
</Card>
))}
</SimpleGrid>
</Stack>
</Card>
))}
</SimpleGrid>
</Stack>
</ScrollArea>
);
}
@@ -280,26 +246,23 @@ const getMatrixSectionLabel = (
return t(`card.${key}` as never);
};
const neutralSurfaceBorder =
"rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45))";
const neutralSurfaceBorder = "rgb(from var(--mantine-color-default-border) r g b / calc(var(--opacity, 1) * 0.45))";
const StatusIcon = ({
status,
label,
size,
style,
}: {
status?: "enabled" | "disabled";
label: string;
size?: number | string;
style?: CSSProperties;
}) => {
const mantineTheme = useMantineTheme();
if (status === "enabled") {
return <IconCircleCheckFilled aria-label={label} size={size} style={style} color={mantineTheme.colors.green[6]} />;
return <IconCircleCheckFilled aria-label={label} style={style} color={mantineTheme.colors.green[6]} />;
}
if (status === "disabled") {
return <IconCircleXFilled aria-label={label} size={size} style={style} color={mantineTheme.colors.red[6]} />;
return <IconCircleXFilled aria-label={label} style={style} color={mantineTheme.colors.red[6]} />;
}
return <IconCircleXFilled aria-label={label} size={size} style={style} color={mantineTheme.colors.gray[6]} />;
return <IconCircleXFilled aria-label={label} style={style} color={mantineTheme.colors.gray[6]} />;
};