stat boxes

This commit is contained in:
Alex Holliday committed 2026-01-25 04:09:40 +00:00
1 parent f7ae11997d
commit a2569c50c7
9 files changed
+286 -17

No files matched your search

@@ -11,7 +11,7 @@ import type { Monitor } from "@/Types/Monitor.js";
interface HeaderMonitorControlsProps {
path: string;
monitor: Monitor;
monitor?: Monitor;
isAdmin: boolean;
}
@@ -23,9 +23,13 @@ export const HeaderMonitorControls = ({
const { t } = useTranslation();
const navigate = useNavigate();
const theme = useTheme();
if (!monitor) {
return null;
}
return (
<Stack
spacing={{ xs: theme.spacing(4), md: 0 }}
spacing={{ xs: theme.spacing(8), md: 0 }}
direction={{ xs: "column", md: "row" }}
alignItems={"center"}
justifyContent={"space-between"}
@@ -0,0 +1,86 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import { lighten } from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import type { PaletteKey } from "@/Utils/Theme/v2Theme";
import { BaseBox, TooltipWithInfo } from "@/Components/v2/design-elements";
import type { SxProps } from "@mui/material";
type GradientBox = React.PropsWithChildren<{
palette?: PaletteKey;
sx?: SxProps;
}>;
export const GradientBox = ({ children, palette, sx }: GradientBox) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const isLight = theme.palette.mode === "light";
const paper = theme.palette.background.paper;
const paperStart = lighten(paper, isLight ? 0.06 : 0.08);
const paperEnd = paper;
const bg = palette
? `linear-gradient(135deg, ${theme.palette[palette].light} 0%, ${theme.palette[palette].main} 100%)`
: `linear-gradient(135deg, ${paperStart} 0%, ${paperEnd} 100%)`;
return (
<BaseBox
sx={{
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
width: isSmall ? `100%` : `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
background: bg,
...sx,
}}
>
{children}
</BaseBox>
);
};
type StatBoxProps = React.PropsWithChildren<{
title: string;
subtitle: string;
palette?: PaletteKey;
sx?: SxProps;
tooltip?: string;
onClick?: () => void;
}>;
export const StatBox = ({
title,
subtitle,
palette,
children,
sx,
tooltip,
onClick,
}: StatBoxProps) => {
const theme = useTheme();
const textColor = palette ? theme.palette[palette].contrastText : "inherit";
return (
<GradientBox
palette={palette}
sx={{
...(sx as object),
...(onClick ? { cursor: "pointer", "&:hover": { opacity: 0.95 } } : {}),
}}
>
<Stack onClick={onClick}>
<Box sx={{ display: "flex", alignItems: "center", gap: theme.spacing(2) }}>
<Typography color={textColor}>{title}</Typography>
{tooltip && (
<TooltipWithInfo
title={tooltip}
iconColor={textColor as string}
iconSize={14}
/>
)}
</Box>
<Typography color={textColor}>{subtitle}</Typography>
{children}
</Stack>
</GradientBox>
);
};
@@ -0,0 +1,79 @@
import MuiTooltip from "@mui/material/Tooltip";
import { useTheme } from "@mui/material/styles";
import { Info } from "lucide-react";
import type { TooltipProps } from "@mui/material/Tooltip";
type StyledTooltipProps = TooltipProps;
export const Tooltip = ({ placement = "top", ...props }: StyledTooltipProps) => {
const theme = useTheme();
return (
<MuiTooltip
title={props.title}
placement={placement}
slotProps={{
tooltip: {
sx: {
background:
theme.palette.mode === "dark"
? "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)"
: "linear-gradient(135deg, #2c3e50 0%, #1a252f 100%)",
backgroundColor: "transparent",
color: "#ffffff",
fontSize: "13px",
padding: `${theme.spacing(4)} ${theme.spacing(5)}`,
borderRadius: `${theme.shape.borderRadius}px`,
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.4)",
},
},
}}
>
{props.children}
</MuiTooltip>
);
};
export interface TooltipWithInfoProps extends Omit<StyledTooltipProps, "children"> {
iconSize?: number;
iconColor?: string;
}
export const TooltipWithInfo = ({
iconSize = 14,
iconColor,
...props
}: TooltipWithInfoProps) => {
const theme = useTheme();
const defaultColor = theme.palette.text.secondary;
return (
<Tooltip {...props}>
<span
style={{
display: "inline-flex",
alignItems: "center",
cursor: "help",
}}
>
<Info
size={iconSize}
strokeWidth={1.5}
style={{
opacity: 0.7,
color: iconColor || defaultColor,
transition: "opacity 0.2s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.opacity = "1";
}}
onMouseLeave={(e) => {
e.currentTarget.style.opacity = "0.7";
}}
/>
</span>
</Tooltip>
);
};
@@ -12,3 +12,5 @@ export * from "./PulseDot";
export * from "./Dot";
export * from "./MonitorStatus";
export { default as Icon } from "./Icon";
export * from "./Tooltip";
export * from "./StatBox";
@@ -0,0 +1,69 @@
import Stack from "@mui/material/Stack";
import { StatBox } from "@/Components/v2/design-elements";
import { useTheme } from "@mui/material/styles";
import type { MonitorStats, Monitor } from "@/Types/Monitor";
import { getHumanReadableDuration } from "@/Utils/timeUtils.js";
import { getStatusPalette } from "@/Utils/MonitorUtils";
import { useTranslation } from "react-i18next";
interface MonitorStatBoxesProps {
monitor?: Monitor;
monitorStats: MonitorStats | null;
certificateExpiry?: string;
}
export const MonitorStatBoxes = ({
monitor,
monitorStats,
certificateExpiry,
}: MonitorStatBoxesProps) => {
const theme = useTheme();
const { t } = useTranslation();
if (!monitorStats || !monitor) {
return null;
}
const timeOfLastFailure = monitorStats?.timeOfLastFailure || 0;
const timeSinceLastFailure =
timeOfLastFailure > 0
? Date.now() - timeOfLastFailure
: Date.now() - new Date(monitorStats?.createdAt);
// Determine time since last check
const timeOfLastCheck = monitorStats?.lastCheckTimestamp || 0;
const timeSinceLastCheck = Date.now() - timeOfLastCheck;
const streakTime = getHumanReadableDuration(timeSinceLastFailure);
const lastCheckTime = getHumanReadableDuration(timeSinceLastCheck);
const palette = getStatusPalette(monitor?.status);
return (
<Stack
direction={{ xs: "column", md: "row" }}
gap={theme.spacing(8)}
>
<StatBox
palette={palette}
title={t("pages.common.monitors.statBoxes.activeFor")}
subtitle={streakTime}
/>
<StatBox
title={t("pages.common.monitors.statBoxes.lastCheck")}
subtitle={lastCheckTime}
/>
<StatBox
title={t("pages.common.monitors.statBoxes.lastResponseTime")}
subtitle={monitorStats?.lastResponseTime + " ms"}
/>
{monitor?.type === "http" && (
<StatBox
title={t("pages.common.monitors.statBoxes.certificateExpiry")}
subtitle={certificateExpiry || "N/A"}
/>
)}
</Stack>
);
};
@@ -1 +1,2 @@
export * from "./ControlsFilter";
export * from "./MonitorStatBoxes";
+27 -12
View File
@@ -9,10 +9,15 @@ import { useGet } from "@/Hooks/UseApi";
import type { MonitorDetailsResponse } from "@/Types/Monitor";
import type { ChecksResponse } from "@/Types/Check";
import type { RootState } from "@/Types/state";
import useCertificateFetch from "./Hooks/useCertificateFetch";
import { MonitorStatBoxes } from "@/Components/v2/monitors";
import { formatDateWithTz } from "@/Utils/timeUtils";
const certificateDateFormat = "MMM D, YYYY h A";
interface CertificateResponse {
certificateDate: string;
}
const UptimeDetailsPage = () => {
const isAdmin = useIsAdmin();
const { monitorId } = useParams<{ monitorId: string }>();
@@ -43,12 +48,22 @@ const UptimeDetailsPage = () => {
const monitor = monitorData?.monitor;
const monitorStats = monitorDetailsData?.monitorStats ?? null;
const [certificateExpiry] = useCertificateFetch({
monitor,
monitorId,
certificateDateFormat,
uiTimezone,
});
// Certificate fetch - only for HTTP monitors
const certificateUrl = useMemo(() => {
if (!monitorId || monitor?.type !== "http") {
return null;
}
return `/monitors/certificate/${monitorId}`;
}, [monitorId, monitor?.type]);
const { data: certificateData } = useGet<CertificateResponse>(certificateUrl);
const certificateExpiry = useMemo(() => {
if (!certificateData?.certificateDate) {
return undefined;
}
return formatDateWithTz(certificateData.certificateDate, certificateDateFormat, uiTimezone) ?? "N/A";
}, [certificateData, uiTimezone]);
const checksUrl = useMemo(() => {
if (!monitorId || !monitor?.type) {
@@ -79,7 +94,6 @@ const UptimeDetailsPage = () => {
monitorError,
refetchMonitor,
monitorStats,
certificateExpiry,
checksIsLoading,
checksError,
checks,
@@ -89,10 +103,6 @@ const UptimeDetailsPage = () => {
setDateRange,
});
if (!monitor) {
return null;
}
return (
<BasePage>
<HeaderMonitorControls
@@ -100,6 +110,11 @@ const UptimeDetailsPage = () => {
monitor={monitor}
isAdmin={isAdmin}
/>
<MonitorStatBoxes
monitor={monitor}
monitorStats={monitorStats}
certificateExpiry={certificateExpiry}
/>
</BasePage>
);
};
+10 -3
View File
@@ -56,11 +56,18 @@ export interface MonitorGroupedCheck {
}
export interface MonitorStats {
id: string;
monitorId: string;
avgResponseTime: number;
totalChecks: number;
upChecks: number;
downChecks: number;
totalUpChecks: number;
totalDownChecks: number;
uptimePercentage: number;
averageResponseTime: number;
lastCheckTimestamp: number;
lastResponseTime: number;
timeOfLastFailure?: number;
createdAt: string;
updatedAt: string;
}
export interface MonitorData {
+6
View File
@@ -169,6 +169,12 @@
"pause": "Pause",
"resume": "Resume",
"delete": "Delete"
},
"statBoxes": {
"activeFor": "Active for",
"lastCheck": "Last check",
"lastResponseTime": "Last response time",
"certificateExpiry": "Certificate expiry"
}
}
},