diff --git a/Client/src/Pages/Uptime/NewDetails/Components/ChartBoxes/index.jsx b/Client/src/Pages/Uptime/NewDetails/Components/ChartBoxes/index.jsx
new file mode 100644
index 000000000..afe93d028
--- /dev/null
+++ b/Client/src/Pages/Uptime/NewDetails/Components/ChartBoxes/index.jsx
@@ -0,0 +1,134 @@
+// Components
+import { Stack, Typography, Box } from "@mui/material";
+import ChartBox from "../Charts/ChartBox";
+import UptimeIcon from "../../../../../assets/icons/uptime-icon.svg?react";
+import IncidentsIcon from "../../../../../assets/icons/incidents.svg?react";
+import AverageResponseIcon from "../../../../../assets/icons/average-response-icon.svg?react";
+import UpBarChart from "../Charts/UpBarChart";
+import DownBarChart from "../Charts/DownBarChart";
+import ResponseGaugeChart from "../Charts/ResponseGaugeChart";
+
+// Utils
+import { formatDateWithTz } from "../../../../../Utils/timeUtils";
+import PropTypes from "prop-types";
+import { useTheme } from "@emotion/react";
+
+const ChartBoxes = ({
+ monitor,
+ dateRange,
+ uiTimezone,
+ dateFormat,
+ hoveredUptimeData,
+ setHoveredUptimeData,
+ hoveredIncidentsData,
+ setHoveredIncidentsData,
+}) => {
+ const theme = useTheme();
+ return (
+
+ }
+ header="Uptime"
+ >
+
+
+ Total Checks
+
+ {hoveredUptimeData !== null
+ ? hoveredUptimeData.totalChecks
+ : (monitor?.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?.avgResponseTime ?? 0)
+ : Math.floor(
+ ((monitor?.upChecks?.totalChecks ?? 0) /
+ (monitor?.totalChecks ?? 1)) *
+ 100
+ )}
+
+ {hoveredUptimeData !== null ? " ms" : " %"}
+
+
+
+
+
+
+ }
+ header="Incidents"
+ >
+
+
+ {hoveredIncidentsData !== null
+ ? hoveredIncidentsData.totalChecks
+ : (monitor?.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 = {
+ monitor: PropTypes.object.isRequired,
+ 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/NewDetails/Components/Charts/ChartBox.jsx b/Client/src/Pages/Uptime/NewDetails/Components/Charts/ChartBox.jsx
new file mode 100644
index 000000000..e62949004
--- /dev/null
+++ b/Client/src/Pages/Uptime/NewDetails/Components/Charts/ChartBox.jsx
@@ -0,0 +1,74 @@
+import { Stack, Typography } from "@mui/material";
+import { useTheme } from "@emotion/react";
+import IconBox from "../../../../../Components/IconBox";
+import PropTypes from "prop-types";
+const ChartBox = ({ children, icon, header }) => {
+ const theme = useTheme();
+ return (
+ span": {
+ color: theme.palette.primary.contrastText,
+ fontSize: 20,
+ "& span": {
+ opacity: 0.8,
+ marginLeft: 2,
+ fontSize: 15,
+ },
+ },
+ "& .MuiStack-root": {
+ flexDirection: "row",
+ gap: theme.spacing(6),
+ },
+ "& .MuiStack-root:first-of-type": {
+ alignItems: "center",
+ },
+ "& tspan, & text": {
+ fill: theme.palette.primary.contrastTextTertiary,
+ },
+ "& path": {
+ transition: "fill 300ms ease, stroke-width 400ms ease",
+ },
+ }}
+ >
+
+ {icon}
+ {header}
+
+
+ {children}
+
+ );
+};
+
+export default ChartBox;
+
+ChartBox.propTypes = {
+ children: PropTypes.node,
+ icon: PropTypes.node.isRequired,
+ header: PropTypes.string.isRequired,
+};
diff --git a/Client/src/Pages/Uptime/NewDetails/Components/Charts/CustomLabels.jsx b/Client/src/Pages/Uptime/NewDetails/Components/Charts/CustomLabels.jsx
new file mode 100644
index 000000000..8df33953f
--- /dev/null
+++ b/Client/src/Pages/Uptime/NewDetails/Components/Charts/CustomLabels.jsx
@@ -0,0 +1,42 @@
+import PropTypes from "prop-types";
+import { useSelector } from "react-redux";
+import { formatDateWithTz } from "../../../../../Utils/timeUtils";
+
+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/NewDetails/Components/Charts/DownBarChart.jsx b/Client/src/Pages/Uptime/NewDetails/Components/Charts/DownBarChart.jsx
new file mode 100644
index 000000000..dcd3a06be
--- /dev/null
+++ b/Client/src/Pages/Uptime/NewDetails/Components/Charts/DownBarChart.jsx
@@ -0,0 +1,92 @@
+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";
+
+const DownBarChart = memo(({ monitor, 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);
+ }}
+ >
+
+ }
+ />
+
+ {monitor?.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/NewDetails/Components/Charts/ResponseGaugeChart.jsx b/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseGaugeChart.jsx
new file mode 100644
index 000000000..e94af8441
--- /dev/null
+++ b/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseGaugeChart.jsx
@@ -0,0 +1,117 @@
+import PropTypes from "prop-types";
+import { useTheme } from "@mui/material";
+import { ResponsiveContainer, RadialBarChart, RadialBar, Cell } from "recharts";
+
+const ResponseGaugeChart = ({ avgResponseTime }) => {
+ const theme = useTheme();
+
+ 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.contrastText,
+ }
+ : responseTime <= 500
+ ? {
+ category: "Fair",
+ main: theme.palette.success.main,
+ bg: theme.palette.success.contrastText,
+ }
+ : 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.contrastText,
+ };
+
+ return (
+
+
+
+ low
+
+
+ high
+
+
+ {responseProps.category}
+
+
+ {responseTime} ms
+
+
+ |
+ |
+
+
+
+ );
+};
+
+ResponseGaugeChart.propTypes = {
+ avgResponseTime: PropTypes.number.isRequired,
+};
+
+export default ResponseGaugeChart;
diff --git a/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseTimeChart.jsx b/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseTimeChart.jsx
new file mode 100644
index 000000000..c28163394
--- /dev/null
+++ b/Client/src/Pages/Uptime/NewDetails/Components/Charts/ResponseTimeChart.jsx
@@ -0,0 +1,18 @@
+import ChartBox from "./ChartBox";
+import MonitorDetailsAreaChart from "../../../../../Components/Charts/MonitorDetailsAreaChart";
+import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react";
+const ResponseTImeChart = ({ monitor, dateRange }) => {
+ return (
+ }
+ header="Response Times"
+ >
+
+
+ );
+};
+
+export default ResponseTImeChart;
diff --git a/Client/src/Pages/Uptime/NewDetails/Components/Charts/UpBarChart.jsx b/Client/src/Pages/Uptime/NewDetails/Components/Charts/UpBarChart.jsx
new file mode 100644
index 000000000..1df79c26e
--- /dev/null
+++ b/Client/src/Pages/Uptime/NewDetails/Components/Charts/UpBarChart.jsx
@@ -0,0 +1,109 @@
+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";
+
+const getThemeColor = (responseTime) => {
+ if (responseTime < 200) {
+ return "success";
+ } else if (responseTime < 300) {
+ return "warning";
+ } else {
+ return "error";
+ }
+};
+
+const UpBarChart = memo(({ monitor, type, onBarHover }) => {
+ const theme = useTheme();
+ const [chartHovered, setChartHovered] = useState(false);
+ const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
+
+ return (
+
+ {
+ setChartHovered(true);
+ onBarHover({ time: null, totalChecks: 0, avgResponseTime: 0 });
+ }}
+ onMouseLeave={() => {
+ setChartHovered(false);
+ setHoveredBarIndex(null);
+ onBarHover(null);
+ }}
+ >
+
+ }
+ />
+
+ {monitor?.groupedUpChecks?.map((entry, index) => {
+ const themeColor = getThemeColor(entry.avgResponseTime);
+ 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/NewDetails/Components/ConfigButton/index.jsx b/Client/src/Pages/Uptime/NewDetails/Components/ConfigButton/index.jsx
new file mode 100644
index 000000000..9981b4e1c
--- /dev/null
+++ b/Client/src/Pages/Uptime/NewDetails/Components/ConfigButton/index.jsx
@@ -0,0 +1,35 @@
+import { Button, Box } from "@mui/material";
+import { useTheme } from "@emotion/react";
+import { useNavigate } from "react-router-dom";
+import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react";
+
+const ConfigButton = ({ shouldRender, monitorId }) => {
+ const theme = useTheme();
+ const navigate = useNavigate();
+
+ if (!shouldRender) return null;
+
+ return (
+
+
+
+ );
+};
+
+export default ConfigButton;
diff --git a/Client/src/Pages/Uptime/NewDetails/Components/MonitorHeader/index.jsx b/Client/src/Pages/Uptime/NewDetails/Components/MonitorHeader/index.jsx
new file mode 100644
index 000000000..5802fd82d
--- /dev/null
+++ b/Client/src/Pages/Uptime/NewDetails/Components/MonitorHeader/index.jsx
@@ -0,0 +1,43 @@
+import { Stack, Typography } from "@mui/material";
+import PulseDot from "../../../../../Components/Animated/PulseDot";
+import Dot from "../../../../../Components/Dot";
+import { useTheme } from "@emotion/react";
+import useUtils from "../../../Home/Hooks/useUtils";
+import { formatDurationRounded } from "../../../../../Utils/timeUtils";
+import ConfigButton from "../ConfigButton";
+
+const MonitorHeader = ({ monitor }) => {
+ const theme = useTheme();
+ const { statusColor, statusMsg, determineState } = useUtils();
+
+ return (
+
+
+ {monitor.name}
+
+
+
+ {monitor?.url?.replace(/^https?:\/\//, "") || "..."}
+
+
+
+ Checking every {formatDurationRounded(monitor?.interval)}.
+
+
+
+
+
+ );
+};
+
+export default MonitorHeader;
diff --git a/Client/src/Pages/Uptime/NewDetails/Components/ResponseTable/index.jsx b/Client/src/Pages/Uptime/NewDetails/Components/ResponseTable/index.jsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/Client/src/Pages/Uptime/NewDetails/Components/StatusBoxes/index.jsx b/Client/src/Pages/Uptime/NewDetails/Components/StatusBoxes/index.jsx
new file mode 100644
index 000000000..e994dd63b
--- /dev/null
+++ b/Client/src/Pages/Uptime/NewDetails/Components/StatusBoxes/index.jsx
@@ -0,0 +1,72 @@
+// Components
+import { Stack, Typography } from "@mui/material";
+import StatBox from "../../../../../Components/StatBox";
+
+// Utils
+import { useTheme } from "@mui/material/styles";
+import useUtils from "../../../Home/Hooks/useUtils";
+import { getHumanReadableDuration } from "../../../../../Utils/timeUtils";
+
+const StatusBoxes = ({ monitor, certificateExpiry }) => {
+ const theme = useTheme();
+ const { time: streakTime, units: streakUnits } = getHumanReadableDuration(
+ monitor?.uptimeStreak
+ );
+
+ const { time: lastCheckTime, units: lastCheckUnits } = getHumanReadableDuration(
+ monitor?.timeSinceLastCheck
+ );
+
+ const { determineState } = useUtils();
+ return (
+
+
+ {streakTime}
+ {streakUnits}
+ >
+ }
+ />
+
+ {lastCheckTime}
+ {lastCheckUnits}
+ {"ago"}
+ >
+ }
+ />
+
+ {monitor?.latestResponseTime}
+ {"ms"}
+ >
+ }
+ />
+
+ {certificateExpiry}
+
+ }
+ />
+
+ );
+};
+
+export default StatusBoxes;
diff --git a/Client/src/Pages/Uptime/NewDetails/Components/TimeFramePicker/index.jsx b/Client/src/Pages/Uptime/NewDetails/Components/TimeFramePicker/index.jsx
new file mode 100644
index 000000000..ffb581e23
--- /dev/null
+++ b/Client/src/Pages/Uptime/NewDetails/Components/TimeFramePicker/index.jsx
@@ -0,0 +1,44 @@
+import { Stack, Typography, Button, ButtonGroup } from "@mui/material";
+import { useTheme } from "@emotion/react";
+const TimeFramePicker = ({ dateRange, setDateRange }) => {
+ const theme = useTheme();
+ return (
+
+
+ Showing statistics for past{" "}
+ {dateRange === "day" ? "24 hours" : dateRange === "week" ? "7 days" : "30 days"}.
+
+
+
+
+
+
+
+ );
+};
+
+export default TimeFramePicker;
diff --git a/Client/src/Pages/Uptime/NewDetails/Hooks/useMonitorFetch.jsx b/Client/src/Pages/Uptime/NewDetails/Hooks/useMonitorFetch.jsx
index e7ed825f2..067545f51 100644
--- a/Client/src/Pages/Uptime/NewDetails/Hooks/useMonitorFetch.jsx
+++ b/Client/src/Pages/Uptime/NewDetails/Hooks/useMonitorFetch.jsx
@@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
export const useMonitorFetch = ({ authToken, monitorId, dateRange }) => {
const [monitorIsLoading, setMonitorsIsLoading] = useState(false);
- const [monitor, setMonitor] = useState([]);
+ const [monitor, setMonitor] = useState({});
const navigate = useNavigate();
useEffect(() => {
diff --git a/Client/src/Pages/Uptime/NewDetails/index.jsx b/Client/src/Pages/Uptime/NewDetails/index.jsx
index 5494146e6..eca12ec57 100644
--- a/Client/src/Pages/Uptime/NewDetails/index.jsx
+++ b/Client/src/Pages/Uptime/NewDetails/index.jsx
@@ -1,6 +1,10 @@
// Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
-
+import MonitorHeader from "./Components/MonitorHeader";
+import StatusBoxes from "./Components/StatusBoxes";
+import TimeFramePicker from "./Components/TimeFramePicker";
+import ChartBoxes from "./Components/ChartBoxes";
+import ResponseTimeChart from "./Components/Charts/ResponseTimeChart";
// MUI Components
import { Stack } from "@mui/material";
@@ -8,6 +12,7 @@ import { Stack } from "@mui/material";
import { useState } from "react";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
+import { useTheme } from "@emotion/react";
import useMonitorFetch from "./Hooks/useMonitorFetch";
import useCertificateFetch from "./Hooks/useCertificateFetch";
// Constants
@@ -26,10 +31,13 @@ const UptimeDetails = () => {
// Local state
const [dateRange, setDateRange] = useState("day");
+ const [hoveredUptimeData, setHoveredUptimeData] = useState(null);
+ const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null);
// Utils
const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D";
const { monitorId } = useParams();
+ const theme = useTheme();
const { monitor, monitorIsLoading } = useMonitorFetch({
authToken,
@@ -45,11 +53,32 @@ const UptimeDetails = () => {
uiTimezone,
});
- console.log(monitor);
- console.log(certificateExpiry);
return (
-
+
+
+
+
+
+
);
};