mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-18 23:48:43 -05:00
finish refactor
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useSelector } from "react-redux";
|
||||
import { formatDateWithTz } from "../../../../Utils/timeUtils";
|
||||
|
||||
const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => {
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
const dateFormat = type === "day" ? "MMM D, h:mm A" : "MMM D";
|
||||
|
||||
return (
|
||||
<>
|
||||
<text
|
||||
x={x}
|
||||
y={height}
|
||||
dy={-3}
|
||||
textAnchor="start"
|
||||
fontSize={11}
|
||||
>
|
||||
{formatDateWithTz(firstDataPoint._id, dateFormat, uiTimezone)}
|
||||
</text>
|
||||
<text
|
||||
x={width}
|
||||
y={height}
|
||||
dy={-3}
|
||||
textAnchor="end"
|
||||
fontSize={11}
|
||||
>
|
||||
{formatDateWithTz(lastDataPoint._id, dateFormat, uiTimezone)}
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
type: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CustomLabels;
|
||||
@@ -1,92 +0,0 @@
|
||||
import { memo, useState } from "react";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
|
||||
import PropTypes from "prop-types";
|
||||
import CustomLabels from "./CustomLabels";
|
||||
|
||||
const DownBarChart = memo(({ monitor, type, onBarHover }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [chartHovered, setChartHovered] = useState(false);
|
||||
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={250}
|
||||
height={155}
|
||||
>
|
||||
<BarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={monitor.groupedDownChecks}
|
||||
onMouseEnter={() => {
|
||||
setChartHovered(true);
|
||||
onBarHover({ time: null, totalChecks: 0 });
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setChartHovered(false);
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover(null);
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<CustomLabels
|
||||
x={0}
|
||||
y={0}
|
||||
width="100%"
|
||||
height="100%"
|
||||
firstDataPoint={monitor.groupedDownChecks?.[0] ?? {}}
|
||||
lastDataPoint={
|
||||
monitor.groupedDownChecks?.[monitor.groupedDownChecks.length - 1] ?? {}
|
||||
}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="avgResponseTime"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{monitor.groupedDownChecks.map((entry, index) => {
|
||||
return (
|
||||
<Cell
|
||||
key={`cell-${entry.time}`}
|
||||
fill={
|
||||
hoveredBarIndex === index
|
||||
? theme.palette.error.main
|
||||
: chartHovered
|
||||
? theme.palette.error.light // CAIO_REVIEW
|
||||
: theme.palette.error.main
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
setHoveredBarIndex(index);
|
||||
onBarHover(entry);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover({ time: null, totalChecks: 0 });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
|
||||
DownBarChart.displayName = "DownBarChart";
|
||||
DownBarChart.propTypes = {
|
||||
monitor: PropTypes.shape({
|
||||
groupedDownChecks: PropTypes.arrayOf(PropTypes.object),
|
||||
}),
|
||||
type: PropTypes.string,
|
||||
onBarHover: PropTypes.func,
|
||||
};
|
||||
export default DownBarChart;
|
||||
@@ -1,107 +0,0 @@
|
||||
import { memo, useState } from "react";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
|
||||
import PropTypes from "prop-types";
|
||||
import CustomLabels from "./CustomLabels";
|
||||
|
||||
const getThemeColor = (responseTime) => {
|
||||
if (responseTime < 200) {
|
||||
return "success";
|
||||
} else if (responseTime < 300) {
|
||||
return "warning";
|
||||
} else {
|
||||
return "error";
|
||||
}
|
||||
};
|
||||
|
||||
const UpBarChart = memo(({ monitor, type, onBarHover }) => {
|
||||
const theme = useTheme();
|
||||
const [chartHovered, setChartHovered] = useState(false);
|
||||
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={210}
|
||||
height={155}
|
||||
>
|
||||
<BarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={monitor.groupedUpChecks}
|
||||
onMouseEnter={() => {
|
||||
setChartHovered(true);
|
||||
onBarHover({ time: null, totalChecks: 0, avgResponseTime: 0 });
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setChartHovered(false);
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover(null);
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<CustomLabels
|
||||
x={0}
|
||||
y={0}
|
||||
width="100%"
|
||||
height="100%"
|
||||
firstDataPoint={monitor.groupedUpChecks[0]}
|
||||
lastDataPoint={monitor.groupedUpChecks[monitor.groupedUpChecks.length - 1]}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="avgResponseTime"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{monitor.groupedUpChecks.map((entry, index) => {
|
||||
const themeColor = getThemeColor(entry.avgResponseTime);
|
||||
return (
|
||||
<Cell
|
||||
key={`cell-${entry.time}`}
|
||||
fill={
|
||||
hoveredBarIndex === index
|
||||
? theme.palette[themeColor].main
|
||||
: chartHovered
|
||||
? theme.palette[themeColor].light // CAIO_REVIEW
|
||||
: theme.palette[themeColor].main
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
setHoveredBarIndex(index);
|
||||
onBarHover(entry);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover({
|
||||
time: null,
|
||||
totalChecks: 0,
|
||||
groupUptimePercentage: 0,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
|
||||
// Add display name for the component
|
||||
UpBarChart.displayName = "UpBarChart";
|
||||
|
||||
// Validate props using PropTypes
|
||||
UpBarChart.propTypes = {
|
||||
monitor: PropTypes.shape({
|
||||
groupedUpChecks: PropTypes.array,
|
||||
}),
|
||||
type: PropTypes.string,
|
||||
onBarHover: PropTypes.func,
|
||||
};
|
||||
export default UpBarChart;
|
||||
+7
-1
@@ -7,13 +7,14 @@ import AverageResponseIcon from "../../../../../assets/icons/average-response-ic
|
||||
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,
|
||||
@@ -24,6 +25,11 @@ const ChartBoxes = ({
|
||||
setHoveredIncidentsData,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (!shouldRender) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
@@ -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;
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import IconBox from "../../../../../Components/IconBox";
|
||||
import PropTypes from "prop-types";
|
||||
const ChartBox = ({ children, icon, header }) => {
|
||||
const ChartBox = ({ children, icon, header, height = "300px" }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
@@ -10,7 +10,7 @@ const ChartBox = ({ children, icon, header }) => {
|
||||
justifyContent: "space-between",
|
||||
flex: "1 30%",
|
||||
gap: theme.spacing(8),
|
||||
height: 300,
|
||||
height,
|
||||
minWidth: 250,
|
||||
padding: theme.spacing(8),
|
||||
border: 1,
|
||||
+6
-1
@@ -1,7 +1,12 @@
|
||||
import ChartBox from "./ChartBox";
|
||||
import MonitorDetailsAreaChart from "../../../../../Components/Charts/MonitorDetailsAreaChart";
|
||||
import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react";
|
||||
const ResponseTImeChart = ({ monitor, dateRange }) => {
|
||||
import SkeletonLayout from "./ResponseTimeChartSkeleton";
|
||||
const ResponseTImeChart = ({ shouldRender = true, monitor, dateRange }) => {
|
||||
if (!shouldRender) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChartBox
|
||||
icon={<ResponseTimeIcon />}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Skeleton } from "@mui/material";
|
||||
const ResponseTimeChartSkeleton = () => {
|
||||
return (
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={300}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponseTimeChartSkeleton;
|
||||
+14
-2
@@ -5,10 +5,16 @@ 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 = ({ monitor }) => {
|
||||
const MonitorHeader = ({ shouldRender = true, isAdmin, monitor }) => {
|
||||
const theme = useTheme();
|
||||
const { statusColor, statusMsg, determineState } = useUtils();
|
||||
console.log(shouldRender);
|
||||
if (!shouldRender) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
@@ -33,11 +39,17 @@ const MonitorHeader = ({ monitor }) => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
<ConfigButton
|
||||
shouldRender={true}
|
||||
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,88 @@
|
||||
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 = {
|
||||
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;
|
||||
+6
-2
@@ -1,13 +1,16 @@
|
||||
// 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";
|
||||
|
||||
const StatusBoxes = ({ monitor, certificateExpiry }) => {
|
||||
const StatusBoxes = ({ shouldRender, monitor, certificateExpiry }) => {
|
||||
if (!shouldRender) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
const theme = useTheme();
|
||||
const { time: streakTime, units: streakUnits } = getHumanReadableDuration(
|
||||
monitor?.uptimeStreak
|
||||
@@ -18,6 +21,7 @@ const StatusBoxes = ({ monitor, certificateExpiry }) => {
|
||||
);
|
||||
|
||||
const { determineState } = useUtils();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
@@ -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;
|
||||
+8
-1
@@ -1,7 +1,14 @@
|
||||
import { Stack, Typography, Button, ButtonGroup } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
const TimeFramePicker = ({ dateRange, setDateRange }) => {
|
||||
import SkeletonLayout from "./skeleton";
|
||||
|
||||
const TimeFramePicker = ({ shouldRender = true, dateRange, setDateRange }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (!shouldRender) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
@@ -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,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;
|
||||
@@ -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;
|
||||
@@ -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,117 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { ResponsiveContainer, RadialBarChart, RadialBar, Cell } from "recharts";
|
||||
|
||||
const ResponseGaugeChart = ({ avgResponseTime }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
let max = 1000; // max ms
|
||||
|
||||
const data = [
|
||||
{ response: max, fill: "transparent", background: false },
|
||||
{ response: avgResponseTime, background: true },
|
||||
];
|
||||
let responseTime = Math.floor(avgResponseTime);
|
||||
let responseProps =
|
||||
responseTime <= 200
|
||||
? {
|
||||
category: "Excellent",
|
||||
main: theme.palette.success.main,
|
||||
bg: theme.palette.success.contrastText,
|
||||
}
|
||||
: responseTime <= 500
|
||||
? {
|
||||
category: "Fair",
|
||||
main: theme.palette.success.main,
|
||||
bg: theme.palette.success.contrastText,
|
||||
}
|
||||
: responseTime <= 600
|
||||
? {
|
||||
category: "Acceptable",
|
||||
main: theme.palette.warning.main,
|
||||
bg: theme.palette.warning.lowContrast,
|
||||
}
|
||||
: {
|
||||
category: "Poor",
|
||||
main: theme.palette.error.main,
|
||||
bg: theme.palette.error.contrastText,
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={210}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
ResponseGaugeChart.propTypes = {
|
||||
avgResponseTime: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default ResponseGaugeChart;
|
||||
@@ -1,86 +0,0 @@
|
||||
// Components
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import MonitorHeader from "./Components/MonitorHeader";
|
||||
import StatusBoxes from "./Components/StatusBoxes";
|
||||
import TimeFramePicker from "./Components/TimeFramePicker";
|
||||
import ChartBoxes from "./Components/ChartBoxes";
|
||||
import ResponseTimeChart from "./Components/Charts/ResponseTimeChart";
|
||||
// MUI Components
|
||||
import { Stack } from "@mui/material";
|
||||
|
||||
// Utils
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import useMonitorFetch from "./Hooks/useMonitorFetch";
|
||||
import useCertificateFetch from "./Hooks/useCertificateFetch";
|
||||
// Constants
|
||||
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);
|
||||
|
||||
// Local state
|
||||
const [dateRange, setDateRange] = useState("day");
|
||||
const [hoveredUptimeData, setHoveredUptimeData] = useState(null);
|
||||
const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null);
|
||||
|
||||
// Utils
|
||||
const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D";
|
||||
const { monitorId } = useParams();
|
||||
const theme = useTheme();
|
||||
|
||||
const { monitor, monitorIsLoading } = useMonitorFetch({
|
||||
authToken,
|
||||
monitorId,
|
||||
dateRange,
|
||||
});
|
||||
|
||||
const { certificateExpiry, certificateIsLoading } = useCertificateFetch({
|
||||
monitor,
|
||||
authToken,
|
||||
monitorId,
|
||||
certificateDateFormat,
|
||||
uiTimezone,
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorHeader monitor={monitor} />
|
||||
<StatusBoxes
|
||||
monitor={monitor}
|
||||
certificateExpiry={certificateExpiry}
|
||||
/>
|
||||
<TimeFramePicker
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
<ChartBoxes
|
||||
monitor={monitor}
|
||||
uiTimezone={uiTimezone}
|
||||
dateRange={dateRange}
|
||||
dateFormat={dateFormat}
|
||||
hoveredUptimeData={hoveredUptimeData}
|
||||
setHoveredUptimeData={setHoveredUptimeData}
|
||||
hoveredIncidentsData={hoveredIncidentsData}
|
||||
setHoveredIncidentsData={setHoveredIncidentsData}
|
||||
/>
|
||||
<ResponseTimeChart
|
||||
monitor={monitor}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default UptimeDetails;
|
||||
@@ -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,9 +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 UptimeDetails from "../Pages/Uptime/NewDetails";
|
||||
import Maintenance from "../Pages/Maintenance";
|
||||
import Configure from "../Pages/Uptime/Configure";
|
||||
import PageSpeed from "../Pages/PageSpeed";
|
||||
@@ -48,7 +47,7 @@ const Routes = () => {
|
||||
/>
|
||||
<Route
|
||||
path="/uptime"
|
||||
element={<Monitors />}
|
||||
element={<Uptime />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
Reference in New Issue
Block a user