diff --git a/Client/src/Components/Animated/PulseDot.jsx b/Client/src/Components/Animated/PulseDot.jsx index 38e956b50..9ac536fe6 100644 --- a/Client/src/Components/Animated/PulseDot.jsx +++ b/Client/src/Components/Animated/PulseDot.jsx @@ -41,8 +41,8 @@ const PulseDot = ({ color }) => { "&::after": { content: `""`, position: "absolute", - width: "6px", - height: "6px", + width: "7px", + height: "7px", borderRadius: "50%", backgroundColor: "white", top: "50%", diff --git a/Client/src/Components/Charts/MonitorDetails60MinChart/index.jsx b/Client/src/Components/Charts/MonitorDetails60MinChart/index.jsx deleted file mode 100644 index 57cb344e7..000000000 --- a/Client/src/Components/Charts/MonitorDetails60MinChart/index.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import { BarChart, Bar, Cell, ReferenceLine, Label } from "recharts"; - -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; - -const MonitorDetails60MinChart = ({ data }) => { - const theme = useTheme(); - - const labelStyle = { - fontSize: "10px", - fill: theme.palette.text.tertiary, - }; - - const color = { - true: theme.palette.success.main, - false: theme.palette.error.text, - undefined: theme.palette.unresolved.main, - }; - return ( - - - {data.map((check, index) => ( - - ))} - - - - - - - - - ); -}; - -MonitorDetails60MinChart.propTypes = { - data: PropTypes.array.isRequired, -}; - -export default MonitorDetails60MinChart; diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx index 4e5d1d7b0..68fdb1cd4 100644 --- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx +++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx @@ -1,6 +1,6 @@ import PropTypes from "prop-types"; import { AreaChart, Area, XAxis, Tooltip, ResponsiveContainer } from "recharts"; -import { Box, Typography } from "@mui/material"; +import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import "./index.css"; @@ -16,14 +16,15 @@ const CustomToolTip = ({ active, payload, label }) => { border: 1, borderColor: theme.palette.border.dark, borderRadius: theme.shape.borderRadius, - py: theme.spacing(6), - px: theme.spacing(8), + py: theme.spacing(2), + px: theme.spacing(4), }} > {new Date(label).toLocaleDateString("en-US", { @@ -38,15 +39,39 @@ const CustomToolTip = ({ active, payload, label }) => { hour12: true, // AM/PM format })} - - Response Time (ms): {payload[0].payload.originalResponseTime} - {" "} + + + + + Response Time + {" "} + + {payload[0].payload.originalResponseTime} + + {" "} + ms + + + + {/* Display original value */} ); @@ -64,11 +89,13 @@ const MonitorDetailsAreaChart = ({ checks }) => { }); }; + const theme = useTheme(); + return ( - + { bottom: 0, }} > + + + + + + } /> diff --git a/Client/src/Layouts/HomeLayout/index.css b/Client/src/Layouts/HomeLayout/index.css index 1434ff9d9..855bf33fc 100644 --- a/Client/src/Layouts/HomeLayout/index.css +++ b/Client/src/Layouts/HomeLayout/index.css @@ -3,9 +3,7 @@ } .home-layout { - display: flex; position: relative; - gap: var(--env-var-spacing-2); min-height: 100vh; max-width: 1400px; margin: 0 auto; diff --git a/Client/src/Layouts/HomeLayout/index.jsx b/Client/src/Layouts/HomeLayout/index.jsx index 92d763f0c..fd92c59fa 100644 --- a/Client/src/Layouts/HomeLayout/index.jsx +++ b/Client/src/Layouts/HomeLayout/index.jsx @@ -1,6 +1,6 @@ import Sidebar from "../../Components/Sidebar"; import { Outlet } from "react-router"; -import { Box } from "@mui/material"; +import { Box, Stack } from "@mui/material"; import { useTheme } from "@emotion/react"; import "./index.css"; @@ -10,10 +10,14 @@ const HomeLayout = () => { return ( - + - + ); }; diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index 5d756298e..3152cf506 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -83,7 +83,7 @@ const Incidents = () => { }; return ( - + {loading ? ( ) : ( diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx index 7dd6a1e8f..1ee171034 100644 --- a/Client/src/Pages/Monitors/Configure/index.jsx +++ b/Client/src/Pages/Monitors/Configure/index.jsx @@ -255,6 +255,7 @@ const Configure = () => { color="secondary" loading={isLoading} sx={{ + border: "none", backgroundColor: theme.palette.background.main, px: theme.spacing(5), mr: theme.spacing(6), diff --git a/Client/src/Pages/Monitors/Details/Charts/index.jsx b/Client/src/Pages/Monitors/Details/Charts/index.jsx new file mode 100644 index 000000000..1d0bb3bf2 --- /dev/null +++ b/Client/src/Pages/Monitors/Details/Charts/index.jsx @@ -0,0 +1,280 @@ +import { useTheme } from "@emotion/react"; +import { + BarChart, + Bar, + XAxis, + CartesianGrid, + ResponsiveContainer, + Cell, + RadialBarChart, + RadialBar, +} from "recharts"; +import { formatDate } from "../../../../Utils/timeUtils"; +import { useState } from "react"; + +const CustomLabels = ({ + x, + width, + height, + firstDataPoint, + lastDataPoint, + type, +}) => { + let options = { + month: "short", + year: undefined, + hour: undefined, + minute: undefined, + }; + if (type === "day") delete options.hour; + + return ( + <> + + {formatDate(new Date(firstDataPoint.time), options)} + + + {formatDate(new Date(lastDataPoint.time), options)} + + + ); +}; + +export const UpBarChart = ({ data, type, onBarHover }) => { + const theme = useTheme(); + + const [chartHovered, setChartHovered] = useState(false); + const [hoveredBarIndex, setHoveredBarIndex] = useState(null); + + const getColorRange = (uptime) => { + return uptime > 80 + ? { main: theme.palette.success.main, light: theme.palette.success.light } + : uptime > 50 + ? { main: theme.palette.warning.main, light: theme.palette.warning.light } + : { main: theme.palette.error.text, light: theme.palette.error.light }; + }; + + // TODO - REMOVE THIS LATER + let reversedData = [...data].reverse(); + + return ( + + { + setChartHovered(true); + onBarHover({ time: null, totalChecks: 0, uptimePercentage: 0 }); + }} + onMouseLeave={() => { + setChartHovered(false); + setHoveredBarIndex(null); + onBarHover(null); + }} + > + + } + /> + + {reversedData.map((entry, index) => { + let { main, light } = getColorRange(entry.uptimePercentage); + return ( + { + setHoveredBarIndex(index); + onBarHover(entry); + }} + onMouseLeave={() => { + setHoveredBarIndex(null); + onBarHover({ + time: null, + totalChecks: 0, + uptimePercentage: 0, + }); + }} + /> + ); + })} + + + + ); +}; + +export const DownBarChart = ({ data, type, onBarHover }) => { + const theme = useTheme(); + + const [chartHovered, setChartHovered] = useState(false); + const [hoveredBarIndex, setHoveredBarIndex] = useState(null); + + // TODO - REMOVE THIS LATER + let reversedData = [...data].reverse(); + + return ( + + { + setChartHovered(true); + onBarHover({ time: null, totalIncidents: 0 }); + }} + onMouseLeave={() => { + setChartHovered(false); + setHoveredBarIndex(null); + onBarHover(null); + }} + > + + } + /> + + {reversedData.map((entry, index) => ( + { + setHoveredBarIndex(index); + onBarHover(entry); + }} + onMouseLeave={() => { + setHoveredBarIndex(null); + onBarHover({ time: null, totalIncidents: 0 }); + }} + /> + ))} + + + + ); +}; + +export const ResponseGaugeChart = ({ data }) => { + const theme = useTheme(); + + let max = 1000; // max ms + data = [{ response: max, fill: "transparent", background: false }, ...data]; + + let responseTime = Math.floor(data[1].response); + let responseProps = + responseTime <= 200 + ? { + category: "Excellent", + main: theme.palette.success.main, + bg: theme.palette.success.bg, + } + : responseTime <= 500 + ? { + category: "Fair", + main: theme.palette.success.main, + bg: theme.palette.success.bg, + } + : responseTime <= 600 + ? { + category: "Acceptable", + main: theme.palette.warning.main, + bg: theme.palette.warning.bg, + } + : { + category: "Poor", + main: theme.palette.error.text, + bg: theme.palette.error.bg, + }; + + return ( + + + + low + + + high + + + {responseProps.category} + + + {responseTime}{" "} + ms + + + + + + + + ); +}; diff --git a/Client/src/Pages/Monitors/Details/index.css b/Client/src/Pages/Monitors/Details/index.css index 59ffc5d85..8b1378917 100644 --- a/Client/src/Pages/Monitors/Details/index.css +++ b/Client/src/Pages/Monitors/Details/index.css @@ -1,18 +1 @@ -.monitor-details h1.MuiTypography-root { - font-size: var(--env-var-font-size-large-plus); - font-weight: 600; -} -.monitor-details h2.MuiTypography-root { - font-size: var(--env-var-font-size-large); -} -.monitor-details h2.MuiTypography-root { - font-weight: 600; -} -.monitor-details button.MuiButtonBase-root { - height: var(--env-var-height-2); - line-height: 1; -} -.monitor-details p.MuiTypography-root, -.monitor-details p.MuiTypography-root span.MuiTypography-root { - font-size: var(--env-var-font-size-small-plus); -} + diff --git a/Client/src/Pages/Monitors/Details/index.jsx b/Client/src/Pages/Monitors/Details/index.jsx index 833e11ef9..e31de38d7 100644 --- a/Client/src/Pages/Monitors/Details/index.jsx +++ b/Client/src/Pages/Monitors/Details/index.jsx @@ -1,70 +1,41 @@ import PropTypes from "prop-types"; import { useEffect, useState, useCallback } from "react"; -import { Box, Button, Stack, Typography, useTheme } from "@mui/material"; +import { + Box, + Button, + Popover, + 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 { + formatDate, formatDuration, formatDurationRounded, + formatDurationSplit, } from "../../../Utils/timeUtils"; import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart"; import ButtonGroup from "@mui/material/ButtonGroup"; import SettingsIcon from "../../../assets/icons/settings-bold.svg?react"; +import CertificateIcon from "../../../assets/icons/certificate.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"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import PulseDot from "../../../Components/Animated/PulseDot"; +import { StatBox, ChartBox, IconBox } from "./styled"; +import { DownBarChart, ResponseGaugeChart, UpBarChart } from "./Charts"; import SkeletonLayout from "./skeleton"; import "./index.css"; -const StatBox = ({ title, value }) => { - const theme = useTheme(); - return ( - - - {title} - - - {value} - - - ); -}; - -StatBox.propTypes = { - title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), -}; - /** * Details page component displaying monitor details and related information. * @component @@ -78,6 +49,14 @@ const DetailsPage = ({ isAdmin }) => { const [certificateExpiry, setCertificateExpiry] = useState("N/A"); const navigate = useNavigate(); + const [anchorEl, setAnchorEl] = useState(null); + const openCertificate = (event) => { + setAnchorEl(event.currentTarget); + }; + const closeCertificate = () => { + setAnchorEl(null); + }; + const fetchMonitor = useCallback(async () => { try { const res = await networkService.getStatsByMonitorId( @@ -110,7 +89,16 @@ const DetailsPage = ({ isAdmin }) => { authToken, monitorId ); - setCertificateExpiry(res?.data?.data?.certificateDate ?? "N/A"); + + let [month, day, year] = res?.data?.data?.certificateDate.split("/"); + const date = new Date(year, month - 1, day); + + setCertificateExpiry( + formatDate(date, { + hour: undefined, + minute: undefined, + }) ?? "N/A" + ); } catch (error) { console.error(error); } @@ -118,8 +106,21 @@ const DetailsPage = ({ isAdmin }) => { fetchCertificate(); }, [authToken, monitorId, monitor]); + const splitDuration = (duration) => { + const { time, format } = formatDurationSplit(duration); + return ( + <> + {time} + {format} + + ); + }; + let loading = Object.keys(monitor).length === 0; + const [hoveredUptimeData, setHoveredUptimeData] = useState(null); + const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null); + const statusColor = { true: theme.palette.success.main, false: theme.palette.error.main, @@ -144,119 +145,209 @@ const DetailsPage = ({ isAdmin }) => { { name: "details", path: `/monitors/${monitorId}` }, ]} /> - + - - {monitor.url?.replace(/^https?:\/\//, "") || "..."} + {monitor.name} - - - {statusMsg[monitor?.status ?? undefined]} - {" "} - Checking every {formatDurationRounded(monitor?.interval)}. - + + + + + + {monitor.url?.replace(/^https?:\/\//, "") || "..."} + + + Checking every {formatDurationRounded(monitor?.interval)}. + + - {isAdmin && ( - - )} + + Certificate Expiry + + + {certificateExpiry} + + + {isAdmin && ( + + )} + - + - - - - - - Avg. Response Time{" "} - (24-hr) - + sx={ + monitor?.status === undefined + ? { + backgroundColor: theme.palette.warning.light, + borderColor: theme.palette.warning.border, + "& h2": { color: theme.palette.warning.main }, + } + : monitor?.status + ? { + backgroundColor: theme.palette.success.bg, + borderColor: theme.palette.success.light, + "& h2": { color: theme.palette.success.main }, + } + : { + backgroundColor: theme.palette.error.bg, + borderColor: theme.palette.error.light, + "& h2": { color: theme.palette.error.main }, + } } - value={parseFloat(monitor?.avgResponseTime24hours) - .toFixed(2) - .replace(/\.?0+$/, "")} - /> - - Uptime (24-hr) - - } - value={`${parseFloat(monitor?.uptime24Hours) - .toFixed(2) - .replace(/\.?0+$/, "")}%`} - /> - - Uptime (30-day) - - } - value={`${parseFloat(monitor?.uptime30Days) - .toFixed(2) - .replace(/\.?0+$/, "")}%`} - /> + > + active for + + {splitDuration(monitor?.uptimeDuration)} + + + + last check + + {splitDuration(monitor?.lastChecked)} + ago + + + + last response time + + {monitor?.latestResponseTime} + ms + + - - Response Times + + Showing statistics for past{" "} + {dateRange === "day" + ? "24 hours" + : dateRange === "week" + ? "7 days" + : "30 days"} + . - + - - - + + + + + + + Uptime + + + + Total Checks + + {hoveredUptimeData !== null + ? hoveredUptimeData.totalChecks + : monitor?.periodTotalChecks} + + {hoveredUptimeData !== null && + hoveredUptimeData.time !== null && ( + + {formatDate(new Date(hoveredUptimeData.time), { + month: "short", + year: undefined, + minute: undefined, + hour: dateRange === "day" ? "numeric" : undefined, + })} + + )} + + + Uptime Percentage + + {hoveredUptimeData !== null + ? Math.floor( + hoveredUptimeData.uptimePercentage * 10 + ) / 10 + : Math.floor(monitor?.periodUptime * 10) / 10} + % + + + + + + + + + + + Incidents + + + Total Incidents + + {hoveredIncidentsData !== null + ? hoveredIncidentsData.totalIncidents + : monitor?.periodIncidents} + + {hoveredIncidentsData !== null && + hoveredIncidentsData.time !== null && ( + + {formatDate(new Date(hoveredIncidentsData.time), { + month: "short", + year: undefined, + minute: undefined, + hour: dateRange === "day" ? "numeric" : undefined, + })} + + )} + + + + + + + + + + Average Response Time + + + + + + + + + + Response Times + + + + + + + + + + History + + + + + - - - History - - - )} diff --git a/Client/src/Pages/Monitors/Details/styled.jsx b/Client/src/Pages/Monitors/Details/styled.jsx new file mode 100644 index 000000000..99112802d --- /dev/null +++ b/Client/src/Pages/Monitors/Details/styled.jsx @@ -0,0 +1,95 @@ +import { Box, 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.border.light, + borderRadius: 4, + backgroundColor: theme.palette.background.main, + "& h2": { + color: theme.palette.text.secondary, + fontSize: 15, + fontWeight: 500, + }, + "& .MuiBox-root:not(.area-tooltip) p": { + color: theme.palette.text.tertiary, + fontSize: 13, + }, + "& .MuiBox-root > span": { + color: theme.palette.text.primary, + 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.text.tertiary, + }, + "& path": { + transition: "fill 300ms ease", + }, +})); + +export const IconBox = styled(Box)(({ theme }) => ({ + height: 34, + minWidth: 34, + width: 34, + position: "relative", + border: 1, + borderStyle: "solid", + borderColor: theme.palette.border.dark, + borderRadius: 4, + backgroundColor: theme.palette.background.accent, + "& svg": { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: 20, + height: 20, + "& path": { + stroke: theme.palette.text.tertiary, + }, + }, +})); + +export const StatBox = styled(Box)(({ theme }) => ({ + padding: `${theme.spacing(4)} ${theme.spacing(8)}`, + minWidth: 200, + width: 225, + border: 1, + borderStyle: "solid", + borderColor: theme.palette.border.light, + borderRadius: 4, + backgroundColor: theme.palette.background.main, + "& h2": { + fontSize: 13, + fontWeight: 500, + color: theme.palette.text.secondary, + textTransform: "uppercase", + }, + "& p": { + fontSize: 18, + color: theme.palette.text.primary, + marginTop: theme.spacing(2), + "& span": { + color: theme.palette.text.tertiary, + marginLeft: theme.spacing(2), + fontSize: 15, + }, + }, +})); diff --git a/Client/src/Pages/Monitors/Home/StatusBox.jsx b/Client/src/Pages/Monitors/Home/StatusBox.jsx index 08b2e88d9..ece8cde16 100644 --- a/Client/src/Pages/Monitors/Home/StatusBox.jsx +++ b/Client/src/Pages/Monitors/Home/StatusBox.jsx @@ -48,8 +48,7 @@ const StatusBox = ({ title, value }) => { borderColor={theme.palette.border.light} borderRadius={theme.shape.borderRadius} backgroundColor={theme.palette.background.main} - px={theme.spacing(12)} - py={theme.spacing(8)} + p={theme.spacing(8)} overflow="hidden" sx={{ "&:hover": { diff --git a/Client/src/Pages/Monitors/Home/index.jsx b/Client/src/Pages/Monitors/Home/index.jsx index 318a9da20..64c01b7ac 100644 --- a/Client/src/Pages/Monitors/Home/index.jsx +++ b/Client/src/Pages/Monitors/Home/index.jsx @@ -43,24 +43,8 @@ const Monitors = ({ isAdmin }) => { let loading = monitorState.isLoading && monitorState.monitors.length === 0; - const now = new Date(); - const hour = now.getHours(); - - let greeting = ""; - let emoji = ""; - if (hour < 12) { - greeting = "morning"; - emoji = "πŸŒ…"; - } else if (hour < 18) { - greeting = "afternoon"; - emoji = "🌞"; - } else { - greeting = "evening"; - emoji = "πŸŒ™"; - } - return ( - + {loading ? ( ) : ( @@ -95,7 +79,7 @@ const Monitors = ({ isAdmin }) => { {monitorState.monitors?.length !== 0 && ( <> @@ -105,8 +89,7 @@ const Monitors = ({ isAdmin }) => { { } The response from the axios GET request. + * + */ + async getAggregateStatsById(authToken, monitorId, dateRange) { + const params = new URLSearchParams(); + if (dateRange) params.append("dateRange", dateRange); + + return this.axiosInstance.get( + `/monitors/aggregate/${monitorId}?${params.toString()}`, + { + headers: { + Authorization: `Bearer ${authToken}`, + }, + } + ); + } + /** * ************************************ * Updates a single monitor diff --git a/Client/src/Utils/Theme/darkTheme.js b/Client/src/Utils/Theme/darkTheme.js index 008de87db..71955b83e 100644 --- a/Client/src/Utils/Theme/darkTheme.js +++ b/Client/src/Utils/Theme/darkTheme.js @@ -4,16 +4,16 @@ const text = { primary: "#fafafa", secondary: "#e6e6e6", tertiary: "#a1a1aa", - accent: "#e6e6e6", + accent: "#8e8e8f", disabled: "rgba(172, 172, 172, 0.3)", }; const background = { main: "#151518", alt: "#09090b", - fill: "#2e2e2e", + fill: "#2D2D33", accent: "#18181a", }; -const border = { light: "#27272a", dark: "#2c2c2c" }; +const border = { light: "#27272a", dark: "#36363e" }; const fontFamilyDefault = '"Inter","system-ui", "Avenir", "Helvetica", "Arial", sans-serif'; @@ -25,7 +25,7 @@ const darkTheme = createTheme({ palette: { mode: "dark", primary: { main: "#1570ef" }, - secondary: { main: "#2e2e2e" }, + secondary: { main: "#2D2D33" }, text: text, background: background, border: border, @@ -39,22 +39,22 @@ const darkTheme = createTheme({ success: { text: "#079455", main: "#45bb7a", - light: "#1e1e1e", - bg: "#27272a", + light: "#1c4428", + bg: "#12261e", }, error: { text: "#f04438", main: "#d32f2f", - light: "#1e1e1e", - bg: "#27272a", + light: "#542426", + bg: "#301a1f", dark: "#932020", border: "#f04438", }, warning: { text: "#e88c30", main: "#FF9F00", - light: "#27272a", - bg: "#1E1E1E", + light: "#272115", + bg: "#624711", border: "#e88c30", }, percentage: { @@ -98,6 +98,15 @@ const darkTheme = createTheme({ backgroundColor: theme.palette.secondary.main, }, }, + { + props: (props) => + props.variant === "contained" && props.color === "secondary", + style: { + border: 1, + borderStyle: "solid", + borderColor: theme.palette.border.dark, + }, + }, ], fontWeight: 400, borderRadius: 4, diff --git a/Client/src/Utils/Theme/lightTheme.js b/Client/src/Utils/Theme/lightTheme.js index 6688a0ca4..4d554c54c 100644 --- a/Client/src/Utils/Theme/lightTheme.js +++ b/Client/src/Utils/Theme/lightTheme.js @@ -95,6 +95,15 @@ const lightTheme = createTheme({ backgroundColor: theme.palette.secondary.main, }, }, + { + props: (props) => + props.variant === "contained" && props.color === "secondary", + style: { + border: 1, + borderStyle: "solid", + borderColor: theme.palette.border.light, + }, + }, ], fontWeight: 400, borderRadius: 4, diff --git a/Client/src/Utils/greeting.jsx b/Client/src/Utils/greeting.jsx index cb878c991..7da08b340 100644 --- a/Client/src/Utils/greeting.jsx +++ b/Client/src/Utils/greeting.jsx @@ -160,6 +160,7 @@ const Greeting = ({ type = "" }) => { lineHeight={1} fontWeight={500} color={theme.palette.text.primary} + mb={theme.spacing(1)} > { {append} β€” Here’s an overview of your {type} monitors. diff --git a/Client/src/Utils/timeUtils.js b/Client/src/Utils/timeUtils.js index 2d1a2f180..f9f916d04 100644 --- a/Client/src/Utils/timeUtils.js +++ b/Client/src/Utils/timeUtils.js @@ -43,6 +43,23 @@ export const formatDurationRounded = (ms) => { return time; }; +export const formatDurationSplit = (ms) => { + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + return days > 0 + ? { time: days, format: days === 1 ? "day" : "days" } + : hours > 0 + ? { time: hours, format: hours === 1 ? "hour" : "hours" } + : minutes > 0 + ? { time: minutes, format: minutes === 1 ? "minute" : "minutes" } + : seconds > 0 + ? { time: seconds, format: seconds === 1 ? "second" : "seconds" } + : { time: 0, format: "seconds" }; +}; + export const formatDate = (date, customOptions) => { const options = { year: "numeric", @@ -51,9 +68,11 @@ export const formatDate = (date, customOptions) => { hour: "numeric", minute: "numeric", hour12: true, - ...customOptions + ...customOptions, }; // Return the date using the specified options - return date.toLocaleString("en-US", options); + return date + .toLocaleString("en-US", options) + .replace(/\b(AM|PM)\b/g, (match) => match.toLowerCase()); }; diff --git a/Client/src/assets/icons/average-response-icon.svg b/Client/src/assets/icons/average-response-icon.svg new file mode 100644 index 000000000..572e2002f --- /dev/null +++ b/Client/src/assets/icons/average-response-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/Client/src/assets/icons/certificate.svg b/Client/src/assets/icons/certificate.svg new file mode 100644 index 000000000..a985bef9f --- /dev/null +++ b/Client/src/assets/icons/certificate.svg @@ -0,0 +1,3 @@ + + + diff --git a/Client/src/assets/icons/history-icon.svg b/Client/src/assets/icons/history-icon.svg new file mode 100644 index 000000000..7343455ec --- /dev/null +++ b/Client/src/assets/icons/history-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/Client/src/assets/icons/response-time-icon.svg b/Client/src/assets/icons/response-time-icon.svg new file mode 100644 index 000000000..db5b2af38 --- /dev/null +++ b/Client/src/assets/icons/response-time-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/Client/src/assets/icons/top-right-arrow.svg b/Client/src/assets/icons/top-right-arrow.svg index 471b9454c..d569cdd7e 100644 --- a/Client/src/assets/icons/top-right-arrow.svg +++ b/Client/src/assets/icons/top-right-arrow.svg @@ -1,3 +1,3 @@ - + diff --git a/Client/src/assets/icons/uptime-icon.svg b/Client/src/assets/icons/uptime-icon.svg new file mode 100644 index 000000000..55706ae71 --- /dev/null +++ b/Client/src/assets/icons/uptime-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/Client/src/index.css b/Client/src/index.css index 105776bd8..740a22b7a 100644 --- a/Client/src/index.css +++ b/Client/src/index.css @@ -4,6 +4,10 @@ box-sizing: border-box; } +html { + scroll-behavior: smooth; +} + :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; font-weight: 400;