feat: unifi controller integration (#2236)
* feat: unifi controller integration * fix: pr feedback * fix: pr feedback * fix: pr feedback * fix: formatting * fix: pr feedback * fix: typecheck --------- Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com> Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Box } from "@mantine/core";
|
||||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import objectSupport from "dayjs/plugin/objectSupport";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
import { WifiVariant } from "./variants/wifi-variant";
|
||||
import { WiredVariant } from "./variants/wired-variant";
|
||||
|
||||
dayjs.extend(objectSupport);
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(duration);
|
||||
|
||||
export default function NetworkControllerNetworkStatusWidget({
|
||||
options,
|
||||
integrationIds,
|
||||
}: WidgetComponentProps<"networkControllerStatus">) {
|
||||
const [summaries] = clientApi.widget.networkController.summary.useSuspenseQuery(
|
||||
{
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
},
|
||||
);
|
||||
const utils = clientApi.useUtils();
|
||||
|
||||
clientApi.widget.networkController.subscribeToSummary.useSubscription(
|
||||
{
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
onData: (data) => {
|
||||
utils.widget.networkController.summary.setData(
|
||||
{
|
||||
integrationIds,
|
||||
},
|
||||
(prevData) => {
|
||||
if (!prevData) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return prevData.map((item) =>
|
||||
item.integration.id === data.integration.id
|
||||
? {
|
||||
...item,
|
||||
summary: data.summary,
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
: item,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IconServerOff, IconTopologyFull } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../../definition";
|
||||
import { optionsBuilder } from "../../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("networkControllerStatus", {
|
||||
icon: IconTopologyFull,
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
content: factory.select({
|
||||
options: (["wifi", "wired"] as const).map((value) => ({
|
||||
value,
|
||||
label: (t) => t(`widget.networkControllerStatus.option.content.option.${value}.label`),
|
||||
})),
|
||||
defaultValue: "wifi",
|
||||
}),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("networkController"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
message: (t) => t("widget.networkController.error.internalServerError"),
|
||||
},
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Stack, Text } from "@mantine/core";
|
||||
|
||||
export const StatRow = ({ label, value }: { label: string; value: string | number }) => {
|
||||
return (
|
||||
<Stack gap={0}>
|
||||
<Text size={"2xl"} fw={900} lh={1}>
|
||||
{value}
|
||||
</Text>
|
||||
<Text size={"md"} c={"dimmed"}>
|
||||
{label}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Group, Stack, Text } from "@mantine/core";
|
||||
import { IconWifi } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { StatRow } from "./stat-row";
|
||||
|
||||
export const WifiVariant = ({ countGuests, countUsers }: { countUsers: number; countGuests: number }) => {
|
||||
const t = useScopedI18n("widget.networkControllerStatus.card");
|
||||
return (
|
||||
<>
|
||||
<Group gap={"xs"} wrap={"nowrap"} mb={"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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Group, Stack, Text } from "@mantine/core";
|
||||
import { IconNetwork } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { StatRow } from "./stat-row";
|
||||
|
||||
export const WiredVariant = ({ countGuests, countUsers }: { countUsers: number; countGuests: number }) => {
|
||||
const t = useScopedI18n("widget.networkControllerStatus.card");
|
||||
return (
|
||||
<>
|
||||
<Group gap={"xs"} wrap={"nowrap"} mb={"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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Box, Center, List, Text, useMantineTheme } from "@mantine/core";
|
||||
import { IconCircleCheckFilled, IconCircleXFilled } from "@tabler/icons-react";
|
||||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import objectSupport from "dayjs/plugin/objectSupport";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
|
||||
dayjs.extend(objectSupport);
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(duration);
|
||||
|
||||
export default function NetworkControllerSummaryWidget({
|
||||
integrationIds,
|
||||
}: WidgetComponentProps<"networkControllerSummary">) {
|
||||
const [summaries] = clientApi.widget.networkController.summary.useSuspenseQuery(
|
||||
{
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
retry: false,
|
||||
},
|
||||
);
|
||||
const utils = clientApi.useUtils();
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
clientApi.widget.networkController.subscribeToSummary.useSubscription(
|
||||
{
|
||||
integrationIds,
|
||||
},
|
||||
{
|
||||
onData: (data) => {
|
||||
utils.widget.networkController.summary.setData(
|
||||
{
|
||||
integrationIds,
|
||||
},
|
||||
(prevData) => {
|
||||
if (!prevData) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return prevData.map((item) =>
|
||||
item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
const StatusIcon = ({ status }: { status?: "enabled" | "disabled" }) => {
|
||||
const mantineTheme = useMantineTheme();
|
||||
if (status === "enabled") {
|
||||
return <IconCircleCheckFilled size={20} color={mantineTheme.colors.green[6]} />;
|
||||
}
|
||||
return <IconCircleXFilled size={20} color={mantineTheme.colors.red[6]} />;
|
||||
};
|
||||
20
packages/widgets/src/network-controller/summary/index.ts
Normal file
20
packages/widgets/src/network-controller/summary/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { IconServerOff, IconTopologyFull } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../../definition";
|
||||
import { optionsBuilder } from "../../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("networkControllerSummary", {
|
||||
icon: IconTopologyFull,
|
||||
createOptions() {
|
||||
return optionsBuilder.from(() => ({}));
|
||||
},
|
||||
supportedIntegrations: getIntegrationKindsByCategory("networkController"),
|
||||
errors: {
|
||||
INTERNAL_SERVER_ERROR: {
|
||||
icon: IconServerOff,
|
||||
message: (t) => t("widget.networkController.error.internalServerError"),
|
||||
},
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
Reference in New Issue
Block a user