Merge pull request #580 from bluewave-labs/fix/incident-pagination

Fix/incident pagination, resolves #576
This commit is contained in:
Alexander Holliday authored and GitHub committed 2024-08-09 14:54:09 -07:00
commit b3badc7226
12 files changed
+395 -198

No files matched your search

@@ -79,7 +79,6 @@ const MonitorDetailsAreaChart = ({ checks }) => {
MonitorDetailsAreaChart.propTypes = {
checks: PropTypes.array,
filter: PropTypes.string,
};
CustomToolTip.propTypes = {
@@ -0,0 +1,142 @@
import PropTypes from "prop-types";
import {
TableContainer,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
Pagination,
PaginationItem,
Paper,
} from "@mui/material";
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 axiosInstance from "../../../Utils/axiosConfig";
import { StatusLabel } from "../../../Components/Label";
const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
const { authToken, user } = useSelector((state) => state.auth);
const [checks, setChecks] = useState([]);
const [checksCount, setChecksCount] = useState(0);
const [paginationController, setPaginationController] = useState({
page: 0,
rowsPerPage: 12,
});
useEffect(() => {
const fetchPage = async () => {
try {
let url = `/checks/${selectedMonitor}?sortOrder=desc&filter=${filter}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`;
if (selectedMonitor === "0") {
url = `/checks/user/${user._id}?sortOrder=desc&filter=${filter}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`;
}
const res = await axiosInstance.get(url, {
headers: {
Authorization: `Bearer ${authToken}`,
},
});
setChecks(res.data.data.checks);
setChecksCount(res.data.data.checksCount);
} catch (error) {
console.log(error);
}
};
fetchPage();
}, [
authToken,
user,
selectedMonitor,
filter,
paginationController.page,
paginationController.rowsPerPage,
]);
const handlePageChange = (_, newPage) => {
console.log(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}
sx={{
"&:focus": {
outline: "none",
},
}}
/>
)}
/>
);
}
return (
<>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Monitor Name</TableCell>
<TableCell>Status</TableCell>
<TableCell>Date & Time</TableCell>
<TableCell>Message</TableCell>
</TableRow>
</TableHead>
<TableBody>
{checks.map((check) => {
const status = check.status === true ? "up" : "down";
return (
<TableRow key={check._id}>
<TableCell>{monitors[check.monitorId]?.name}</TableCell>
<TableCell>
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
</TableCell>
<TableCell>
{new Date(check.createdAt).toLocaleString()}
</TableCell>
<TableCell>{check.statusCode}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
{paginationComponent}
</>
);
};
IncidentTable.propTypes = {
monitors: PropTypes.object.isRequired,
selectedMonitor: PropTypes.string.isRequired,
filter: PropTypes.string.isRequired,
};
export default IncidentTable;
+3
View File
@@ -14,6 +14,9 @@
height: 34px;
border-color: var(--env-var-color-16);
}
.incidents .MuiPagination-root {
background-color: var(--env-var-color-8);
}
/* remove later */
.incidents .MuiSelect-select {
+61 -146
View File
@@ -1,15 +1,11 @@
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { Typography, ButtonGroup, Stack, Box, Skeleton } from "@mui/material";
import WestRoundedIcon from "@mui/icons-material/WestRounded";
import { ButtonGroup, Stack, Skeleton, Typography } from "@mui/material";
import Button from "../../Components/Button";
import axiosInstance from "../../Utils/axiosConfig";
import BasicTable from "../../Components/BasicTable";
import { StatusLabel } from "../../Components/Label";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router";
import Select from "../../Components/Inputs/Select";
import IncidentTable from "./IncidentTable";
import "./index.css";
/**
@@ -40,8 +36,8 @@ const SkeletonLayout = () => {
const Incidents = () => {
const theme = useTheme();
const navigate = useNavigate();
const authState = useSelector((state) => state.auth);
const [monitors, setMonitors] = useState({});
const [selectedMonitor, setSelectedMonitor] = useState("0");
const [loading, setLoading] = useState(false);
@@ -49,21 +45,11 @@ const Incidents = () => {
// TODO do something with these filters
const [filter, setFilter] = useState("all");
let data = {
cols: [
{ id: 1, name: "Status" },
{ id: 2, name: "Timestamp" },
{ id: 3, name: "Monitor" },
{ id: 4, name: "Message" },
],
rows: [],
};
useEffect(() => {
const fetchIncidents = async () => {
const fetchMonitors = async () => {
setLoading(true);
const res = await axiosInstance.get(
`/monitors/user/${authState.user._id}?status=false`,
`/monitors/user/${authState.user._id}?status=false&limit=1`,
{
headers: {
Authorization: `Bearer ${authState.authToken}`,
@@ -82,141 +68,70 @@ const Incidents = () => {
setLoading(false);
};
fetchIncidents();
fetchMonitors();
}, [authState]);
useEffect(() => {}, []);
const handleSelect = (event) => {
setSelectedMonitor(event.target.value);
};
let incidents = [];
let hasAnyIncidents = false;
let hasSpecificIncidents = false;
if (selectedMonitor === "0") {
Object.values(monitors).forEach((monitor) => {
incidents = incidents.concat(monitor.checks);
});
if (incidents.length !== 0) hasAnyIncidents = true;
hasSpecificIncidents = hasAnyIncidents;
} else {
const monitor = monitors[selectedMonitor];
incidents = monitor.checks;
if (incidents.length !== 0) hasSpecificIncidents = true;
hasAnyIncidents = true;
}
incidents = incidents
.sort((a, b) => {
return new Date(b.createdAt) - new Date(a.createdAt);
})
.filter((incident) => {
if (filter === "all") return true;
else if (filter === "resolve") return incident.statusCode === 5000;
else if (filter === "down") return incident.status === false;
})
.map((incident, idx) => {
return {
id: idx,
data: [
{
id: idx + 0,
data: <StatusLabel status="down" text="Down" />,
},
{ id: idx + 1, data: new Date(incident.createdAt).toLocaleString() },
{ id: idx + 2, data: monitors[incident.monitorId].name },
{ id: idx + 3, data: incident.statusCode },
],
};
});
data.rows = incidents;
return (
<>
<Stack className="incidents" gap={theme.gap.large}>
{loading ? (
<SkeletonLayout />
) : hasAnyIncidents ? (
<>
<Stack direction="row" alignItems="center" gap={theme.gap.medium}>
<Typography component="h1">Incident history for: </Typography>
<Select
id="incidents-select-monitor"
placeholder="All servers"
value={selectedMonitor}
onChange={handleSelect}
items={Object.values(monitors)}
/>
{hasAnyIncidents && hasSpecificIncidents && (
<ButtonGroup sx={{ ml: "auto" }}>
<Button
level="secondary"
label="All"
onClick={() => setFilter("all")}
sx={{
backgroundColor:
filter === "all" && theme.palette.otherColors.fillGray,
}}
/>
<Button
level="secondary"
label="Down"
onClick={() => setFilter("down")}
sx={{
backgroundColor:
filter === "down" && theme.palette.otherColors.fillGray,
}}
/>
<Button
level="secondary"
label="Cannot Resolve"
onClick={() => setFilter("resolve")}
sx={{
backgroundColor:
filter === "resolve" &&
theme.palette.otherColors.fillGray,
}}
/>
</ButtonGroup>
)}
</Stack>
{hasAnyIncidents && hasSpecificIncidents ? (
<BasicTable data={data} paginated={true} rowsPerPage={12} />
) : (
<Typography alignSelf="center" mt={theme.gap.xxl}>
The monitor you have selected, has no recorded incidents yet.
</Typography>
)}
</>
) : (
<Box>
<Button
level="tertiary"
label="Back"
animate="slideLeft"
img={<WestRoundedIcon />}
onClick={() => navigate(-1)}
sx={{
backgroundColor: theme.palette.otherColors.fillGray,
px: theme.gap.ml,
mb: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.small,
fill: theme.palette.otherColors.slateGray,
},
}}
<Stack className="incidents" gap={theme.gap.large}>
{loading ? (
<SkeletonLayout />
) : (
<>
<Stack direction="row" alignItems="center" gap={theme.gap.medium}>
<Typography display="inline-block" component="h1">
Incidents for
</Typography>
<Select
id="incidents-select-monitor"
placeholder="All servers"
value={selectedMonitor}
onChange={handleSelect}
items={Object.values(monitors)}
/>
<Box
padding={theme.gap.xxl}
gap={theme.gap.small}
border={1}
borderRadius={`${theme.shape.borderRadius}px`}
borderColor={theme.palette.otherColors.graishWhite}
backgroundColor={theme.palette.otherColors.white}
>
<Typography textAlign="center">No incidents, yet.</Typography>
</Box>
</Box>
)}
</Stack>
</>
<ButtonGroup sx={{ ml: "auto" }}>
<Button
level="secondary"
label="All"
onClick={() => setFilter("all")}
sx={{
backgroundColor:
filter === "all" && theme.palette.otherColors.fillGray,
}}
/>
<Button
level="secondary"
label="Down"
onClick={() => setFilter("down")}
sx={{
backgroundColor:
filter === "down" && theme.palette.otherColors.fillGray,
}}
/>
<Button
level="secondary"
label="Cannot Resolve"
onClick={() => setFilter("resolve")}
sx={{
backgroundColor:
filter === "resolve" && theme.palette.otherColors.fillGray,
}}
/>
</ButtonGroup>
</Stack>
<IncidentTable
monitors={monitors}
selectedMonitor={selectedMonitor}
filter={filter}
/>
</>
)}
</Stack>
);
};
@@ -6,8 +6,6 @@ import {
TableRow,
TableCell,
TableBody,
TableFooter,
TablePagination,
PaginationItem,
Pagination,
Paper,
@@ -20,7 +18,7 @@ import { StatusLabel } from "../../../../Components/Label";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
const PaginationTable = ({ monitorId, filter }) => {
const PaginationTable = ({ monitorId, dateRange }) => {
const { authToken } = useSelector((state) => state.auth);
const [checks, setChecks] = useState([]);
const [checksCount, setChecksCount] = useState(0);
@@ -33,7 +31,7 @@ const PaginationTable = ({ monitorId, filter }) => {
const fetchPage = async () => {
try {
const res = await axiosInstance.get(
`/checks/${monitorId}?sortOrder=desc&filter=${filter}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`,
`/checks/${monitorId}?sortOrder=desc&dateRange=${dateRange}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
@@ -50,7 +48,7 @@ const PaginationTable = ({ monitorId, filter }) => {
}, [
authToken,
monitorId,
filter,
dateRange,
paginationController.page,
paginationController.rowsPerPage,
]);
@@ -58,16 +56,16 @@ const PaginationTable = ({ monitorId, filter }) => {
const handlePageChange = (_, newPage) => {
setPaginationController({
...paginationController,
page: newPage - 1,
page: newPage - 1, // 0-indexed
});
};
let paginationComponent = <></>;
if (checksCount > paginationController.rowsPerPage) {
paginationComponent = (
<Pagination
count={Math.ceil(checksCount / paginationController.rowsPerPage)}
page={paginationController.page + 1}
page={paginationController.page + 1} //0-indexed
onChange={handlePageChange}
shape="rounded"
renderItem={(item) => (
@@ -133,7 +131,7 @@ const PaginationTable = ({ monitorId, filter }) => {
PaginationTable.propTypes = {
monitorId: PropTypes.string.isRequired,
filter: PropTypes.string.isRequired,
dateRange: PropTypes.string.isRequired,
};
export default PaginationTable;
+13 -14
View File
@@ -113,13 +113,13 @@ const DetailsPage = () => {
const [monitor, setMonitor] = useState({});
const { monitorId } = useParams();
const { authToken } = useSelector((state) => state.auth);
const [filter, setFilter] = useState("day");
const [dateRange, setDateRange] = useState("day");
const navigate = useNavigate();
const fetchMonitor = useCallback(async () => {
try {
const res = await axiosInstance.get(
`/monitors/${monitorId}?sortOrder=asc&filter=${filter}&numToDisplay=50&normalize=true`,
`/monitors/${monitorId}?sortOrder=asc&filter=${dateRange}&numToDisplay=50&normalize=true`,
{
headers: {
Authorization: `Bearer ${authToken}`,
@@ -131,7 +131,7 @@ const DetailsPage = () => {
console.error("Error fetching monitor of id: " + monitorId);
navigate("/not-found");
}
}, [authToken, monitorId, navigate, filter]);
}, [authToken, monitorId, navigate, dateRange]);
useEffect(() => {
fetchMonitor();
@@ -301,44 +301,43 @@ const DetailsPage = () => {
<Button
level="secondary"
label="Day"
onClick={() => setFilter("day")}
onClick={() => setDateRange("day")}
sx={{
backgroundColor:
filter === "day" && theme.palette.otherColors.fillGray,
dateRange === "day" &&
theme.palette.otherColors.fillGray,
}}
/>
<Button
level="secondary"
label="Week"
onClick={() => setFilter("week")}
onClick={() => setDateRange("week")}
sx={{
backgroundColor:
filter === "week" && theme.palette.otherColors.fillGray,
dateRange === "week" &&
theme.palette.otherColors.fillGray,
}}
/>
<Button
level="secondary"
label="Month"
onClick={() => setFilter("month")}
onClick={() => setDateRange("month")}
sx={{
backgroundColor:
filter === "month" &&
dateRange === "month" &&
theme.palette.otherColors.fillGray,
}}
/>
</ButtonGroup>
</Stack>
<Box sx={{ height: "200px" }}>
<MonitorDetailsAreaChart
checks={monitor.checks}
filter={filter}
/>
<MonitorDetailsAreaChart checks={monitor.checks} />
</Box>
</Box>
<Stack gap={theme.gap.ml}>
<Typography component="h2">History</Typography>
{/* TODO New Table */}
<PaginationTable monitorId={monitorId} filter={filter} />
<PaginationTable monitorId={monitorId} dateRange={dateRange} />
{/* <BasicTable data={data} paginated={true} /> */}
</Stack>
</Stack>
+28
View File
@@ -3,6 +3,8 @@ const {
createCheckBodyValidation,
getChecksParamValidation,
getChecksQueryValidation,
getUserChecksParamValidation,
getUserChecksQueryValidation,
deleteChecksParamValidation,
} = require("../validation/joi");
const { successMessages } = require("../utils/messages");
@@ -60,6 +62,31 @@ const getChecks = async (req, res, next) => {
}
};
const getUserChecks = async (req, res, next) => {
try {
await getUserChecksParamValidation.validateAsync(req.params);
await getUserChecksQueryValidation.validateAsync(req.query);
} catch (error) {
error.status = 422;
error.service = SERVICE_NAME;
error.message =
error.details?.[0]?.message || error.message || "Validation Error";
next(error);
return;
}
try {
const checkData = await req.db.getUserChecks(req);
return res.status(200).json({
success: true,
msg: successMessages.CHECK_GET,
data: checkData,
});
} catch (error) {
error.service = SERVICE_NAME;
next(error);
}
};
const deleteChecks = async (req, res, next) => {
try {
await deleteChecksParamValidation.validateAsync(req.params);
@@ -88,5 +115,6 @@ const deleteChecks = async (req, res, next) => {
module.exports = {
createCheck,
getChecks,
getUserChecks,
deleteChecks,
};
+2
View File
@@ -90,6 +90,7 @@ const {
createCheck,
getChecksCount,
getChecks,
getUserChecks,
deleteChecks,
} = require("./modules/checkModule");
@@ -148,6 +149,7 @@ module.exports = {
createCheck,
getChecksCount,
getChecks,
getUserChecks,
deleteChecks,
createAlert,
getAlertsByUserId,
+106 -14
View File
@@ -1,9 +1,12 @@
const Check = require("../../../models/Check");
const filterLookup = {
const Monitor = require("../../../models/Monitor");
const mongoose = require("mongoose");
const dateRangeLookup = {
day: new Date(new Date().setDate(new Date().getDate() - 1)),
week: new Date(new Date().setDate(new Date().getDate() - 7)),
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
};
/**
* Create a check for a monitor
* @async
@@ -28,13 +31,32 @@ const createCheck = async (checkData) => {
const getChecksCount = async (req) => {
const monitorId = req.params.monitorId;
const dateRange = req.query.dateRange;
const filter = req.query.filter;
console.log(filter);
// Build query
const checksQuery = { monitorId: monitorId };
// Filter checks by "day", "week", or "month"
if (filter !== undefined) {
checksQuery.createdAt = { $gte: filterLookup[filter] };
if (dateRange !== undefined) {
checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
}
if (filter !== undefined) {
checksQuery.status = false;
switch (filter) {
case "all":
break;
case "down":
break;
case "resolve":
checksQuery.statusCode = 5000;
break;
default:
console.log("default");
break;
}
}
const count = await Check.countDocuments(checksQuery);
return count;
};
@@ -50,23 +72,34 @@ const getChecksCount = async (req) => {
const getChecks = async (req) => {
try {
const { monitorId } = req.params;
let { sortOrder, limit, filter, page, rowsPerPage } = req.query;
let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query;
// Default limit to 0 if not provided
if (limit === undefined) limit = 0;
limit = limit === "undefined" ? 0 : limit;
// Default sort order is newest -> oldest
if (sortOrder === "asc") {
sortOrder = 1;
} else if (sortOrder === "desc") {
sortOrder = -1;
} else sortOrder = -1;
sortOrder = sortOrder === "asc" ? 1 : -1;
// Build query
const checksQuery = { monitorId: monitorId };
// Filter checks by "day", "week", or "month"
if (dateRange !== undefined) {
checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
}
// Fitler checks by status
if (filter !== undefined) {
checksQuery.createdAt = { $gte: filterLookup[filter] };
checksQuery.status = false;
switch (filter) {
case "all":
break;
case "down":
break;
case "resolve":
checksQuery.statusCode = 5000;
break;
default:
console.log("default");
break;
}
}
// Need to skip and limit here
@@ -84,8 +117,61 @@ const getChecks = async (req) => {
}
};
const getUserChecks = async (req) => {
const { userId } = req.params;
let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query;
// Get monitorIDs
const userMonitors = await Monitor.find({ userId: userId });
const monitorIds = userMonitors.map((monitor) => monitor._id);
//Build check query
// Default limit to 0 if not provided
limit = limit === "undefined" ? 0 : limit;
// Default sort order is newest -> oldest
sortOrder = sortOrder === "asc" ? 1 : -1;
checksQuery = { monitorId: { $in: monitorIds } };
if (filter !== undefined) {
checksQuery.status = false;
switch (filter) {
case "all":
break;
case "down":
break;
case "resolve":
checksQuery.statusCode = 5000;
break;
default:
console.log("default");
break;
}
}
if (dateRange !== undefined) {
checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
}
// Skip and limit for pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
const checksCount = await Check.countDocuments(checksQuery);
const checks = await Check.find(checksQuery)
.skip(skip)
.limit(rowsPerPage)
.sort({ createdAt: sortOrder });
return { checksCount, checks };
};
/**
* Delete all checks for a monitor
* Delete all checks for a user
* @async
* @param {string} monitorId
* @returns {number}
@@ -100,4 +186,10 @@ const deleteChecks = async (monitorId) => {
throw error;
}
};
module.exports = { createCheck, getChecksCount, getChecks, deleteChecks };
module.exports = {
createCheck,
getChecksCount,
getChecks,
getUserChecks,
deleteChecks,
};
+13 -12
View File
@@ -4,7 +4,7 @@ const Notification = require("../models/Notification");
/**
* Check Schema for MongoDB collection.
*
*
* Represents a check associated with a monitor, storing information
* about the status and response of a particular check event.
*/
@@ -12,7 +12,7 @@ const CheckSchema = mongoose.Schema(
{
/**
* Reference to the associated Monitor document.
*
*
* @type {mongoose.Schema.Types.ObjectId}
*/
monitorId: {
@@ -22,7 +22,7 @@ const CheckSchema = mongoose.Schema(
},
/**
* Status of the check (true for up, false for down).
*
*
* @type {Boolean}
*/
status: {
@@ -30,23 +30,23 @@ const CheckSchema = mongoose.Schema(
},
/**
* Response time of the check in milliseconds.
*
*
* @type {Number}
*/
responseTime: {
type: Number,
type: Number,
},
/**
* HTTP status code received during the check.
*
*
* @type {Number}
*/
statusCode: {
type: Number,
type: Number,
},
/**
* Message or description of the check result.
*
*
* @type {String}
*/
message: {
@@ -54,7 +54,7 @@ const CheckSchema = mongoose.Schema(
},
/**
* Expiry date of the check, auto-calculated to expire after 30 days.
*
*
* @type {Date}
*/
expiry: {
@@ -70,7 +70,7 @@ const CheckSchema = mongoose.Schema(
/**
* Pre-save middleware to handle status change notifications.
*
*
* This middleware checks if the status of the monitor associated
* with the check has changed and sends notifications if necessary.
*/
@@ -79,7 +79,9 @@ CheckSchema.pre("save", async function (next) {
const monitor = await mongoose.model("Monitor").findById(this.monitorId);
if (monitor) {
const notifications = await Notification.find({ monitorId: this.monitorId });
const notifications = await Notification.find({
monitorId: this.monitorId,
});
// Check if there are any notifications
if (notifications && notifications.length > 0) {
@@ -128,5 +130,4 @@ CheckSchema.pre("save", async function (next) {
}
});
module.exports = mongoose.model("Check", CheckSchema);
+3 -1
View File
@@ -21,4 +21,6 @@ router.delete(
checkController.deleteChecks
);
module.exports = router;
router.get("/user/:userId", checkController.getUserChecks);
module.exports = router;
+17 -1
View File
@@ -235,7 +235,21 @@ const getChecksParamValidation = joi.object({
const getChecksQueryValidation = joi.object({
sortOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
filter: joi.string().valid("day", "week", "month"),
dateRange: joi.string().valid("day", "week", "month"),
filter: joi.string().valid("all", "down", "resolve"),
page: joi.number(),
rowsPerPage: joi.number(),
});
const getUserChecksParamValidation = joi.object({
userId: joi.string().required(),
});
const getUserChecksQueryValidation = joi.object({
sortOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
dateRange: joi.string().valid("day", "week", "month"),
filter: joi.string().valid("all", "down", "resolve"),
page: joi.number(),
rowsPerPage: joi.number(),
});
@@ -320,6 +334,8 @@ module.exports = {
createCheckBodyValidation,
getChecksParamValidation,
getChecksQueryValidation,
getUserChecksParamValidation,
getUserChecksQueryValidation,
deleteChecksParamValidation,
deleteUserParamValidation,
getPageSpeedCheckParamValidation,