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/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..f7e7cdd8b --- /dev/null +++ b/client/src/Pages/Dashboard/components/cards/CurrentlyDownCard.tsx @@ -0,0 +1,58 @@ +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 { typographyLevels } from "@/Utils/Theme/Palette"; +import { LAYOUT } 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/UptimeCard.tsx b/client/src/Pages/Dashboard/components/cards/UptimeCard.tsx new file mode 100644 index 000000000..1bf2706bd --- /dev/null +++ b/client/src/Pages/Dashboard/components/cards/UptimeCard.tsx @@ -0,0 +1,90 @@ +import IconButton from "@mui/material/IconButton"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +import { useDispatch, useSelector } from "react-redux"; +import { ArrowDown, ArrowUp } from "lucide-react"; +import { Icon } from "@/Components/design-elements"; +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 { LAYOUT } from "@/Utils/Theme/constants"; +import { setDashboardUptimeSortOrder } from "@/Features/UI/uiSlice"; +import type { Monitor } from "@/Types/Monitor"; +import type { AppDispatch, RootState } from "@/store"; + +const TOP_N = 5; + +interface UptimeCardProps { + monitors: Monitor[]; +} + +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) => 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; + return ( + + ); + })} + + )} + + ); +}; diff --git a/client/src/Pages/Dashboard/index.tsx b/client/src/Pages/Dashboard/index.tsx index f86b224f1..fac8d15fc 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 { 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"; @@ -48,6 +50,8 @@ const Dashboard = () => { const cardComponents: Record = { monitorStatus: , + currentlyDown: , + uptime: , monitorsByType: , }; diff --git a/client/src/Types/Dashboard.ts b/client/src/Types/Dashboard.ts index a397ec807..4390402f6 100644 --- a/client/src/Types/Dashboard.ts +++ b/client/src/Types/Dashboard.ts @@ -1,2 +1,10 @@ -export const dashboardCardKeys = ["monitorStatus", "monitorsByType"] as const; +export const dashboardCardKeys = [ + "monitorStatus", + "currentlyDown", + "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 4c0161001..cff7434a4 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1936,16 +1936,27 @@ }, "cards": { "monitorStatus": "Monitor status", + "currentlyDown": "Currently down", + "uptime": "Monitor uptime", "monitorsByType": "Monitors by type" }, + "uptimeCard": { + "toggleSortOrder": "Toggle sort order" + }, + "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" } }