fix(widgets): restore compact v1 surfaces (#6710)
This commit is contained in:
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]} />;
|
||||
};
|
||||
Reference in new issue
Block a user