diff --git a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx index f908e4185..35abb1cd9 100644 --- a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx +++ b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx @@ -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 ( ; + +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 ( + + {children} + + ); +}; + +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 ( + + + + {title} + {tooltip && ( + + )} + + {subtitle} + {children} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/Tooltip.tsx b/client/src/Components/v2/design-elements/Tooltip.tsx new file mode 100644 index 000000000..f83b1331c --- /dev/null +++ b/client/src/Components/v2/design-elements/Tooltip.tsx @@ -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 ( + + {props.children} + + ); +}; + +export interface TooltipWithInfoProps extends Omit { + iconSize?: number; + iconColor?: string; +} + +export const TooltipWithInfo = ({ + iconSize = 14, + iconColor, + ...props +}: TooltipWithInfoProps) => { + const theme = useTheme(); + + const defaultColor = theme.palette.text.secondary; + + return ( + + + { + e.currentTarget.style.opacity = "1"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.opacity = "0.7"; + }} + /> + + + ); +}; diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx index e71cfb2ec..967e61160 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/v2/design-elements/index.tsx @@ -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"; diff --git a/client/src/Components/v2/monitors/MonitorStatBoxes.tsx b/client/src/Components/v2/monitors/MonitorStatBoxes.tsx new file mode 100644 index 000000000..1e19d44ea --- /dev/null +++ b/client/src/Components/v2/monitors/MonitorStatBoxes.tsx @@ -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 ( + + + + + + {monitor?.type === "http" && ( + + )} + + ); +}; diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx index a7cfa34fd..60bfcc78d 100644 --- a/client/src/Components/v2/monitors/index.tsx +++ b/client/src/Components/v2/monitors/index.tsx @@ -1 +1,2 @@ export * from "./ControlsFilter"; +export * from "./MonitorStatBoxes"; diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index a816bd4d9..cdb69b5cd 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -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(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 ( { monitor={monitor} isAdmin={isAdmin} /> + ); }; diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 414dd0164..e6098121a 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -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 { diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 11b003442..318398579 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -169,6 +169,12 @@ "pause": "Pause", "resume": "Resume", "delete": "Delete" + }, + "statBoxes": { + "activeFor": "Active for", + "lastCheck": "Last check", + "lastResponseTime": "Last response time", + "certificateExpiry": "Certificate expiry" } } },