feat(dashboard): add currently down and lowest uptime dashboard cards

This commit is contained in:
Malena Aguiar committed 2026-09-22 16:18:51 -06:00
1 parent a5e66318b5
commit bdd4634a3b
6 files changed
+167 -11

No files matched your search

@@ -1,8 +1,11 @@
import { useState } from "react"; import { useMemo, useState } from "react";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import FormControlLabel from "@mui/material/FormControlLabel"; import FormControlLabel from "@mui/material/FormControlLabel";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Checkbox, Dialog } from "@/Components/inputs"; import { Checkbox, Dialog } from "@/Components/inputs";
import { LAYOUT } from "@/Utils/Theme/constants";
import { type DashboardCardKey, dashboardCardKeys } from "@/Types/Dashboard"; import { type DashboardCardKey, dashboardCardKeys } from "@/Types/Dashboard";
interface EditCardsModalProps { interface EditCardsModalProps {
@@ -18,16 +21,19 @@ export const EditCardsModal = ({
onClose, onClose,
onSave, onSave,
}: EditCardsModalProps) => { }: EditCardsModalProps) => {
const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const [draft, setDraft] = useState<Set<DashboardCardKey>>(new Set(visibleCards)); const [cardsSelection, setCardsSelection] = useState<Set<DashboardCardKey>>(
new Set(visibleCards)
);
const handleClose = () => { const handleClose = () => {
setDraft(new Set(visibleCards)); setCardsSelection(new Set(visibleCards));
onClose(); onClose();
}; };
const handleToggle = (key: DashboardCardKey) => { const handleToggle = (key: DashboardCardKey) => {
setDraft((prev) => { setCardsSelection((prev) => {
const next = new Set(prev); const next = new Set(prev);
if (next.has(key)) { if (next.has(key)) {
next.delete(key); next.delete(key);
@@ -38,26 +44,40 @@ export const EditCardsModal = ({
}); });
}; };
const sortedCardKeys = useMemo(
() =>
[...dashboardCardKeys].sort((a, b) =>
t(`pages.dashboard.cards.${a}`).localeCompare(t(`pages.dashboard.cards.${b}`))
),
[t]
);
return ( return (
<Dialog <Dialog
open={open} open={open}
title={t("pages.dashboard.editCardsModal.title")} title={t("pages.dashboard.editCardsModal.title")}
content={t("pages.dashboard.editCardsModal.description")} content={t("pages.dashboard.editCardsModal.description")}
onCancel={handleClose} onCancel={handleClose}
onConfirm={() => onSave(draft)} onConfirm={() => onSave(cardsSelection)}
confirmText={t("pages.dashboard.editCardsModal.done")} confirmText={t("pages.dashboard.editCardsModal.done")}
maxWidth="xs" maxWidth="xs"
fullWidth fullWidth
> >
<Stack> <Stack gap={theme.spacing(LAYOUT.XXS)}>
{dashboardCardKeys.map((key: DashboardCardKey) => ( <Typography
variant="eyebrow"
color={theme.palette.text.disabled}
>
{t("pages.dashboard.editCardsModal.groups.uptime")}
</Typography>
{sortedCardKeys.map((key: DashboardCardKey) => (
<FormControlLabel <FormControlLabel
key={key} key={key}
label={t(`pages.dashboard.cards.${key}`)} label={t(`pages.dashboard.cards.${key}`)}
control={ control={
<Checkbox <Checkbox
checked={draft.has(key)} checked={cardsSelection.has(key)}
disabled={draft.has(key) && draft.size === 1} disabled={cardsSelection.has(key) && cardsSelection.size === 1}
onChange={() => handleToggle(key)} onChange={() => handleToggle(key)}
/> />
} }
@@ -0,0 +1,59 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import { DashboardCard } from "@/Pages/Dashboard/components/DashboardCard";
import { Dot } from "@/Components/design-elements";
import { getStatusColor } from "@/Utils/MonitorUtils";
import { typographyLevels } from "@/Utils/Theme/Palette";
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
import type { Monitor } from "@/Types/Monitor";
interface CurrentlyDownCardProps {
monitors: Monitor[];
}
export const CurrentlyDownCard = ({ monitors }: CurrentlyDownCardProps) => {
const theme = useTheme();
const { t } = useTranslation();
const downMonitors = monitors.filter((monitor) => monitor.status === "down");
return (
<DashboardCard
title={t("pages.dashboard.cards.currentlyDown")}
topRight={downMonitors.length > 0 ? downMonitors.length : undefined}
>
{downMonitors.length === 0 ? (
<Typography
fontSize={typographyLevels.m}
color={theme.palette.text.disabled}
>
{t("pages.dashboard.currentlyDown.empty")}
</Typography>
) : (
<Stack gap={theme.spacing(SPACING.SM)}>
{downMonitors.map((monitor) => (
<Stack
key={monitor.id}
direction="row"
alignItems="center"
gap={theme.spacing(LAYOUT.XS)}
>
<Dot color={getStatusColor(monitor.status, theme)} />
<Typography
fontSize={typographyLevels.m}
color={theme.palette.text.primary}
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
>
{monitor.name}
</Typography>
</Stack>
))}
</Stack>
)}
</DashboardCard>
);
};
@@ -0,0 +1,60 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import { DashboardCard } from "@/Pages/Dashboard/components/DashboardCard";
import { BarRow } from "@/Pages/Dashboard/components/cards/BarRow";
import { getUptimePercentageColor } from "@/Utils/MonitorUtils";
import { formatPercentage } from "@/Utils/FormatUtils";
import { typographyLevels } from "@/Utils/Theme/Palette";
import { SPACING } from "@/Utils/Theme/constants";
import type { Monitor } from "@/Types/Monitor";
const TOP_N = 5;
interface LowestUptimeCardProps {
monitors: Monitor[];
}
export const LowestUptimeCard = ({ monitors }: LowestUptimeCardProps) => {
const theme = useTheme();
const { t } = useTranslation();
const ranked = monitors
.filter((monitor) => monitor.status !== "paused")
.filter(
(monitor): monitor is Monitor & { uptimePercentage: number } =>
typeof monitor.uptimePercentage === "number"
)
.sort((a, b) => a.uptimePercentage - b.uptimePercentage)
.slice(0, TOP_N);
return (
<DashboardCard title={t("pages.dashboard.cards.lowestUptime")}>
{ranked.length === 0 ? (
<Typography
fontSize={typographyLevels.m}
color={theme.palette.text.disabled}
>
{t("pages.dashboard.noData")}
</Typography>
) : (
<Stack gap={theme.spacing(SPACING.SM)}>
{ranked.map((monitor) => {
const paletteKey = getUptimePercentageColor(monitor.uptimePercentage);
const color = theme.palette[paletteKey].main;
return (
<BarRow
key={monitor.id}
label={monitor.name}
value={formatPercentage(monitor.uptimePercentage)}
fillRatio={1 - monitor.uptimePercentage}
valueColor={color}
/>
);
})}
</Stack>
)}
</DashboardCard>
);
};
+4
View File
@@ -14,6 +14,8 @@ import { setDashboardVisibleCards } from "@/Features/UI/uiSlice";
import type { RootState, AppDispatch } from "@/store"; import type { RootState, AppDispatch } from "@/store";
import { MonitorStatusCard } from "@/Pages/Dashboard/components/cards/MonitorStatusCard"; import { MonitorStatusCard } from "@/Pages/Dashboard/components/cards/MonitorStatusCard";
import { CurrentlyDownCard } from "@/Pages/Dashboard/components/cards/CurrentlyDownCard";
import { LowestUptimeCard } from "@/Pages/Dashboard/components/cards/LowestUptimeCard";
import { MonitorsByTypeCard } from "@/Pages/Dashboard/components/cards/MonitorsByTypeCard"; import { MonitorsByTypeCard } from "@/Pages/Dashboard/components/cards/MonitorsByTypeCard";
import { EditCardsModal } from "@/Pages/Dashboard/components/EditCardsModal"; import { EditCardsModal } from "@/Pages/Dashboard/components/EditCardsModal";
import { type DashboardCardKey, dashboardCardKeys } from "@/Types/Dashboard"; import { type DashboardCardKey, dashboardCardKeys } from "@/Types/Dashboard";
@@ -48,6 +50,8 @@ const Dashboard = () => {
const cardComponents: Record<DashboardCardKey, ReactNode> = { const cardComponents: Record<DashboardCardKey, ReactNode> = {
monitorStatus: <MonitorStatusCard summary={summary} />, monitorStatus: <MonitorStatusCard summary={summary} />,
currentlyDown: <CurrentlyDownCard monitors={monitors ?? []} />,
lowestUptime: <LowestUptimeCard monitors={monitors ?? []} />,
monitorsByType: <MonitorsByTypeCard monitorsByType={monitorsByType} />, monitorsByType: <MonitorsByTypeCard monitorsByType={monitorsByType} />,
}; };
+6 -1
View File
@@ -1,2 +1,7 @@
export const dashboardCardKeys = ["monitorStatus", "monitorsByType"] as const; export const dashboardCardKeys = [
"monitorStatus",
"currentlyDown",
"lowestUptime",
"monitorsByType",
] as const;
export type DashboardCardKey = (typeof dashboardCardKeys)[number]; export type DashboardCardKey = (typeof dashboardCardKeys)[number];
+9 -1
View File
@@ -1924,16 +1924,24 @@
}, },
"cards": { "cards": {
"monitorStatus": "Monitor status", "monitorStatus": "Monitor status",
"currentlyDown": "Currently down",
"lowestUptime": "Lowest uptime",
"monitorsByType": "Monitors by type" "monitorsByType": "Monitors by type"
}, },
"currentlyDown": {
"empty": "No monitors are down at this moment."
},
"header": { "header": {
"title": "Dashboard", "title": "Dashboard",
"description": "See every monitor type in one place and spot what needs attention." "description": "See every monitor type in one place and spot what needs attention."
}, },
"editCards": "Edit cards", "editCards": "Edit cards",
"editCardsModal": { "editCardsModal": {
"title": "Edit cards", "title": "Dashboard cards",
"description": "Select the cards to display on your dashboard.", "description": "Select the cards to display on your dashboard.",
"groups": {
"uptime": "Uptime"
},
"done": "Done" "done": "Done"
} }
} }