diff --git a/Client/src/Components/Dot/index.jsx b/Client/src/Components/Dot/index.jsx
new file mode 100644
index 000000000..0fd51730f
--- /dev/null
+++ b/Client/src/Components/Dot/index.jsx
@@ -0,0 +1,23 @@
+import PropTypes from "prop-types";
+
+const Dot = ({ color = "gray", size = "4px" }) => {
+ return (
+
+ );
+};
+
+Dot.propTypes = {
+ color: PropTypes.string,
+ size: PropTypes.string,
+};
+
+export default Dot;
diff --git a/Client/src/Components/Table/TablePagination/index.jsx b/Client/src/Components/Table/TablePagination/index.jsx
index 61e89232f..518c18599 100644
--- a/Client/src/Components/Table/TablePagination/index.jsx
+++ b/Client/src/Components/Table/TablePagination/index.jsx
@@ -5,6 +5,7 @@ import { TablePaginationActions } from "./Actions";
import SelectorVertical from "../../../assets/icons/selector-vertical.svg?react";
Pagination.propTypes = {
+ paginationLabel: PropTypes.string, // Label for the pagination.
itemCount: PropTypes.number.isRequired, // Total number of items for pagination.
page: PropTypes.number.isRequired, // Current page index.
rowsPerPage: PropTypes.number.isRequired, // Number of rows displayed per page.
diff --git a/Client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx b/Client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx
new file mode 100644
index 000000000..d9e581267
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx
@@ -0,0 +1,140 @@
+// 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";
+import SkeletonLayout from "./skeleton";
+// Utils
+import { formatDateWithTz } from "../../../../../Utils/timeUtils";
+import PropTypes from "prop-types";
+import { useTheme } from "@emotion/react";
+
+const ChartBoxes = ({
+ shouldRender = true,
+ monitor,
+ dateRange,
+ uiTimezone,
+ dateFormat,
+ hoveredUptimeData,
+ setHoveredUptimeData,
+ hoveredIncidentsData,
+ setHoveredIncidentsData,
+}) => {
+ const theme = useTheme();
+
+ if (!shouldRender) {
+ return ;
+ }
+
+ 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/Details/Components/ChartBoxes/skeleton.jsx b/Client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx
new file mode 100644
index 000000000..750b3b20a
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx
@@ -0,0 +1,30 @@
+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/ChartBox.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/ChartBox.jsx
new file mode 100644
index 000000000..3d4d73421
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Components/Charts/ChartBox.jsx
@@ -0,0 +1,75 @@
+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, height = "300px" }) => {
+ 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,
+ height: PropTypes.string,
+};
diff --git a/Client/src/Pages/Uptime/Details/Charts/CustomLabels.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx
similarity index 85%
rename from Client/src/Pages/Uptime/Details/Charts/CustomLabels.jsx
rename to Client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx
index 10494830f..8df33953f 100644
--- a/Client/src/Pages/Uptime/Details/Charts/CustomLabels.jsx
+++ b/Client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx
@@ -1,6 +1,6 @@
import PropTypes from "prop-types";
import { useSelector } from "react-redux";
-import { formatDateWithTz } from "../../../../Utils/timeUtils";
+import { formatDateWithTz } from "../../../../../Utils/timeUtils";
const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
@@ -34,8 +34,8 @@ CustomLabels.propTypes = {
x: PropTypes.number.isRequired,
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
- firstDataPoint: PropTypes.object.isRequired,
- lastDataPoint: PropTypes.object.isRequired,
+ firstDataPoint: PropTypes.object,
+ lastDataPoint: PropTypes.object,
type: PropTypes.string.isRequired,
};
diff --git a/Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx
similarity index 91%
rename from Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx
rename to Client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx
index ae2adda0f..dcd3a06be 100644
--- a/Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx
+++ b/Client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx
@@ -40,9 +40,9 @@ const DownBarChart = memo(({ monitor, type, onBarHover }) => {
y={0}
width="100%"
height="100%"
- firstDataPoint={monitor.groupedDownChecks?.[0] ?? {}}
+ firstDataPoint={monitor?.groupedDownChecks?.[0] ?? {}}
lastDataPoint={
- monitor.groupedDownChecks?.[monitor.groupedDownChecks.length - 1] ?? {}
+ monitor?.groupedDownChecks?.[monitor?.groupedDownChecks?.length - 1] ?? {}
}
type={type}
/>
@@ -53,7 +53,7 @@ const DownBarChart = memo(({ monitor, type, onBarHover }) => {
maxBarSize={7}
background={{ fill: "transparent" }}
>
- {monitor.groupedDownChecks.map((entry, index) => {
+ {monitor?.groupedDownChecks?.map((entry, index) => {
return (
{
+ if (!shouldRender) {
+ return ;
+ }
+
+ return (
+ }
+ header="Response Times"
+ >
+
+
+ );
+};
+
+ResponseTImeChart.propTypes = {
+ shouldRender: PropTypes.bool,
+ monitor: PropTypes.object,
+ 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
new file mode 100644
index 000000000..a70757e36
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx
@@ -0,0 +1,12 @@
+import { Skeleton } from "@mui/material";
+const ResponseTimeChartSkeleton = () => {
+ return (
+
+ );
+};
+
+export default ResponseTimeChartSkeleton;
diff --git a/Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx
similarity index 90%
rename from Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx
rename to Client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx
index 223dc8eef..1df79c26e 100644
--- a/Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx
+++ b/Client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx
@@ -28,7 +28,7 @@ const UpBarChart = memo(({ monitor, type, onBarHover }) => {
{
setChartHovered(true);
onBarHover({ time: null, totalChecks: 0, avgResponseTime: 0 });
@@ -49,8 +49,10 @@ const UpBarChart = memo(({ monitor, type, onBarHover }) => {
y={0}
width="100%"
height="100%"
- firstDataPoint={monitor.groupedUpChecks[0]}
- lastDataPoint={monitor.groupedUpChecks[monitor.groupedUpChecks.length - 1]}
+ firstDataPoint={monitor?.groupedUpChecks?.[0]}
+ lastDataPoint={
+ monitor?.groupedUpChecks?.[monitor?.groupedUpChecks?.length - 1]
+ }
type={type}
/>
}
@@ -60,7 +62,7 @@ const UpBarChart = memo(({ monitor, type, onBarHover }) => {
maxBarSize={7}
background={{ fill: "transparent" }}
>
- {monitor.groupedUpChecks.map((entry, index) => {
+ {monitor?.groupedUpChecks?.map((entry, index) => {
const themeColor = getThemeColor(entry.avgResponseTime);
return (
{
+ const theme = useTheme();
+ const navigate = useNavigate();
+
+ if (!shouldRender) return null;
+
+ return (
+
+
+
+ );
+};
+
+ConfigButton.propTypes = {
+ shouldRender: PropTypes.bool,
+ monitorId: PropTypes.string,
+};
+
+export default ConfigButton;
diff --git a/Client/src/Pages/Uptime/Details/Components/MonitorHeader/index.jsx b/Client/src/Pages/Uptime/Details/Components/MonitorHeader/index.jsx
new file mode 100644
index 000000000..fddf9c3e5
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Components/MonitorHeader/index.jsx
@@ -0,0 +1,55 @@
+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";
+import SkeletonLayout from "./skeleton";
+import PropTypes from "prop-types";
+
+const MonitorHeader = ({ shouldRender = true, isAdmin, monitor }) => {
+ const theme = useTheme();
+ const { statusColor, statusMsg, determineState } = useUtils();
+ console.log(shouldRender);
+ if (!shouldRender) {
+ return ;
+ }
+
+ return (
+
+
+ {monitor.name}
+
+
+
+ {monitor?.url?.replace(/^https?:\/\//, "") || "..."}
+
+
+
+ Checking every {formatDurationRounded(monitor?.interval)}.
+
+
+
+
+
+ );
+};
+
+MonitorHeader.propTypes = {
+ shouldRender: PropTypes.bool,
+ isAdmin: PropTypes.bool,
+ monitor: PropTypes.object,
+};
+
+export default MonitorHeader;
diff --git a/Client/src/Pages/Uptime/Details/Components/MonitorHeader/skeleton.jsx b/Client/src/Pages/Uptime/Details/Components/MonitorHeader/skeleton.jsx
new file mode 100644
index 000000000..64dc0547f
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Components/MonitorHeader/skeleton.jsx
@@ -0,0 +1,23 @@
+import { Stack, Skeleton } from "@mui/material";
+
+const SkeletonLayout = () => {
+ return (
+
+
+
+
+ );
+};
+
+export default SkeletonLayout;
diff --git a/Client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx b/Client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx
new file mode 100644
index 000000000..56ea8eca7
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx
@@ -0,0 +1,89 @@
+import ChartBox from "../Charts/ChartBox";
+import PropTypes from "prop-types";
+import HistoryIcon from "../../../../../assets/icons/history-icon.svg?react";
+import Table from "../../../../../Components/Table";
+import TablePagination from "../../../../../Components/Table/TablePagination";
+import { StatusLabel } from "../../../../../Components/Label";
+import { formatDateWithTz } from "../../../../../Utils/timeUtils";
+import SkeletonLayout from "./skeleton";
+const ResponseTable = ({
+ shouldRender = true,
+ checks,
+ checksCount,
+ uiTimezone,
+ page,
+ setPage,
+ rowsPerPage,
+ setRowsPerPage,
+}) => {
+ if (!shouldRender) {
+ return ;
+ }
+
+ const headers = [
+ {
+ id: "status",
+ content: "Status",
+ render: (row) => {
+ const status = row.status === true ? "up" : "down";
+
+ return (
+
+ );
+ },
+ },
+ {
+ id: "date",
+ content: "Date & Time",
+ render: (row) =>
+ formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone),
+ },
+ {
+ id: "statusCode",
+ content: "Status code",
+ render: (row) => (row.statusCode ? row.statusCode : "N/A"),
+ },
+ {
+ id: "message",
+ content: "Message",
+ render: (row) => row.message,
+ },
+ ];
+
+ return (
+ }
+ header="Response Times"
+ height="100%"
+ >
+
+
+
+ );
+};
+
+ResponseTable.propTypes = {
+ shouldRender: PropTypes.bool,
+ checks: PropTypes.array.isRequired,
+ checksCount: PropTypes.number.isRequired,
+ 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
new file mode 100644
index 000000000..2665ffaf1
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx
@@ -0,0 +1,13 @@
+import { Skeleton } from "@mui/material";
+
+const SkeletonLayout = () => {
+ return (
+
+ );
+};
+
+export default SkeletonLayout;
diff --git a/Client/src/Pages/Uptime/Details/Components/StatusBoxes/index.jsx b/Client/src/Pages/Uptime/Details/Components/StatusBoxes/index.jsx
new file mode 100644
index 000000000..21922537f
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Components/StatusBoxes/index.jsx
@@ -0,0 +1,83 @@
+// Components
+import { Stack, Typography } from "@mui/material";
+import StatBox from "../../../../../Components/StatBox";
+import SkeletonLayout from "./skeleton";
+// Utils
+import { useTheme } from "@mui/material/styles";
+import useUtils from "../../../Home/Hooks/useUtils";
+import { getHumanReadableDuration } from "../../../../../Utils/timeUtils";
+import PropTypes from "prop-types";
+const StatusBoxes = ({ shouldRender, monitor, certificateExpiry }) => {
+ // Utils
+ const theme = useTheme();
+ const { determineState } = useUtils();
+
+ if (!shouldRender) {
+ return ;
+ }
+ const { time: streakTime, units: streakUnits } = getHumanReadableDuration(
+ monitor?.uptimeStreak
+ );
+
+ const { time: lastCheckTime, units: lastCheckUnits } = getHumanReadableDuration(
+ monitor?.timeSinceLastCheck
+ );
+
+ return (
+
+
+ {streakTime}
+ {streakUnits}
+ >
+ }
+ />
+
+ {lastCheckTime}
+ {lastCheckUnits}
+ {"ago"}
+ >
+ }
+ />
+
+ {monitor?.latestResponseTime}
+ {"ms"}
+ >
+ }
+ />
+
+ {certificateExpiry}
+
+ }
+ />
+
+ );
+};
+
+StatusBoxes.propTypes = {
+ shouldRender: PropTypes.bool,
+ monitor: PropTypes.object,
+ certificateExpiry: PropTypes.string,
+};
+
+export default StatusBoxes;
diff --git a/Client/src/Pages/Uptime/Details/Components/StatusBoxes/skeleton.jsx b/Client/src/Pages/Uptime/Details/Components/StatusBoxes/skeleton.jsx
new file mode 100644
index 000000000..993b410c8
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Components/StatusBoxes/skeleton.jsx
@@ -0,0 +1,30 @@
+import { Stack, Skeleton } 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/TimeFramePicker/index.jsx b/Client/src/Pages/Uptime/Details/Components/TimeFramePicker/index.jsx
new file mode 100644
index 000000000..789030282
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Components/TimeFramePicker/index.jsx
@@ -0,0 +1,58 @@
+import { Stack, Typography, Button, ButtonGroup } from "@mui/material";
+import { useTheme } from "@emotion/react";
+import SkeletonLayout from "./skeleton";
+import PropTypes from "prop-types";
+
+const TimeFramePicker = ({ shouldRender = true, dateRange, setDateRange }) => {
+ const theme = useTheme();
+
+ if (!shouldRender) {
+ return ;
+ }
+
+ return (
+
+
+ Showing statistics for past{" "}
+ {dateRange === "day" ? "24 hours" : dateRange === "week" ? "7 days" : "30 days"}.
+
+
+
+
+
+
+
+ );
+};
+
+TimeFramePicker.propTypes = {
+ shouldRender: PropTypes.bool,
+ dateRange: PropTypes.string,
+ setDateRange: PropTypes.func,
+};
+
+export default TimeFramePicker;
diff --git a/Client/src/Pages/Uptime/Details/Components/TimeFramePicker/skeleton.jsx b/Client/src/Pages/Uptime/Details/Components/TimeFramePicker/skeleton.jsx
new file mode 100644
index 000000000..d7c395d54
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Components/TimeFramePicker/skeleton.jsx
@@ -0,0 +1,23 @@
+import { Stack, Skeleton } from "@mui/material";
+
+const SkeletonLayout = () => {
+ return (
+
+
+
+
+ );
+};
+
+export default SkeletonLayout;
diff --git a/Client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx b/Client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx
new file mode 100644
index 000000000..9e1c3b44f
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx
@@ -0,0 +1,46 @@
+import { logger } from "../../../../Utils/Logger";
+import { useEffect, useState } from "react";
+import { networkService } from "../../../../main";
+import { formatDateWithTz } from "../../../../Utils/timeUtils";
+
+const useCertificateFetch = ({
+ monitor,
+ authToken,
+ monitorId,
+ certificateDateFormat,
+ uiTimezone,
+}) => {
+ const [certificateExpiry, setCertificateExpiry] = useState("N/A");
+ const [certificateIsLoading, setCertificateIsLoading] = useState(false);
+
+ useEffect(() => {
+ const fetchCertificate = async () => {
+ if (monitor?.type !== "http") {
+ return;
+ }
+
+ try {
+ setCertificateIsLoading(true);
+ const res = await networkService.getCertificateExpiry({
+ authToken: authToken,
+ monitorId: monitorId,
+ });
+ if (res?.data?.data?.certificateDate) {
+ const date = res.data.data.certificateDate;
+ setCertificateExpiry(
+ formatDateWithTz(date, certificateDateFormat, uiTimezone) ?? "N/A"
+ );
+ }
+ } catch (error) {
+ setCertificateExpiry("N/A");
+ logger.error(error);
+ } finally {
+ setCertificateIsLoading(false);
+ }
+ };
+ fetchCertificate();
+ }, [authToken, monitorId, certificateDateFormat, uiTimezone, monitor]);
+ return { certificateExpiry, certificateIsLoading };
+};
+
+export default useCertificateFetch;
diff --git a/Client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx b/Client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx
new file mode 100644
index 000000000..2a50c2172
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx
@@ -0,0 +1,45 @@
+import { useState } from "react";
+import { useEffect } from "react";
+import { logger } from "../../../../Utils/Logger";
+import { networkService } from "../../../../main";
+
+export const useChecksFetch = ({
+ authToken,
+ monitorId,
+ dateRange,
+ page,
+ rowsPerPage,
+}) => {
+ const [checks, setChecks] = useState([]);
+ const [checksCount, setChecksCount] = useState(0);
+ const [checksAreLoading, setChecksAreLoading] = useState(false);
+
+ useEffect(() => {
+ const fetchChecks = async () => {
+ try {
+ setChecksAreLoading(true);
+ const res = await networkService.getChecksByMonitor({
+ authToken: authToken,
+ monitorId: monitorId,
+ sortOrder: "desc",
+ limit: null,
+ dateRange: dateRange,
+ filter: null,
+ page: page,
+ rowsPerPage: rowsPerPage,
+ });
+ setChecks(res.data.data.checks);
+ setChecksCount(res.data.data.checksCount);
+ } catch (error) {
+ logger.error(error);
+ } finally {
+ setChecksAreLoading(false);
+ }
+ };
+ fetchChecks();
+ }, [authToken, monitorId, dateRange, page, rowsPerPage]);
+
+ return { checks, checksCount, checksAreLoading };
+};
+
+export default useChecksFetch;
diff --git a/Client/src/Pages/Uptime/Details/Hooks/useMonitorFetch.jsx b/Client/src/Pages/Uptime/Details/Hooks/useMonitorFetch.jsx
new file mode 100644
index 000000000..067545f51
--- /dev/null
+++ b/Client/src/Pages/Uptime/Details/Hooks/useMonitorFetch.jsx
@@ -0,0 +1,34 @@
+import { useEffect, useState } from "react";
+import { networkService } from "../../../../main";
+import { logger } from "../../../../Utils/Logger";
+import { useNavigate } from "react-router-dom";
+
+export const useMonitorFetch = ({ authToken, monitorId, dateRange }) => {
+ const [monitorIsLoading, setMonitorsIsLoading] = useState(false);
+ const [monitor, setMonitor] = useState({});
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const fetchMonitors = async () => {
+ try {
+ setMonitorsIsLoading(true);
+ const res = await networkService.getUptimeDetailsById({
+ authToken: authToken,
+ monitorId: monitorId,
+ dateRange: dateRange,
+ normalize: true,
+ });
+ setMonitor(res?.data?.data ?? {});
+ } catch (error) {
+ logger.error(error);
+ navigate("/not-found", { replace: true });
+ } finally {
+ setMonitorsIsLoading(false);
+ }
+ };
+ fetchMonitors();
+ }, [authToken, dateRange, monitorId, navigate]);
+ return { monitor, monitorIsLoading };
+};
+
+export default useMonitorFetch;
diff --git a/Client/src/Pages/Uptime/Details/PaginationTable/index.jsx b/Client/src/Pages/Uptime/Details/PaginationTable/index.jsx
deleted file mode 100644
index 93e9532c5..000000000
--- a/Client/src/Pages/Uptime/Details/PaginationTable/index.jsx
+++ /dev/null
@@ -1,163 +0,0 @@
-import PropTypes from "prop-types";
-import {
- TableContainer,
- Table,
- TableHead,
- TableRow,
- TableCell,
- TableBody,
- PaginationItem,
- Pagination,
- Paper,
-} from "@mui/material";
-
-import { useState, useEffect } from "react";
-import { useSelector } from "react-redux";
-import { networkService } from "../../../../main";
-import { StatusLabel } from "../../../../Components/Label";
-import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
-import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
-import { logger } from "../../../../Utils/Logger";
-import { formatDateWithTz } from "../../../../Utils/timeUtils";
-import { useTheme } from "@emotion/react";
-const PaginationTable = ({ monitorId, dateRange }) => {
- const theme = useTheme();
- const { authToken } = useSelector((state) => state.auth);
- const [checks, setChecks] = useState([]);
- const [checksCount, setChecksCount] = useState(0);
- const [paginationController, setPaginationController] = useState({
- page: 0,
- rowsPerPage: 5,
- });
- const uiTimezone = useSelector((state) => state.ui.timezone);
-
- useEffect(() => {
- setPaginationController((prevPaginationController) => ({
- ...prevPaginationController,
- page: 0,
- }));
- }, [dateRange]);
-
- useEffect(() => {
- const fetchPage = async () => {
- try {
- const res = await networkService.getChecksByMonitor({
- authToken: authToken,
- monitorId: monitorId,
- sortOrder: "desc",
- limit: null,
- dateRange: dateRange,
- filter: null,
- page: paginationController.page,
- rowsPerPage: paginationController.rowsPerPage,
- });
- setChecks(res.data.data.checks);
- setChecksCount(res.data.data.checksCount);
- } catch (error) {
- logger.error(error);
- }
- };
- fetchPage();
- }, [
- authToken,
- monitorId,
- dateRange,
- paginationController.page,
- paginationController.rowsPerPage,
- ]);
-
- const handlePageChange = (_, newPage) => {
- setPaginationController({
- ...paginationController,
- page: newPage - 1, // 0-indexed
- });
- };
-
- let paginationComponent = <>>;
- if (checksCount > paginationController.rowsPerPage) {
- paginationComponent = (
- (
-
- )}
- />
- );
- }
-
- return (
- <>
-
-
-
-
- Status
- Date & Time
- Status Code
- Message
-
-
-
- {checks.map((check) => {
- const status = check.status === true ? "up" : "down";
-
- return (
-
-
-
-
-
- {formatDateWithTz(
- check.createdAt,
- "ddd, MMMM D, YYYY, HH:mm A",
- uiTimezone
- )}
-
- {check.statusCode ? check.statusCode : "N/A"}
- {check.message}
-
- );
- })}
-
-
-
- {paginationComponent}
- >
- );
-};
-
-PaginationTable.propTypes = {
- monitorId: PropTypes.string.isRequired,
- dateRange: PropTypes.string.isRequired,
-};
-
-export default PaginationTable;
diff --git a/Client/src/Pages/Uptime/Details/index.css b/Client/src/Pages/Uptime/Details/index.css
deleted file mode 100644
index e69de29bb..000000000
diff --git a/Client/src/Pages/Uptime/Details/index.jsx b/Client/src/Pages/Uptime/Details/index.jsx
index be589c1ba..81265eb56 100644
--- a/Client/src/Pages/Uptime/Details/index.jsx
+++ b/Client/src/Pages/Uptime/Details/index.jsx
@@ -1,476 +1,128 @@
-import PropTypes from "prop-types";
-import { useEffect, useState, useCallback } from "react";
-import { Box, Button, Stack, Tooltip, Typography, useTheme } from "@mui/material";
-import { useSelector } from "react-redux";
-import { useNavigate, useParams } from "react-router-dom";
-import { networkService } from "../../../main";
-import { logger } from "../../../Utils/Logger";
-import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart";
-import ButtonGroup from "@mui/material/ButtonGroup";
-import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
-import UptimeIcon from "../../../assets/icons/uptime-icon.svg?react";
-import ResponseTimeIcon from "../../../assets/icons/response-time-icon.svg?react";
-import AverageResponseIcon from "../../../assets/icons/average-response-icon.svg?react";
-import IncidentsIcon from "../../../assets/icons/incidents.svg?react";
-import HistoryIcon from "../../../assets/icons/history-icon.svg?react";
-import PaginationTable from "./PaginationTable";
+// Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
-import PulseDot from "../../../Components/Animated/PulseDot";
-import { ChartBox } from "./styled";
-import SkeletonLayout from "./skeleton";
-import "./index.css";
-import useUtils from "../Home/Hooks/useUtils";
-import { formatDateWithTz, formatDurationSplit } from "../../../Utils/timeUtils";
-import { useIsAdmin } from "../../../Hooks/useIsAdmin";
-import IconBox from "../../../Components/IconBox";
-import StatBox from "../../../Components/StatBox";
-import UpBarChart from "./Charts/UpBarChart";
-import DownBarChart from "./Charts/DownBarChart";
-import ResponseGaugeChart from "./Charts/ResponseGaugeChart";
-/**
- * Details page component displaying monitor details and related information.
- * @component
- */
-const DetailsPage = () => {
- const theme = useTheme();
- const { statusColor, statusMsg, determineState } = useUtils();
- const isAdmin = useIsAdmin();
- const [monitor, setMonitor] = useState({});
- const { monitorId } = useParams();
- const { authToken } = useSelector((state) => state.auth);
- const [dateRange, setDateRange] = useState("day");
- const [certificateExpiry, setCertificateExpiry] = useState("N/A");
- const navigate = useNavigate();
+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";
+import ResponseTable from "./Components/ResponseTable";
+// MUI Components
+import { Stack } from "@mui/material";
- const certificateDateFormat = "MMM D, YYYY h A";
- const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D";
+// Utils
+import { useState } from "react";
+import { useParams } from "react-router-dom";
+import { useSelector } from "react-redux";
+import { useTheme } from "@emotion/react";
+import { useIsAdmin } from "../../../Hooks/useIsAdmin";
+import useMonitorFetch from "./Hooks/useMonitorFetch";
+import useCertificateFetch from "./Hooks/useCertificateFetch";
+import useChecksFetch from "./Hooks/useChecksFetch";
+
+// Constants
+const BREADCRUMBS = [
+ { name: "uptime", path: "/uptime" },
+ { name: "details", path: "" },
+ // { name: "details", path: `/uptime/${monitorId}` }, Is this needed? We can't click on this anywy
+];
+
+const certificateDateFormat = "MMM D, YYYY h A";
+
+const UptimeDetails = () => {
+ // Redux state
+ const { authToken } = useSelector((state) => state.auth);
const uiTimezone = useSelector((state) => state.ui.timezone);
- const fetchMonitor = useCallback(async () => {
- try {
- const res = await networkService.getUptimeDetailsById({
- authToken: authToken,
- monitorId: monitorId,
- dateRange: dateRange,
- normalize: true,
- });
- setMonitor(res?.data?.data ?? {});
- } catch (error) {
- logger.error(error);
- navigate("/not-found", { replace: true });
- }
- }, [authToken, monitorId, navigate, dateRange]);
-
- useEffect(() => {
- fetchMonitor();
- }, [fetchMonitor]);
-
- useEffect(() => {
- const fetchCertificate = async () => {
- if (monitor?.type !== "http") {
- return;
- }
- try {
- const res = await networkService.getCertificateExpiry({
- authToken: authToken,
- monitorId: monitorId,
- });
- if (res?.data?.data?.certificateDate) {
- const date = res.data.data.certificateDate;
- setCertificateExpiry(
- formatDateWithTz(date, certificateDateFormat, uiTimezone) ?? "N/A"
- );
- }
- } catch (error) {
- setCertificateExpiry("N/A");
- console.error(error);
- }
- };
- fetchCertificate();
- }, [authToken, monitorId, monitor, uiTimezone, dateFormat]);
-
- const splitDuration = (duration) => {
- const { time, format } = formatDurationSplit(duration);
- return (
- <>
- {time}
- {format}
- >
- );
- };
-
- let loading = Object.keys(monitor).length === 0;
-
+ // Local state
+ const [dateRange, setDateRange] = useState("day");
const [hoveredUptimeData, setHoveredUptimeData] = useState(null);
const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null);
+ const [page, setPage] = useState(0);
+ const [rowsPerPage, setRowsPerPage] = useState(5);
+
+ // Utils
+ const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D";
+ const { monitorId } = useParams();
+ const theme = useTheme();
+ const isAdmin = useIsAdmin();
+
+ const { monitor, monitorIsLoading } = useMonitorFetch({
+ authToken,
+ monitorId,
+ dateRange,
+ });
+
+ const { certificateExpiry, certificateIsLoading } = useCertificateFetch({
+ monitor,
+ authToken,
+ monitorId,
+ certificateDateFormat,
+ uiTimezone,
+ });
+
+ const { checks, checksCount, checksAreLoading } = useChecksFetch({
+ authToken,
+ monitorId,
+ dateRange,
+ page,
+ rowsPerPage,
+ });
+
+ // Handlers
+ const handlePageChange = (_, newPage) => {
+ setPage(newPage);
+ };
+
+ const handleChangeRowsPerPage = (event) => {
+ setRowsPerPage(event.target.value);
+ };
- const BREADCRUMBS = [
- { name: "uptime", path: "/uptime" },
- { name: "details", path: `/uptime/${monitorId}` },
- ];
return (
-
- {loading ? (
-
- ) : (
- <>
-
-
-
-
-
- {monitor.name}
-
-
- {/* TODO there is a tooltip at BarChart component. Wrap the Tooltip on our own component */}
-
-
-
-
-
-
- {monitor.url?.replace(/^https?:\/\//, "") || "..."}
-
-
- {/* Checking every {formatDurationRounded(monitor?.interval)}. */}
-
-
-
-
- {isAdmin && (
-
- )}
-
-
-
-
-
-
- {monitor?.latestResponseTime}
- {"ms"}
- >
- }
- />
-
- {certificateExpiry}
-
- }
- />
-
-
-
-
- Showing statistics for past{" "}
- {dateRange === "day"
- ? "24 hours"
- : dateRange === "week"
- ? "7 days"
- : "30 days"}
- .
-
-
-
-
-
-
-
-
-
-
-
-
-
- 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" : " %"}
-
-
-
-
-
-
-
-
-
-
-
- Incidents
-
-
- Total 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
- )}
-
- )}
-
-
-
-
-
-
-
-
- Average Response Time
-
-
-
-
-
-
-
-
- Response Times
-
-
-
-
-
-
-
-
-
- History
-
-
-
-
-
-
-
- >
- )}
-
+
+
+
+
+
+
+
+
+
);
};
-DetailsPage.propTypes = {
- isAdmin: PropTypes.bool,
-};
-export default DetailsPage;
+export default UptimeDetails;
diff --git a/Client/src/Pages/Uptime/Details/skeleton.jsx b/Client/src/Pages/Uptime/Details/skeleton.jsx
deleted file mode 100644
index 0bdc03ca8..000000000
--- a/Client/src/Pages/Uptime/Details/skeleton.jsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import { Box, Skeleton, Stack, useTheme } from "@mui/material";
-
-/**
- * Renders a skeleton layout.
- *
- * @returns {JSX.Element}
- */
-const SkeletonLayout = () => {
- const theme = useTheme();
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-export default SkeletonLayout;
diff --git a/Client/src/Pages/Uptime/Details/styled.jsx b/Client/src/Pages/Uptime/Details/styled.jsx
deleted file mode 100644
index 164fee457..000000000
--- a/Client/src/Pages/Uptime/Details/styled.jsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { Stack, styled } from "@mui/material";
-
-export const ChartBox = styled(Stack)(({ theme }) => ({
- flex: "1 30%",
- gap: theme.spacing(8),
- height: 300,
- minWidth: 250,
- padding: theme.spacing(8),
- border: 1,
- borderStyle: "solid",
- borderColor: theme.palette.primary.lowContrast,
- borderRadius: 4,
- backgroundColor: theme.palette.primary.main,
- "& h2": {
- color: theme.palette.primary.contrastTextSecondary,
- fontSize: 15,
- fontWeight: 500,
- },
- "& .MuiBox-root:not(.area-tooltip) p": {
- color: theme.palette.primary.contrastTextTertiary,
- fontSize: 13,
- },
- "& .MuiBox-root > 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",
- },
-}));
diff --git a/Client/src/Pages/Uptime/Home/Components/Host/index.jsx b/Client/src/Pages/Uptime/Home/Components/Host/index.jsx
index 91b7c418b..c2f082f3a 100644
--- a/Client/src/Pages/Uptime/Home/Components/Host/index.jsx
+++ b/Client/src/Pages/Uptime/Home/Components/Host/index.jsx
@@ -1,6 +1,7 @@
-import { Stack, Box, Typography } from "@mui/material";
+import { Stack, Typography } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
+import Dot from "../../../../../Components/Dot";
/**
* Host component.
* This subcomponent receives a params object and displays the host details.
@@ -26,16 +27,7 @@ const Host = ({ url, title, percentageColor, percentage }) => {
{title}
{percentageColor && percentage && (
<>
-
+
{
const teamId = user.teamId;
const { monitorsAreLoading, monitors, filteredMonitors, monitorsSummary } =
- useMonitorFetch({
+ useMonitorsFetch({
authToken,
teamId,
limit: 25,
@@ -102,7 +102,7 @@ const UptimeMonitors = () => {
return (
diff --git a/Client/src/Routes/index.jsx b/Client/src/Routes/index.jsx
index b5f4dd376..575c108cf 100644
--- a/Client/src/Routes/index.jsx
+++ b/Client/src/Routes/index.jsx
@@ -6,7 +6,9 @@ import NotFound from "../Pages/NotFound";
import Login from "../Pages/Auth/Login/Login";
import Register from "../Pages/Auth/Register/Register";
import Account from "../Pages/Account";
-import Monitors from "../Pages/Uptime/Home";
+import Uptime from "../Pages/Uptime/Home";
+import UptimeDetails from "../Pages/Uptime/Details";
+
import CreateMonitor from "../Pages/Uptime/CreateUptime";
import CreateInfrastructureMonitor from "../Pages/Infrastructure/CreateMonitor";
import Incidents from "../Pages/Incidents";
@@ -18,7 +20,6 @@ import CheckEmail from "../Pages/Auth/CheckEmail";
import SetNewPassword from "../Pages/Auth/SetNewPassword";
import NewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed";
import ProtectedRoute from "../Components/ProtectedRoute";
-import Details from "../Pages/Uptime/Details";
import Maintenance from "../Pages/Maintenance";
import Configure from "../Pages/Uptime/Configure";
import PageSpeed from "../Pages/PageSpeed";
@@ -46,7 +47,7 @@ const Routes = () => {
/>
}
+ element={}
/>
{
/>
}
+ element={}
/>
{
: { time: 0, format: "seconds" };
};
+export const getHumanReadableDuration = (ms) => {
+ const durationObj = dayjs.duration(ms);
+ if (durationObj.asDays() >= 1) {
+ const days = Math.floor(durationObj.asDays());
+ return { time: days, units: days === 1 ? "day" : "days" };
+ } else if (durationObj.asHours() >= 1) {
+ const hours = Math.floor(durationObj.asHours());
+ return { time: hours, units: hours === 1 ? "hour" : "hours" };
+ } else if (durationObj.asMinutes() >= 1) {
+ const minutes = Math.floor(durationObj.asMinutes());
+ return { time: minutes, units: minutes === 1 ? "minute" : "minutes" };
+ } else {
+ const seconds = Math.floor(durationObj.asSeconds());
+ return { time: seconds, units: seconds === 1 ? "second" : "seconds" };
+ }
+};
+
export const formatDate = (date, customOptions) => {
const options = {
year: "numeric",
| |