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",