fix(widgets): report every health state in the UniFi site summary
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2c0f0bbdd3
commit
145dc17d8d
2 files changed
+60
-33
No files matched your search
@@ -25,7 +25,7 @@ export default function UnifiSiteManagerWidget({
|
||||
width,
|
||||
}: WidgetComponentProps<"unifiSiteManager">) {
|
||||
const t = useI18n();
|
||||
const { data: results = [] } = clientApi.widget.unifiSiteManager.overview.useQuery({ integrationIds });
|
||||
const { data: results = [], isFetching } = clientApi.widget.unifiSiteManager.overview.useQuery({ integrationIds });
|
||||
|
||||
const sites = results[0]?.overview.sites ?? [];
|
||||
const isNarrow = width < NARROW_WIDTH;
|
||||
@@ -165,6 +165,9 @@ export default function UnifiSiteManagerWidget({
|
||||
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>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Box, Group, Text, Tooltip } from "@mantine/core";
|
||||
import { Box, Group, Stack, Text, Tooltip } from "@mantine/core";
|
||||
|
||||
import type { UnifiSiteHealthSegment, UnifiSiteHealthState, UnifiSiteHealthSummary } from "@homarr/integrations/types";
|
||||
|
||||
@@ -20,6 +20,25 @@ const STATE_COLOR: Record<UnifiSiteHealthState, string> = {
|
||||
|
||||
const SEGMENT_HEIGHT = 14;
|
||||
|
||||
const NOTABLE_STATES = ["down", "degraded", "unknown"] as const;
|
||||
|
||||
/**
|
||||
* TOUS les états non nuls sont listés, sans plafond sur leur nombre.
|
||||
* N'en retenir qu'un ferait disparaître les heures des autres, et le lecteur
|
||||
* en déduirait un reste sain qui serait faux. `unknown` compte au même titre :
|
||||
* une fenêtre partiellement rapportée n'est pas une fenêtre partiellement
|
||||
* dégradée, et la taire laisse croire que les heures manquantes étaient mauvaises.
|
||||
* Pour qui ne distingue pas les trois couleurs de la barre, cette phrase EST la
|
||||
* donnée — toute omission ici est une omission tout court.
|
||||
*/
|
||||
const composeSummary = (summary: UnifiSiteHealthSummary, labels: Record<UnifiSiteHealthState, string>) => {
|
||||
const notable = NOTABLE_STATES.filter((state) => summary[state] > 0).map(
|
||||
(state) => `${summary[state]}h ${labels[state]}`,
|
||||
);
|
||||
|
||||
return notable.length > 0 ? notable.join(" · ") : `${summary.healthy}h ${labels.healthy}`;
|
||||
};
|
||||
|
||||
export const HealthBar = ({
|
||||
segments,
|
||||
summary,
|
||||
@@ -30,35 +49,40 @@ export const HealthBar = ({
|
||||
summary: UnifiSiteHealthSummary;
|
||||
formatHour: (at: Date) => string;
|
||||
labels: Record<UnifiSiteHealthState, string>;
|
||||
}) => (
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<Group gap={1} wrap="nowrap" style={{ flex: 1, minWidth: 0 }}>
|
||||
{segments.map((segment) => (
|
||||
<Tooltip
|
||||
key={segment.at.toISOString()}
|
||||
label={`${formatHour(segment.at)} — ${labels[segment.state]}`}
|
||||
openDelay={150}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 2,
|
||||
height: SEGMENT_HEIGHT,
|
||||
borderRadius: 1,
|
||||
background: STATE_COLOR[segment.state],
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Group>
|
||||
}) => {
|
||||
const summaryText = composeSummary(summary, labels);
|
||||
|
||||
{/* Le canal qui porte l'information quand la couleur ne passe pas. */}
|
||||
<Text size="xs" c="dimmed" ff="monospace" style={{ whiteSpace: "nowrap" }}>
|
||||
{summary.down > 0
|
||||
? `${summary.down}h ${labels.down}`
|
||||
: summary.degraded > 0
|
||||
? `${summary.degraded}h ${labels.degraded}`
|
||||
: `${summary.healthy}h ${labels.healthy}`}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
return (
|
||||
<Stack gap={2}>
|
||||
{/* Les segments n'ont que le survol : sans nom accessible, la barre est
|
||||
muette au clavier et au toucher. Le nom réutilise le résumé composé
|
||||
ci-dessus pour que les deux ne puissent pas diverger. */}
|
||||
<Group gap={1} wrap="nowrap" aria-label={summaryText}>
|
||||
{segments.map((segment) => (
|
||||
<Tooltip
|
||||
key={segment.at.toISOString()}
|
||||
label={`${formatHour(segment.at)} — ${labels[segment.state]}`}
|
||||
openDelay={150}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 2,
|
||||
height: SEGMENT_HEIGHT,
|
||||
borderRadius: 1,
|
||||
background: STATE_COLOR[segment.state],
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Group>
|
||||
|
||||
{/* Le canal qui porte l'information quand la couleur ne passe pas.
|
||||
Sous la barre et sans `nowrap` : il dispose de toute la largeur de la
|
||||
cellule et passe à la ligne plutôt que d'être tronqué. */}
|
||||
<Text size="xs" c="dimmed" ff="monospace">
|
||||
{summaryText}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user