Merge pull request #797 from bluewave-labs/feat/monitor-details-revamp

FE - Monitor details revamp
This commit is contained in:
Alexander Holliday
2024-09-04 19:32:03 -07:00
committed by GitHub
25 changed files with 922 additions and 291 deletions

View File

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

View File

@@ -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 (
<BarChart
width={data.length * 10 + 30}
height={35}
data={data}
margin={{ top: 14, left: 15, right: 15, bottom: 2 }}
style={{ alignSelf: "baseline" }}
>
<Bar dataKey="value" barSize={10}>
{data.map((check, index) => (
<Cell key={`cell-${index}`} fill={color[check.status]} />
))}
</Bar>
<ReferenceLine x={0} stroke="black" strokeDasharray="3 3">
<Label value="60 mins" position="top" style={labelStyle} />
</ReferenceLine>
<ReferenceLine
x={Math.floor(data.length * (2 / 3))}
stroke="black"
strokeDasharray="3 3"
>
<Label value="20 mins" position="top" style={labelStyle} />
</ReferenceLine>
<ReferenceLine x={data.length - 1} stroke="black" strokeDasharray="3 3">
<Label value="now" position="top" style={labelStyle} />
</ReferenceLine>
</BarChart>
);
};
MonitorDetails60MinChart.propTypes = {
data: PropTypes.array.isRequired,
};
export default MonitorDetails60MinChart;

View File

