feat(dashboard): add currently down and lowest uptime dashboard cards
This commit is contained in:
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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];
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user