Files
homarr/packages/widgets/src/unifi-site-manager/component.tsx
T

176 lines
6.2 KiB
TypeScript

"use client";
import { Box, Text } from "@mantine/core";
import type { DataTableColumn } from "mantine-datatable";
import { clientApi } from "@homarr/api/client";
import type { UnifiSiteHealthState, UnifiSiteManagerSite } from "@homarr/integrations/types";
import { useI18n } from "@homarr/translation/client";
import { HomarrDataTable } from "../common/homarr-data-table";
import type { WidgetComponentProps } from "../definition";
import { HealthBar } from "./health-bar";
const EMPTY_VALUE = "—";
/** En dessous, les colonnes secondaires ne tiennent plus lisiblement. */
const NARROW_WIDTH = 420;
const formatHour = (at: Date) => at.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
export default function UnifiSiteManagerWidget({
integrationIds,
options,
isEditMode,
width,
}: WidgetComponentProps<"unifiSiteManager">) {
const t = useI18n();
const { data: results = [], isFetching } = clientApi.widget.unifiSiteManager.overview.useQuery({ integrationIds });
const sites = results[0]?.overview.sites ?? [];
const isNarrow = width < NARROW_WIDTH;
const labels: Record<UnifiSiteHealthState, string> = {
healthy: t("widget.unifiSiteManager.health.healthy"),
degraded: t("widget.unifiSiteManager.health.degraded"),
down: t("widget.unifiSiteManager.health.down"),
unknown: t("widget.unifiSiteManager.health.unknown"),
};
const columns: DataTableColumn<UnifiSiteManagerSite>[] = [
{
accessor: "name",
title: t("widget.unifiSiteManager.column.site"),
sortable: true,
ellipsis: true,
},
{
accessor: "health",
title: t("widget.unifiSiteManager.column.health"),
width: isNarrow ? 140 : 220,
render: (site) => (
<HealthBar segments={site.health} summary={site.summary} formatHour={formatHour} labels={labels} />
),
},
{
accessor: "latencyMs",
title: t("widget.unifiSiteManager.column.latency"),
sortable: true,
render: (site) => {
if (site.latencyMs === null) return EMPTY_VALUE;
// Le cloud rapporte la latence et la perte de paquets indépendamment :
// une période mesurée pour l'une peut ne pas l'être pour l'autre. Un 0
// par défaut affirmerait à tort une perte mesurée à zéro, alors qu'elle
// n'a simplement pas été relevée sur cette période.
if (site.packetLossPct === null) {
return t("widget.unifiSiteManager.latencyOnly", { value: `${site.latencyMs}` });
}
return t("widget.unifiSiteManager.latency", {
value: `${site.latencyMs}`,
loss: `${site.packetLossPct}`,
});
},
},
...(options.showIsp && !isNarrow
? [
{
accessor: "ispName",
title: t("widget.unifiSiteManager.column.isp"),
sortable: true,
render: (site: UnifiSiteManagerSite) => site.ispName ?? EMPTY_VALUE,
},
]
: []),
...(options.showExternalIp && !isNarrow
? [
{
accessor: "externalIps",
title: t("widget.unifiSiteManager.column.externalIp"),
render: (site: UnifiSiteManagerSite) =>
site.externalIps.length === 0 ? EMPTY_VALUE : site.externalIps.join(" · "),
},
]
: []),
{
accessor: "deviceCount",
title: t("widget.unifiSiteManager.column.devices"),
sortable: true,
render: (site) => (
<Text size="sm" c={site.offlineDeviceCount > 0 ? "red" : undefined}>
{t("widget.unifiSiteManager.devicesOnline", {
online: `${site.deviceCount - site.offlineDeviceCount}`,
total: `${site.deviceCount}`,
})}
</Text>
),
},
{
accessor: "pendingUpdateCount",
title: t("widget.unifiSiteManager.column.updates"),
sortable: true,
render: (site) => (
<Text size="sm" c={site.pendingUpdateCount > 0 ? "yellow" : undefined}>
{site.pendingUpdateCount}
</Text>
),
},
{
accessor: "sdWan",
title: t("widget.unifiSiteManager.column.sdWan"),
render: (site) => {
// `null` couvre DEUX cas que le contrat ne distingue pas : le site
// n'appartient à aucune configuration, ou le cloud n'a pas répondu.
// Le tiret n'affirme donc ni l'un ni l'autre — il dit « on ne sait pas »,
// ce qui est vrai dans les deux cas. Ne pas le remplacer par un libellé
// du type « aucun mesh », qui trancherait à tort.
if (site.sdWan === null) return EMPTY_VALUE;
const { offline, connected, pingMs, errorCount } = site.sdWan;
const color = offline || !connected ? "red" : errorCount > 0 ? "yellow" : undefined;
const label = offline
? t("widget.unifiSiteManager.sdWan.offline")
: connected
? t("widget.unifiSiteManager.sdWan.connected")
: t("widget.unifiSiteManager.sdWan.disconnected");
return (
<Text size="sm" c={color}>
{/* Le ping n'apparaît que mesuré : le cloud rapporte 0 quand il ne
l'a pas mesuré, et l'afficher se lirait comme une latence nulle. */}
{pingMs === null ? label : `${label} · ${pingMs} ms`}
</Text>
);
},
},
{
accessor: "consoleUpdateAvailable",
title: t("widget.unifiSiteManager.column.console"),
// `null` = aucun host correspondant. Ce n'est pas « à jour ».
render: (site) =>
site.consoleUpdateAvailable === null
? EMPTY_VALUE
: site.consoleUpdateAvailable
? t("widget.unifiSiteManager.consoleUpdateAvailable")
: t("widget.unifiSiteManager.consoleUpToDate"),
},
];
return (
<Box h="100%">
<HomarrDataTable
isEditMode={isEditMode}
cellPadding={isNarrow ? "2px 6px" : "4px 8px"}
records={sites}
columns={columns}
idAccessor="siteId"
// Sans cela, « aucun site » s'affiche pendant la première requête : une
// affirmation sur le compte, fausse tant qu'on n'a rien reçu.
fetching={isFetching && sites.length === 0}
noRecordsText={t("widget.unifiSiteManager.noSites")}
/>
</Box>
);
}