diff --git a/client/src/App.jsx b/client/src/App.jsx
index af2f6c11c..da83f9061 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -37,7 +37,14 @@ function App() {
-
+
);
}
diff --git a/client/src/Components/v1/Charts/BarChart/index.jsx b/client/src/Components/v1/Charts/BarChart/index.jsx
index 9f0ed8aa6..77b836cae 100644
--- a/client/src/Components/v1/Charts/BarChart/index.jsx
+++ b/client/src/Components/v1/Charts/BarChart/index.jsx
@@ -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";
diff --git a/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx b/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx
index b37990e46..8b6c2b165 100644
--- a/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx
+++ b/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx
@@ -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 }) => {
diff --git a/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx
index a9ef04a53..dafca7b40 100644
--- a/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx
+++ b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx
@@ -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,
diff --git a/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx b/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx
index 309201b84..2dc5e61f0 100644
--- a/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx
+++ b/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx
@@ -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";
diff --git a/client/src/Components/v1/Charts/Utils/chartUtils.jsx b/client/src/Components/v1/Charts/Utils/chartUtils.jsx
index 1bab80595..7797ba462 100644
--- a/client/src/Components/v1/Charts/Utils/chartUtils.jsx
+++ b/client/src/Components/v1/Charts/Utils/chartUtils.jsx
@@ -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";
/**
diff --git a/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx
index febf449d4..c6a3cfcfa 100644
--- a/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx
+++ b/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx
@@ -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";
diff --git a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx
new file mode 100644
index 000000000..17576a8b2
--- /dev/null
+++ b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx
@@ -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();
+
+ if (!monitor) {
+ return null;
+ }
+ return (
+
+
+
+ }
+ onClick={async () => {
+ await post(`/notifications/test/all`, { monitorId: monitor.id });
+ }}
+ sx={{
+ whiteSpace: "nowrap",
+ }}
+ >
+ {t("common.buttons.testNotifications")}
+
+ }
+ onClick={() => {
+ navigate(`/incidents/${monitor?.id}`);
+ }}
+ >
+ {t("common.buttons.incidents")}
+
+ {isAdmin && (
+ : }
+ onClick={async () => {
+ await post(`/monitors/pause/${monitor.id}`, {});
+ await refetch();
+ }}
+ >
+ {monitor?.isActive ? t("pause") : t("resume")}
+
+ )}
+ {isAdmin && (
+ }
+ onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
+ >
+ Configure
+
+ )}
+
+
+ );
+};
diff --git a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx
new file mode 100644
index 000000000..57b61473a
--- /dev/null
+++ b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx
@@ -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,
+ newValue: string | null
+ ) => {
+ if (newValue !== null) {
+ setDateRange(newValue);
+ }
+ };
+
+ let timeFramePicker = null;
+
+ if (hasDateRange) {
+ timeFramePicker = (
+
+
+ Recent
+
+
+ Day
+
+
+ Week
+
+
+ Month
+
+
+ );
+ }
+
+ return (
+
+
+ Showing statistics for past{" "}
+ {dateRange === "recent"
+ ? "2 hours"
+ : dateRange === "day"
+ ? "24 hours"
+ : dateRange === "week"
+ ? "7 days"
+ : "30 days"}
+ .
+
+ {timeFramePicker}
+
+ );
+};
diff --git a/client/src/Components/v2/common/index.ts b/client/src/Components/v2/common/index.ts
index e354983eb..d0d1262b7 100644
--- a/client/src/Components/v2/common/index.ts
+++ b/client/src/Components/v2/common/index.ts
@@ -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";
diff --git a/client/src/Components/v2/design-elements/BaseChart.tsx b/client/src/Components/v2/design-elements/BaseChart.tsx
new file mode 100644
index 000000000..fd9251915
--- /dev/null
+++ b/client/src/Components/v2/design-elements/BaseChart.tsx
@@ -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;
+}>;
+
+export const BaseChart = ({
+ children,
+ icon,
+ title,
+ width = "100%",
+ maxWidth = "100%",
+ padding,
+}: BaseChartProps) => {
+ const theme = useTheme();
+ return (
+
+
+
+ {icon && (
+
+ {icon}
+
+ )}
+ {title}
+
+ {children}
+
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/BasePage.tsx b/client/src/Components/v2/design-elements/BasePage.tsx
index c853a8d30..c8618fa67 100644
--- a/client/src/Components/v2/design-elements/BasePage.tsx
+++ b/client/src/Components/v2/design-elements/BasePage.tsx
@@ -55,7 +55,7 @@ export const BasePage = ({
spacing={theme.spacing(10)}
{...props}
>
-
+
{children}
);
diff --git a/client/src/Components/v2/design-elements/Breadcrumb.tsx b/client/src/Components/v2/design-elements/Breadcrumb.tsx
index 4489f992d..492ed0ec9 100644
--- a/client/src/Components/v2/design-elements/Breadcrumb.tsx
+++ b/client/src/Components/v2/design-elements/Breadcrumb.tsx
@@ -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 (
-
-
+ {hasDetailsPage ? (
+
+
+ {basePage.charAt(0).toUpperCase() + basePage.slice(1)}
+
+
+ ) : (
- Home
+ {basePage.charAt(0).toUpperCase() + basePage.slice(1)}
-
- {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 ? (
-
- {displayName}
-
- ) : (
-
-
- {displayName}
-
-
- );
- })}
+ )}
+ {hasDetailsPage && (
+
+ Details
+
+ )}
);
};
diff --git a/client/src/Components/v2/design-elements/Dot.tsx b/client/src/Components/v2/design-elements/Dot.tsx
new file mode 100644
index 000000000..541e90812
--- /dev/null
+++ b/client/src/Components/v2/design-elements/Dot.tsx
@@ -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 (
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/Icon.tsx b/client/src/Components/v2/design-elements/Icon.tsx
new file mode 100644
index 000000000..b32cd84b2
--- /dev/null
+++ b/client/src/Components/v2/design-elements/Icon.tsx
@@ -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 (
+
+ );
+};
+
+export default Icon;
diff --git a/client/src/Components/v2/design-elements/MonitorStatus.tsx b/client/src/Components/v2/design-elements/MonitorStatus.tsx
new file mode 100644
index 000000000..0ecb77df8
--- /dev/null
+++ b/client/src/Components/v2/design-elements/MonitorStatus.tsx
@@ -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 (
+
+
+ {monitor.name}
+
+
+
+
+ {formatUrl(monitor?.url)}
+
+ {!isSmall && (
+ <>
+
+
+ Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })}
+
+ >
+ )}
+
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/PulseDot.tsx b/client/src/Components/v2/design-elements/PulseDot.tsx
new file mode 100644
index 000000000..ab792b628
--- /dev/null
+++ b/client/src/Components/v2/design-elements/PulseDot.tsx
@@ -0,0 +1,48 @@
+import { Box, Stack, useTheme } from "@mui/material";
+
+interface PulseDotProps {
+ color: string;
+}
+
+export const PulseDot = ({ color }: PulseDotProps) => {
+ const theme = useTheme();
+
+ return (
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/design-elements/StatBox.tsx b/client/src/Components/v2/design-elements/StatBox.tsx
new file mode 100644
index 000000000..b78a003dc
--- /dev/null
+++ b/client/src/Components/v2/design-elements/StatBox.tsx
@@ -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 (
+
+ {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/StatusLabel.tsx b/client/src/Components/v2/design-elements/StatusLabel.tsx
index 30adf5917..c9991f0d4 100644
--- a/client/src/Components/v2/design-elements/StatusLabel.tsx
+++ b/client/src/Components/v2/design-elements/StatusLabel.tsx
@@ -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");
}
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 374dabef7..6ebffc624 100644
--- a/client/src/Components/v2/design-elements/index.tsx
+++ b/client/src/Components/v2/design-elements/index.tsx
@@ -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";
diff --git a/client/src/Components/v2/inputs/ToggleButton.tsx b/client/src/Components/v2/inputs/ToggleButton.tsx
new file mode 100644
index 000000000..40b4309b8
--- /dev/null
+++ b/client/src/Components/v2/inputs/ToggleButton.tsx
@@ -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 (
+
+ );
+};
+
+export const ToggleButtonGroupInput = ({ sx, ...props }: ToggleButtonGroupProps) => {
+ const theme = useTheme();
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx
index 9a76f6f7f..87698a4f0 100644
--- a/client/src/Components/v2/inputs/index.tsx
+++ b/client/src/Components/v2/inputs/index.tsx
@@ -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";
diff --git a/client/src/Components/v2/monitors/MonitorStatBoxes.tsx b/client/src/Components/v2/monitors/MonitorStatBoxes.tsx
new file mode 100644
index 000000000..65be06f3b
--- /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/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 (
+
+
+
+
+
+ {monitor?.type === "http" && (
+
+ )}
+
+ );
+};
diff --git a/client/src/Components/v2/monitors/charts/HistogramDetails.tsx b/client/src/Components/v2/monitors/charts/HistogramDetails.tsx
new file mode 100644
index 000000000..8858a9784
--- /dev/null
+++ b/client/src/Components/v2/monitors/charts/HistogramDetails.tsx
@@ -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 (
+
+ {formatDateWithTz(payload?.value, format, uiTimezone)}
+
+ );
+};
+
+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 (
+
+ {formatDateWithTz(String(label), format, uiTimezone)}
+ Response time: {responseTime} ms
+
+ );
+};
+
+export const HistogramDetails = ({
+ checks,
+ range,
+}: {
+ checks: GroupedCheck[];
+ range: string;
+}) => {
+ const theme = useTheme();
+ const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
+ return (
+
+ }
+ title="Response times"
+ >
+
+
+
+
+
+
+
+
+
+ (
+
+ )}
+ />
+
+ (
+
+ )}
+ />
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx
new file mode 100644
index 000000000..1c25c339e
--- /dev/null
+++ b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx
@@ -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 (
+ <>
+
+ {formatDateWithTz(p1.bucketDate, dateFormat, uiTimezone)}
+
+
+ {formatDateWithTz(p2.bucketDate, dateFormat, uiTimezone)}
+
+ >
+ );
+};
+
+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 (
+
+ {dateLabel ? (
+
+ {dateLabel}
+
+ ) : null}
+ {titleText}
+ {Math.floor(avg)} ms
+
+ );
+ };
+
+ const totalChecks = checks.reduce((acc, check) => acc + (check.totalChecks || 0), 0);
+
+ return (
+
+
+
+
+ Total checks: {totalChecks}
+
+
+
+
+
+ }
+ />
+ }
+ />
+
+ {checks?.map((groupedCheck, index) => {
+ const fillColor = getResponseTimeColor(groupedCheck.avgResponseTime);
+ return (
+ |
+ );
+ })}
+
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/monitors/charts/RadialAvgResponse.tsx b/client/src/Components/v2/monitors/charts/RadialAvgResponse.tsx
new file mode 100644
index 000000000..b54bfc764
--- /dev/null
+++ b/client/src/Components/v2/monitors/charts/RadialAvgResponse.tsx
@@ -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 = {
+ success: "Excellent",
+ warning: "Average",
+ danger: "Poor",
+ };
+
+ return (
+
+ }
+ title={t("common.charts.labels.averageResponseTime")}
+ >
+
+
+
+
+ |
+ |
+
+
+
+
+ {t("common.charts.labels.low")}
+ {t("common.charts.labels.high")}
+
+
+
+ {msg[palette]}
+
+ {`${avg?.toFixed()}ms`}
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx
index a7cfa34fd..de38d2cab 100644
--- a/client/src/Components/v2/monitors/index.tsx
+++ b/client/src/Components/v2/monitors/index.tsx
@@ -1 +1,5 @@
export * from "./ControlsFilter";
+export * from "./MonitorStatBoxes";
+export * from "./charts/HistogramStatus";
+export * from "./charts/RadialAvgResponse";
+export * from "./charts/HistogramDetails";
diff --git a/client/src/Hooks/UseToast.ts b/client/src/Hooks/UseToast.ts
index 2ee275253..8823aa675 100644
--- a/client/src/Hooks/UseToast.ts
+++ b/client/src/Hooks/UseToast.ts
@@ -1,4 +1,5 @@
import { toast, type ToastOptions } from "react-toastify";
+
export const useToast = () => {
const showToast = (message: string, options?: ToastOptions) => {
toast.dismiss();
diff --git a/client/src/Pages/Checks/Components/IncidentTable/index.jsx b/client/src/Pages/Checks/Components/IncidentTable/index.jsx
index 31475f8b1..857f31bff 100644
--- a/client/src/Pages/Checks/Components/IncidentTable/index.jsx
+++ b/client/src/Pages/Checks/Components/IncidentTable/index.jsx
@@ -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";
diff --git a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx
index d68b0b85e..bfca9e93f 100644
--- a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx
+++ b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx
@@ -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";
diff --git a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx
index 05242c1f3..2a03a8822 100644
--- a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx
+++ b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx
@@ -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";
diff --git a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx
index 7c6fb611a..ca5bb640f 100644
--- a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx
+++ b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx
@@ -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";
diff --git a/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js b/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js
index e9b1da6d1..a4f434a80 100644
--- a/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js
+++ b/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js
@@ -1,4 +1,4 @@
-import { getHumanReadableDuration } from "@/Utils/timeUtils.js";
+import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js";
const useGetIncidentsDuration = (incident, isActive) => {
if (!incident?.startTime) {
return "-";
diff --git a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx
index b169a6a85..13ed63618 100644
--- a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx
+++ b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx
@@ -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";
diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx
index 07b504c77..194e13821 100644
--- a/client/src/Pages/Logs/Diagnostics/index.jsx
+++ b/client/src/Pages/Logs/Diagnostics/index.jsx
@@ -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 = () => {
diff --git a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx
index 22a61084d..a5e853160 100644
--- a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx
+++ b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx
@@ -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 = {
diff --git a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx
index 3414334a7..4ab56bef9 100644
--- a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx
+++ b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx
@@ -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",
diff --git a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx
index 765858050..11875bb6a 100644
--- a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx
+++ b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx
@@ -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";
diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx
index 06b92c933..d8c118237 100644
--- a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx
+++ b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx
@@ -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 = {
diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx
index 872b1e293..6ff26c739 100644
--- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx
+++ b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx
@@ -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 }) => {
diff --git a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx
index 4642e2336..4a85863c4 100644
--- a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx
+++ b/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx
@@ -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";
diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx
deleted file mode 100644
index b8496f5ea..000000000
--- a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx
+++ /dev/null
@@ -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 ;
- }
-
- const noIncidentsMessage = "Great. No Incidents, yet!";
-
- return (
-
-
- }
- header="Uptime"
- isEmpty={monitorData?.groupedUpChecks?.length === 0}
- >
-
-
- Total Checks
-
- {hoveredUptimeData !== null
- ? hoveredUptimeData.totalChecks
- : (monitorData?.groupedUpChecks?.reduce((count, checkGroup) => {
- return count + checkGroup.totalChecks;
- }, 0) ?? 0)}
-
- {hoveredUptimeData !== null && hoveredUptimeData.time !== null && (
-
- {formatDateWithTz(hoveredUptimeData._id, dateFormat, uiTimezone)}
-
- )}
-
-
-
- {hoveredUptimeData !== null ? "Avg Response Time" : "Uptime Percentage"}
-
-
- {hoveredUptimeData !== null
- ? Math.floor(hoveredUptimeData?.originalAvgResponseTime ?? 0)
- : Math.floor((monitorData?.groupedUptimePercentage ?? 0) * 100)}
-
- {hoveredUptimeData !== null ? " ms" : " %"}
-
-
-
-
-
-
-
- }
- header="Incidents"
- noDataMessage={noIncidentsMessage}
- isEmpty={monitorData?.groupedDownChecks?.length === 0}
- >
-
-
-
- {hoveredIncidentsData !== null
- ? hoveredIncidentsData.totalChecks
- : (monitorData?.groupedDownChecks?.reduce((count, checkGroup) => {
- return count + checkGroup.totalChecks;
- }, 0) ?? 0)}
-
- {hoveredIncidentsData !== null && hoveredIncidentsData.time !== null && (
-
- {formatDateWithTz(hoveredIncidentsData._id, dateFormat, uiTimezone)}
-
- )}
-
-
-
-
-
- }
- header="Average Response Time"
- >
-
-
-
- );
-};
-
-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,
-};
diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx b/client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx
deleted file mode 100644
index 750b3b20a..000000000
--- a/client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Skeleton, Stack } from "@mui/material";
-import { useTheme } from "@emotion/react";
-
-const SkeletonLayout = () => {
- const theme = useTheme();
- return (
-
-
-
-
-
- );
-};
-
-export default SkeletonLayout;
diff --git a/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx b/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx
deleted file mode 100644
index 472c8d058..000000000
--- a/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx
+++ /dev/null
@@ -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 (
- <>
-
- {formatDateWithTz(firstDataPoint._id, dateFormat, uiTimezone)}
-
-
- {formatDateWithTz(lastDataPoint._id, dateFormat, uiTimezone)}
-
- >
- );
-};
-
-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;
diff --git a/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx
deleted file mode 100644
index c5e08ecca..000000000
--- a/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx
+++ /dev/null
@@ -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 (
-
- {
- setChartHovered(true);
- onBarHover({ time: null, totalChecks: 0 });
- }}
- onMouseLeave={() => {
- setChartHovered(false);
- setHoveredBarIndex(null);
- onBarHover(null);
- }}
- >
-
- }
- />
-
- {groupedDownChecks?.map((entry, index) => {
- return (
- {
- setHoveredBarIndex(index);
- onBarHover(entry);
- }}
- onMouseLeave={() => {
- setHoveredBarIndex(null);
- onBarHover({ time: null, totalChecks: 0 });
- }}
- />
- );
- })}
- |
-
-
- );
-});
-
-DownBarChart.displayName = "DownBarChart";
-DownBarChart.propTypes = {
- monitor: PropTypes.shape({
- groupedDownChecks: PropTypes.arrayOf(PropTypes.object),
- }),
- type: PropTypes.string,
- onBarHover: PropTypes.func,
-};
-export default DownBarChart;
diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx
deleted file mode 100644
index 084a711c3..000000000
--- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx
+++ /dev/null
@@ -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 (
-
-
-
- {t("low")}
-
-
- {t("high")}
-
-
- {responseProps.category}
-
-
- {responseTime} ms
-
-
- |
- |
-
-
-
- );
-};
-
-ResponseGaugeChart.propTypes = {
- avgResponseTime: PropTypes.number.isRequired,
-};
-
-export default ResponseGaugeChart;
diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx
deleted file mode 100644
index 7e7a940e0..000000000
--- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx
+++ /dev/null
@@ -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 ;
- }
-
- return (
-
- }
- header="Response Times"
- >
-
-
- );
-};
-
-ResponseTImeChart.propTypes = {
- isLoading: PropTypes.bool,
- groupedChecks: PropTypes.array,
- dateRange: PropTypes.string,
-};
-
-export default ResponseTImeChart;
diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx
deleted file mode 100644
index a70757e36..000000000
--- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Skeleton } from "@mui/material";
-const ResponseTimeChartSkeleton = () => {
- return (
-
- );
-};
-
-export default ResponseTimeChartSkeleton;
diff --git a/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx
deleted file mode 100644
index 8f1af8912..000000000
--- a/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx
+++ /dev/null
@@ -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 (
-
- {
- setChartHovered(true);
- onBarHover({ time: null, totalChecks: 0, originalAvgResponseTime: 0 });
- }}
- onMouseLeave={() => {
- setChartHovered(false);
- setHoveredBarIndex(null);
- onBarHover(null);
- }}
- >
-
- }
- />
-
- {groupedUpChecks?.map((entry, index) => {
- const themeColor = getThemeColor(entry.originalAvgResponseTime);
- return (
- {
- setHoveredBarIndex(index);
- onBarHover(entry);
- }}
- onMouseLeave={() => {
- setHoveredBarIndex(null);
- onBarHover({
- time: null,
- totalChecks: 0,
- groupUptimePercentage: 0,
- });
- }}
- />
- );
- })}
- |
-
-
- );
-});
-
-// 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;
diff --git a/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx
new file mode 100644
index 000000000..2cfdc1f15
--- /dev/null
+++ b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx
@@ -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[] = [
+ {
+ id: "status",
+ content: t("common.table.headers.status"),
+ render: (row) => {
+ return ;
+ },
+ },
+ {
+ 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 | null,
+ newPage: number
+ ) => {
+ setPage(newPage);
+ };
+
+ const handleRowsPerPageChange = (
+ e: React.ChangeEvent
+ ) => {
+ const value = Number(e.target.value);
+ setPage(0);
+ setRowsPerPage(value);
+ };
+
+ return (
+
+ {
+ navigate(`/checks/${row.id}`);
+ }}
+ />
+
+
+ );
+};
diff --git a/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx b/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx
deleted file mode 100644
index 2d7991324..000000000
--- a/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx
+++ /dev/null
@@ -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 (
-
-
- {t("responseTimeBreakdown")}
-
- {timingDetails.map((detail, index, array) => (
-
-
- {detail.label}:
-
-
- {Math.round(detail.value)} ms
-
-
- ))}
-
- );
-};
-
-const ResponseTable = ({
- isLoading = false,
- checks = [],
- checksCount,
- uiTimezone,
- page,
- setPage,
- rowsPerPage,
- setRowsPerPage,
-}) => {
- const { t } = useTranslation();
- const theme = useTheme();
-
- if (isLoading) {
- return ;
- }
-
- const headers = [
- {
- id: "status",
- content: t("status"),
- render: (row) => {
- const status = row.status === true ? "up" : "down";
-
- return (
-
- );
- },
- },
- {
- 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 (
-
-
- {responseTimeDisplay}
-
-
- );
- },
- },
- ];
-
- return (
-
- }
- header="Response Times"
- height="100%"
- >
-
-
-
- );
-};
-
-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;
diff --git a/client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx b/client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx
deleted file mode 100644
index 2665ffaf1..000000000
--- a/client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Skeleton } from "@mui/material";
-
-const SkeletonLayout = () => {
- return (
-
- );
-};
-
-export default SkeletonLayout;
diff --git a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx
deleted file mode 100644
index 252873cf9..000000000
--- a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx
+++ /dev/null
@@ -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 (
-
-
-
- {lastCheckTime}
- {"ago"}
- >
- }
- />
-
- {monitorStats?.lastResponseTime}
- {"ms"}
- >
- }
- />
- {monitor?.type === "http" && (
-
- {certificateExpiry}
-
- }
- />
- )}
-
- );
-};
-
-UptimeStatusBoxes.propTypes = {
- isLoading: PropTypes.bool,
- monitor: PropTypes.object,
- monitorStats: PropTypes.object,
- certificateExpiry: PropTypes.string,
-};
-
-export default UptimeStatusBoxes;
diff --git a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx b/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx
index e74a99d82..8912438d8 100644
--- a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx
+++ b/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx
@@ -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,
diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx
new file mode 100644
index 000000000..1a6177ddc
--- /dev/null
+++ b/client/src/Pages/Uptime/Details/index.tsx
@@ -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(0);
+ const [rowsPerPage, setRowsPerPage] = useState(5);
+ const [dateRange, setDateRange] = useState("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(
+ 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(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(
+ checksUrl,
+ {},
+ { keepPreviousData: true }
+ );
+
+ const checks = checksData?.checks ?? [];
+ const checksCount = checksData?.checksCount ?? 0;
+
+ return (
+
+
+
+
+
+ }
+ checks={monitorData?.groupedUpChecks ?? []}
+ range={dateRange}
+ />
+ }
+ checks={monitorData?.groupedDownChecks ?? []}
+ range={dateRange}
+ />
+
+
+
+
+
+ );
+};
+
+export default UptimeDetailsPage;
diff --git a/client/src/Pages/Uptime/Details/index.jsx b/client/src/Pages/Uptime/Details/old.jsx
similarity index 100%
rename from client/src/Pages/Uptime/Details/index.jsx
rename to client/src/Pages/Uptime/Details/old.jsx
diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx
index 5b05817bb..e3e90429a 100644
--- a/client/src/Pages/Uptime/Monitors/index.tsx
+++ b/client/src/Pages/Uptime/Monitors/index.tsx
@@ -88,7 +88,11 @@ const UptimeMonitorsPage = () => {
data: monitors,
isLoading: monitorsLoading,
error,
- } = useGet("/monitors/team?type=http&type=ping&type=port&type=docker");
+ } = useGet(
+ "/monitors/team?type=http&type=ping&type=port&type=docker",
+ {},
+ { keepPreviousData: true }
+ );
const {
data: monitorsWithChecksData,
@@ -98,7 +102,7 @@ const UptimeMonitorsPage = () => {
} = useGet(
monitorsWithChecksUrl,
{},
- { refreshInterval: 5000 }
+ { refreshInterval: 5000, keepPreviousData: true }
);
const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {};
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index e37bae063..715d4374e 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -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 = () => {
/>
}
+ element={
+ <>
+
+
+
+ >
+ }
/>
{
const tickFormatLookup: Record = {
- "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 = {
- "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) {
diff --git a/client/src/Utils/timeUtils.js b/client/src/Utils/timeUtilsLegacy.js
similarity index 87%
rename from client/src/Utils/timeUtils.js
rename to client/src/Utils/timeUtilsLegacy.js
index f6e2dcec0..365572f6b 100644
--- a/client/src/Utils/timeUtils.js
+++ b/client/src/Utils/timeUtilsLegacy.js
@@ -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";
+ }
+};
diff --git a/client/src/index.css b/client/src/index.css
index 8b2a1cfce..48737d4bb 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -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;
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index de02815b1..d098e54de 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -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",
diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts
index b9cf2041f..0783c0040 100644
--- a/server/src/repositories/checks/MongoChecksRepistory.ts
+++ b/server/src/repositories/checks/MongoChecksRepistory.ts
@@ -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 } },
],
},
},
diff --git a/server/src/types/check.ts b/server/src/types/check.ts
index 3195cc06a..ee40ff6dc 100644
--- a/server/src/types/check.ts
+++ b/server/src/types/check.ts
@@ -157,7 +157,7 @@ export interface HardwareChecksResult {
}
export interface GroupedCheck {
- _id: string;
+ bucketDate: string;
avgResponseTime: number;
totalChecks: number;
}