Merge pull request #3186 from bluewave-labs/feat/v2-uptime-details

feat/v2 uptime details
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-01-26 09:16:12 -08:00
commit d0727f91dd
67 files changed
+1599 -1038

No files matched your search

+8 -1
View File
@@ -37,7 +37,14 @@ function App() {
<AppLayout>
<Routes />
</AppLayout>
<ToastContainer />
<ToastContainer
newestOnTop={true}
theme={mode}
style={{
"--toastify-color-progress-light": "#7C8BA1",
"--toastify-color-progress-dark": "#7C8BA1",
}}
/>
</ThemeProvider>
);
}
@@ -1,6 +1,6 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { useEffect, useState } from "react";
import "./index.css";
import { useSelector } from "react-redux";
@@ -5,7 +5,7 @@ import { Stack, Box, Tooltip, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { useSelector } from "react-redux";
const PlaceholderCheck = ({ daysToShow }) => {
@@ -12,7 +12,7 @@ import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import {
tooltipDateFormatLookup,
tickDateFormatLookup,
@@ -1,6 +1,6 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { useEffect, useState, forwardRef } from "react";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
import { useSelector } from "react-redux";
import { useTheme } from "@mui/material";
import { Text } from "recharts";
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { Box, Stack, Typography } from "@mui/material";
import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions.js";
/**
@@ -4,7 +4,7 @@ import PulseDot from "../Animated/PulseDot.jsx";
import Typography from "@mui/material/Typography";
import Dot from "../Dot/index.jsx";
// Utils
import { formatDurationRounded } from "../../../Utils/timeUtils.js";
import { formatDurationRounded } from "../../../Utils/timeUtilsLegacy.js";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js";
@@ -0,0 +1,102 @@
import Stack from "@mui/material/Stack";
import { Icon, MonitorStatus } from "@/Components/v2/design-elements";
import { Button } from "@/Components/v2/inputs";
import { Settings, Pause, Play, Mail, Bug } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@mui/material/styles";
import { usePost } from "@/Hooks/UseApi";
import type { Monitor } from "@/Types/Monitor.js";
interface HeaderMonitorControlsProps {
path: string;
monitor?: Monitor;
isAdmin: boolean;
refetch: Function;
}
export const HeaderMonitorControls = ({
path,
monitor,
isAdmin,
refetch,
}: HeaderMonitorControlsProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const theme = useTheme();
const {
post,
loading: isPosting,
// error: postError,
} = usePost<any, Monitor>();
if (!monitor) {
return null;
}
return (
<Stack
spacing={{ xs: theme.spacing(8), md: 0 }}
direction={{ xs: "column", md: "row" }}
alignItems={"center"}
justifyContent={"space-between"}
>
<MonitorStatus monitor={monitor} />
<Stack
width={{ xs: "100%", md: "auto" }}
direction={{ xs: "column", md: "row" }}
gap={theme.spacing(2)}
>
<Button
variant="contained"
color="secondary"
loading={isPosting}
startIcon={<Icon icon={Mail} />}
onClick={async () => {
await post(`/notifications/test/all`, { monitorId: monitor.id });
}}
sx={{
whiteSpace: "nowrap",
}}
>
{t("common.buttons.testNotifications")}
</Button>
<Button
variant="contained"
color="secondary"
startIcon={<Icon icon={Bug} />}
onClick={() => {
navigate(`/incidents/${monitor?.id}`);
}}
>
{t("common.buttons.incidents")}
</Button>
{isAdmin && (
<Button
variant="contained"
color="secondary"
loading={isPosting}
startIcon={monitor?.isActive ? <Icon icon={Pause} /> : <Icon icon={Play} />}
onClick={async () => {
await post(`/monitors/pause/${monitor.id}`, {});
await refetch();
}}
>
{monitor?.isActive ? t("pause") : t("resume")}
</Button>
)}
{isAdmin && (
<Button
variant="contained"
color="secondary"
startIcon={<Icon icon={Settings} />}
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
>
Configure
</Button>
)}
</Stack>
</Stack>
);
};
@@ -0,0 +1,89 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { ToggleButtonGroup, ToggleButton } from "@/Components/v2/inputs";
import { useTheme } from "@mui/material/styles";
interface MonitorTimeFrameHeaderProps {
isLoading?: boolean;
hasDateRange?: boolean;
dateRange: string;
setDateRange: (dateRange: string) => void;
}
export const HeaderTimeRange = ({
isLoading = false,
hasDateRange = true,
dateRange,
setDateRange,
}: MonitorTimeFrameHeaderProps) => {
const theme = useTheme();
const handleChange = (
_event: React.MouseEvent<HTMLElement>,
newValue: string | null
) => {
if (newValue !== null) {
setDateRange(newValue);
}
};
let timeFramePicker = null;
if (hasDateRange) {
timeFramePicker = (
<ToggleButtonGroup
value={dateRange}
exclusive
onChange={handleChange}
size="small"
>
<ToggleButton
disabled={isLoading}
value="recent"
>
Recent
</ToggleButton>
<ToggleButton
disabled={isLoading}
value="day"
>
Day
</ToggleButton>
<ToggleButton
disabled={isLoading}
value="week"
>
Week
</ToggleButton>
<ToggleButton
disabled={isLoading}
value="month"
>
Month
</ToggleButton>
</ToggleButtonGroup>
);
}
return (
<Stack
direction="row"
justifyContent="flex-end"
alignItems="center"
gap={theme.spacing(4)}
>
<Typography variant="body2">
Showing statistics for past{" "}
{dateRange === "recent"
? "2 hours"
: dateRange === "day"
? "24 hours"
: dateRange === "week"
? "7 days"
: "30 days"}
.
</Typography>
{timeFramePicker}
</Stack>
);
};
+2
View File
@@ -2,3 +2,5 @@ export * from "../common/charts/HistogramResponseTime";
export * from "../common/charts/HeatmapResponseTime";
export * from "../common/charts/HeatmapResponseTimeTooltip";
export * from "../common/controls/HeaderCreate";
export * from "../common/controls/HeaderMonitorControls";
export * from "../common/controls/HeaderTimeRange";
@@ -0,0 +1,72 @@
import { BaseBox } from "../design-elements";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import type { ResponsiveStyleValue } from "@mui/system";
import { useTheme } from "@mui/material/styles";
type BaseChartProps = React.PropsWithChildren<{
icon: React.ReactNode;
title: string;
width?: number | string;
maxWidth?: number | string;
padding?: number | string | ResponsiveStyleValue<number | string>;
}>;
export const BaseChart = ({
children,
icon,
title,
width = "100%",
maxWidth = "100%",
padding,
}: BaseChartProps) => {
const theme = useTheme();
return (
<BaseBox
sx={{
padding: padding ?? theme.spacing(8),
display: "flex",
flex: 1,
width: width,
maxWidth: maxWidth,
}}
>
<Stack
gap={theme.spacing(8)}
flex={1}
>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(4)}
>
{icon && (
<BaseBox
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 34,
height: 34,
backgroundColor: theme.palette.action.hover,
"& svg": {
width: 20,
height: 20,
"& path": {
stroke: theme.palette.text.secondary,
},
},
}}
>
{icon}
</BaseBox>
)}
<Typography variant="h2">{title}</Typography>
</Stack>
<Box flex={1}>{children}</Box>
</Stack>
</BaseBox>
);
};
@@ -55,7 +55,7 @@ export const BasePage = ({
spacing={theme.spacing(10)}
{...props}
>
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
<Breadcrumb />
{children}
</Stack>
);
@@ -2,25 +2,21 @@ import MuiBreadcrumbs from "@mui/material/Breadcrumbs";
import Typography from "@mui/material/Typography";
import { Link, useLocation } from "react-router-dom";
import { useTheme } from "@mui/material/styles";
import { ChevronRight, Home } from "lucide-react";
import { useTranslation } from "react-i18next";
import { ChevronRight } from "lucide-react";
export const Breadcrumb = ({ breadcrumbOverride }: { breadcrumbOverride?: string[] }) => {
const { t } = useTranslation();
const isId = (segment: string): boolean => {
return segment.length === 24 || /^[a-f0-9-]{36}$/.test(segment);
};
export const Breadcrumb = () => {
const theme = useTheme();
const location = useLocation();
// Don't show breadcrumb on dashboard
if (location.pathname === "/" || location.pathname === "/uptime") {
return null;
}
const segments = location.pathname.split("/").filter((x) => x);
const pathnames = breadcrumbOverride || location.pathname.split("/").filter((x) => x);
// Don't show if only one level (e.g., /uptime)
if (pathnames.length === 0) {
return null;
}
// Build simplified breadcrumb: "uptime" or "uptime / details"
const basePage = segments[0] || "home";
const hasDetailsPage = segments.length > 1 && isId(segments[1]);
return (
<MuiBreadcrumbs
@@ -38,80 +34,45 @@ export const Breadcrumb = ({ breadcrumbOverride }: { breadcrumbOverride?: string
},
}}
>
<Link
to="/uptime"
style={{
display: "flex",
alignItems: "center",
gap: theme.spacing(2),
textDecoration: "none",
color: theme.palette.text.secondary,
}}
>
<Home
size={16}
strokeWidth={1.5}
/>
{hasDetailsPage ? (
<Link
to={`/${basePage}`}
style={{ textDecoration: "none" }}
>
<Typography
sx={{
fontSize: "14px",
color: theme.palette.text.secondary,
"&:hover": {
color: theme.palette.primary.main,
},
}}
>
{basePage.charAt(0).toUpperCase() + basePage.slice(1)}
</Typography>
</Link>
) : (
<Typography
sx={{
fontSize: "14px",
color: theme.palette.text.secondary,
"&:hover": {
color: theme.palette.primary.main,
},
fontWeight: 600,
color: theme.palette.primary.main,
}}
>
Home
{basePage.charAt(0).toUpperCase() + basePage.slice(1)}
</Typography>
</Link>
{pathnames.map((value, index) => {
const last = index === pathnames.length - 1;
const to = `/${pathnames.slice(0, index + 1).join("/")}`;
let displayName = "";
// Paths that are IDs should be detail pages
if (value.length === 24 || value.match(/^[a-f0-9-]{36}$/)) {
displayName = "Details";
} else {
displayName =
t(`common.breadcrumbs.${value}`) ||
value.charAt(0).toUpperCase() + value.slice(1);
}
return last ? (
<Typography
key={to}
sx={{
fontSize: "14px",
fontWeight: 600,
color: theme.palette.primary.main,
}}
>
{displayName}
</Typography>
) : (
<Link
key={to}
to={to}
style={{
textDecoration: "none",
}}
>
<Typography
sx={{
fontSize: "14px",
color: theme.palette.text.secondary,
"&:hover": {
color: theme.palette.primary.main,
},
}}
>
{displayName}
</Typography>
</Link>
);
})}
)}
{hasDetailsPage && (
<Typography
sx={{
fontSize: "14px",
fontWeight: 600,
color: theme.palette.primary.main,
}}
>
Details
</Typography>
)}
</MuiBreadcrumbs>
);
};
@@ -0,0 +1,23 @@
import type { CSSProperties } from "react";
interface DotProps {
color?: string;
size?: string;
style?: CSSProperties;
}
export const Dot = ({ color = "gray", size = "4px", style }: DotProps) => {
return (
<span
style={{
content: '""',
width: size,
height: size,
borderRadius: "50%",
backgroundColor: color,
opacity: 0.8,
...style,
}}
/>
);
};
@@ -0,0 +1,18 @@
import type { LucideIcon } from "lucide-react";
interface IconProps {
icon: LucideIcon;
size?: number;
strokeWidth?: number;
}
const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5 }: IconProps) => {
return (
<Icon
size={size}
strokeWidth={strokeWidth}
/>
);
};
export default Icon;
@@ -0,0 +1,59 @@
import type { Monitor } from "@/Types/Monitor";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { PulseDot, Dot } from "@/Components/v2/design-elements";
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
import { useTheme } from "@mui/material/styles";
import prettyMilliseconds from "pretty-ms";
import { typographyLevels } from "@/Utils/Theme/v2Palette";
import useMediaQuery from "@mui/material/useMediaQuery";
export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
if (!monitor) {
return null;
}
return (
<Stack>
<Typography
fontSize={typographyLevels.xl}
fontWeight={500}
color={theme.palette.primary.contrastText}
overflow={"hidden"}
textOverflow={"ellipsis"}
whiteSpace={"nowrap"}
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
>
{monitor.name}
</Typography>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(4)}
>
<PulseDot color={getStatusColor(monitor.status, theme)} />
<Typography
color={theme.palette.text.secondary}
fontSize={typographyLevels.l}
fontWeight={"bolder"}
fontFamily={"monospace"}
overflow={"hidden"}
textOverflow={"ellipsis"}
whiteSpace={"nowrap"}
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
>
{formatUrl(monitor?.url)}
</Typography>
{!isSmall && (
<>
<Dot />
<Typography>
Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })}
</Typography>
</>
)}
</Stack>
</Stack>
);
};
@@ -0,0 +1,48 @@
import { Box, Stack, useTheme } from "@mui/material";
interface PulseDotProps {
color: string;
}
export const PulseDot = ({ color }: PulseDotProps) => {
const theme = useTheme();
return (
<Stack
width="26px"
height="24px"
alignItems="center"
justifyContent="center"
>
<Box
minWidth="18px"
minHeight="18px"
sx={{
position: "relative",
backgroundColor: color,
borderRadius: "50%",
"&::before": {
content: `""`,
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "inherit",
borderRadius: "50%",
animation: "ripple 1.8s ease-out infinite",
},
"&::after": {
content: `""`,
position: "absolute",
width: "7px",
height: "7px",
borderRadius: "50%",
backgroundColor: theme.palette.background.paper,
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
},
}}
/>
</Stack>
);
};
@@ -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>
);
};
@@ -17,14 +17,17 @@ export const StatusLabel = ({
sx,
}: {
status: MonitorStatus;
isActive: boolean;
isActive?: boolean;
sx?: SxProps;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const palette = getStatusPalette(status);
const determineStatus = (isActive: boolean, status: MonitorStatus): string => {
const determineStatus = (
isActive: boolean | undefined,
status: MonitorStatus
): string => {
if (isActive === false) {
return t("pages.common.monitors.status.paused");
}
@@ -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>
);
};
@@ -8,3 +8,10 @@ export * from "./BaseBox";
export * from "./Table";
export { DataTable as Table } from "./Table";
export * from "./StatusLabel";
export * from "./PulseDot";
export * from "./Dot";
export * from "./MonitorStatus";
export { default as Icon } from "./Icon";
export * from "./Tooltip";
export * from "./StatBox";
export * from "./BaseChart";
@@ -0,0 +1,52 @@
import ToggleButton from "@mui/material/ToggleButton";
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
import type { ToggleButtonProps } from "@mui/material/ToggleButton";
import type { ToggleButtonGroupProps } from "@mui/material/ToggleButtonGroup";
import { useTheme } from "@mui/material/styles";
export const ToggleButtonInput = ({ sx, ...props }: ToggleButtonProps) => {
return (
<ToggleButton
{...props}
sx={{
px: 8,
textTransform: "none",
height: 34,
fontWeight: 400,
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
boxShadow: "none",
"&:hover": {
boxShadow: "none",
},
...sx,
}}
/>
);
};
export const ToggleButtonGroupInput = ({ sx, ...props }: ToggleButtonGroupProps) => {
const theme = useTheme();
return (
<ToggleButtonGroup
{...props}
sx={{
"& .MuiToggleButtonGroup-grouped": {
borderColor: theme.palette.divider,
borderRadius: 0,
"&:first-of-type": {
borderTopLeftRadius: 2,
borderBottomLeftRadius: 2,
},
"&:last-of-type": {
borderTopRightRadius: 2,
borderBottomRightRadius: 2,
},
},
...sx,
}}
/>
);
};
@@ -4,3 +4,7 @@ export { SelectInput as Select } from "./Select";
export { CheckboxInput as Checkbox } from "./Checkbox";
export { AutoCompleteInput as Autocomplete } from "./AutoComplete";
export { TextInput as TextField } from "./TextInput";
export {
ToggleButtonInput as ToggleButton,
ToggleButtonGroupInput as ToggleButtonGroup,
} from "./ToggleButton";
@@ -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/timeUtilsLegacy.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 || 0).getTime();
// 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>
);
};
@@ -0,0 +1,163 @@
import { BaseChart, BaseBox } from "@/Components/v2/design-elements";
import { Clock } from "lucide-react";
import {
AreaChart,
Area,
XAxis,
Tooltip,
CartesianGrid,
ResponsiveContainer,
Text,
} from "recharts";
import Typography from "@mui/material/Typography";
import { useSelector } from "react-redux";
import {
formatDateWithTz,
tickDateFormatLookup,
tooltipDateFormatLookup,
} from "@/Utils/TimeUtils";
import { useTheme } from "@mui/material/styles";
import type { GroupedCheck } from "@/Types/Check";
import type { RootState } from "@/Types/state";
type XTickProps = {
x: number;
y: number;
payload: { value: any };
range: string;
};
export const XTick = ({ x, y, payload, range }: XTickProps) => {
const format = tickDateFormatLookup(range);
const theme = useTheme();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
return (
<Text
x={x}
y={y + 10}
textAnchor="middle"
fill={theme.palette.text.secondary}
fontSize={11}
fontWeight={400}
>
{formatDateWithTz(payload?.value, format, uiTimezone)}
</Text>
);
};
type ResponseTimeToolTipProps = {
active?: boolean | undefined;
payload?: any[];
label?: string | number;
range: string;
theme: any;
uiTimezone: string;
};
const ResponseTimeToolTip = ({
active,
payload,
label,
range,
theme,
uiTimezone,
}: ResponseTimeToolTipProps) => {
if (!label) return null;
if (!payload) return null;
if (!active) return null;
const format = tooltipDateFormatLookup(range);
const responseTime = Math.floor(payload?.[0]?.payload?.avgResponseTime || 0);
return (
<BaseBox sx={{ py: theme.spacing(2), px: theme.spacing(4) }}>
<Typography>{formatDateWithTz(String(label), format, uiTimezone)}</Typography>
<Typography>Response time: {responseTime} ms</Typography>
</BaseBox>
);
};
export const HistogramDetails = ({
checks,
range,
}: {
checks: GroupedCheck[];
range: string;
}) => {
const theme = useTheme();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
return (
<BaseChart
icon={
<Clock
size={20}
strokeWidth={1.5}
/>
}
title="Response times"
>
<ResponsiveContainer
width="100%"
height={300}
>
<AreaChart data={checks?.slice()}>
<CartesianGrid
stroke={theme.palette.divider}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<defs>
<linearGradient
id="colorUv"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme.palette.primary.main}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette.primary.light}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis
axisLine={false}
tickLine={false}
dataKey="bucketDate"
tick={(props) => (
<XTick
{...props}
range={range}
/>
)}
/>
<Tooltip
content={(props) => (
<ResponseTimeToolTip
{...props}
range={range}
theme={theme}
uiTimezone={uiTimezone}
/>
)}
/>
<Area
type="monotone"
dataKey="avgResponseTime"
stroke={theme.palette.primary.main}
fill="url(#colorUv)"
/>
</AreaChart>
</ResponsiveContainer>
</BaseChart>
);
};
@@ -0,0 +1,166 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { BaseChart } from "@/Components/v2/design-elements";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import { formatDateWithTz } from "@/Utils/timeUtilsLegacy";
import { useTranslation } from "react-i18next";
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell, Tooltip } from "recharts";
import { getResponseTimeColor } from "@/Utils/MonitorUtils";
import type { GroupedCheck } from "@/Types/Check";
import type { RootState } from "@/Types/state";
interface XLabelProps {
p1: GroupedCheck;
p2: GroupedCheck;
range: string;
}
const XLabel = ({ p1, p2, range }: XLabelProps) => {
const theme = useTheme();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
const dateFormat = range === "day" ? "MMM D, h:mm A" : "MMM D";
return (
<>
<text
x={0}
y="100%"
dy={-3}
textAnchor="start"
fontSize={11}
fill={theme.palette.text.secondary}
>
{formatDateWithTz(p1.bucketDate, dateFormat, uiTimezone)}
</text>
<text
x="100%"
y="100%"
dy={-3}
textAnchor="end"
fontSize={11}
fill={theme.palette.text.secondary}
>
{formatDateWithTz(p2.bucketDate, dateFormat, uiTimezone)}
</text>
</>
);
};
interface HistogramStatusProps {
title: string;
icon: React.ReactNode;
checks?: GroupedCheck[];
range: string;
}
export const HistogramStatus = ({
title,
icon,
checks = [],
range,
}: HistogramStatusProps) => {
const { t } = useTranslation();
const theme = useTheme();
const CustomTooltip = ({
active,
payload,
}: {
active?: boolean;
payload?: Array<{ payload: GroupedCheck }>;
}) => {
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
if (!active || !payload?.length) return null;
const d = payload[0]?.payload;
const avg = d?.avgResponseTime ?? 0;
const titleText = t("common.charts.labels.averageResponseTime");
const fmt = range === "30d" ? "MMM D, YYYY" : "ddd, MMM D, YYYY, h:mm A";
let dateLabel = "";
if (d?.bucketDate) {
const base = new Date(d.bucketDate);
const midBucket =
range === "30d" ? new Date(base.getTime() + 12 * 60 * 60 * 1000) : base;
dateLabel = formatDateWithTz(midBucket.toISOString(), fmt, uiTimezone);
}
return (
<Stack
sx={{
p: 1,
bgcolor: "background.paper",
border: 1,
borderColor: "divider",
borderRadius: 1,
}}
>
{dateLabel ? (
<Typography
sx={{ color: "text.secondary" }}
variant="caption"
>
{dateLabel}
</Typography>
) : null}
<Typography variant="caption">{titleText}</Typography>
<Typography variant="body2">{Math.floor(avg)} ms</Typography>
</Stack>
);
};
const totalChecks = checks.reduce((acc, check) => acc + (check.totalChecks || 0), 0);
return (
<BaseChart
icon={icon}
title={title}
>
<Stack gap={theme.spacing(8)}>
<Stack
position="relative"
direction="row"
justifyContent="space-between"
>
<Stack>
<Typography>Total checks: {totalChecks}</Typography>
</Stack>
</Stack>
<ResponsiveContainer
width="100%"
height={155}
>
<BarChart data={checks}>
<XAxis
stroke={theme.palette.divider}
height={15}
tick={false}
label={
<XLabel
p1={checks[0]}
p2={checks[checks.length - 1]}
range={range}
/>
}
/>
<Tooltip
cursor={false}
content={<CustomTooltip />}
/>
<Bar
dataKey="avgResponseTime"
maxBarSize={7}
background={{ fill: "transparent" }}
>
{checks?.map((groupedCheck, index) => {
const fillColor = getResponseTimeColor(groupedCheck.avgResponseTime);
return (
<Cell
key={groupedCheck.bucketDate || `check-${index}`}
fill={theme.palette[fillColor].main}
/>
);
})}
</Bar>
</BarChart>
</ResponsiveContainer>
</Stack>
</BaseChart>
);
};
@@ -0,0 +1,91 @@
import { BaseChart } from "@/Components/v2/design-elements";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Gauge } from "lucide-react";
import { Cell, RadialBarChart, RadialBar, ResponsiveContainer } from "recharts";
import { useTranslation } from "react-i18next";
import { getResponseTimeColor } from "@/Utils/MonitorUtils";
import { useTheme } from "@mui/material/styles";
export const RadialAvgResponse = ({ avg, max }: { avg: number; max: number }) => {
const { t } = useTranslation();
const theme = useTheme();
const chartData = [
{ name: "max", value: max - avg, color: "transparent" },
{ name: "avg", value: avg, color: "red" },
];
const palette = getResponseTimeColor(avg);
const msg: Record<string, string> = {
success: "Excellent",
warning: "Average",
danger: "Poor",
};
return (
<BaseChart
icon={
<Gauge
size={20}
strokeWidth={1.5}
/>
}
title={t("common.charts.labels.averageResponseTime")}
>
<Stack
height="100%"
position={"relative"}
justifyContent={"flex-end"}
>
<ResponsiveContainer
width="100%"
height={155}
>
<RadialBarChart
cy="100%"
data={chartData}
startAngle={180}
endAngle={0}
innerRadius={"120%"}
outerRadius={"200%"}
>
<RadialBar
dataKey="value"
background={{ fill: theme.palette[palette].light }}
>
<Cell visibility={"hidden"} />
<Cell fill={theme.palette[palette].main} />
</RadialBar>
</RadialBarChart>
</ResponsiveContainer>
<Stack
direction={"row"}
justifyContent={"space-between"}
>
<Typography variant="body2">{t("common.charts.labels.low")}</Typography>
<Typography variant="body2">{t("common.charts.labels.high")}</Typography>
</Stack>
<Stack
position="absolute"
top={"50%"}
right={"50%"}
sx={{
transform: "translate(50%, 0%)",
}}
>
<Typography
variant="h6"
textAlign={"center"}
>
{msg[palette]}
</Typography>
<Typography
variant="h6"
textAlign={"center"}
>{`${avg?.toFixed()}ms`}</Typography>
</Stack>
</Stack>
</BaseChart>
);
};
@@ -1 +1,5 @@
export * from "./ControlsFilter";
export * from "./MonitorStatBoxes";
export * from "./charts/HistogramStatus";
export * from "./charts/RadialAvgResponse";
export * from "./charts/HistogramDetails";
+1
View File
@@ -1,4 +1,5 @@
import { toast, type ToastOptions } from "react-toastify";
export const useToast = () => {
const showToast = (message: string, options?: ToastOptions) => {
toast.dismiss();
@@ -13,7 +13,7 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx";
//Utils
import { formatDateWithTz } from "@/Utils/timeUtils.js";
import { formatDateWithTz } from "@/Utils/timeUtilsLegacy.js";
import { useSelector } from "react-redux";
import { useState } from "react";
import PropTypes from "prop-types";
@@ -9,7 +9,7 @@ import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx";
import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { formatDateWithTz } from "@/Utils/timeUtils";
import { formatDateWithTz } from "@/Utils/timeUtilsLegacy.js";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
@@ -9,7 +9,7 @@ import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx";
import IncidentActionsMenu from "./IncidentActionsMenu.jsx";
//Utils
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import { getHumanReadableDuration } from "@/Utils/timeUtils.js";
import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js";
import Icon from "@/Components/v1/Icon";
import { useTranslation } from "react-i18next";
import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration";
@@ -1,4 +1,4 @@
import { getHumanReadableDuration } from "@/Utils/timeUtils.js";
import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js";
const useGetIncidentsDuration = (incident, isActive) => {
if (!incident?.startTime) {
return "-";
@@ -7,7 +7,7 @@ import CircularProgress from "@mui/material/CircularProgress";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
import { getHumanReadableDuration } from "../../../../../Utils/timeUtils.js";
import { getHumanReadableDuration } from "../../../../../Utils/timeUtilsLegacy.js";
import { formatBytes } from "../../utils/utils.js";
import { useTranslation } from "react-i18next";
+1 -1
View File
@@ -7,7 +7,7 @@ import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useFetchDiagnostics } from "../../../Hooks/logHooks.js";
import { getHumanReadableDuration } from "../../../Utils/timeUtils.js";
import { getHumanReadableDuration } from "../../../Utils/timeUtilsLegacy.js";
import { formatBytes, getPercentage } from "./utils/utils.js";
const Diagnostics = () => {
@@ -6,7 +6,7 @@ import {
MS_PER_HOUR,
MS_PER_DAY,
MS_PER_WEEK,
} from "../../../../Utils/timeUtils.js";
} from "../../../../Utils/timeUtilsLegacy.js";
const useMaintenanceActions = () => {
const MS_LOOKUP = {
@@ -6,7 +6,7 @@ import {
MS_PER_HOUR,
MS_PER_DAY,
MS_PER_WEEK,
} from "../../../../Utils/timeUtils.js";
} from "../../../../Utils/timeUtilsLegacy.js";
const useMaintenanceData = () => {
const REVERSE_REPEAT_LOOKUP = {
0: "none",
@@ -6,7 +6,7 @@ import Icon from "@/Components/v1/Icon";
import ActionsMenu from "./ActionsMenu/index.jsx";
import { memo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { formatDurationRounded } from "../../../Utils/timeUtils.js";
import { formatDurationRounded } from "../../../Utils/timeUtilsLegacy.js";
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import { setRowsPerPage } from "../../../Features/UI/uiSlice.js";
import { useTranslation } from "react-i18next";
@@ -11,7 +11,7 @@ import {
import { useTheme } from "@emotion/react";
import { useMemo, useState } from "react";
import { Box, Stack, Typography } from "@mui/material";
import { formatDateWithTz } from "../../../../../Utils/timeUtils.js";
import { formatDateWithTz } from "../../../../../Utils/timeUtilsLegacy.js";
import { useSelector } from "react-redux";
const config = {
@@ -1,7 +1,7 @@
import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
import StatBox from "@/Components/v1/StatBox/index.jsx";
import { Typography } from "@mui/material";
import { getHumanReadableDuration } from "../../../../../Utils/timeUtils.js";
import { getHumanReadableDuration } from "../../../../../Utils/timeUtilsLegacy.js";
import { useTranslation } from "react-i18next";
const PageSpeedStatusBoxes = ({ shouldRender, monitorStats }) => {
@@ -6,7 +6,7 @@ import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
import { useSelector } from "react-redux";
import { formatDateWithTz, formatDurationSplit } from "@/Utils/timeUtils.js";
import { formatDateWithTz, formatDurationSplit } from "@/Utils/timeUtilsLegacy.js";
import { useMonitorUtils } from "@/Hooks/useMonitorUtils.js";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -1,160 +0,0 @@
// Components
import { Stack, Typography, Box } from "@mui/material";
import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
import Icon from "@/Components/v1/Icon";
import UpBarChart from "../Charts/UpBarChart.jsx";
import DownBarChart from "../Charts/DownBarChart.jsx";
import ResponseGaugeChart from "../Charts/ResponseGaugeChart.jsx";
import SkeletonLayout from "./skeleton.jsx";
// Utils
import { formatDateWithTz } from "../../../../../Utils/timeUtils.js";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { useState } from "react";
const ChartBoxes = ({
isLoading = false,
monitorData,
dateRange,
uiTimezone,
dateFormat,
}) => {
// Local state
const [hoveredUptimeData, setHoveredUptimeData] = useState(null);
const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null);
const theme = useTheme();
if (isLoading) {
return <SkeletonLayout />;
}
const noIncidentsMessage = "Great. No Incidents, yet!";
return (
<Stack
direction="row"
flexWrap="wrap"
gap={theme.spacing(8)}
>
<ChartBox
icon={
<Icon
name="Activity"
size={20}
/>
}
header="Uptime"
isEmpty={monitorData?.groupedUpChecks?.length === 0}
>
<Stack
width={"100%"}
justifyContent="space-between"
direction="row"
>
<Box position="relative">
<Typography>Total Checks</Typography>
<Typography component="span">
{hoveredUptimeData !== null
? hoveredUptimeData.totalChecks
: (monitorData?.groupedUpChecks?.reduce((count, checkGroup) => {
return count + checkGroup.totalChecks;
}, 0) ?? 0)}
</Typography>
{hoveredUptimeData !== null && hoveredUptimeData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(hoveredUptimeData._id, dateFormat, uiTimezone)}
</Typography>
)}
</Box>
<Box>
<Typography>
{hoveredUptimeData !== null ? "Avg Response Time" : "Uptime Percentage"}
</Typography>
<Typography component="span">
{hoveredUptimeData !== null
? Math.floor(hoveredUptimeData?.originalAvgResponseTime ?? 0)
: Math.floor((monitorData?.groupedUptimePercentage ?? 0) * 100)}
<Typography component="span">
{hoveredUptimeData !== null ? " ms" : " %"}
</Typography>
</Typography>
</Box>
</Stack>
<UpBarChart
groupedUpChecks={monitorData?.groupedUpChecks}
type={dateRange}
onBarHover={setHoveredUptimeData}
/>
</ChartBox>
<ChartBox
icon={
<Icon
name="AlertTriangle"
size={20}
/>
}
header="Incidents"
noDataMessage={noIncidentsMessage}
isEmpty={monitorData?.groupedDownChecks?.length === 0}
>
<Stack width={"100%"}>
<Box position="relative">
<Typography component="span">
{hoveredIncidentsData !== null
? hoveredIncidentsData.totalChecks
: (monitorData?.groupedDownChecks?.reduce((count, checkGroup) => {
return count + checkGroup.totalChecks;
}, 0) ?? 0)}
</Typography>
{hoveredIncidentsData !== null && hoveredIncidentsData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(hoveredIncidentsData._id, dateFormat, uiTimezone)}
</Typography>
)}
</Box>
</Stack>
<DownBarChart
groupedDownChecks={monitorData?.groupedDownChecks}
type={dateRange}
onBarHover={setHoveredIncidentsData}
/>
</ChartBox>
<ChartBox
icon={
<Icon
name="BarChart3"
size={20}
/>
}
header="Average Response Time"
>
<ResponseGaugeChart avgResponseTime={monitorData?.groupedAvgResponseTime ?? 0} />
</ChartBox>
</Stack>
);
};
export default ChartBoxes;
ChartBoxes.propTypes = {
isLoading: PropTypes.bool,
monitorData: PropTypes.object,
dateRange: PropTypes.string.isRequired,
uiTimezone: PropTypes.string.isRequired,
dateFormat: PropTypes.string.isRequired,
hoveredUptimeData: PropTypes.object,
setHoveredUptimeData: PropTypes.func,
hoveredIncidentsData: PropTypes.object,
setHoveredIncidentsData: PropTypes.func,
};
@@ -1,30 +0,0 @@
import { Skeleton, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
direction="row"
gap={theme.spacing(8)}
>
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -1,42 +0,0 @@
import PropTypes from "prop-types";
import { useSelector } from "react-redux";
import { formatDateWithTz } from "../../../../../Utils/timeUtils.js";
const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const dateFormat = type === "day" ? "MMM D, h:mm A" : "MMM D";
return (
<>
<text
x={x}
y={height}
dy={-3}
textAnchor="start"
fontSize={11}
>
{formatDateWithTz(firstDataPoint._id, dateFormat, uiTimezone)}
</text>
<text
x={width}
y={height}
dy={-3}
textAnchor="end"
fontSize={11}
>
{formatDateWithTz(lastDataPoint._id, dateFormat, uiTimezone)}
</text>
</>
);
};
CustomLabels.propTypes = {
x: PropTypes.number.isRequired,
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
firstDataPoint: PropTypes.object,
lastDataPoint: PropTypes.object,
type: PropTypes.string.isRequired,
};
export default CustomLabels;
@@ -1,90 +0,0 @@
import { memo, useState } from "react";
import { useTheme } from "@mui/material";
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
import PropTypes from "prop-types";
import CustomLabels from "./CustomLabels.jsx";
const DownBarChart = memo(({ groupedDownChecks = [], type, onBarHover }) => {
const theme = useTheme();
const [chartHovered, setChartHovered] = useState(false);
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
return (
<ResponsiveContainer
width="100%"
minWidth={250}
height={155}
>
<BarChart
width="100%"
height="100%"
data={groupedDownChecks}
onMouseEnter={() => {
setChartHovered(true);
onBarHover({ time: null, totalChecks: 0 });
}}
onMouseLeave={() => {
setChartHovered(false);
setHoveredBarIndex(null);
onBarHover(null);
}}
>
<XAxis
stroke={theme.palette.primary.lowContrast}
height={15}
tick={false}
label={
<CustomLabels
x={0}
y={0}
width="100%"
height="100%"
firstDataPoint={groupedDownChecks?.[0] ?? {}}
lastDataPoint={groupedDownChecks?.[groupedDownChecks?.length - 1] ?? {}}
type={type}
/>
}
/>
<Bar
dataKey="avgResponseTime"
maxBarSize={7}
background={{ fill: "transparent" }}
>
{groupedDownChecks?.map((entry, index) => {
return (
<Cell
key={`cell-${entry.time}`}
fill={
hoveredBarIndex === index
? theme.palette.error.main
: chartHovered
? theme.palette.error.light // CAIO_REVIEW
: theme.palette.error.main
}
onMouseEnter={() => {
setHoveredBarIndex(index);
onBarHover(entry);
}}
onMouseLeave={() => {
setHoveredBarIndex(null);
onBarHover({ time: null, totalChecks: 0 });
}}
/>
);
})}
</Bar>
</BarChart>
</ResponsiveContainer>
);
});
DownBarChart.displayName = "DownBarChart";
DownBarChart.propTypes = {
monitor: PropTypes.shape({
groupedDownChecks: PropTypes.arrayOf(PropTypes.object),
}),
type: PropTypes.string,
onBarHover: PropTypes.func,
};
export default DownBarChart;
@@ -1,118 +0,0 @@
import PropTypes from "prop-types";
import { useTheme } from "@mui/material";
import { ResponsiveContainer, RadialBarChart, RadialBar, Cell } from "recharts";
import { useTranslation } from "react-i18next";
const ResponseGaugeChart = ({ avgResponseTime }) => {
const theme = useTheme();
const { t } = useTranslation();
let max = 1000; // max ms
const data = [
{ response: max, fill: "transparent", background: false },
{ response: avgResponseTime, background: true },
];
let responseTime = Math.floor(avgResponseTime);
let responseProps =
responseTime <= 200
? {
category: "Excellent",
main: theme.palette.success.main,
bg: theme.palette.success.lowContrast,
}
: responseTime <= 500
? {
category: "Fair",
main: theme.palette.success.main,
bg: theme.palette.success.lowContrast,
}
: responseTime <= 600
? {
category: "Acceptable",
main: theme.palette.warning.main,
bg: theme.palette.warning.lowContrast,
}
: {
category: "Poor",
main: theme.palette.error.main,
bg: theme.palette.error.lowContrast,
};
return (
<ResponsiveContainer
width="100%"
minWidth={210}
height={155}
>
<RadialBarChart
width="100%"
height="100%"
cy="89%"
data={data}
startAngle={180}
endAngle={0}
innerRadius={100}
outerRadius={150}
>
<text
x={0}
y="100%"
dx="5%"
dy={-2}
textAnchor="start"
fontSize={11}
>
{t("low")}
</text>
<text
x="100%"
y="100%"
dx="-3%"
dy={-2}
textAnchor="end"
fontSize={11}
>
{t("high")}
</text>
<text
x="50%"
y="45%"
textAnchor="middle"
dominantBaseline="middle"
fontSize={18}
fontWeight={400}
>
{responseProps.category}
</text>
<text
x="50%"
y="70%"
textAnchor="middle"
alignmentBaseline="hanging"
fontSize={25}
>
<tspan fontWeight={600}>{responseTime}</tspan> <tspan opacity={0.8}>ms</tspan>
</text>
<RadialBar
background={{ fill: responseProps.bg }}
clockWise
dataKey="response"
stroke="none"
>
<Cell
fill="transparent"
background={false}
barSize={0}
/>
<Cell fill={responseProps.main} />
</RadialBar>
</RadialBarChart>
</ResponsiveContainer>
);
};
ResponseGaugeChart.propTypes = {
avgResponseTime: PropTypes.number.isRequired,
};
export default ResponseGaugeChart;
@@ -1,36 +0,0 @@
import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
import MonitorDetailsAreaChart from "@/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx";
import Icon from "@/Components/v1/Icon";
import SkeletonLayout from "./ResponseTimeChartSkeleton.jsx";
import PropTypes from "prop-types";
const ResponseTImeChart = ({ isLoading = false, groupedChecks = [], dateRange }) => {
if (isLoading) {
return <SkeletonLayout />;
}
return (
<ChartBox
icon={
<Icon
name="TrendingUp"
size={20}
/>
}
header="Response Times"
>
<MonitorDetailsAreaChart
checks={groupedChecks}
dateRange={dateRange}
/>
</ChartBox>
);
};
ResponseTImeChart.propTypes = {
isLoading: PropTypes.bool,
groupedChecks: PropTypes.array,
dateRange: PropTypes.string,
};
export default ResponseTImeChart;
@@ -1,12 +0,0 @@
import { Skeleton } from "@mui/material";
const ResponseTimeChartSkeleton = () => {
return (
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
);
};
export default ResponseTimeChartSkeleton;
@@ -1,107 +0,0 @@
import { memo, useState } from "react";
import { useTheme } from "@mui/material";
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
import PropTypes from "prop-types";
import CustomLabels from "./CustomLabels.jsx";
const getThemeColor = (responseTime) => {
if (responseTime < 200) {
return "success";
} else if (responseTime < 300) {
return "warning";
} else {
return "error";
}
};
const UpBarChart = memo(({ groupedUpChecks = [], type, onBarHover }) => {
const theme = useTheme();
const [chartHovered, setChartHovered] = useState(false);
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
return (
<ResponsiveContainer
width="100%"
minWidth={210}
height={155}
>
<BarChart
width="100%"
height="100%"
data={groupedUpChecks}
onMouseEnter={() => {
setChartHovered(true);
onBarHover({ time: null, totalChecks: 0, originalAvgResponseTime: 0 });
}}
onMouseLeave={() => {
setChartHovered(false);
setHoveredBarIndex(null);
onBarHover(null);
}}
>
<XAxis
stroke={theme.palette.primary.lowContrast}
height={15}
tick={false}
label={
<CustomLabels
x={0}
y={0}
width="100%"
height="100%"
firstDataPoint={groupedUpChecks?.[0]}
lastDataPoint={groupedUpChecks?.[groupedUpChecks?.length - 1]}
type={type}
/>
}
/>
<Bar
dataKey="avgResponseTime"
maxBarSize={7}
background={{ fill: "transparent" }}
>
{groupedUpChecks?.map((entry, index) => {
const themeColor = getThemeColor(entry.originalAvgResponseTime);
return (
<Cell
key={`cell-${entry.time}`}
fill={
hoveredBarIndex === index
? theme.palette[themeColor].main
: chartHovered
? theme.palette[themeColor].light
: theme.palette[themeColor].main
}
onMouseEnter={() => {
setHoveredBarIndex(index);
onBarHover(entry);
}}
onMouseLeave={() => {
setHoveredBarIndex(null);
onBarHover({
time: null,
totalChecks: 0,
groupUptimePercentage: 0,
});
}}
/>
);
})}
</Bar>
</BarChart>
</ResponsiveContainer>
);
});
// Add display name for the component
UpBarChart.displayName = "UpBarChart";
// Validate props using PropTypes
UpBarChart.propTypes = {
monitor: PropTypes.shape({
groupedUpChecks: PropTypes.array,
}),
type: PropTypes.string,
onBarHover: PropTypes.func,
};
export default UpBarChart;
@@ -0,0 +1,100 @@
import { Table, Pagination, StatusLabel } from "@/Components/v2/design-elements";
import Box from "@mui/material/Box";
import type { Header } from "@/Components/v2/design-elements/";
import type { Check } from "@/Types/Check";
import { useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
import { formatDateWithTz } from "@/Utils/TimeUtils";
import type { RootState } from "@/Types/state";
import { useSelector } from "react-redux";
const getHeaders = (t: Function, uiTimezone: string) => {
const headers: Header<Check>[] = [
{
id: "status",
content: t("common.table.headers.status"),
render: (row) => {
return <StatusLabel status={row.status} />;
},
},
{
id: "date",
content: t("pages.checks.table.headers.dateTime"),
render: (row) => {
return formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone);
},
},
{
id: "message",
content: t("common.table.headers.message"),
render: (row) => {
return row.message || "N/A";
},
},
{
id: "statusCode",
content: t("pages.checks.table.headers.statusCode"),
render: (row) => {
return row.statusCode || "N/A";
},
},
];
return headers;
};
export const ChecksTable = ({
checks,
count,
page,
setPage,
rowsPerPage,
setRowsPerPage,
}: {
checks: Check[];
count: number;
page: number;
setPage: (page: number) => void;
rowsPerPage: number;
setRowsPerPage: (rowsPerPage: number) => void;
}) => {
const navigate = useNavigate();
const { t } = useTranslation();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
const headers = getHeaders(t, uiTimezone);
const handlePageChange = (
_e: React.MouseEvent<HTMLButtonElement> | null,
newPage: number
) => {
setPage(newPage);
};
const handleRowsPerPageChange = (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => {
const value = Number(e.target.value);
setPage(0);
setRowsPerPage(value);
};
return (
<Box>
<Table
headers={headers}
data={checks}
onRowClick={(row) => {
navigate(`/checks/${row.id}`);
}}
/>
<Pagination
component="div"
count={count}
page={page}
rowsPerPage={rowsPerPage}
onPageChange={handlePageChange}
onRowsPerPageChange={handleRowsPerPageChange}
/>
</Box>
);
};
@@ -1,222 +0,0 @@
import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
import PropTypes from "prop-types";
import Icon from "@/Components/v1/Icon";
import Table from "@/Components/v1/Table/index.jsx";
import TablePagination from "@/Components/v1/Table/TablePagination/index.jsx";
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import { useTranslation } from "react-i18next";
import { formatDateWithTz } from "../../../../../Utils/timeUtils.js";
import SkeletonLayout from "./skeleton.jsx";
import Tooltip from "@mui/material/Tooltip";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Stack from "@mui/material/Stack";
import { lighten, useTheme } from "@mui/material";
/**
* Creates tooltip content with detailed timing breakdown
* Following the pattern from IncidentTable's GetTooltip function
* @param {Object} timings - Timing object (guaranteed to have phases by caller)
* @param {Object} theme - MUI theme object
* @param {Function} t - Translation function
* @returns {JSX.Element} Tooltip content
*/
const GetTooltip = (timings, theme, t) => {
const phases = timings.phases;
const timingDetails = [
{ label: t("dnsLookup"), value: phases.dns },
{ label: t("tcpConnection"), value: phases.tcp },
{ label: t("tlsHandshake"), value: phases.tls },
{ label: t("waitTime"), value: phases.wait },
{ label: t("timeToFirstByte"), value: phases.firstByte },
{ label: t("download"), value: phases.download },
{ label: t("total"), value: phases.total },
].filter((item) => item.value > 0);
return (
<Stack
sx={{
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
variant="body2"
sx={{
fontWeight: 600,
marginBottom: theme.spacing(1),
color: theme.palette.primary.contrastText,
}}
>
{t("responseTimeBreakdown")}
</Typography>
{timingDetails.map((detail, index, array) => (
<Box
key={index}
sx={{
display: "flex",
justifyContent: "space-between",
gap: theme.spacing(4),
marginBottom: index < array.length - 1 ? theme.spacing(0.5) : 0,
}}
>
<Typography
variant="body2"
sx={{ color: theme.palette.primary.contrastText }}
>
{detail.label}:
</Typography>
<Typography
variant="body2"
sx={{
fontWeight: index === array.length - 1 ? 600 : 400,
color: theme.palette.primary.contrastText,
}}
>
{Math.round(detail.value)} ms
</Typography>
</Box>
))}
</Stack>
);
};
const ResponseTable = ({
isLoading = false,
checks = [],
checksCount,
uiTimezone,
page,
setPage,
rowsPerPage,
setRowsPerPage,
}) => {
const { t } = useTranslation();
const theme = useTheme();
if (isLoading) {
return <SkeletonLayout />;
}
const headers = [
{
id: "status",
content: t("status"),
render: (row) => {
const status = row.status === true ? "up" : "down";
return (
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
);
},
},
{
id: "date",
content: t("date&Time"),
render: (row) =>
formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone),
},
{
id: "statusCode",
content: t("statusCode"),
render: (row) => (row.statusCode ? row.statusCode : "N/A"),
},
{
id: "message",
content: t("message"),
render: (row) => row.message,
},
{
id: "responseTime",
content: t("responseTime"),
render: (row) => {
const hasTimings = row.timings && row.timings.phases;
const responseTime = row.responseTime;
const responseTimeDisplay =
responseTime !== null && responseTime !== undefined
? `${Math.round(responseTime)} ms`
: "N/A";
if (!hasTimings) {
return responseTimeDisplay;
}
return (
<Tooltip
title={GetTooltip(row.timings, theme, t)}
placement="top"
arrow
enterDelay={300}
enterNextDelay={300}
slotProps={{
tooltip: {
sx: {
backgroundColor: lighten(theme.palette.primary.main, 0.1),
border: `1px solid ${theme.palette.primary.lowContrast}`,
borderRadius: theme.shape.borderRadius,
"& .MuiTooltip-arrow": {
color: lighten(theme.palette.primary.main, 0.1),
"&::before": {
border: `1px solid ${theme.palette.primary.lowContrast}`,
},
},
},
},
}}
>
<Box
sx={{
cursor: "help",
display: "inline-block",
}}
>
{responseTimeDisplay}
</Box>
</Tooltip>
);
},
},
];
return (
<ChartBox
icon={
<Icon
name="History"
size={20}
/>
}
header="Response Times"
height="100%"
>
<Table
headers={headers}
data={checks}
/>
<TablePagination
page={page}
handleChangePage={setPage}
rowsPerPage={rowsPerPage}
handleChangeRowsPerPage={setRowsPerPage}
itemCount={checksCount}
/>
</ChartBox>
);
};
ResponseTable.propTypes = {
isLoading: PropTypes.bool,
checks: PropTypes.array,
checksCount: PropTypes.number,
uiTimezone: PropTypes.string.isRequired,
page: PropTypes.number.isRequired,
setPage: PropTypes.func.isRequired,
rowsPerPage: PropTypes.number.isRequired,
setRowsPerPage: PropTypes.func.isRequired,
};
export default ResponseTable;
@@ -1,13 +0,0 @@
import { Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
);
};
export default SkeletonLayout;
@@ -1,84 +0,0 @@
import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
import StatBox from "@/Components/v1/StatBox/index.jsx";
import PropTypes from "prop-types";
import { getHumanReadableDuration } from "../../../../../Utils/timeUtils.js";
import { useTheme } from "@mui/material/styles";
import { Typography } from "@mui/material";
import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js";
const UptimeStatusBoxes = ({
isLoading = false,
monitor,
monitorStats,
certificateExpiry,
}) => {
const theme = useTheme();
const { determineState } = useMonitorUtils();
// Determine time since last failure
const timeOfLastFailure = monitorStats?.timeOfLastFailure;
const timeSinceLastFailure =
timeOfLastFailure > 0
? Date.now() - timeOfLastFailure
: Date.now() - new Date(monitorStats?.createdAt);
// Determine time since last check
const timeOfLastCheck = monitorStats?.lastCheckTimestamp;
const timeSinceLastCheck = Date.now() - timeOfLastCheck;
const streakTime = getHumanReadableDuration(timeSinceLastFailure);
const lastCheckTime = getHumanReadableDuration(timeSinceLastCheck);
return (
<StatusBoxes shouldRender={!isLoading}>
<StatBox
gradient={true}
status={determineState(monitor)}
heading={"active for"}
subHeading={streakTime}
/>
<StatBox
heading="last check"
subHeading={
<>
{lastCheckTime}
<Typography component="span">{"ago"}</Typography>
</>
}
/>
<StatBox
heading="last response time"
subHeading={
<>
{monitorStats?.lastResponseTime}
<Typography component="span">{"ms"}</Typography>
</>
}
/>
{monitor?.type === "http" && (
<StatBox
heading="certificate expiry"
subHeading={
<Typography
component="span"
fontSize={13}
color={theme.palette.primary.contrastText}
>
{certificateExpiry}
</Typography>
}
/>
)}
</StatusBoxes>
);
};
UptimeStatusBoxes.propTypes = {
isLoading: PropTypes.bool,
monitor: PropTypes.object,
monitorStats: PropTypes.object,
certificateExpiry: PropTypes.string,
};
export default UptimeStatusBoxes;
@@ -1,7 +1,7 @@
import { logger } from "../../../../Utils/Logger.js";
import { useEffect, useState } from "react";
import { networkService } from "../../../../main.jsx";
import { formatDateWithTz } from "../../../../Utils/timeUtils.js";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
const useCertificateFetch = ({
monitor,
+166
View File
@@ -0,0 +1,166 @@
import { BasePage } from "@/Components/v2/design-elements";
import { HeaderMonitorControls, HeaderTimeRange } from "@/Components/v2/common";
import Stack from "@mui/material/Stack";
import {
HistogramStatus,
RadialAvgResponse,
HistogramDetails,
} from "@/Components/v2/monitors";
import { TrendingUp, AlertTriangle } from "lucide-react";
import { ChecksTable } from "@/Pages/Uptime/Details/Components/ChecksTable";
import { useTheme } from "@mui/material/styles";
import { useIsAdmin } from "@/Hooks/useIsAdmin";
import { useState, useMemo } from "react";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { useGet } from "@/Hooks/UseApi";
import type { MonitorDetailsResponse } from "@/Types/Monitor";
import type { ChecksResponse } from "@/Types/Check";
import type { RootState } from "@/Types/state";
import { MonitorStatBoxes } from "@/Components/v2/monitors";
import { formatDateWithTz } from "@/Utils/timeUtilsLegacy";
import { t } from "i18next";
const certificateDateFormat = "MMM D, YYYY h A";
interface CertificateResponse {
certificateDate: string;
}
const UptimeDetailsPage = () => {
const theme = useTheme();
const isAdmin = useIsAdmin();
const { monitorId } = useParams<{ monitorId: string }>();
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
const [page, setPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(5);
const [dateRange, setDateRange] = useState<string>("recent");
const monitorDetailsUrl = useMemo(() => {
if (!monitorId) {
return null;
}
const params = new URLSearchParams();
params.append("dateRange", dateRange);
params.append("normalize", "true");
return `/monitors/uptime/details/${monitorId}?${params.toString()}`;
}, [monitorId, dateRange]);
const {
data: monitorDetailsData,
isLoading: monitorIsLoading,
refetch: refetchMonitor,
} = useGet<MonitorDetailsResponse>(
monitorDetailsUrl,
{},
{ refreshInterval: 10000, keepPreviousData: true }
);
const monitorData = monitorDetailsData?.monitorData;
const monitor = monitorData?.monitor;
const monitorStats = monitorDetailsData?.monitorStats ?? null;
// 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) {
return null;
}
const params = new URLSearchParams();
params.append("type", monitor.type);
params.append("sortOrder", "desc");
params.append("dateRange", dateRange);
params.append("page", String(page));
params.append("rowsPerPage", String(rowsPerPage));
return `/checks/${monitorId}?${params.toString()}`;
}, [monitorId, monitor?.type, dateRange, page, rowsPerPage]);
const { data: checksData, isLoading: checksIsLoading } = useGet<ChecksResponse>(
checksUrl,
{},
{ keepPreviousData: true }
);
const checks = checksData?.checks ?? [];
const checksCount = checksData?.checksCount ?? 0;
return (
<BasePage>
<HeaderMonitorControls
path="uptime"
monitor={monitor}
isAdmin={isAdmin}
refetch={refetchMonitor}
/>
<MonitorStatBoxes
monitor={monitor}
monitorStats={monitorStats}
certificateExpiry={certificateExpiry}
/>
<HeaderTimeRange
isLoading={monitorIsLoading || checksIsLoading}
hasDateRange={true}
dateRange={dateRange}
setDateRange={setDateRange}
/>
<Stack
direction={{ xs: "column", md: "row" }}
gap={theme.spacing(8)}
>
<HistogramStatus
title={t("common.charts.labels.uptime")}
icon={<TrendingUp />}
checks={monitorData?.groupedUpChecks ?? []}
range={dateRange}
/>
<HistogramStatus
title={t("common.charts.labels.downtime")}
icon={<AlertTriangle />}
checks={monitorData?.groupedDownChecks ?? []}
range={dateRange}
/>
<RadialAvgResponse
avg={monitorStats?.avgResponseTime || 0}
max={500}
/>
</Stack>
<HistogramDetails
checks={monitorData?.groupedChecks || []}
range={dateRange}
/>
<ChecksTable
checks={checks}
count={checksCount}
page={page}
setPage={setPage}
rowsPerPage={rowsPerPage}
setRowsPerPage={setRowsPerPage}
/>
</BasePage>
);
};
export default UptimeDetailsPage;
+6 -2
View File
@@ -88,7 +88,11 @@ const UptimeMonitorsPage = () => {
data: monitors,
isLoading: monitorsLoading,
error,
} = useGet<Monitor[]>("/monitors/team?type=http&type=ping&type=port&type=docker");
} = useGet<Monitor[]>(
"/monitors/team?type=http&type=ping&type=port&type=docker",
{},
{ keepPreviousData: true }
);
const {
data: monitorsWithChecksData,
@@ -98,7 +102,7 @@ const UptimeMonitorsPage = () => {
} = useGet<MonitorsWithChecksResponse>(
monitorsWithChecksUrl,
{},
{ refreshInterval: 5000 }
{ refreshInterval: 5000, keepPreviousData: true }
);
const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {};
+8 -2
View File
@@ -16,7 +16,7 @@ import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed.jsx";
// Uptime
import Uptime from "../Pages/Uptime/Monitors";
import UptimeDetails from "../Pages/Uptime/Details/index.jsx";
import UptimeDetails from "../Pages/Uptime/Details";
import UptimeCreate from "../Pages/Uptime/Create/index.jsx";
// PageSpeed
@@ -103,7 +103,13 @@ const Routes = () => {
/>
<Route
path="/uptime/:monitorId/"
element={<UptimeDetails />}
element={
<>
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
<UptimeDetails />
</ThemeProvider>
</>
}
/>
<Route
path="/uptime/configure/:monitorId/"
+6 -1
View File
@@ -128,7 +128,7 @@ export interface Check {
}
export interface GroupedCheck {
_id: string;
bucketDate: string;
avgResponseTime: number;
totalChecks: number;
}
@@ -139,3 +139,8 @@ export interface LatestCheck {
checkedAt: string;
id: string;
}
export interface ChecksResponse {
checks: Check[];
checksCount: number;
}
+30 -1
View File
@@ -1,4 +1,4 @@
import type { Check } from "@/Types/Check";
import type { Check, GroupedCheck } from "@/Types/Check";
export type MonitorStatus = boolean | undefined;
export const MonitorTypes = [
@@ -47,3 +47,32 @@ export interface MonitorsWithChecksResponse {
monitors: MonitorWithChecks[];
summary: MonitorsSummary;
}
export interface MonitorStats {
id: string;
monitorId: string;
avgResponseTime: number;
totalChecks: number;
totalUpChecks: number;
totalDownChecks: number;
uptimePercentage: number;
lastCheckTimestamp: number;
lastResponseTime: number;
timeOfLastFailure?: number;
createdAt: string;
updatedAt: string;
}
export interface MonitorData {
monitor: Monitor;
groupedChecks: GroupedCheck[];
groupedUpChecks: GroupedCheck[];
groupedDownChecks: GroupedCheck[];
groupedAvgResponseTime: number;
groupedUptimePercentage: number;
}
export interface MonitorDetailsResponse {
monitorData: MonitorData;
monitorStats: MonitorStats | null;
}
+8 -8
View File
@@ -25,10 +25,10 @@ export const formatDateWithTz = (timestamp: string, format: string, timezone: st
export const tickDateFormatLookup = (range: string) => {
const tickFormatLookup: Record<string, string> = {
"1h": "h:mm A",
"24h": "h:mm A",
"7d": "MM/D, h:mm A",
"30d": "ddd. M/D",
recent: "h:mm A",
day: "h:mm A",
week: "MM/D, h:mm A",
month: "ddd. M/D",
};
const format = tickFormatLookup[range];
if (format === undefined) {
@@ -39,10 +39,10 @@ export const tickDateFormatLookup = (range: string) => {
export const tooltipDateFormatLookup = (range: string) => {
const dateFormatLookup: Record<string, string> = {
"1h": "ddd. MMMM D, YYYY, hh:mm A",
"24h": "ddd. MMMM D, YYYY, hh:mm A",
"7d": "ddd. MMMM D, YYYY, hh:mm A",
"30d": "ddd. MMMM D, YYYY",
recent: "ddd. MMMM D, YYYY, hh:mm A",
day: "ddd. MMMM D, YYYY, hh:mm A",
week: "ddd. MMMM D, YYYY, hh:mm A",
month: "ddd. MMMM D, YYYY",
};
const format = dateFormatLookup[range];
if (format === undefined) {
@@ -135,3 +135,33 @@ export const formatDateWithTz = (timestamp, format, timezone) => {
const formattedDate = dayjs(timestamp).tz(timezone).format(format);
return formattedDate;
};
export const tickDateFormatLookup = (range) => {
switch (range) {
case "recent":
return "h:mm A";
case "day":
return "h A";
case "week":
return "MMM D";
case "month":
return "MMM D";
default:
return "MMM D, h A";
}
};
export const tooltipDateFormatLookup = (range) => {
switch (range) {
case "recent":
return "MMM D, h:mm A";
case "day":
return "MMM D, h:mm A";
case "week":
return "ddd, MMM D";
case "month":
return "ddd, MMM D";
default:
return "MMM D, h:mm A";
}
};
+3 -2
View File
@@ -50,7 +50,8 @@ html {
cursor: not-allowed;
}
.Toastify__toast-container {
/* .Toastify__toast-container {
min-width: 300px;
width: auto;
}
.Toastify__toast-body .alert {
@@ -68,7 +69,7 @@ html {
}
.Toastify [class*="Toastify__toast-theme"] {
background-color: transparent;
}
} */
.MuiTouchRipple-root {
display: none;
+26 -1
View File
@@ -4,7 +4,9 @@
"monitoringAgentName": "Capture",
"buttons": {
"toggleTheme": "Toggles light & dark",
"create": "Create"
"testNotifications": "Test notifications",
"create": "Create",
"incidents": "Incidents"
},
"toasts": {
"networkError": "Network error",
@@ -22,6 +24,15 @@
"type": "Type"
},
"empty": "Nothing h"
},
"charts": {
"labels": {
"uptime": "Uptime",
"downtime": "Downtime",
"averageResponseTime": "Average response time",
"low": "Low",
"high": "High"
}
}
},
"submit": "Submit",
@@ -167,6 +178,12 @@
"pause": "Pause",
"resume": "Resume",
"delete": "Delete"
},
"statBoxes": {
"activeFor": "Active for",
"lastCheck": "Last check",
"lastResponseTime": "Last response time",
"certificateExpiry": "Certificate expiry"
}
}
},
@@ -191,6 +208,14 @@
"placeholder": "Search monitors..."
}
}
},
"checks": {
"table": {
"headers": {
"dateTime": "Date & time",
"statusCode": "Status code"
}
}
}
},
"distributedUptimeStatusCreateStatusPage": "status page",
@@ -483,6 +483,7 @@ class MongoChecksRepository implements IChecksRepository {
},
},
{ $sort: { _id: 1 } },
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
],
groupedUpChecks: [
{ $match: { status: true } },
@@ -496,6 +497,7 @@ class MongoChecksRepository implements IChecksRepository {
},
},
{ $sort: { _id: 1 } },
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
],
groupedDownChecks: [
{ $match: { status: false } },
@@ -509,6 +511,7 @@ class MongoChecksRepository implements IChecksRepository {
},
},
{ $sort: { _id: 1 } },
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
],
},
},
+1 -1
View File
@@ -157,7 +157,7 @@ export interface HardwareChecksResult {
}
export interface GroupedCheck {
_id: string;
bucketDate: string;
avgResponseTime: number;
totalChecks: number;
}