Files
Checkmate/client/src/Pages/Dashboard/Components/Cards/MonitorStatusCard.tsx
T
gorkem-bwl 49af07009d feat(client): reorder cards on hover, and tidy dashboard styling
Adds move-up / move-down controls to each card, revealed on hover or
when something inside the card takes focus, so a settled dashboard stays
quiet but the controls are still reachable by keyboard.

The controls reach the card header through context rather than a prop,
so none of the 17 card components has to know its own position — the
grid owns ordering. Positions resolve against the *visible* order, not
the stored array: a non-admin never sees the admin cards, so moving a
card past one of them would otherwise appear to do nothing. Hidden cards
keep their stored positions.

Styling fixes:

- 'Reset to defaults' no longer renders as the prominent green contained
  button. It is a destructive secondary action, so it moves to the quiet
  outlined slot beside the close action. The dialog now has no confirm
  button at all, since every change applies immediately and there is
  nothing to confirm.
- Icons go through the shared Icon component at the codebase's usual 16px
  instead of ad-hoc 14px with a one-off stroke width, and the header
  spinner matches at 16px.
- Named the remaining magic numbers — header height, bar thickness,
  sparkline and column widths, status tile width, control sizes — so they
  read as decisions rather than arbitrary pixels. There is no size-token
  scale in the theme (LAYOUT is spacing only) and peer components
  hardcode their own dimensions, so module constants match the house
  pattern rather than inventing a scale.
- Centralised the two poll rates instead of repeating 30000 and 60000
  across cards, and hoisted a stray sx flexShrink onto the component.

Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd
2026-08-29 09:43:02 -04:00

103 lines
3.4 KiB
TypeScript

import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { Link as RouterLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Dot } from "@/Components/design-elements";
import { LAYOUT } from "@/Utils/Theme/constants";
import { typographyLevels } from "@/Utils/Theme/Palette";
import { getStatusColor } from "@/Utils/MonitorUtils";
import { DashboardCard, CardMessage } from "../DashboardCard";
import { useMonitors } from "../../useDashboardData";
import type { MonitorStatus, MonitorsSummary } from "@/Types/Monitor";
// Wide enough for a three-digit count beside its label before the tiles wrap.
const TILE_MIN_WIDTH = 96;
// Every state the summary reports, in the order they read: healthy first, then
// what is wrong, then what is not being checked. All six are listed so the
// tiles always sum to totalMonitors — omitting one leaves an unexplained gap.
const COUNTS: { key: string; status: MonitorStatus; field: keyof MonitorsSummary }[] = [
{ key: "up", status: "up", field: "upMonitors" },
{ key: "down", status: "down", field: "downMonitors" },
{ key: "breached", status: "breached", field: "breachedMonitors" },
{ key: "maintenance", status: "maintenance", field: "maintenanceMonitors" },
{ key: "paused", status: "paused", field: "pausedMonitors" },
{ key: "initializing", status: "initializing", field: "initializingMonitors" },
];
export const MonitorStatusCard = () => {
const theme = useTheme();
const { t } = useTranslation();
const { data, isLoading, isValidating, error } = useMonitors();
const summary = data?.summary ?? null;
return (
<DashboardCard
title={t("pages.dashboard.cards.monitorStatus.title")}
to="/uptime"
isLoading={isLoading && !summary}
error={error}
isStale={isValidating && Boolean(data)}
>
{!summary || summary.totalMonitors === 0 ? (
<CardMessage text={t("pages.dashboard.cards.monitorStatus.empty")} />
) : (
<Stack
direction="row"
flexWrap="wrap"
gap={theme.spacing(LAYOUT.LG)}
>
{COUNTS.map(({ key, status, field }) => (
<Stack
key={key}
component={RouterLink}
// Plain /uptime: the monitors list does not read query
// params, so a ?status= filter would be silently dropped and
// the link would lie about where it goes.
to="/uptime"
gap={theme.spacing(LAYOUT.XXS)}
flex="1 1 0"
minWidth={TILE_MIN_WIDTH}
px={theme.spacing(LAYOUT.SM)}
py={theme.spacing(LAYOUT.XS)}
borderRadius={theme.shape.borderRadius}
sx={{
textDecoration: "none",
"&:hover": { backgroundColor: theme.palette.action.hover },
}}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(LAYOUT.XS)}
>
<Dot
color={getStatusColor(status, theme)}
size="md"
/>
<Typography
fontSize={typographyLevels.s}
color={theme.palette.text.secondary}
>
{t(`pages.common.monitors.status.${key}`)}
</Typography>
</Stack>
<Typography
fontSize={typographyLevels.xxl}
fontWeight={300}
color={theme.palette.text.primary}
lineHeight={1.1}
>
{summary[field]}
</Typography>
</Stack>
))}
</Stack>
)}
</DashboardCard>
);
};