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 ( - + + + + + + ); };