From bdd4634a3bbe44f11814f1fd718f4bee77a20a0d Mon Sep 17 00:00:00 2001 From: Malena Aguiar Date: Tue, 22 Sep 2026 16:12:52 -0600 Subject: [PATCH 1/2] feat(dashboard): add currently down and lowest uptime dashboard cards --- .../Dashboard/components/EditCardsModal.tsx | 38 +++++++++--- .../components/cards/CurrentlyDownCard.tsx | 59 ++++++++++++++++++ .../components/cards/LowestUptimeCard.tsx | 60 +++++++++++++++++++ client/src/Pages/Dashboard/index.tsx | 4 ++ client/src/Types/Dashboard.ts | 7 ++- client/src/locales/en.json | 10 +++- 6 files changed, 167 insertions(+), 11 deletions(-) create mode 100644 client/src/Pages/Dashboard/components/cards/CurrentlyDownCard.tsx create mode 100644 client/src/Pages/Dashboard/components/cards/LowestUptimeCard.tsx diff --git a/client/src/Pages/Dashboard/components/EditCardsModal.tsx b/client/src/Pages/Dashboard/components/EditCardsModal.tsx index 84257c1ec..faf13c407 100644 --- a/client/src/Pages/Dashboard/components/EditCardsModal.tsx +++ b/client/src/Pages/Dashboard/components/EditCardsModal.tsx @@ -1,8 +1,11 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import Stack from "@mui/material/Stack"; import FormControlLabel from "@mui/material/FormControlLabel"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; import { Checkbox, Dialog } from "@/Components/inputs"; +import { LAYOUT } from "@/Utils/Theme/constants"; import { type DashboardCardKey, dashboardCardKeys } from "@/Types/Dashboard"; interface EditCardsModalProps { @@ -18,16 +21,19 @@ export const EditCardsModal = ({ onClose, onSave, }: EditCardsModalProps) => { + const theme = useTheme(); const { t } = useTranslation(); - const [draft, setDraft] = useState>(new Set(visibleCards)); + const [cardsSelection, setCardsSelection] = useState>( + new Set(visibleCards) + ); const handleClose = () => { - setDraft(new Set(visibleCards)); + setCardsSelection(new Set(visibleCards)); onClose(); }; const handleToggle = (key: DashboardCardKey) => { - setDraft((prev) => { + setCardsSelection((prev) => { const next = new Set(prev); if (next.has(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 ( onSave(draft)} + onConfirm={() => onSave(cardsSelection)} confirmText={t("pages.dashboard.editCardsModal.done")} maxWidth="xs" fullWidth > - - {dashboardCardKeys.map((key: DashboardCardKey) => ( + + + {t("pages.dashboard.editCardsModal.groups.uptime")} + + {sortedCardKeys.map((key: DashboardCardKey) => ( handleToggle(key)} /> } diff --git a/client/src/Pages/Dashboard/components/cards/CurrentlyDownCard.tsx b/client/src/Pages/Dashboard/components/cards/CurrentlyDownCard.tsx new file mode 100644 index 000000000..478b2efa3 --- /dev/null +++ b/client/src/Pages/Dashboard/components/cards/CurrentlyDownCard.tsx @@ -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 ( + 0 ? downMonitors.length : undefined} + > + {downMonitors.length === 0 ? ( + + {t("pages.dashboard.currentlyDown.empty")} + + ) : ( + + {downMonitors.map((monitor) => ( + + + + {monitor.name} + + + ))} + + )} + + ); +}; diff --git a/client/src/Pages/Dashboard/components/cards/LowestUptimeCard.tsx b/client/src/Pages/Dashboard/components/cards/LowestUptimeCard.tsx new file mode 100644 index 000000000..f8840081e --- /dev/null +++ b/client/src/Pages/Dashboard/components/cards/LowestUptimeCard.tsx @@ -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 ( + + {ranked.length === 0 ? ( + + {t("pages.dashboard.noData")} + + ) : ( + + {ranked.map((monitor) => { + const paletteKey = getUptimePercentageColor(monitor.uptimePercentage); + const color = theme.palette[paletteKey].main; + return ( + + ); + })} + + )} + + ); +}; diff --git a/client/src/Pages/Dashboard/index.tsx b/client/src/Pages/Dashboard/index.tsx index f86b224f1..1656b9709 100644 --- a/client/src/Pages/Dashboard/index.tsx +++ b/client/src/Pages/Dashboard/index.tsx @@ -14,6 +14,8 @@ import { setDashboardVisibleCards } from "@/Features/UI/uiSlice"; import type { RootState, AppDispatch } from "@/store"; 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 { EditCardsModal } from "@/Pages/Dashboard/components/EditCardsModal"; import { type DashboardCardKey, dashboardCardKeys } from "@/Types/Dashboard"; @@ -48,6 +50,8 @@ const Dashboard = () => { const cardComponents: Record = { monitorStatus: , + currentlyDown: , + lowestUptime: , monitorsByType: , }; diff --git a/client/src/Types/Dashboard.ts b/client/src/Types/Dashboard.ts index a397ec807..6e418f5ed 100644 --- a/client/src/Types/Dashboard.ts +++ b/client/src/Types/Dashboard.ts @@ -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]; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 1b1337dd2..ba0e3e897 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1924,16 +1924,24 @@ }, "cards": { "monitorStatus": "Monitor status", + "currentlyDown": "Currently down", + "lowestUptime": "Lowest uptime", "monitorsByType": "Monitors by type" }, + "currentlyDown": { + "empty": "No monitors are down at this moment." + }, "header": { "title": "Dashboard", "description": "See every monitor type in one place and spot what needs attention." }, "editCards": "Edit cards", "editCardsModal": { - "title": "Edit cards", + "title": "Dashboard cards", "description": "Select the cards to display on your dashboard.", + "groups": { + "uptime": "Uptime" + }, "done": "Done" } } From b0ed75382ebd1f533da86177e0d55996c451911e Mon Sep 17 00:00:00 2001 From: Malena Aguiar Date: Wed, 23 Sep 2026 17:57:28 -0600 Subject: [PATCH 2/2] feat(dashboard): add code refactors --- client/src/Features/UI/uiSlice.ts | 12 ++++- .../components/cards/CurrentlyDownCard.tsx | 7 ++- .../{LowestUptimeCard.tsx => UptimeCard.tsx} | 44 ++++++++++++++++--- client/src/Pages/Dashboard/index.tsx | 4 +- client/src/Types/Dashboard.ts | 5 ++- client/src/locales/en.json | 5 ++- 6 files changed, 61 insertions(+), 16 deletions(-) rename client/src/Pages/Dashboard/components/cards/{LowestUptimeCard.tsx => UptimeCard.tsx} (52%) diff --git a/client/src/Features/UI/uiSlice.ts b/client/src/Features/UI/uiSlice.ts index 55e83333c..aad3b63f5 100644 --- a/client/src/Features/UI/uiSlice.ts +++ b/client/src/Features/UI/uiSlice.ts @@ -1,4 +1,8 @@ -import { dashboardCardKeys, type DashboardCardKey } from "@/Types/Dashboard"; +import { + dashboardCardKeys, + type DashboardCardKey, + type DashboardSortOrder, +} from "@/Types/Dashboard"; import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; type ThemeMode = "light" | "dark"; @@ -38,6 +42,7 @@ interface UIState { starPromptOpen: boolean; chartType: ChartType; dashboardVisibleCards: DashboardCardKey[]; + dashboardUptimeSortOrder: DashboardSortOrder; } const initialMode: ThemeMode = window?.matchMedia?.("(prefers-color-scheme: dark)") @@ -78,6 +83,7 @@ const initialState: UIState = { starPromptOpen: true, chartType: "histogram", dashboardVisibleCards: [...dashboardCardKeys], + dashboardUptimeSortOrder: "ascending", }; const uiSlice = createSlice({ @@ -124,6 +130,9 @@ const uiSlice = createSlice({ setDashboardVisibleCards: (state, action: PayloadAction) => { state.dashboardVisibleCards = action.payload; }, + setDashboardUptimeSortOrder: (state, action: PayloadAction) => { + state.dashboardUptimeSortOrder = action.payload; + }, }, }); @@ -141,4 +150,5 @@ export const { setStarPromptOpen, setChartType, setDashboardVisibleCards, + setDashboardUptimeSortOrder, } = uiSlice.actions; diff --git a/client/src/Pages/Dashboard/components/cards/CurrentlyDownCard.tsx b/client/src/Pages/Dashboard/components/cards/CurrentlyDownCard.tsx index 478b2efa3..f7e7cdd8b 100644 --- a/client/src/Pages/Dashboard/components/cards/CurrentlyDownCard.tsx +++ b/client/src/Pages/Dashboard/components/cards/CurrentlyDownCard.tsx @@ -4,9 +4,8 @@ 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 { LAYOUT } from "@/Utils/Theme/constants"; import type { Monitor } from "@/Types/Monitor"; interface CurrentlyDownCardProps { @@ -32,7 +31,7 @@ export const CurrentlyDownCard = ({ monitors }: CurrentlyDownCardProps) => { {t("pages.dashboard.currentlyDown.empty")} ) : ( - + {downMonitors.map((monitor) => ( { alignItems="center" gap={theme.spacing(LAYOUT.XS)} > - + { +export const UptimeCard = ({ monitors }: UptimeCardProps) => { const theme = useTheme(); const { t } = useTranslation(); + const dispatch = useDispatch(); + const sortOrder = useSelector((state: RootState) => state.ui.dashboardUptimeSortOrder); + const direction = sortOrder === "ascending" ? 1 : -1; 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) + .sort((a, b) => direction * (a.uptimePercentage - b.uptimePercentage)) .slice(0, TOP_N); return ( - + + dispatch( + setDashboardUptimeSortOrder( + sortOrder === "ascending" ? "descending" : "ascending" + ) + ) + } + > + + + } + > {ranked.length === 0 ? ( { {t("pages.dashboard.noData")} ) : ( - + {ranked.map((monitor) => { const paletteKey = getUptimePercentageColor(monitor.uptimePercentage); const color = theme.palette[paletteKey].main; @@ -48,7 +78,7 @@ export const LowestUptimeCard = ({ monitors }: LowestUptimeCardProps) => { key={monitor.id} label={monitor.name} value={formatPercentage(monitor.uptimePercentage)} - fillRatio={1 - monitor.uptimePercentage} + fillRatio={monitor.uptimePercentage} valueColor={color} /> ); diff --git a/client/src/Pages/Dashboard/index.tsx b/client/src/Pages/Dashboard/index.tsx index 1656b9709..fac8d15fc 100644 --- a/client/src/Pages/Dashboard/index.tsx +++ b/client/src/Pages/Dashboard/index.tsx @@ -15,7 +15,7 @@ import type { RootState, AppDispatch } from "@/store"; 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 { UptimeCard } from "@/Pages/Dashboard/components/cards/UptimeCard"; import { MonitorsByTypeCard } from "@/Pages/Dashboard/components/cards/MonitorsByTypeCard"; import { EditCardsModal } from "@/Pages/Dashboard/components/EditCardsModal"; import { type DashboardCardKey, dashboardCardKeys } from "@/Types/Dashboard"; @@ -51,7 +51,7 @@ const Dashboard = () => { const cardComponents: Record = { monitorStatus: , currentlyDown: , - lowestUptime: , + uptime: , monitorsByType: , }; diff --git a/client/src/Types/Dashboard.ts b/client/src/Types/Dashboard.ts index 6e418f5ed..4390402f6 100644 --- a/client/src/Types/Dashboard.ts +++ b/client/src/Types/Dashboard.ts @@ -1,7 +1,10 @@ export const dashboardCardKeys = [ "monitorStatus", "currentlyDown", - "lowestUptime", + "uptime", "monitorsByType", ] as const; export type DashboardCardKey = (typeof dashboardCardKeys)[number]; + +export const dashboardSortOrders = ["ascending", "descending"] as const; +export type DashboardSortOrder = (typeof dashboardSortOrders)[number]; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index ba0e3e897..19878f77f 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1925,9 +1925,12 @@ "cards": { "monitorStatus": "Monitor status", "currentlyDown": "Currently down", - "lowestUptime": "Lowest uptime", + "uptime": "Monitor uptime", "monitorsByType": "Monitors by type" }, + "uptimeCard": { + "toggleSortOrder": "Toggle sort order" + }, "currentlyDown": { "empty": "No monitors are down at this moment." },