@@ -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),
}}
>
<Typography
sx={{
color: theme.palette.primary.main,
fontSize: 13,
color: theme.palette.text.tertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{new Date(label).toLocaleDateString("en-US", {
@@ -38,15 +39,39 @@ const CustomToolTip = ({ active, payload, label }) => {
hour12: true, // AM/PM format
})}
</Typography>
<Typography
mt={theme.spacing(2.5)}
sx={{
color: theme.palette.text.secondary,
fontSize: 13,
}}
>
Response Time (ms): {payload[0].payload.originalResponseTime}
</Typography>{" "}
<Box mt={theme.spacing(1)}>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={theme.palette.primary.main}
sx={{ borderRadius: "50%" }}
/>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(3)}
sx={{
"& span": {
color: theme.palette.text.tertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Typography component="span" sx={{ opacity: 0.8 }}>
Response Time
</Typography>{" "}
<Typography component="span">
{payload[0].payload.originalResponseTime}
<Typography component="span" sx={{ opacity: 0.8 }}>
{" "}
ms
</Typography>
</Typography>
</Stack>
</Box>
{/* Display original value */}
</Box>
);
@@ -64,11 +89,13 @@ const MonitorDetailsAreaChart = ({ checks }) => {
});
};
const theme = useTheme();
return (
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer width="100%" height={220}>
<AreaChart
width={500}
height={400}
width="100%"
height="100%"
data={checks}
margin={{
top: 10,
@@ -77,18 +104,34 @@ const MonitorDetailsAreaChart = ({ checks }) => {
bottom: 0,
}}
>
<defs>
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={theme.palette.primary.main}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette.primary.light}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis
stroke={theme.palette.border.dark}
dataKey="createdAt"
tickFormatter={formatDate}
tick={{ fontSize: "13px" }}
tickLine={false}
height={18}
/>
<Tooltip content={<CustomToolTip />} />
<Area
type="monotone"
dataKey="responseTime"
stroke="#29afee"
fill="#eaf2fd"
stroke={theme.palette.primary.main}
fill="url(#colorUv)"
/>
</AreaChart>
</ResponsiveContainer>

View File

@@ -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;

View File

@@ -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 (
<Box backgroundColor={theme.palette.background.alt}>
<Box className="home-layout">
<Stack
className="home-layout"
flexDirection="row"
gap={theme.spacing(14)}
>
<Sidebar />
<Outlet />
</Box>
</Stack>
</Box>
);
};

View File

@@ -83,7 +83,7 @@ const Incidents = () => {
};
return (
<Stack className="incidents" pt={theme.spacing(21)} gap={theme.spacing(12)}>
<Stack className="incidents" pt={theme.spacing(20)} gap={theme.spacing(12)}>
{loading ? (
<SkeletonLayout />
) : (

View File

@@ -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),

View File

@@ -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 (
<>
<text x={x} y={height} dy={-3} textAnchor="start" fontSize={11}>
{formatDate(new Date(firstDataPoint.time), options)}
</text>
<text x={width} y={height} dy={-3} textAnchor="end" fontSize={11}>
{formatDate(new Date(lastDataPoint.time), options)}
</text>
</>
);
};
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 (
<ResponsiveContainer width="100%" height={155}>
<BarChart
width="100%"
height="100%"
data={reversedData}
onMouseEnter={() => {
setChartHovered(true);
onBarHover({ time: null, totalChecks: 0, uptimePercentage: 0 });
}}
onMouseLeave={() => {
setChartHovered(false);
setHoveredBarIndex(null);
onBarHover(null);
}}
>
<XAxis
stroke={theme.palette.border.dark}
height={15}
tick={false}
label={
<CustomLabels
x={0}
y={0}
width="100%"
height="100%"
firstDataPoint={reversedData[0]}
lastDataPoint={reversedData[reversedData.length - 1]}
type={type}
/>
}
/>
<Bar
dataKey="totalChecks"
maxBarSize={7}
background={{ fill: "transparent" }}
>
{reversedData.map((entry, index) => {
let { main, light } = getColorRange(entry.uptimePercentage);
return (
<Cell
key={`cell-${entry.time}`}
fill={
hoveredBarIndex === index ? main : chartHovered ? light : main
}
onMouseEnter={() => {
setHoveredBarIndex(index);
onBarHover(entry);
}}
onMouseLeave={() => {
setHoveredBarIndex(null);
onBarHover({
time: null,
totalChecks: 0,
uptimePercentage: 0,
});
}}
/>
);
})}
</Bar>
</BarChart>
</ResponsiveContainer>
);
};
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 (
<ResponsiveContainer width="100%" height={155}>
<BarChart
width="100%"
height="100%"
data={reversedData}
onMouseEnter={() => {
setChartHovered(true);
onBarHover({ time: null, totalIncidents: 0 });
}}
onMouseLeave={() => {
setChartHovered(false);
setHoveredBarIndex(null);
onBarHover(null);
}}
>
<XAxis
stroke={theme.palette.border.dark}
height={15}
tick={false}
label={
<CustomLabels
x={0}
y={0}
width="100%"
height="100%"
firstDataPoint={reversedData[0]}
lastDataPoint={reversedData[reversedData.length - 1]}
type={type}
/>
}
/>
<Bar
dataKey="totalIncidents"
maxBarSize={7}
background={{ fill: "transparent" }}
>
{reversedData.map((entry, index) => (
<Cell
key={`cell-${entry.time}`}
fill={
hoveredBarIndex === index
? theme.palette.error.text
: chartHovered
? theme.palette.error.light
: theme.palette.error.text
}
onMouseEnter={() => {
setHoveredBarIndex(index);
onBarHover(entry);
}}
onMouseLeave={() => {
setHoveredBarIndex(null);
onBarHover({ time: null, totalIncidents: 0 });
}}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
);
};
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 (
<ResponsiveContainer width="100%" height={155}>
<RadialBarChart
width="100%"
height="100%"
cy="89%"
data={data}
startAngle={180}
endAngle={0}
innerRadius={100}
outerRadius={150}
>
<text x={0} y="100%" dx="5%" dy={-2} textAnchor="start" fontSize={11}>
low
</text>
<text x="100%" y="100%" dx="-3%" dy={-2} textAnchor="end" fontSize={11}>
high
</text>
<text
x="50%"
y="45%"
textAnchor="middle"
dominantBaseline="middle"
fontSize={18}
fontWeight={400}
>
{responseProps.category}
</text>
<text
x="50%"
y="55%"
textAnchor="middle"
dominantBaseline="hanging"
fontSize={25}
>
<tspan fontWeight={600}>{responseTime}</tspan>{" "}
<tspan opacity={0.8}>ms</tspan>
</text>
<RadialBar
background={{ fill: responseProps.bg }}
clockWise
dataKey="response"
stroke="none"
>
<Cell fill="transparent" background={false} barSize={0} />
<Cell fill={responseProps.main} />
</RadialBar>
</RadialBarChart>
</ResponsiveContainer>
);
};

View File

@@ -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);
}

View File

@@ -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 (
<Box
className="stat-box"
flex="20%"
minWidth="100px"
px={theme.spacing(8)}
py={theme.spacing(4)}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
>
<Typography
variant="h6"
mb={theme.spacing(2)}
fontSize={14}
fontWeight={500}
color={theme.palette.primary.main}
sx={{
"& span": {
color: theme.palette.text.accent,
fontSize: 13,
fontStyle: "italic",
},
}}
>
{title}
</Typography>
<Typography
variant="h4"
fontWeight={500}
fontSize={13}
color={theme.palette.text.secondary}
>
{value}
</Typography>
</Box>
);
};
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}
<Typography component="span">{format}</Typography>
</>
);
};
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}` },
]}
/>
<Stack gap={theme.spacing(12)} mt={theme.spacing(12)}>
<Stack gap={theme.spacing(10)} mt={theme.spacing(10)}>
<Stack direction="row" gap={theme.spacing(2)}>
<PulseDot color={statusColor[monitor?.status ?? undefined]} />
<Box>
<Typography
component="h1"
fontSize={22}
fontWeight={500}
color={theme.palette.text.primary}
lineHeight={1}
>
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
{monitor.name}
</Typography>
<Typography
mt={theme.spacing(4)}
color={theme.palette.text.tertiary}
<Stack
direction="row"
alignItems="flex-end"
gap={theme.spacing(2)}
>
<Typography
component="span"
sx={{
color: statusColor[monitor?.status ?? undefined],
<Tooltip
title={statusMsg[monitor?.status ?? undefined]}
disableInteractive
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -8],
},
},
],
},
}}
>
{statusMsg[monitor?.status ?? undefined]}
</Typography>{" "}
Checking every {formatDurationRounded(monitor?.interval)}.
</Typography>
<Box>
<PulseDot
color={statusColor[monitor?.status ?? undefined]}
/>
</Box>
</Tooltip>
<Typography
component="h2"
color={theme.palette.text.secondary}
>
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography
ml={theme.spacing(6)}
lineHeight="20px"
fontSize={12}
position="relative"
color={theme.palette.text.tertiary}
sx={{
"&:before": {
position: "absolute",
content: `""`,
width: 4,
height: 4,
borderRadius: "50%",
backgroundColor: theme.palette.text.tertiary,
opacity: 0.8,
left: -9,
top: "42%",
},
}}
>
Checking every {formatDurationRounded(monitor?.interval)}.
</Typography>
</Stack>
</Box>
{isAdmin && (
<Button
variant="contained"
color="secondary"
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
<Stack
direction="row"
height={34}
sx={{
ml: "auto",
alignSelf: "flex-end",
}}
>
<IconBox
mr={theme.spacing(4)}
onClick={openCertificate}
sx={{
ml: "auto",
alignSelf: "flex-end",
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
stroke: theme.palette.other.icon,
cursor: "pointer",
}}
>
<CertificateIcon />
</IconBox>
<Popover
id="certificate-dropdown"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={closeCertificate}
disableScrollLock
marginThreshold={null}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
slotProps={{
paper: {
sx: {
mt: theme.spacing(4),
py: theme.spacing(2),
px: theme.spacing(4),
width: 140,
backgroundColor: theme.palette.background.accent,
},
},
}}
>
<SettingsIcon /> Configure
</Button>
)}
<Typography fontSize={12} color={theme.palette.text.tertiary}>
Certificate Expiry
</Typography>
<Typography
component="span"
fontSize={13}
color={theme.palette.text.primary}
>
{certificateExpiry}
</Typography>
</Popover>
{isAdmin && (
<Button
variant="contained"
color="secondary"
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
sx={{
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
stroke: theme.palette.text.tertiary,
},
},
}}
>
<SettingsIcon /> Configure
</Button>
)}
</Stack>
</Stack>
<Stack
direction="row"
justifyContent="space-between"
gap={theme.spacing(12)}
flexWrap="wrap"
>
<Stack direction="row" gap={theme.spacing(8)}>
<StatBox
title="Currently up for"
value={formatDuration(monitor?.uptimeDuration)}
/>
<StatBox
title="Last check"
value={`${formatDurationRounded(monitor?.lastChecked)} ago`}
/>
<StatBox title="Incidents" value={monitor?.incidents} />
<StatBox title="Certificate Expiry" value={certificateExpiry} />
<StatBox
title="Latest response time"
value={monitor?.latestResponseTime}
/>
<StatBox
title={
<>
Avg. Response Time{" "}
<Typography component="span">(24-hr)</Typography>
</>
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+$/, "")}
/>
<StatBox
title={
<>
Uptime <Typography component="span">(24-hr)</Typography>
</>
}
value={`${parseFloat(monitor?.uptime24Hours)
.toFixed(2)
.replace(/\.?0+$/, "")}%`}
/>
<StatBox
title={
<>
Uptime <Typography component="span">(30-day)</Typography>
</>
}
value={`${parseFloat(monitor?.uptime30Days)
.toFixed(2)
.replace(/\.?0+$/, "")}%`}
/>
>
<Typography component="h2">active for</Typography>
<Typography>
{splitDuration(monitor?.uptimeDuration)}
</Typography>
</StatBox>
<StatBox>
<Typography component="h2">last check</Typography>
<Typography>
{splitDuration(monitor?.lastChecked)}
<Typography component="span">ago</Typography>
</Typography>
</StatBox>
<StatBox>
<Typography component="h2">last response time</Typography>
<Typography>
{monitor?.latestResponseTime}
<Typography component="span">ms</Typography>
</Typography>
</StatBox>
</Stack>
<Box>
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-end"
gap={theme.spacing(4)}
mb={theme.spacing(8)}
>
<Typography
component="h2"
alignSelf="flex-end"
color={theme.palette.text.secondary}
>
Response Times
<Typography fontSize={12} color={theme.palette.text.tertiary}>
Showing statistics for past{" "}
{dateRange === "day"
? "24 hours"
: dateRange === "week"
? "7 days"
: "30 days"}
.
</Typography>
<ButtonGroup>
<ButtonGroup sx={{ height: 32 }}>
<Button
variant="group"
filled={(dateRange === "day").toString()}
@@ -280,26 +371,152 @@ const DetailsPage = ({ isAdmin }) => {
</Button>
</ButtonGroup>
</Stack>
<Box
p={theme.spacing(10)}
pb={theme.spacing(2)}
backgroundColor={theme.palette.background.main}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
sx={{ height: "250px" }}
>
<MonitorDetailsAreaChart
checks={[...monitor.checks].reverse()}
/>
</Box>
<Stack direction="row" flexWrap="wrap" gap={theme.spacing(8)}>
<ChartBox>
<Stack>
<IconBox>
<UptimeIcon />
</IconBox>
<Typography component="h2">Uptime</Typography>
</Stack>
<Stack justifyContent="space-between">
<Box position="relative">
<Typography>Total Checks</Typography>
<Typography component="span">
{hoveredUptimeData !== null
? hoveredUptimeData.totalChecks
: monitor?.periodTotalChecks}
</Typography>
{hoveredUptimeData !== null &&
hoveredUptimeData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.text.tertiary}
>
{formatDate(new Date(hoveredUptimeData.time), {
month: "short",
year: undefined,
minute: undefined,
hour: dateRange === "day" ? "numeric" : undefined,
})}
</Typography>
)}
</Box>
<Box>
<Typography>Uptime Percentage</Typography>
<Typography component="span">
{hoveredUptimeData !== null
? Math.floor(
hoveredUptimeData.uptimePercentage * 10
) / 10
: Math.floor(monitor?.periodUptime * 10) / 10}
<Typography component="span">%</Typography>
</Typography>
</Box>
</Stack>
<UpBarChart
data={monitor?.aggregateData}
type={dateRange}
onBarHover={setHoveredUptimeData}
/>
</ChartBox>
<ChartBox>
<Stack>
<IconBox>
<IncidentsIcon />
</IconBox>
<Typography component="h2">Incidents</Typography>
</Stack>
<Box position="relative">
<Typography>Total Incidents</Typography>
<Typography component="span">
{hoveredIncidentsData !== null
? hoveredIncidentsData.totalIncidents
: monitor?.periodIncidents}
</Typography>
{hoveredIncidentsData !== null &&
hoveredIncidentsData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.text.tertiary}
>
{formatDate(new Date(hoveredIncidentsData.time), {
month: "short",
year: undefined,
minute: undefined,
hour: dateRange === "day" ? "numeric" : undefined,
})}
</Typography>
)}
</Box>
<DownBarChart
data={monitor?.aggregateData}
type={dateRange}
onBarHover={setHoveredIncidentsData}
/>
</ChartBox>
<ChartBox justifyContent="space-between">
<Stack>
<IconBox>
<AverageResponseIcon />
</IconBox>
<Typography component="h2">
Average Response Time
</Typography>
</Stack>
<ResponseGaugeChart
data={[{ response: monitor?.periodAvgResponseTime }]}
/>
</ChartBox>
<ChartBox
sx={{
"& tspan": {
fontSize: 11,
},
}}
>
<Stack>
<IconBox>
<ResponseTimeIcon />
</IconBox>
<Typography component="h2">Response Times</Typography>
</Stack>
<MonitorDetailsAreaChart
checks={[...monitor.checks].reverse()}
/>
</ChartBox>
<ChartBox
gap={theme.spacing(8)}
sx={{
flex: "100%",
height: "fit-content",
"& nav": { mt: theme.spacing(12) },
}}
>
<Stack mb={theme.spacing(8)}>
<IconBox>
<HistoryIcon />
</IconBox>
<Typography
component="h2"
color={theme.palette.text.secondary}
>
History
</Typography>
</Stack>
<PaginationTable
monitorId={monitorId}
dateRange={dateRange}
/>
</ChartBox>
</Stack>
</Box>
<Stack gap={theme.spacing(8)}>
<Typography component="h2" color={theme.palette.text.secondary}>
History
</Typography>
<PaginationTable monitorId={monitorId} dateRange={dateRange} />
</Stack>
</Stack>
</>
)}

View File

@@ -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,
},
},
}));

View File

@@ -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": {

View File

@@ -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 (
<Stack className="monitors" gap={theme.spacing(12)}>
<Stack className="monitors" gap={theme.spacing(8)}>
{loading ? (
<SkeletonLayout />
) : (
@@ -95,7 +79,7 @@ const Monitors = ({ isAdmin }) => {
{monitorState.monitors?.length !== 0 && (
<>
<Stack
gap={theme.spacing(12)}
gap={theme.spacing(8)}
direction="row"
justifyContent="space-between"
>
@@ -105,8 +89,7 @@ const Monitors = ({ isAdmin }) => {
</Stack>
<Box
flex={1}
px={theme.spacing(16)}
py={theme.spacing(12)}
p={theme.spacing(10)}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
@@ -115,7 +98,7 @@ const Monitors = ({ isAdmin }) => {
<Stack
direction="row"
alignItems="center"
mb={theme.spacing(12)}
mb={theme.spacing(8)}
>
<Typography
component="h2"

View File

@@ -146,6 +146,31 @@ class NetworkService {
);
}
/**
* ************************************
* Gets aggregate stats by monitor ID
* ************************************
*
* @async
* @param {string} authToken - The authorization token to be used in the request header.
* @param {string} monitorId - The ID of the monitor whose certificate expiry is to be retrieved.
* @returns {Promise<AxiosResponse>} 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

View File

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

View File

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

View File

@@ -160,6 +160,7 @@ const Greeting = ({ type = "" }) => {
lineHeight={1}
fontWeight={500}
color={theme.palette.text.primary}
mb={theme.spacing(1)}
>
<Typography
component="span"
@@ -173,11 +174,10 @@ const Greeting = ({ type = "" }) => {
</Typography>
</Typography>
<Typography
sx={{ opacity: 0.8 }}
lineHeight={1}
fontSize={14}
fontWeight={300}
color={theme.palette.text.secondary}
fontWeight={400}
color={theme.palette.text.accent}
>
{append} Heres an overview of your {type} monitors.
</Typography>

View File

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

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 20V13M12 20V10M4 20L4 16M13.4067 5.0275L18.5751 6.96567M10.7988 5.40092L5.20023 9.59983M21.0607 6.43934C21.6464 7.02513 21.6464 7.97487 21.0607 8.56066C20.4749 9.14645 19.5251 9.14645 18.9393 8.56066C18.3536 7.97487 18.3536 7.02513 18.9393 6.43934C19.5251 5.85355 20.4749 5.85355 21.0607 6.43934ZM5.06066 9.43934C5.64645 10.0251 5.64645 10.9749 5.06066 11.5607C4.47487 12.1464 3.52513 12.1464 2.93934 11.5607C2.35355 10.9749 2.35355 10.0251 2.93934 9.43934C3.52513 8.85355 4.47487 8.85355 5.06066 9.43934ZM13.0607 3.43934C13.6464 4.02513 13.6464 4.97487 13.0607 5.56066C12.4749 6.14645 11.5251 6.14645 10.9393 5.56066C10.3536 4.97487 10.3536 4.02513 10.9393 3.43934C11.5251 2.85355 12.4749 2.85355 13.0607 3.43934Z" stroke="black" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 915 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 10H3M21 12.5V8.8C21 7.11984 21 6.27976 20.673 5.63803C20.3854 5.07354 19.9265 4.6146 19.362 4.32698C18.7202 4 17.8802 4 16.2 4H7.8C6.11984 4 5.27976 4 4.63803 4.32698C4.07354 4.6146 3.6146 5.07354 3.32698 5.63803C3 6.27976 3 7.11984 3 8.8V17.2C3 18.8802 3 19.7202 3.32698 20.362C3.6146 20.9265 4.07354 21.3854 4.63803 21.673C5.27976 22 6.11984 22 7.8 22H12M16 2V6M8 2V6M14.5 19L16.5 21L21 16.5" stroke="black" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 594 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.07598 7.48282L7.36402 10.5457C7.58715 10.705 7.69872 10.7847 7.81548 10.8031C7.91821 10.8192 8.02343 10.8029 8.11648 10.7565C8.22223 10.7037 8.30449 10.594 8.46901 10.3747L9.37511 9.16652C9.42164 9.10448 9.4449 9.07347 9.47224 9.04671C9.49652 9.02295 9.52315 9.00173 9.55173 8.98338C9.58392 8.9627 9.61935 8.94696 9.6902 8.91546L13.5588 7.19609C13.7192 7.12482 13.7993 7.08918 13.8598 7.03352C13.9133 6.9843 13.9554 6.924 13.9832 6.85684C14.0146 6.78091 14.0204 6.69336 14.0321 6.51826L14.3154 2.2694M13.5 13.5L16.116 14.6211C16.4195 14.7512 16.5713 14.8163 16.6517 14.9243C16.7222 15.0191 16.7569 15.1358 16.7496 15.2537C16.7413 15.3881 16.6497 15.5255 16.4665 15.8002L15.2375 17.6438C15.1507 17.774 15.1072 17.8391 15.0499 17.8863C14.9991 17.928 14.9406 17.9593 14.8777 17.9784C14.8067 18 14.7284 18 14.5719 18H12.5766C12.3693 18 12.2656 18 12.1774 17.9653C12.0995 17.9347 12.0305 17.885 11.9768 17.8208C11.916 17.7481 11.8832 17.6497 11.8177 17.453L11.1048 15.3144C11.0661 15.1983 11.0468 15.1403 11.0417 15.0814C11.0372 15.0291 11.0409 14.9764 11.0528 14.9253C11.0662 14.8677 11.0935 14.813 11.1482 14.7036L11.6897 13.6206C11.7997 13.4005 11.8547 13.2905 11.9395 13.2222C12.0141 13.162 12.1046 13.1246 12.1999 13.1143C12.3081 13.1027 12.4248 13.1416 12.6582 13.2194L13.5 13.5ZM22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" stroke="black" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 7L14.1314 14.8686C13.7354 15.2646 13.5373 15.4627 13.309 15.5368C13.1082 15.6021 12.8918 15.6021 12.691 15.5368C12.4627 15.4627 12.2646 15.2646 11.8686 14.8686L9.13137 12.1314C8.73535 11.7354 8.53735 11.5373 8.30902 11.4632C8.10817 11.3979 7.89183 11.3979 7.69098 11.4632C7.46265 11.5373 7.26465 11.7354 6.86863 12.1314L2 17M22 7H15M22 7V14" stroke="black" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 541 B

View File

@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 17L17 7M17 7H7M17 7V17" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 17L17 7M17 7H7M17 7V17" stroke="#667085" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 224 B

After

Width:  |  Height:  |  Size: 224 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 7H4.6C4.03995 7 3.75992 7 3.54601 7.10899C3.35785 7.20487 3.20487 7.35785 3.10899 7.54601C3 7.75992 3 8.03995 3 8.6V19.4C3 19.9601 3 20.2401 3.10899 20.454C3.20487 20.6422 3.35785 20.7951 3.54601 20.891C3.75992 21 4.03995 21 4.6 21H9M9 21H15M9 21L9 4.6C9 4.03995 9 3.75992 9.10899 3.54601C9.20487 3.35785 9.35785 3.20487 9.54601 3.10899C9.75992 3 10.0399 3 10.6 3L13.4 3C13.9601 3 14.2401 3 14.454 3.10899C14.6422 3.20487 14.7951 3.35785 14.891 3.54601C15 3.75992 15 4.03995 15 4.6V21M15 11H19.4C19.9601 11 20.2401 11 20.454 11.109C20.6422 11.2049 20.7951 11.3578 20.891 11.546C21 11.7599 21 12.0399 21 12.6V19.4C21 19.9601 21 20.2401 20.891 20.454C20.7951 20.6422 20.6422 20.7951 20.454 20.891C20.2401 21 19.9601 21 19.4 21H15" stroke="black" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 927 B

View File

@@ -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;