Merge pull request #1648 from bluewave-labs/fix/fe/uptime-details-refactor

fix: fe/uptime details refactor
This commit is contained in:
Alexander Holliday
2025-01-28 07:17:26 -08:00
committed by GitHub
33 changed files with 1010 additions and 821 deletions
+23
View File
@@ -0,0 +1,23 @@
import PropTypes from "prop-types";
const Dot = ({ color = "gray", size = "4px" }) => {
return (
<span
style={{
content: '""',
width: size,
height: size,
borderRadius: "50%",
backgroundColor: color,
opacity: 0.8,
}}
/>
);
};
Dot.propTypes = {
color: PropTypes.string,
size: PropTypes.string,
};
export default Dot;
@@ -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.
@@ -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 <SkeletonLayout />;
}
return (
<Stack
direction="row"
flexWrap="wrap"
gap={theme.spacing(8)}
>
<ChartBox
icon={<UptimeIcon />}
header="Uptime"
>
<Stack justifyContent="space-between">
<Box position="relative">
<Typography>Total Checks</Typography>
<Typography component="span">
{hoveredUptimeData !== null
? hoveredUptimeData.totalChecks
: (monitor?.groupedUpChecks?.reduce((count, checkGroup) => {
return count + checkGroup.totalChecks;
}, 0) ?? 0)}
</Typography>
{hoveredUptimeData !== null && hoveredUptimeData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(hoveredUptimeData._id, dateFormat, uiTimezone)}
</Typography>
)}
</Box>
<Box>
<Typography>
{hoveredUptimeData !== null ? "Avg Response Time" : "Uptime Percentage"}
</Typography>
<Typography component="span">
{hoveredUptimeData !== null
? Math.floor(hoveredUptimeData?.avgResponseTime ?? 0)
: Math.floor(
((monitor?.upChecks?.totalChecks ?? 0) /
(monitor?.totalChecks ?? 1)) *
100
)}
<Typography component="span">
{hoveredUptimeData !== null ? " ms" : " %"}
</Typography>
</Typography>
</Box>
</Stack>
<UpBarChart
monitor={monitor}
type={dateRange}
onBarHover={setHoveredUptimeData}
/>
</ChartBox>
<ChartBox
icon={<IncidentsIcon />}
header="Incidents"
>
<Box position="relative">
<Typography component="span">
{hoveredIncidentsData !== null
? hoveredIncidentsData.totalChecks
: (monitor?.groupedDownChecks?.reduce((count, checkGroup) => {
return count + checkGroup.totalChecks;
}, 0) ?? 0)}
</Typography>
{hoveredIncidentsData !== null && hoveredIncidentsData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(hoveredIncidentsData._id, dateFormat, uiTimezone)}
</Typography>
)}
</Box>
<DownBarChart
monitor={monitor}
type={dateRange}
onBarHover={setHoveredIncidentsData}
/>
</ChartBox>
<ChartBox
icon={<AverageResponseIcon />}
header="Average Response Time"
>
<ResponseGaugeChart avgResponseTime={monitor.avgResponseTime ?? 0} />
</ChartBox>
</Stack>
);
};
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,
};
@@ -0,0 +1,30 @@
import { Skeleton, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
direction="row"
gap={theme.spacing(8)}
>
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -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 (
<Stack
sx={{
justifyContent: "space-between",
flex: "1 30%",
gap: theme.spacing(8),
height,
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",
},
}}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
>
<IconBox>{icon}</IconBox>
<Typography component="h2">{header}</Typography>
</Stack>
{children}
</Stack>
);
};
export default ChartBox;
ChartBox.propTypes = {
children: PropTypes.node,
icon: PropTypes.node.isRequired,
header: PropTypes.string.isRequired,
height: PropTypes.string,
};
@@ -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,
};
@@ -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 (
<Cell
key={`cell-${entry.time}`}
@@ -0,0 +1,31 @@
import ChartBox from "./ChartBox";
import MonitorDetailsAreaChart from "../../../../../Components/Charts/MonitorDetailsAreaChart";
import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react";
import SkeletonLayout from "./ResponseTimeChartSkeleton";
import PropTypes from "prop-types";
const ResponseTImeChart = ({ shouldRender = true, monitor, dateRange }) => {
if (!shouldRender) {
return <SkeletonLayout />;
}
return (
<ChartBox
icon={<ResponseTimeIcon />}
header="Response Times"
>
<MonitorDetailsAreaChart
checks={monitor.groupedChecks ?? []}
dateRange={dateRange}
/>
</ChartBox>
);
};
ResponseTImeChart.propTypes = {
shouldRender: PropTypes.bool,
monitor: PropTypes.object,
dateRange: PropTypes.string,
};
export default ResponseTImeChart;
@@ -0,0 +1,12 @@
import { Skeleton } from "@mui/material";
const ResponseTimeChartSkeleton = () => {
return (
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
);
};
export default ResponseTimeChartSkeleton;
@@ -28,7 +28,7 @@ const UpBarChart = memo(({ monitor, type, onBarHover }) => {
<BarChart
width="100%"
height="100%"
data={monitor.groupedUpChecks}
data={monitor?.groupedUpChecks}
onMouseEnter={() => {
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 (
<Cell
@@ -0,0 +1,40 @@
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";
import PropTypes from "prop-types";
const ConfigButton = ({ shouldRender, monitorId }) => {
const theme = useTheme();
const navigate = useNavigate();
if (!shouldRender) return null;
return (
<Box alignSelf="flex-end">
<Button
variant="contained"
color="secondary"
onClick={() => navigate(`/uptime/configure/${monitorId}`)}
sx={{
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
/* Should always be contrastText for the button color */
stroke: theme.palette.secondary.contrastText,
},
},
}}
>
<SettingsIcon /> Configure
</Button>
</Box>
);
};
ConfigButton.propTypes = {
shouldRender: PropTypes.bool,
monitorId: PropTypes.string,
};
export default ConfigButton;
@@ -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 <SkeletonLayout />;
}
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Stack>
<Typography variant="h1">{monitor.name}</Typography>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(2)}
>
<PulseDot color={statusColor[determineState(monitor)]} />
<Typography variant="h2">
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Dot />
<Typography>
Checking every {formatDurationRounded(monitor?.interval)}.
</Typography>
</Stack>
</Stack>
<ConfigButton
shouldRender={isAdmin}
monitorId={monitor._id}
/>
</Stack>
);
};
MonitorHeader.propTypes = {
shouldRender: PropTypes.bool,
isAdmin: PropTypes.bool,
monitor: PropTypes.object,
};
export default MonitorHeader;
@@ -0,0 +1,23 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Skeleton
height={40}
variant="rounded"
width="15%"
/>
<Skeleton
height={40}
variant="rounded"
width="15%"
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -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 <SkeletonLayout />;
}
const headers = [
{
id: "status",
content: "Status",
render: (row) => {
const status = row.status === true ? "up" : "down";
return (
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
);
},
},
{
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 (
<ChartBox
icon={<HistoryIcon />}
header="Response Times"
height="100%"
>
<Table
headers={headers}
data={checks}
/>
<TablePagination
page={page}
handleChangePage={setPage}
rowsPerPage={rowsPerPage}
handleChangeRowsPerPage={setRowsPerPage}
itemCount={checksCount}
/>
</ChartBox>
);
};
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;
@@ -0,0 +1,13 @@
import { Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
);
};
export default SkeletonLayout;
@@ -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 <SkeletonLayout />;
}
const { time: streakTime, units: streakUnits } = getHumanReadableDuration(
monitor?.uptimeStreak
);
const { time: lastCheckTime, units: lastCheckUnits } = getHumanReadableDuration(
monitor?.timeSinceLastCheck
);
return (
<Stack
direction="row"
gap={theme.spacing(8)}
>
<StatBox
gradient={true}
status={determineState(monitor)}
heading={"active for"}
subHeading={
<>
{streakTime}
<Typography component="span">{streakUnits}</Typography>
</>
}
/>
<StatBox
heading="last check"
subHeading={
<>
{lastCheckTime}
<Typography component="span">{lastCheckUnits}</Typography>
<Typography component="span">{"ago"}</Typography>
</>
}
/>
<StatBox
heading="last response time"
subHeading={
<>
{monitor?.latestResponseTime}
<Typography component="span">{"ms"}</Typography>
</>
}
/>
<StatBox
heading="certificate expiry"
subHeading={
<Typography
component="span"
fontSize={13}
color={theme.palette.primary.contrastText}
>
{certificateExpiry}
</Typography>
}
/>
</Stack>
);
};
StatusBoxes.propTypes = {
shouldRender: PropTypes.bool,
monitor: PropTypes.object,
certificateExpiry: PropTypes.string,
};
export default StatusBoxes;
@@ -0,0 +1,30 @@
import { Stack, Skeleton } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(4)}
>
<Skeleton
variant="rounded"
width="33%"
height={50}
/>
<Skeleton
variant="rounded"
width="33%"
height={50}
/>
<Skeleton
variant="rounded"
width="33%"
height={50}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -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 <SkeletonLayout />;
}
return (
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-end"
gap={theme.spacing(4)}
mb={theme.spacing(8)}
>
<Typography variant="body2">
Showing statistics for past{" "}
{dateRange === "day" ? "24 hours" : dateRange === "week" ? "7 days" : "30 days"}.
</Typography>
<ButtonGroup sx={{ height: 32 }}>
<Button
variant="group"
filled={(dateRange === "day").toString()}
onClick={() => setDateRange("day")}
>
Day
</Button>
<Button
variant="group"
filled={(dateRange === "week").toString()}
onClick={() => setDateRange("week")}
>
Week
</Button>
<Button
variant="group"
filled={(dateRange === "month").toString()}
onClick={() => setDateRange("month")}
>
Month
</Button>
</ButtonGroup>
</Stack>
);
};
TimeFramePicker.propTypes = {
shouldRender: PropTypes.bool,
dateRange: PropTypes.string,
setDateRange: PropTypes.func,
};
export default TimeFramePicker;
@@ -0,0 +1,23 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Skeleton
variant="rounded"
width="20%"
height={34}
/>
<Skeleton
variant="rounded"
width="20%"
height={34}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -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;
@@ -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;
@@ -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;
@@ -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 = (
<Pagination
count={Math.ceil(checksCount / paginationController.rowsPerPage)}
page={paginationController.page + 1} //0-indexed
onChange={handlePageChange}
shape="rounded"
renderItem={(item) => (
<PaginationItem
slots={{
previous: ArrowBackRoundedIcon,
next: ArrowForwardRoundedIcon,
}}
{...item}
/>
)}
/>
);
}
return (
<>
<TableContainer component={Paper}>
<Table
sx={{
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
paddingLeft: theme.spacing(8),
},
"& :is(th)": {
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
fontSize: "12px",
},
"& :is(td)": {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastTextSecondary,
},
}}
>
<TableHead>
<TableRow>
<TableCell>Status</TableCell>
<TableCell>Date & Time</TableCell>
<TableCell>Status Code</TableCell>
<TableCell>Message</TableCell>
</TableRow>
</TableHead>
<TableBody>
{checks.map((check) => {
const status = check.status === true ? "up" : "down";
return (
<TableRow key={check._id}>
<TableCell>
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
</TableCell>
<TableCell>
{formatDateWithTz(
check.createdAt,
"ddd, MMMM D, YYYY, HH:mm A",
uiTimezone
)}
</TableCell>
<TableCell>{check.statusCode ? check.statusCode : "N/A"}</TableCell>
<TableCell>{check.message}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
{paginationComponent}
</>
);
};
PaginationTable.propTypes = {
monitorId: PropTypes.string.isRequired,
dateRange: PropTypes.string.isRequired,
};
export default PaginationTable;
+117 -465
View File
@@ -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}
<Typography component="span">{format}</Typography>
</>
);
};
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 (
<Box className="monitor-details">
{loading ? (
<SkeletonLayout />
) : (
<>
<Breadcrumbs list={BREADCRUMBS} />
<Stack
gap={theme.spacing(10)}
mt={theme.spacing(10)}
>
<Stack
direction="row"
gap={theme.spacing(2)}
>
<Box>
<Typography
component="h1"
variant="h1"
>
{monitor.name}
</Typography>
<Stack
direction="row"
alignItems="center"
height="fit-content"
gap={theme.spacing(2)}
>
{/* TODO there is a tooltip at BarChart component. Wrap the Tooltip on our own component */}
<Tooltip
title={statusMsg[determineState(monitor)]}
disableInteractive
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -8],
},
},
],
sx: {
"& .MuiTooltip-tooltip": {
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
px: theme.spacing(4),
py: theme.spacing(3),
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shape.boxShadow,
/* TODO Font size should point to theme */
fontSize: 12,
fontWeight: 500,
},
},
},
}}
>
<Box>
<PulseDot color={statusColor[determineState(monitor)]} />
</Box>
</Tooltip>
<Typography
component="h2"
variant="h2"
>
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography
position="relative"
variant="body2"
mt={theme.spacing(1)}
ml={theme.spacing(6)}
sx={{
"&:before": {
position: "absolute",
content: `""`,
width: 4,
height: 4,
borderRadius: "50%",
backgroundColor: theme.palette.primary.contrastTextTertiary,
opacity: 0.8,
left: -9,
top: "50%",
transform: "translateY(-50%)",
},
}}
>
{/* Checking every {formatDurationRounded(monitor?.interval)}. */}
</Typography>
</Stack>
</Box>
<Stack
direction="row"
height={34}
sx={{
ml: "auto",
alignSelf: "flex-end",
}}
>
{isAdmin && (
<Button
variant="contained"
color="secondary"
onClick={() => navigate(`/uptime/configure/${monitorId}`)}
sx={{
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
/* Should always be contrastText for the button color */
stroke: theme.palette.secondary.contrastText,
},
},
}}
>
<SettingsIcon /> Configure
</Button>
)}
</Stack>
</Stack>
<Stack
direction="row"
gap={theme.spacing(8)}
>
<StatBox
/* sx={getStatusStyles(determineState(monitor))} */
/* statusStyles[determineState(monitor)] */
gradient={true}
status={determineState(monitor)}
heading={"active for"}
subHeading={splitDuration(monitor?.uptimeStreak)}
/>
<StatBox
heading="last check"
subHeading={splitDuration(monitor?.timeSinceLastCheck)}
/>
<StatBox
heading="last response time"
subHeading={
<>
{monitor?.latestResponseTime}
<Typography component="span">{"ms"}</Typography>
</>
}
/>
<StatBox
heading="certificate expiry"
subHeading={
<Typography
component="span"
fontSize={13}
color={theme.palette.primary.contrastText}
>
{certificateExpiry}
</Typography>
}
/>
</Stack>
<Box>
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-end"
gap={theme.spacing(4)}
mb={theme.spacing(8)}
>
<Typography variant="body2">
Showing statistics for past{" "}
{dateRange === "day"
? "24 hours"
: dateRange === "week"
? "7 days"
: "30 days"}
.
</Typography>
<ButtonGroup sx={{ height: 32 }}>
<Button
variant="group"
filled={(dateRange === "day").toString()}
onClick={() => setDateRange("day")}
>
Day
</Button>
<Button
variant="group"
filled={(dateRange === "week").toString()}
onClick={() => setDateRange("week")}
>
Week
</Button>
<Button
variant="group"
filled={(dateRange === "month").toString()}
onClick={() => setDateRange("month")}
>
Month
</Button>
</ButtonGroup>
</Stack>
<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?.groupedUpChecks?.reduce((count, checkGroup) => {
return count + checkGroup.totalChecks;
}, 0) ?? 0)}
</Typography>
{hoveredUptimeData !== null && hoveredUptimeData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(
hoveredUptimeData._id,
dateFormat,
uiTimezone
)}
</Typography>
)}
</Box>
<Box>
<Typography>
{hoveredUptimeData !== null
? "Avg Response Time"
: "Uptime Percentage"}
</Typography>
<Typography component="span">
{hoveredUptimeData !== null
? Math.floor(hoveredUptimeData?.avgResponseTime ?? 0)
: Math.floor(
((monitor?.upChecks?.totalChecks ?? 0) /
(monitor?.totalChecks ?? 1)) *
100
)}
<Typography component="span">
{hoveredUptimeData !== null ? " ms" : " %"}
</Typography>
</Typography>
</Box>
</Stack>
<UpBarChart
monitor={monitor}
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.totalChecks
: (monitor?.groupedDownChecks?.reduce((count, checkGroup) => {
return count + checkGroup.totalChecks;
}, 0) ?? 0)}
</Typography>
{hoveredIncidentsData !== null &&
hoveredIncidentsData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(
hoveredIncidentsData._id,
dateFormat,
uiTimezone
)}
</Typography>
)}
</Box>
<DownBarChart
monitor={monitor}
type={dateRange}
onBarHover={setHoveredIncidentsData}
/>
</ChartBox>
<ChartBox justifyContent="space-between">
<Stack>
<IconBox>
<AverageResponseIcon />
</IconBox>
<Typography component="h2">Average Response Time</Typography>
</Stack>
<ResponseGaugeChart avgResponseTime={monitor.avgResponseTime ?? 0} />
</ChartBox>
<ChartBox sx={{ padding: 0 }}>
<Stack
pt={theme.spacing(8)}
pl={theme.spacing(8)}
>
<IconBox>
<ResponseTimeIcon />
</IconBox>
<Typography component="h2">Response Times</Typography>
</Stack>
<MonitorDetailsAreaChart
checks={monitor.groupedChecks ?? []}
dateRange={dateRange}
/>
</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.primary.contrastTextSecondary}
>
History
</Typography>
</Stack>
<PaginationTable
monitorId={monitorId}
dateRange={dateRange}
/>
</ChartBox>
</Stack>
</Box>
</Stack>
</>
)}
</Box>
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorHeader
isAdmin={isAdmin}
shouldRender={!monitorIsLoading}
monitor={monitor}
/>
<StatusBoxes
shouldRender={!monitorIsLoading}
monitor={monitor}
certificateExpiry={certificateExpiry}
/>
<TimeFramePicker
shouldRender={!monitorIsLoading}
dateRange={dateRange}
setDateRange={setDateRange}
/>
<ChartBoxes
shouldRender={!monitorIsLoading}
monitor={monitor}
uiTimezone={uiTimezone}
dateRange={dateRange}
dateFormat={dateFormat}
hoveredUptimeData={hoveredUptimeData}
setHoveredUptimeData={setHoveredUptimeData}
hoveredIncidentsData={hoveredIncidentsData}
setHoveredIncidentsData={setHoveredIncidentsData}
/>
<ResponseTimeChart
shouldRender={!monitorIsLoading}
monitor={monitor}
dateRange={dateRange}
/>
<ResponseTable
shouldRender={!checksAreLoading}
checks={checks}
uiTimezone={uiTimezone}
page={page}
setPage={handlePageChange}
rowsPerPage={rowsPerPage}
setRowsPerPage={handleChangeRowsPerPage}
checksCount={checksCount}
/>
</Stack>
);
};
DetailsPage.propTypes = {
isAdmin: PropTypes.bool,
};
export default DetailsPage;
export default UptimeDetails;
@@ -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 (
<>
<Skeleton
variant="rounded"
width="20%"
height={34}
/>
<Stack
gap={theme.spacing(20)}
mt={theme.spacing(6)}
>
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(4)}
>
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
/>
<Box width="80%">
<Skeleton
variant="rounded"
width="50%"
height={24}
/>
<Skeleton
variant="rounded"
width="50%"
height={18}
sx={{ mt: theme.spacing(4) }}
/>
</Box>
<Skeleton
variant="rounded"
width="20%"
height={34}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
<Stack
direction="row"
justifyContent="space-between"
gap={theme.spacing(12)}
>
<Skeleton
variant="rounded"
width="100%"
height={80}
/>
<Skeleton
variant="rounded"
width="100%"
height={80}
/>
<Skeleton
variant="rounded"
width="100%"
height={80}
/>
</Stack>
<Box>
<Stack
direction="row"
justifyContent="space-between"
mb={theme.spacing(8)}
>
<Skeleton
variant="rounded"
width="20%"
height={24}
sx={{ alignSelf: "flex-end" }}
/>
<Skeleton
variant="rounded"
width="20%"
height={34}
/>
</Stack>
<Box sx={{ height: "200px" }}>
<Skeleton
variant="rounded"
width="100%"
height="100%"
/>
</Box>
</Box>
<Stack gap={theme.spacing(8)}>
<Skeleton
variant="rounded"
width="20%"
height={24}
/>
<Skeleton
variant="rounded"
width="100%"
height={200}
/>
<Skeleton
variant="rounded"
width="100%"
height={50}
/>
</Stack>
</Stack>
</>
);
};
export default SkeletonLayout;
@@ -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",
},
}));
@@ -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 && (
<>
<span
style={{
content: '""',
width: "4px",
height: "4px",
borderRadius: "50%",
backgroundColor: "gray",
opacity: 0.8,
}}
/>
<Dot />
<Typography
component="span"
sx={{
+3 -3
View File
@@ -13,7 +13,7 @@ import { useState, useCallback } from "react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import useMonitorFetch from "./Hooks/useMonitorFetch";
import useMonitorsFetch from "./Hooks/useMonitorsFetch";
import { useSelector, useDispatch } from "react-redux";
import { setRowsPerPage } from "../../../Features/UI/uiSlice";
import PropTypes from "prop-types";
@@ -86,7 +86,7 @@ const UptimeMonitors = () => {
const teamId = user.teamId;
const { monitorsAreLoading, monitors, filteredMonitors, monitorsSummary } =
useMonitorFetch({
useMonitorsFetch({
authToken,
teamId,
limit: 25,
@@ -102,7 +102,7 @@ const UptimeMonitors = () => {
return (
<Stack
className="monitors"
gap={theme.spacing(8)}
gap={theme.spacing(10)}
>
<Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorButton shouldRender={true} />
+5 -4
View File
@@ -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 = () => {
/>
<Route
path="/uptime"
element={<Monitors />}
element={<Uptime />}
/>
<Route
@@ -55,7 +56,7 @@ const Routes = () => {
/>
<Route
path="/uptime/:monitorId/"
element={<Details />}
element={<UptimeDetails />}
/>
<Route
path="/uptime/configure/:monitorId/"
+19
View File
@@ -1,4 +1,5 @@
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import customParseFormat from "dayjs/plugin/customParseFormat";
@@ -6,6 +7,7 @@ import customParseFormat from "dayjs/plugin/customParseFormat";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(customParseFormat);
dayjs.extend(duration);
export const MS_PER_SECOND = 1000;
export const MS_PER_MINUTE = 60 * MS_PER_SECOND;
@@ -75,6 +77,23 @@ export const formatDurationSplit = (ms) => {
: { 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",