feat: modernize dashboard widgets

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-13 19:54:28 +02:00
1 parent 1a5e083fd0
commit 6f2cd7fc2b
270 files changed
+10189 -3544

No files matched your search

@@ -1,7 +1,7 @@
"use client";
import { useMemo } from "react";
import { Box } from "@mantine/core";
import { Box, Card, SimpleGrid } from "@mantine/core";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import objectSupport from "dayjs/plugin/objectSupport";
@@ -20,26 +20,33 @@ dayjs.extend(duration);
export default function NetworkControllerNetworkStatusWidget({
options,
integrationIds,
displayMode,
}: WidgetComponentProps<"networkControllerStatus">) {
const { data: summaries = [] } = clientApi.widget.networkController.summary.useQuery({
integrationIds,
});
const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]);
const isAdvanced = displayMode === "advanced";
const countWifiGuests = data.reduce((sum, summary) => sum + summary.wifi.guests, 0);
const countWifiUsers = data.reduce((sum, summary) => sum + summary.wifi.users, 0);
const countLanGuests = data.reduce((sum, summary) => sum + summary.lan.guests, 0);
const countLanUsers = data.reduce((sum, summary) => sum + summary.lan.users, 0);
return (
<Box p={"sm"}>
{options.content === "wifi" ? (
<WifiVariant
countGuests={data.reduce((sum, summary) => sum + summary.wifi.guests, 0)}
countUsers={data.reduce((sum, summary) => sum + summary.wifi.users, 0)}
/>
) : (
<WiredVariant
countGuests={data.reduce((sum, summary) => sum + summary.lan.guests, 0)}
countUsers={data.reduce((sum, summary) => sum + summary.lan.users, 0)}
/>
)}
<Box p={isAdvanced ? "md" : "sm"} h="100%">
<SimpleGrid cols={isAdvanced ? 2 : 1} h="100%" spacing="sm">
{(isAdvanced || options.content === "wifi") && (
<Card p={isAdvanced ? "md" : 0} withBorder={isAdvanced}>
<WifiVariant countGuests={countWifiGuests} countUsers={countWifiUsers} compact={!isAdvanced} />
</Card>
)}
{(isAdvanced || options.content === "wired") && (
<Card p={isAdvanced ? "md" : 0} withBorder={isAdvanced}>
<WiredVariant countGuests={countLanGuests} countUsers={countLanUsers} compact={!isAdvanced} />
</Card>
)}
</SimpleGrid>
</Box>
);
}
@@ -1,12 +1,20 @@
import { Stack, Text } from "@mantine/core";
export const StatRow = ({ label, value }: { label: string; value: string | number }) => {
export const StatRow = ({
label,
value,
compact = false,
}: {
label: string;
value: string | number;
compact?: boolean;
}) => {
return (
<Stack gap={0}>
<Text size={"2xl"} fw={900} lh={1}>
<Text size={compact ? "xl" : "2xl"} fw={800} lh={1}>
{value}
</Text>
<Text size={"md"} c={"dimmed"}>
<Text size={compact ? "sm" : "md"} c="dimmed">
{label}
</Text>
</Stack>
@@ -5,19 +5,27 @@ import { useScopedI18n } from "@homarr/translation/client";
import { StatRow } from "./stat-row";
export const WifiVariant = ({ countGuests, countUsers }: { countUsers: number; countGuests: number }) => {
export const WifiVariant = ({
countGuests,
countUsers,
compact = false,
}: {
countUsers: number;
countGuests: number;
compact?: boolean;
}) => {
const t = useScopedI18n("widget.networkControllerStatus.card");
return (
<>
<Group gap={"xs"} wrap={"nowrap"} mb={"md"}>
<Group gap="xs" wrap="nowrap" mb={compact ? "xs" : "md"}>
<IconWifi size={24} />
<Text size={"md"} fw={"bold"}>
{t("variants.wifi.name")}
</Text>
</Group>
<Stack gap={"lg"}>
<StatRow label={t("users.label")} value={countUsers} />
<StatRow label={t("guests.label")} value={countGuests} />
<Stack gap={compact ? "sm" : "lg"}>
<StatRow label={t("users.label")} value={countUsers} compact={compact} />
<StatRow label={t("guests.label")} value={countGuests} compact={compact} />
</Stack>
</>
);
@@ -5,19 +5,27 @@ import { useScopedI18n } from "@homarr/translation/client";
import { StatRow } from "./stat-row";
export const WiredVariant = ({ countGuests, countUsers }: { countUsers: number; countGuests: number }) => {
export const WiredVariant = ({
countGuests,
countUsers,
compact = false,
}: {
countUsers: number;
countGuests: number;
compact?: boolean;
}) => {
const t = useScopedI18n("widget.networkControllerStatus.card");
return (
<>
<Group gap={"xs"} wrap={"nowrap"} mb={"md"}>
<Group gap="xs" wrap="nowrap" mb={compact ? "xs" : "md"}>
<IconNetwork size={24} />
<Text size={"md"} fw={"bold"}>
{t("variants.wired.name")}
</Text>
</Group>
<Stack gap={"lg"}>
<StatRow label={t("users.label")} value={countUsers} />
<StatRow label={t("guests.label")} value={countGuests} />
<Stack gap={compact ? "sm" : "lg"}>
<StatRow label={t("users.label")} value={countUsers} compact={compact} />
<StatRow label={t("guests.label")} value={countGuests} compact={compact} />
</Stack>
</>
);
@@ -1,7 +1,6 @@
"use client";
import { useMemo } from "react";
import { Box, Center, List, Text, useMantineTheme } from "@mantine/core";
import { Badge, Card, 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";
@@ -19,6 +18,8 @@ dayjs.extend(duration);
export default function NetworkControllerSummaryWidget({
integrationIds,
width,
displayMode,
}: WidgetComponentProps<"networkControllerSummary">) {
const { data: summaries = [] } = clientApi.widget.networkController.summary.useQuery({
integrationIds,
@@ -26,33 +27,57 @@ export default function NetworkControllerSummaryWidget({
const t = useI18n();
const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]);
const isAdvanced = displayMode === "advanced";
const columns = isAdvanced && width >= 720 ? 2 : 1;
return (
<Box h="100%" p="sm">
<Center h={"100%"}>
<List spacing={"xs"} center>
<List.Item icon={<StatusIcon status={data[0]?.wanStatus} />}>WAN</List.Item>
<List.Item icon={<StatusIcon status={data[0]?.www.status} />}>
<Text>
WWW
<Text c={"dimmed"} size={"md"} ms={"xs"} span>
{data[0]?.www.latency}ms
</Text>
</Text>
</List.Item>
<List.Item icon={<StatusIcon status={data[0]?.wifi.status} />}>Wi-Fi</List.Item>
<List.Item icon={<StatusIcon status={data[0]?.vpn.status} />}>
<Text>
VPN
<Text c={"dimmed"} size={"md"} ms={"xs"} span>
{t("widget.networkControllerSummary.card.vpn.countConnected", { count: `${data[0]?.vpn.users}` })}
</Text>
</Text>
</List.Item>
</List>
</Center>
</Box>
<ScrollArea h="100%" p={isAdvanced ? "md" : "sm"}>
<SimpleGrid cols={columns} spacing="sm">
{summaries.map(({ integration, summary }) => (
<Card key={integration.id} p={isAdvanced ? "md" : "xs"} withBorder={summaries.length > 1 || isAdvanced}>
<Stack gap="xs">
{(isAdvanced || summaries.length > 1) && (
<Group justify="space-between" wrap="nowrap">
<Text fw={600} size="sm" truncate="end">
{integration.name}
</Text>
<Badge size="xs" variant="light">
{integration.kind}
</Badge>
</Group>
)}
<List spacing="xs" center>
<List.Item icon={<StatusIcon status={summary.wanStatus} />}>WAN</List.Item>
<List.Item icon={<StatusIcon status={summary.www.status} />}>
<Text>
WWW
<Text c="dimmed" size="sm" ms="xs" span>
{summary.www.latency}ms{isAdvanced ? ` · ${summary.www.ping}ms` : ""}
</Text>
</Text>
</List.Item>
<List.Item icon={<StatusIcon status={summary.wifi.status} />}>
Wi-Fi{isAdvanced ? ` · ${summary.wifi.users + summary.wifi.guests}` : ""}
</List.Item>
{isAdvanced && (
<List.Item icon={<StatusIcon status={summary.lan.status} />}>
LAN · {summary.lan.users + summary.lan.guests}
</List.Item>
)}
<List.Item icon={<StatusIcon status={summary.vpn.status} />}>
<Text>
VPN
<Text c="dimmed" size="sm" ms="xs" span>
{t("widget.networkControllerSummary.card.vpn.countConnected", { count: `${summary.vpn.users}` })}
</Text>
</Text>
</List.Item>
</List>
</Stack>
</Card>
))}
</SimpleGrid>
</ScrollArea>
);
}
@@ -61,5 +86,6 @@ const StatusIcon = ({ status }: { status?: "enabled" | "disabled" }) => {
if (status === "enabled") {
return <IconCircleCheckFilled size={20} color={mantineTheme.colors.green[6]} />;
}
return <IconCircleXFilled size={20} color={mantineTheme.colors.red[6]} />;
if (status === "disabled") return <IconCircleXFilled size={20} color={mantineTheme.colors.red[6]} />;
return <IconCircleXFilled size={20} color={mantineTheme.colors.gray[6]} />;
};