176 lines
6.2 KiB
TypeScript
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>
|
|
);
|
|
}
|