Reorder controls stayed visible after a click. The reveal used `:focus-within`, and clicking a button leaves it focused, so that card's controls stayed pinned open long after the pointer left — Failed checks being wherever focus happened to land. Now `:has(:focus-visible)`, which matches keyboard focus only, so a mouse click no longer pins them. Widths: Currently down was full-width and Recent incidents 8-wide, both with more whitespace than content. Currently down carries six columns per row so it is the densest card, but it does not need all twelve; it is now 8. Recent incidents carries four and is now 4, as are Recent errors. Declaring a narrower width is not enough on its own: a card left alone in a row gets stretched to fill it, so Currently down at 8 still rendered at 12 while it was the only card in its row. Registry and default order are now sequenced so widths pair into full rows — 8 beside 4, 6 beside 6 — which is what actually makes the narrow cards render narrow. Verified across the full catalogue, the non-admin subset and the defaults: every mid-page row fills exactly 12 with no gaps. Fonts: every secondary line used typographyLevels.s, which is 11px — body2, a de-emphasis size. The theme's body1 is 13px, so all 24 uses are now typographyLevels.m. Nothing in a card renders below body size any more; only the large figures differ, at xxl. Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd
121 lines
3.2 KiB
TypeScript
121 lines
3.2 KiB
TypeScript
import Stack from "@mui/material/Stack";
|
|
import Typography from "@mui/material/Typography";
|
|
import { useTheme } from "@mui/material/styles";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useMemo } from "react";
|
|
|
|
import { LAYOUT } from "@/Utils/Theme/constants";
|
|
import { typographyLevels } from "@/Utils/Theme/Palette";
|
|
import { DashboardCard, CardMessage } from "../DashboardCard";
|
|
import { CardBar, CardRow } from "../CardPrimitives";
|
|
import { useMonitors } from "../../useDashboardData";
|
|
|
|
interface GroupBucket {
|
|
group: string;
|
|
total: number;
|
|
down: number;
|
|
}
|
|
|
|
export const MonitorsByGroupCard = () => {
|
|
const theme = useTheme();
|
|
const { t } = useTranslation();
|
|
const { data, isLoading, isValidating, error } = useMonitors();
|
|
|
|
// `group` is a plain string | null on the monitor — there is no groups
|
|
// endpoint, so the set is whatever monitors actually declare.
|
|
const buckets = useMemo<GroupBucket[]>(() => {
|
|
const byGroup = new Map<string, GroupBucket>();
|
|
for (const monitor of data?.monitors ?? []) {
|
|
if (!monitor.group) {
|
|
continue;
|
|
}
|
|
const bucket = byGroup.get(monitor.group) ?? {
|
|
group: monitor.group,
|
|
total: 0,
|
|
down: 0,
|
|
};
|
|
bucket.total += 1;
|
|
if (monitor.status === "down" || monitor.status === "breached") {
|
|
bucket.down += 1;
|
|
}
|
|
byGroup.set(monitor.group, bucket);
|
|
}
|
|
return [...byGroup.values()].sort((a, b) => b.total - a.total);
|
|
}, [data]);
|
|
|
|
const max = buckets[0]?.total ?? 0;
|
|
|
|
return (
|
|
<DashboardCard
|
|
title={t("pages.dashboard.cards.monitorsByGroup.title")}
|
|
to="/uptime"
|
|
isLoading={isLoading && !data}
|
|
error={error}
|
|
isStale={isValidating && Boolean(data)}
|
|
>
|
|
{buckets.length === 0 ? (
|
|
<CardMessage text={t("pages.dashboard.cards.monitorsByGroup.empty")} />
|
|
) : (
|
|
<Stack gap={theme.spacing(LAYOUT.SM)}>
|
|
{buckets.map((bucket) => (
|
|
<CardRow key={bucket.group}>
|
|
<Stack
|
|
flex={1}
|
|
minWidth={0}
|
|
gap={theme.spacing(LAYOUT.XXS)}
|
|
>
|
|
<Stack
|
|
direction="row"
|
|
alignItems="center"
|
|
justifyContent="space-between"
|
|
gap={theme.spacing(LAYOUT.SM)}
|
|
>
|
|
<Typography
|
|
fontSize={typographyLevels.m}
|
|
color={theme.palette.text.primary}
|
|
noWrap
|
|
>
|
|
{bucket.group}
|
|
</Typography>
|
|
<Stack
|
|
direction="row"
|
|
alignItems="center"
|
|
gap={theme.spacing(LAYOUT.XS)}
|
|
flexShrink={0}
|
|
>
|
|
{bucket.down > 0 && (
|
|
<Typography
|
|
fontSize={typographyLevels.m}
|
|
color={theme.palette.error.main}
|
|
>
|
|
{t("pages.dashboard.cards.monitorsByType.down", {
|
|
count: bucket.down,
|
|
})}
|
|
</Typography>
|
|
)}
|
|
<Typography
|
|
fontSize={typographyLevels.m}
|
|
color={theme.palette.text.secondary}
|
|
>
|
|
{bucket.total}
|
|
</Typography>
|
|
</Stack>
|
|
</Stack>
|
|
<CardBar
|
|
value={bucket.total}
|
|
max={max}
|
|
color={
|
|
bucket.down > 0
|
|
? theme.palette.error.main
|
|
: theme.palette.primary.main
|
|
}
|
|
/>
|
|
</Stack>
|
|
</CardRow>
|
|
))}
|
|
</Stack>
|
|
)}
|
|
</DashboardCard>
|
|
);
|
|
};
|