mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-01 06:00:24 -05:00
Merge pull request #798 from bluewave-labs/feat/monitor-pagination
Feat/monitor pagination
This commit is contained in:
@@ -15,14 +15,12 @@ import {
|
||||
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { networkService } from "../../../main";
|
||||
import { StatusLabel } from "../../../Components/Label";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
const theme = useTheme();
|
||||
const { authToken, user } = useSelector((state) => state.auth);
|
||||
|
||||
@@ -1,43 +1,12 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
ButtonGroup,
|
||||
Stack,
|
||||
Skeleton,
|
||||
Typography,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import { ButtonGroup, Stack, Typography, Button } from "@mui/material";
|
||||
import { networkService } from "../../main";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
import IncidentTable from "./IncidentTable";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" gap={theme.spacing(6)}>
|
||||
<Skeleton variant="rounded" width={150} height={34} />
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ ml: "auto" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="100%" height={300} />
|
||||
<Skeleton variant="rounded" width="100%" height={100} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
import SkeletonLayout from "./skeleton";
|
||||
|
||||
const Incidents = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" gap={theme.spacing(6)}>
|
||||
<Skeleton variant="rounded" width={150} height={34} />
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ ml: "auto" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="100%" height={300} />
|
||||
<Skeleton variant="rounded" width="100%" height={100} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -0,0 +1,201 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
Paper,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
|
||||
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { logger } from "../../../../Utils/Logger";
|
||||
import Host from "../host";
|
||||
import { StatusLabel } from "../../../../Components/Label";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { useSelector } from "react-redux";
|
||||
import { networkService } from "../../../../main";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import BarChart from "../../../../Components/Charts/BarChart";
|
||||
import ActionsMenu from "../actionsMenu";
|
||||
|
||||
const MonitorTable = ({ isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [paginationController, setPaginationController] = useState({
|
||||
page: 0,
|
||||
rowsPerPage: 14,
|
||||
});
|
||||
const [monitors, setMonitors] = useState([]);
|
||||
const [monitorCount, setMonitorCount] = useState(0);
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
|
||||
const handleActionMenuDelete = () => {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
};
|
||||
|
||||
const handlePageChange = (_, newPage) => {
|
||||
setPaginationController({
|
||||
...paginationController,
|
||||
page: newPage - 1, // 0-indexed
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPage = async () => {
|
||||
try {
|
||||
const { authToken } = authState;
|
||||
const user = jwtDecode(authToken);
|
||||
const res = await networkService.getMonitorsByTeamId(
|
||||
authToken,
|
||||
user.teamId,
|
||||
25,
|
||||
["http", "ping"],
|
||||
null,
|
||||
"desc",
|
||||
true,
|
||||
paginationController.page,
|
||||
paginationController.rowsPerPage
|
||||
);
|
||||
setMonitors(res?.data?.data?.monitors ?? []);
|
||||
setMonitorCount(res?.data?.data?.monitorCount ?? 0);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
fetchPage();
|
||||
}, [
|
||||
updateTrigger,
|
||||
authState,
|
||||
paginationController.page,
|
||||
paginationController.rowsPerPage,
|
||||
]);
|
||||
|
||||
let paginationComponent = <></>;
|
||||
if (monitorCount > paginationController.rowsPerPage) {
|
||||
paginationComponent = (
|
||||
<Pagination
|
||||
count={Math.ceil(monitorCount / 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>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Host</TableCell>
|
||||
<TableCell>
|
||||
{" "}
|
||||
<Box width="max-content">
|
||||
Status
|
||||
<span>
|
||||
<ArrowDownwardRoundedIcon />
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>Response Time</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{monitors.map((monitor) => {
|
||||
let uptimePercentage = "";
|
||||
let percentageColor = theme.palette.percentage.uptimeExcellent;
|
||||
|
||||
// Determine uptime percentage and color based on the monitor's uptimePercentage value
|
||||
if (monitor.uptimePercentage !== undefined) {
|
||||
uptimePercentage =
|
||||
monitor.uptimePercentage === 0
|
||||
? "0"
|
||||
: (monitor.uptimePercentage * 100).toFixed(2);
|
||||
|
||||
percentageColor =
|
||||
monitor.uptimePercentage < 0.25
|
||||
? theme.palette.percentage.uptimePoor
|
||||
: monitor.uptimePercentage < 0.5
|
||||
? theme.palette.percentage.uptimeFair
|
||||
: monitor.uptimePercentage < 0.75
|
||||
? theme.palette.percentage.uptimeGood
|
||||
: theme.palette.percentage.uptimeExcellent;
|
||||
}
|
||||
|
||||
const params = {
|
||||
url: monitor.url,
|
||||
title: monitor.name,
|
||||
percentage: uptimePercentage,
|
||||
percentageColor,
|
||||
status:
|
||||
monitor.status === undefined
|
||||
? "pending"
|
||||
: monitor.status === true
|
||||
? "up"
|
||||
: "down",
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow key={monitor._id}>
|
||||
<TableCell>
|
||||
<Host key={monitor._id} params={params} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={params.status}
|
||||
text={params.status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<BarChart checks={monitor.checks.slice().reverse()} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span style={{ textTransform: "uppercase" }}>
|
||||
{monitor.type}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ActionsMenu
|
||||
monitor={monitor}
|
||||
isAdmin={isAdmin}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{paginationComponent}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorTable.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default MonitorTable;
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import Settings from "../../../assets/icons/settings-bold.svg?react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ActionsMenu = ({ monitor, isAdmin }) => {
|
||||
const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [actions, setActions] = useState({});
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -37,6 +37,7 @@ const ActionsMenu = ({ monitor, isAdmin }) => {
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
setIsOpen(false); // close modal
|
||||
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
|
||||
updateCallback();
|
||||
createToast({ body: "Monitor deleted successfully." });
|
||||
} else {
|
||||
createToast({ body: "Failed to delete monitor." });
|
||||
@@ -222,6 +223,7 @@ ActionsMenu.propTypes = {
|
||||
type: PropTypes.string,
|
||||
}).isRequired,
|
||||
isAdmin: PropTypes.bool,
|
||||
updateCallback: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ActionsMenu;
|
||||
|
||||
@@ -4,15 +4,14 @@ import { useSelector, useDispatch } from "react-redux";
|
||||
import { getUptimeMonitorsByTeamId } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import BasicTable from "../../../Components/BasicTable";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import Fallback from "./fallback";
|
||||
import StatusBox from "./StatusBox";
|
||||
import { buildData } from "./monitorData";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import Greeting from "../../../Utils/greeting";
|
||||
import MonitorTable from "./MonitorTable";
|
||||
|
||||
const Monitors = ({ isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
@@ -25,7 +24,7 @@ const Monitors = ({ isAdmin }) => {
|
||||
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
|
||||
}, [authState.authToken, dispatch]);
|
||||
|
||||
const monitorStats = monitorState.monitors.reduce(
|
||||
const monitorStats = monitorState.monitors.monitors.reduce(
|
||||
(acc, monitor) => {
|
||||
if (monitor.isActive === false) {
|
||||
acc["paused"] += 1;
|
||||
@@ -39,8 +38,6 @@ const Monitors = ({ isAdmin }) => {
|
||||
{ paused: 0, up: 0, down: 0 }
|
||||
);
|
||||
|
||||
const data = buildData(monitorState.monitors, isAdmin, navigate);
|
||||
|
||||
let loading = monitorState.isLoading && monitorState.monitors.length === 0;
|
||||
|
||||
return (
|
||||
@@ -118,7 +115,7 @@ const Monitors = ({ isAdmin }) => {
|
||||
</Box>
|
||||
{/* TODO - add search bar */}
|
||||
</Stack>
|
||||
<BasicTable data={data} paginated={true} table={"monitors"} />
|
||||
<MonitorTable isAdmin={isAdmin} />
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -80,7 +80,9 @@ class NetworkService {
|
||||
types,
|
||||
status,
|
||||
sortOrder,
|
||||
normalize
|
||||
normalize,
|
||||
page,
|
||||
rowsPerPage
|
||||
) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
@@ -93,6 +95,8 @@ class NetworkService {
|
||||
if (status) params.append("status", status);
|
||||
if (sortOrder) params.append("sortOrder", sortOrder);
|
||||
if (normalize) params.append("normalize", normalize);
|
||||
if (page) params.append("page", page);
|
||||
if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
|
||||
|
||||
return this.axiosInstance.get(
|
||||
`/monitors/team/${teamId}?${params.toString()}`,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -302,8 +302,16 @@ const getMonitorById = async (monitorId) => {
|
||||
*/
|
||||
const getMonitorsByTeamId = async (req, res) => {
|
||||
try {
|
||||
let { limit, type, status, sortOrder, normalize } = req.query || {};
|
||||
let { limit, type, status, sortOrder, normalize, page, rowsPerPage } =
|
||||
req.query || {};
|
||||
const monitorQuery = { teamId: req.params.teamId };
|
||||
const monitorsCount = await Monitor.countDocuments(monitorQuery);
|
||||
|
||||
// Pagination
|
||||
let skip = 0;
|
||||
if (page && rowsPerPage) {
|
||||
skip = page * rowsPerPage;
|
||||
}
|
||||
|
||||
if (type !== undefined) {
|
||||
const types = Array.isArray(type) ? type : [type];
|
||||
@@ -320,7 +328,9 @@ const getMonitorsByTeamId = async (req, res) => {
|
||||
// This effectively removes limit, returning all checks
|
||||
if (limit === undefined) limit = 0;
|
||||
|
||||
const monitors = await Monitor.find(monitorQuery);
|
||||
const monitors = await Monitor.find(monitorQuery)
|
||||
.skip(skip)
|
||||
.limit(rowsPerPage);
|
||||
// Map each monitor to include its associated checks
|
||||
const monitorsWithChecks = await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
@@ -349,7 +359,7 @@ const getMonitorsByTeamId = async (req, res) => {
|
||||
return { ...monitor.toObject(), checks };
|
||||
})
|
||||
);
|
||||
return monitorsWithChecks;
|
||||
return { monitors: monitorsWithChecks, monitorCount: monitorsCount };
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -176,6 +176,8 @@ const getMonitorsByTeamIdQueryValidation = joi.object({
|
||||
joi.string().valid("http", "ping", "pagespeed"),
|
||||
joi.array().items(joi.string().valid("http", "ping", "pagespeed"))
|
||||
),
|
||||
page: joi.number(),
|
||||
rowsPerPage: joi.number(),
|
||||
});
|
||||
|
||||
const getMonitorStatsByIdParamValidation = joi.object({
|
||||
|
||||
Reference in New Issue
Block a user