From 4ed995692d76a233eaf9a1b25862f7e7c887f743 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 21 Jan 2025 15:30:51 -0800 Subject: [PATCH 01/11] Refactor incident endpoints to use aggregation --- Server/controllers/checkController.js | 11 +- Server/db/models/Check.js | 7 ++ Server/db/mongo/modules/checkModule.js | 163 ++++++++++++------------- Server/routes/checkRoute.js | 4 +- Server/service/networkService.js | 1 + Server/service/statusService.js | 3 +- 6 files changed, 93 insertions(+), 96 deletions(-) diff --git a/Server/controllers/checkController.js b/Server/controllers/checkController.js index 8988df6d8..ae6b96dbd 100644 --- a/Server/controllers/checkController.js +++ b/Server/controllers/checkController.js @@ -44,7 +44,7 @@ class CheckController { } }; - getChecks = async (req, res, next) => { + getChecksByMonitor = async (req, res, next) => { try { await getChecksParamValidation.validateAsync(req.params); await getChecksQueryValidation.validateAsync(req.query); @@ -54,19 +54,18 @@ class CheckController { } try { - const checks = await this.db.getChecks(req); - const checksCount = await this.db.getChecksCount(req); + const result = await this.db.getChecksByMonitor(req); return res.success({ msg: successMessages.CHECK_GET, - data: { checksCount, checks }, + data: result, }); } catch (error) { next(handleError(error, SERVICE_NAME, "getChecks")); } }; - getTeamChecks = async (req, res, next) => { + getChecksByTeam = async (req, res, next) => { try { await getTeamChecksParamValidation.validateAsync(req.params); await getTeamChecksQueryValidation.validateAsync(req.query); @@ -75,7 +74,7 @@ class CheckController { return; } try { - const checkData = await this.db.getTeamChecks(req); + const checkData = await this.db.getChecksByTeam(req); return res.success({ msg: successMessages.CHECK_GET, diff --git a/Server/db/models/Check.js b/Server/db/models/Check.js index 1946f2c59..dba47a12e 100644 --- a/Server/db/models/Check.js +++ b/Server/db/models/Check.js @@ -12,6 +12,13 @@ const BaseCheckSchema = mongoose.Schema({ immutable: true, index: true, }, + + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + index: true, + }, /** * Status of the check (true for up, false for down). * diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index 0a552012d..870011d9f 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -2,6 +2,8 @@ import Check from "../../models/Check.js"; import Monitor from "../../models/Monitor.js"; import User from "../../models/User.js"; import logger from "../../../utils/logger.js"; +import { ObjectId } from "mongodb"; + const SERVICE_NAME = "checkModule"; const dateRangeLookup = { day: new Date(new Date().setDate(new Date().getDate() - 1)), @@ -56,41 +58,6 @@ const createCheck = async (checkData) => { } }; -const getChecksCount = async (req) => { - const monitorId = req.params.monitorId; - const dateRange = req.query.dateRange; - const filter = req.query.filter; - // Build query - const checksQuery = { monitorId: monitorId }; - // Filter checks by "day", "week", or "month" - 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: - logger.warn({ - message: "invalid filter", - service: SERVICE_NAME, - method: "getChecksCount", - }); - break; - } - } - - const count = await Check.countDocuments(checksQuery); - return count; -}; - /** * Get all checks for a monitor * @async @@ -98,33 +65,32 @@ const getChecksCount = async (req) => { * @returns {Promise>} * @throws {Error} */ - -const getChecks = async (req) => { +const getChecksByMonitor = async (req) => { try { const { monitorId } = req.params; let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query; - // Default limit to 0 if not provided - limit = limit === undefined ? 0 : limit; + page = parseInt(page); + rowsPerPage = parseInt(rowsPerPage); - // Default sort order is newest -> oldest - sortOrder = sortOrder === "asc" ? 1 : -1; + // Match + const matchStage = { + monitorId: ObjectId.createFromHexString(monitorId), + status: false, + ...(dateRange && { + createdAt: { + $gte: dateRangeLookup[dateRange], + }, + }), + }; - // Build query - const checksQuery = { monitorId: monitorId }; - // Filter checks by "day", "week", or "month" - if (dateRange !== undefined) { - checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] }; - } - // Filter checks by status if (filter !== undefined) { - checksQuery.status = false; switch (filter) { case "all": break; case "down": break; case "resolve": - checksQuery.statusCode = 5000; + matchStage.statusCode = 5000; break; default: logger.warn({ @@ -136,16 +102,33 @@ const getChecks = async (req) => { } } - // Need to skip and limit here + //Sort + sortOrder = sortOrder === "asc" ? 1 : -1; + + // Pagination let skip = 0; if (page && rowsPerPage) { skip = page * rowsPerPage; } - const checks = await Check.find(checksQuery) - .skip(skip) - .limit(rowsPerPage) - .sort({ createdAt: sortOrder }); - return checks; + + const checks = await Check.aggregate([ + { $match: matchStage }, + { $sort: { createdAt: sortOrder } }, + { + $facet: { + summary: [{ $count: "checksCount" }], + checks: [{ $skip: skip }, { $limit: rowsPerPage }], + }, + }, + { + $project: { + checksCount: { $arrayElemAt: ["$summary.checksCount", 0] }, + checks: "$checks", + }, + }, + ]); + + return checks[0]; } catch (error) { error.service = SERVICE_NAME; error.method = "getChecks"; @@ -153,32 +136,32 @@ const getChecks = async (req) => { } }; -const getTeamChecks = async (req) => { +const getChecksByTeam = async (req) => { try { - const { teamId } = req.params; let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query; + page = parseInt(page); + rowsPerPage = parseInt(rowsPerPage); - // Get monitorIDs - const userMonitors = await Monitor.find({ teamId: teamId }).select("_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; - - const checksQuery = { monitorId: { $in: userMonitors } }; + const { teamId } = req.params; + const matchStage = { + teamId: ObjectId.createFromHexString(teamId), + status: false, + ...(dateRange && { + createdAt: { + $gte: dateRangeLookup[dateRange], + }, + }), + }; + // Add filter to match stage if (filter !== undefined) { - checksQuery.status = false; switch (filter) { case "all": break; case "down": - checksQuery.status = false; break; case "resolve": - checksQuery.statusCode = 5000; + matchStage.statusCode = 5000; break; default: logger.warn({ @@ -190,27 +173,34 @@ const getTeamChecks = async (req) => { } } - if (dateRange !== undefined) { - checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] }; - } + sortOrder = sortOrder === "asc" ? 1 : -1; - // Skip and limit for pagination + // 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 }) - .select(["monitorId", "status", "responseTime", "statusCode", "message"]); - return { checksCount, checks }; + const checks = await Check.aggregate([ + { $match: matchStage }, + { $sort: { createdAt: sortOrder } }, + { + $facet: { + summary: [{ $count: "checksCount" }], + checks: [{ $skip: skip }, { $limit: rowsPerPage }], + }, + }, + { + $project: { + checksCount: { $arrayElemAt: ["$summary.checksCount", 0] }, + checks: "$checks", + }, + }, + ]); + return checks[0]; } catch (error) { error.service = SERVICE_NAME; - error.method = "getTeamChecks"; + error.method = "getChecksByTeam"; throw error; } }; @@ -298,9 +288,8 @@ const updateChecksTTL = async (teamId, ttl) => { export { createCheck, - getChecksCount, - getChecks, - getTeamChecks, + getChecksByMonitor, + getChecksByTeam, deleteChecks, deleteChecksByTeamId, updateChecksTTL, diff --git a/Server/routes/checkRoute.js b/Server/routes/checkRoute.js index 289e0bc2f..bab45e4d4 100644 --- a/Server/routes/checkRoute.js +++ b/Server/routes/checkRoute.js @@ -11,7 +11,7 @@ class CheckRoutes { } initRoutes() { - this.router.get("/:monitorId", this.checkController.getChecks); + this.router.get("/:monitorId", this.checkController.getChecksByMonitor); this.router.post( "/:monitorId", verifyOwnership(Monitor, "monitorId"), @@ -23,7 +23,7 @@ class CheckRoutes { this.checkController.deleteChecks ); - this.router.get("/team/:teamId", this.checkController.getTeamChecks); + this.router.get("/team/:teamId", this.checkController.getChecksByTeam); this.router.delete( "/team/:teamId", diff --git a/Server/service/networkService.js b/Server/service/networkService.js index 03bd6fef9..54edf39f9 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -131,6 +131,7 @@ class NetworkService { const httpResponse = { monitorId: job.data._id, + teamId: job.data.teamId, type: job.data.type, responseTime, payload: response?.data, diff --git a/Server/service/statusService.js b/Server/service/statusService.js index 5138b73ce..cca443e9e 100644 --- a/Server/service/statusService.js +++ b/Server/service/statusService.js @@ -83,10 +83,11 @@ class StatusService { * @returns {Object} The check object. */ buildCheck = (networkResponse) => { - const { monitorId, type, status, responseTime, code, message, payload } = + const { monitorId, teamId, type, status, responseTime, code, message, payload } = networkResponse; const check = { monitorId, + teamId, status, statusCode: code, responseTime, From 9535ffa192f740ea3b5678770e44019cfca706d1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 21 Jan 2025 15:31:40 -0800 Subject: [PATCH 02/11] Refactor Pagination component to accept a label to improve reusability. Use Pagination component with Incident table --- .../src/Components/HttpStatusLabel/index.jsx | 2 +- .../Table/TablePagination/index.jsx | 16 ++-- .../Pages/Incidents/IncidentTable/index.jsx | 78 ++++++------------- Client/src/Pages/Incidents/index.css | 11 --- Client/src/Pages/Incidents/index.jsx | 1 - Client/src/Pages/Infrastructure/index.jsx | 5 +- .../src/Pages/Uptime/CreateUptime/index.jsx | 20 ++--- Client/src/Pages/Uptime/Home/index.jsx | 5 +- 8 files changed, 49 insertions(+), 89 deletions(-) delete mode 100644 Client/src/Pages/Incidents/index.css diff --git a/Client/src/Components/HttpStatusLabel/index.jsx b/Client/src/Components/HttpStatusLabel/index.jsx index ca9f70fe8..52676ae7c 100644 --- a/Client/src/Components/HttpStatusLabel/index.jsx +++ b/Client/src/Components/HttpStatusLabel/index.jsx @@ -16,7 +16,7 @@ import { BaseLabel } from "../Label"; const DEFAULT_CODE = 9999; // Default code for unknown status const handleStatusCode = (status) => { - if (status >= 100 && status < 600) { + if (status) { return status; } return DEFAULT_CODE; diff --git a/Client/src/Components/Table/TablePagination/index.jsx b/Client/src/Components/Table/TablePagination/index.jsx index 66707494a..61e89232f 100644 --- a/Client/src/Components/Table/TablePagination/index.jsx +++ b/Client/src/Components/Table/TablePagination/index.jsx @@ -5,11 +5,11 @@ import { TablePaginationActions } from "./Actions"; import SelectorVertical from "../../../assets/icons/selector-vertical.svg?react"; Pagination.propTypes = { - monitorCount: PropTypes.number.isRequired, // Total number of items for 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. handleChangePage: PropTypes.func.isRequired, // Function to handle page changes. - handleChangeRowsPerPage: PropTypes.func.isRequired, // Function to handle changes in rows per page. + handleChangeRowsPerPage: PropTypes.func, // Function to handle changes in rows per page. }; const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25]; @@ -18,6 +18,7 @@ const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25]; * Pagination component for table navigation with customized styling and behavior. * * @param {object} props - Component properties. + * @param {string} props.paginationLabel - Label for the pagination. * @param {number} props.monitorCount - Total number of monitors to paginate. * @param {number} props.page - Current page index (0-based). * @param {number} props.rowsPerPage - Number of rows to display per page. @@ -26,7 +27,8 @@ const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25]; * @returns {JSX.Element} The Pagination component. */ function Pagination({ - monitorCount, + paginationLabel, + itemCount, page, rowsPerPage, handleChangePage, @@ -35,7 +37,7 @@ function Pagination({ const theme = useTheme(); const start = page * rowsPerPage + 1; - const end = Math.min(page * rowsPerPage + rowsPerPage, monitorCount); + const end = Math.min(page * rowsPerPage + rowsPerPage, itemCount); const range = `${start} - ${end}`; return ( @@ -51,11 +53,11 @@ function Pagination({ variant="body2" sx={{ opacity: 0.7 }} > - Showing {range} of {monitorCount} monitor(s) + Showing {range} of {itemCount} {paginationLabel} { const uiTimezone = useSelector((state) => state.ui.timezone); @@ -24,18 +23,10 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { const mode = useSelector((state) => state.ui.mode); const [checks, setChecks] = useState([]); const [checksCount, setChecksCount] = useState(0); - const [paginationController, setPaginationController] = useState({ - page: 0, - rowsPerPage: 14, - }); - const [isLoading, setIsLoading] = useState(false); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); - useEffect(() => { - setPaginationController((prevPaginationController) => ({ - ...prevPaginationController, - page: 0, - })); - }, [filter, selectedMonitor]); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { const fetchPage = async () => { @@ -53,8 +44,8 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { limit: null, dateRange: null, filter: filter, - page: paginationController.page, - rowsPerPage: paginationController.rowsPerPage, + page: page, + rowsPerPage: rowsPerPage, }); } else { res = await networkService.getChecksByMonitor({ @@ -64,8 +55,8 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { limit: null, dateRange: null, filter: filter, - page: paginationController.page, - rowsPerPage: paginationController.rowsPerPage, + page, + rowsPerPage, }); } setChecks(res.data.data.checks); @@ -77,21 +68,14 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { } }; fetchPage(); - }, [ - authToken, - user, - monitors, - selectedMonitor, - filter, - paginationController.page, - paginationController.rowsPerPage, - ]); + }, [authToken, user, monitors, selectedMonitor, filter, page, rowsPerPage]); const handlePageChange = (_, newPage) => { - setPaginationController({ - ...paginationController, - page: newPage - 1, // 0-indexed - }); + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(event.target.value); }; const headers = [ @@ -134,28 +118,6 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { { id: "message", content: "Message", render: (row) => row.message }, ]; - let paginationComponent = <>; - if (checksCount > paginationController.rowsPerPage) { - paginationComponent = ( - ( - - )} - sx={{ mt: "auto" }} - /> - ); - } - let sharedStyles = { border: 1, borderColor: theme.palette.primary.lowContrast, @@ -198,8 +160,14 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { headers={headers} data={checks} /> - - {paginationComponent} + )} diff --git a/Client/src/Pages/Incidents/index.css b/Client/src/Pages/Incidents/index.css deleted file mode 100644 index ddbb44e6d..000000000 --- a/Client/src/Pages/Incidents/index.css +++ /dev/null @@ -1,11 +0,0 @@ -.incidents h1.MuiTypography-root { - font-size: var(--env-var-font-size-large); - font-weight: 600; -} -.incidents button.MuiButtonBase-root, -.incidents p.MuiTypography-root { - font-size: var(--env-var-font-size-medium); -} -.incidents button.MuiButtonBase-root { - min-height: 34px; -} diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index 86f5afc5f..6e3f513f7 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -8,7 +8,6 @@ import { useTheme } from "@emotion/react"; import Select from "../../Components/Inputs/Select"; import IncidentTable from "./IncidentTable"; import SkeletonLayout from "./skeleton"; -import "./index.css"; const Incidents = () => { const theme = useTheme(); diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index 08636a05e..0bf367b99 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -13,7 +13,7 @@ import { Box, Button, IconButton, Stack } from "@mui/material"; import Breadcrumbs from "../../Components/Breadcrumbs"; import { StatusLabel } from "../../Components/Label"; import { Heading } from "../../Components/Heading"; -import { Pagination } from "../../Components/Table/TablePagination/index.jsx"; +import Pagination from "../../Components/Table/TablePagination/index.jsx"; // import { getInfrastructureMonitorsByTeamId } from "../../Features/InfrastructureMonitors/infrastructureMonitorsSlice"; import { networkService } from "../../Utils/NetworkService.js"; import CustomGauge from "../../Components/Charts/CustomGauge/index.jsx"; @@ -269,7 +269,8 @@ function Infrastructure() { /> { } if (monitor.type === "http") { - const checkEndpointAction = await dispatch( - checkEndpointResolution({ authToken, monitorURL: form.url }) - ); - if (checkEndpointAction.meta.requestStatus === "rejected") { - createToast({ - body: "The endpoint you entered doesn't resolve. Check the URL again.", - }); - setErrors({ url: "The entered URL is not reachable." }); - return; - } + // const checkEndpointAction = await dispatch( + // checkEndpointResolution({ authToken, monitorURL: form.url }) + // ); + // if (checkEndpointAction.meta.requestStatus === "rejected") { + // createToast({ + // body: "The endpoint you entered doesn't resolve. Check the URL again.", + // }); + // setErrors({ url: "The entered URL is not reachable." }); + // return; + // } } form = { diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index df59185fb..446702ebb 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -5,7 +5,7 @@ import SkeletonLayout from "./skeleton"; import Fallback from "./fallback"; import StatusBox from "./StatusBox"; import UptimeDataTable from "./UptimeDataTable"; -import { Pagination } from "../../../Components/Table/TablePagination"; +import Pagination from "../../../Components/Table/TablePagination"; // Utils import { useTheme } from "@emotion/react"; @@ -225,7 +225,8 @@ const UptimeMonitors = () => { triggerUpdate={triggerUpdate} /> Date: Tue, 21 Jan 2025 15:39:49 -0800 Subject: [PATCH 03/11] Add default key to table rows --- Client/src/Components/Table/index.jsx | 3 ++- Client/src/Pages/Incidents/IncidentTable/index.jsx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Client/src/Components/Table/index.jsx b/Client/src/Components/Table/index.jsx index d8eddcc24..3d170f440 100644 --- a/Client/src/Components/Table/index.jsx +++ b/Client/src/Components/Table/index.jsx @@ -82,9 +82,10 @@ const DataTable = ({ headers, data, config = { emptyView: "No data" } }) => { ) : ( data.map((row) => { + const key = row.id || row._id || Math.random(); return ( config?.onRowClick(row)} > diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index ca40fe66e..ab7af33ca 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -59,6 +59,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { rowsPerPage, }); } + setChecks(res.data.data.checks); setChecksCount(res.data.data.checksCount); } catch (error) { From 9a8ce41c95094316f40c7a5103d1313a22533c5c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 07:06:53 -0800 Subject: [PATCH 04/11] add db utils for seeding db --- Server/db/mongo/utils/seedDb.js | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 Server/db/mongo/utils/seedDb.js diff --git a/Server/db/mongo/utils/seedDb.js b/Server/db/mongo/utils/seedDb.js new file mode 100644 index 000000000..2d9ea4df8 --- /dev/null +++ b/Server/db/mongo/utils/seedDb.js @@ -0,0 +1,42 @@ +import Monitor from "../models/Monitor"; +import Check from "../models/Check"; + +const generateRandomUrl = () => { + const domains = ["example.com", "test.org", "demo.net", "sample.io", "mock.dev"]; + const paths = ["api", "status", "health", "ping", "check"]; + return `https://${domains[Math.floor(Math.random() * domains.length)]}/${paths[Math.floor(Math.random() * paths.length)]}`; +}; + +const generateChecks = (monitorId, count) => { + const checks = []; + const endTime = new Date(Date.now() - 10 * 60 * 1000); // 10 minutes ago + const startTime = new Date(endTime - count * 60 * 1000); // count minutes before endTime + + for (let i = 0; i < count; i++) { + const timestamp = new Date(startTime.getTime() + i * 60 * 1000); + const status = Math.random() > 0.05; // 95% chance of being up + + checks.push({ + monitorId, + status, + responseTime: Math.floor(Math.random() * 1000), // Random response time between 0-1000ms + createdAt: timestamp, + updatedAt: timestamp, + }); + } + + return checks; +}; + +const seedDb = async () => { + try { + await Monitor.deleteMany({}); + await Check.deleteMany({}); + } catch (error) { + console.error(error); + } + + for (let i = 0; i < 300; i++) { + const monitor = await Monitor.create({}); + } +}; From 9ffe17e9ebd82b7ecf27f70ded8126e673188c88 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 08:29:38 -0800 Subject: [PATCH 05/11] add db utils --- Server/controllers/monitorController.js | 10 ++++++++- Server/db/models/Check.js | 1 + Server/db/mongo/modules/checkModule.js | 26 +++++++++++++++++---- Server/db/mongo/utils/seedDb.js | 30 ++++++++++++++++++++----- Server/index.js | 3 +++ Server/routes/monitorRoute.js | 2 ++ 6 files changed, 61 insertions(+), 11 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 097172854..5b880f2c6 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -20,7 +20,7 @@ import { getTokenFromHeaders } from "../utils/utils.js"; import logger from "../utils/logger.js"; import { handleError, handleValidationError } from "./controllerUtils.js"; import axios from "axios"; - +import seedDb from "../db/mongo/utils/seedDb.js"; const SERVICE_NAME = "monitorController"; class MonitorController { @@ -495,6 +495,14 @@ class MonitorController { next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId")); } }; + + seedDb = async (req, res, next) => { + try { + await seedDb(userId, teamId); + } catch (error) { + next(handleError(error, SERVICE_NAME, "seedDb")); + } + }; } export default MonitorController; diff --git a/Server/db/models/Check.js b/Server/db/models/Check.js index dba47a12e..af9f797b2 100644 --- a/Server/db/models/Check.js +++ b/Server/db/models/Check.js @@ -76,6 +76,7 @@ const CheckSchema = mongoose.Schema({ ...BaseCheckSchema.obj }, { timestamps: tr CheckSchema.index({ createdAt: 1 }); CheckSchema.index({ monitorId: 1, createdAt: 1 }); CheckSchema.index({ monitorId: 1, createdAt: -1 }); +CheckSchema.index({ teamId: 1, createdAt: -1 }); export default mongoose.model("Check", CheckSchema); export { BaseCheckSchema }; diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index 870011d9f..edc70e5d6 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -68,10 +68,9 @@ const createCheck = async (checkData) => { const getChecksByMonitor = async (req) => { try { const { monitorId } = req.params; - let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query; + let { sortOrder, dateRange, filter, page, rowsPerPage } = req.query; page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); - // Match const matchStage = { monitorId: ObjectId.createFromHexString(monitorId), @@ -138,10 +137,10 @@ const getChecksByMonitor = async (req) => { const getChecksByTeam = async (req) => { try { - let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query; + let { sortOrder, dateRange, filter, page, rowsPerPage } = req.query; page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); - + !dateRange && (dateRange = "day"); const { teamId } = req.params; const matchStage = { teamId: ObjectId.createFromHexString(teamId), @@ -184,6 +183,7 @@ const getChecksByTeam = async (req) => { const checks = await Check.aggregate([ { $match: matchStage }, { $sort: { createdAt: sortOrder } }, + { $facet: { summary: [{ $count: "checksCount" }], @@ -197,6 +197,24 @@ const getChecksByTeam = async (req) => { }, }, ]); + + const queryPlan = await Check.aggregate([ + { $match: matchStage }, + { $sort: { createdAt: sortOrder } }, + { + $facet: { + summary: [{ $count: "checksCount" }], + checks: [{ $skip: skip }, { $limit: rowsPerPage }], + }, + }, + { + $project: { + checksCount: { $arrayElemAt: ["$summary.checksCount", 0] }, + checks: "$checks", + }, + }, + ]).explain("executionStats"); + console.log(queryPlan); return checks[0]; } catch (error) { error.service = SERVICE_NAME; diff --git a/Server/db/mongo/utils/seedDb.js b/Server/db/mongo/utils/seedDb.js index 2d9ea4df8..b67276027 100644 --- a/Server/db/mongo/utils/seedDb.js +++ b/Server/db/mongo/utils/seedDb.js @@ -1,5 +1,8 @@ -import Monitor from "../models/Monitor"; -import Check from "../models/Check"; +import Monitor from "../../models/Monitor.js"; +import Check from "../../models/Check.js"; +import { ObjectId } from "mongodb"; +const FAKE_TEAM_ID = "67034bceb4c4ea3e8f75fa93"; +const FAKE_USER_ID = "67034bceb4c4ea3e8f75fa95"; const generateRandomUrl = () => { const domains = ["example.com", "test.org", "demo.net", "sample.io", "mock.dev"]; @@ -18,6 +21,7 @@ const generateChecks = (monitorId, count) => { checks.push({ monitorId, + teamId: FAKE_TEAM_ID, status, responseTime: Math.floor(Math.random() * 1000), // Random response time between 0-1000ms createdAt: timestamp, @@ -30,13 +34,27 @@ const generateChecks = (monitorId, count) => { const seedDb = async () => { try { + console.log("Deleting all monitors and checks"); await Monitor.deleteMany({}); await Check.deleteMany({}); + console.log("Adding monitors"); + for (let i = 0; i < 300; i++) { + const monitor = await Monitor.create({ + name: `Monitor ${i}`, + url: generateRandomUrl(), + type: "http", + userId: FAKE_USER_ID, + teamId: FAKE_TEAM_ID, + interval: 60000, + active: true, + }); + console.log(`Adding monitor and checks for monitor ${i}`); + const checks = generateChecks(monitor._id, 10000); + await Check.insertMany(checks); + } } catch (error) { console.error(error); } - - for (let i = 0; i < 300; i++) { - const monitor = await Monitor.create({}); - } }; + +export default seedDb; diff --git a/Server/index.js b/Server/index.js index 7a4129bc9..a4b67386e 100644 --- a/Server/index.js +++ b/Server/index.js @@ -74,6 +74,8 @@ import MongoDB from "./db/mongo/MongoDB.js"; import IORedis from "ioredis"; +import seedDb from "./db/mongo/utils/seedDb.js"; + const SERVICE_NAME = "Server"; const SHUTDOWN_TIMEOUT = 1000; let isShuttingDown = false; @@ -262,6 +264,7 @@ const startApp = async () => { ); // Init job queue await jobQueue.initJobQueue(); + // await seedDb(); // Middleware app.use( cors() diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index 92b529417..964bbee7a 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -75,6 +75,8 @@ class MonitorRoutes { isAllowed(["admin", "superadmin"]), this.monitorController.addDemoMonitors ); + + this.router.post("/seed", isAllowed(["superadmin"]), this.monitorController.seedDb); } getRouter() { From 2a74f2e8dacf3d2ec42a71c015908e62ccd55feb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 11:06:39 -0800 Subject: [PATCH 06/11] remove seed from index --- Server/index.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/Server/index.js b/Server/index.js index a4b67386e..7a4129bc9 100644 --- a/Server/index.js +++ b/Server/index.js @@ -74,8 +74,6 @@ import MongoDB from "./db/mongo/MongoDB.js"; import IORedis from "ioredis"; -import seedDb from "./db/mongo/utils/seedDb.js"; - const SERVICE_NAME = "Server"; const SHUTDOWN_TIMEOUT = 1000; let isShuttingDown = false; @@ -264,7 +262,6 @@ const startApp = async () => { ); // Init job queue await jobQueue.initJobQueue(); - // await seedDb(); // Middleware app.use( cors() From fc36da4f7b65b57c0d39636d00345ff935461f1a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 11:07:32 -0800 Subject: [PATCH 07/11] Add hour filters to check queries --- Server/db/models/Check.js | 1 + Server/db/mongo/modules/checkModule.js | 29 +++++++------------------- Server/db/mongo/utils/seedDb.js | 17 +++++++-------- 3 files changed, 15 insertions(+), 32 deletions(-) diff --git a/Server/db/models/Check.js b/Server/db/models/Check.js index af9f797b2..158749986 100644 --- a/Server/db/models/Check.js +++ b/Server/db/models/Check.js @@ -77,6 +77,7 @@ CheckSchema.index({ createdAt: 1 }); CheckSchema.index({ monitorId: 1, createdAt: 1 }); CheckSchema.index({ monitorId: 1, createdAt: -1 }); CheckSchema.index({ teamId: 1, createdAt: -1 }); +CheckSchema.index({ teamId: 1 }); export default mongoose.model("Check", CheckSchema); export { BaseCheckSchema }; diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index edc70e5d6..ec98cf63c 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -6,9 +6,11 @@ import { ObjectId } from "mongodb"; const SERVICE_NAME = "checkModule"; const dateRangeLookup = { + hour: new Date(new Date().setHours(new Date().getHours() - 1)), 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)), + all: undefined, }; /** @@ -75,7 +77,7 @@ const getChecksByMonitor = async (req) => { const matchStage = { monitorId: ObjectId.createFromHexString(monitorId), status: false, - ...(dateRange && { + ...(dateRangeLookup[dateRange] && { createdAt: { $gte: dateRangeLookup[dateRange], }, @@ -140,18 +142,18 @@ const getChecksByTeam = async (req) => { let { sortOrder, dateRange, filter, page, rowsPerPage } = req.query; page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); - !dateRange && (dateRange = "day"); + console.log(dateRangeLookup[dateRange]); const { teamId } = req.params; const matchStage = { teamId: ObjectId.createFromHexString(teamId), status: false, - ...(dateRange && { + ...(dateRangeLookup[dateRange] && { createdAt: { $gte: dateRangeLookup[dateRange], }, }), }; - + console.log(matchStage); // Add filter to match stage if (filter !== undefined) { switch (filter) { @@ -166,7 +168,7 @@ const getChecksByTeam = async (req) => { logger.warn({ message: "invalid filter", service: SERVICE_NAME, - method: "getTeamChecks", + method: "getChecksByTeam", }); break; } @@ -198,23 +200,6 @@ const getChecksByTeam = async (req) => { }, ]); - const queryPlan = await Check.aggregate([ - { $match: matchStage }, - { $sort: { createdAt: sortOrder } }, - { - $facet: { - summary: [{ $count: "checksCount" }], - checks: [{ $skip: skip }, { $limit: rowsPerPage }], - }, - }, - { - $project: { - checksCount: { $arrayElemAt: ["$summary.checksCount", 0] }, - checks: "$checks", - }, - }, - ]).explain("executionStats"); - console.log(queryPlan); return checks[0]; } catch (error) { error.service = SERVICE_NAME; diff --git a/Server/db/mongo/utils/seedDb.js b/Server/db/mongo/utils/seedDb.js index b67276027..cd4abb8d5 100644 --- a/Server/db/mongo/utils/seedDb.js +++ b/Server/db/mongo/utils/seedDb.js @@ -1,8 +1,5 @@ import Monitor from "../../models/Monitor.js"; import Check from "../../models/Check.js"; -import { ObjectId } from "mongodb"; -const FAKE_TEAM_ID = "67034bceb4c4ea3e8f75fa93"; -const FAKE_USER_ID = "67034bceb4c4ea3e8f75fa95"; const generateRandomUrl = () => { const domains = ["example.com", "test.org", "demo.net", "sample.io", "mock.dev"]; @@ -10,7 +7,7 @@ const generateRandomUrl = () => { return `https://${domains[Math.floor(Math.random() * domains.length)]}/${paths[Math.floor(Math.random() * paths.length)]}`; }; -const generateChecks = (monitorId, count) => { +const generateChecks = (monitorId, teamId, count) => { const checks = []; const endTime = new Date(Date.now() - 10 * 60 * 1000); // 10 minutes ago const startTime = new Date(endTime - count * 60 * 1000); // count minutes before endTime @@ -21,7 +18,7 @@ const generateChecks = (monitorId, count) => { checks.push({ monitorId, - teamId: FAKE_TEAM_ID, + teamId, status, responseTime: Math.floor(Math.random() * 1000), // Random response time between 0-1000ms createdAt: timestamp, @@ -32,7 +29,7 @@ const generateChecks = (monitorId, count) => { return checks; }; -const seedDb = async () => { +const seedDb = async (userId, teamId) => { try { console.log("Deleting all monitors and checks"); await Monitor.deleteMany({}); @@ -43,13 +40,13 @@ const seedDb = async () => { name: `Monitor ${i}`, url: generateRandomUrl(), type: "http", - userId: FAKE_USER_ID, - teamId: FAKE_TEAM_ID, + userId, + teamId, interval: 60000, - active: true, + active: false, }); console.log(`Adding monitor and checks for monitor ${i}`); - const checks = generateChecks(monitor._id, 10000); + const checks = generateChecks(monitor._id, teamId, 10000); await Check.insertMany(checks); } } catch (error) { From f46f8cb12b40bbaa4638fbbf998c4d4f70e0b38d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 11:07:43 -0800 Subject: [PATCH 08/11] Add hour filter to incidents page --- Server/controllers/monitorController.js | 5 ++++- Server/validation/joi.js | 10 +++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 5b880f2c6..86c0fb840 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -498,7 +498,10 @@ class MonitorController { seedDb = async (req, res, next) => { try { - await seedDb(userId, teamId); + const token = getTokenFromHeaders(req.headers); + const { jwtSecret } = this.settingsService.getSettings(); + const { _id, teamId } = jwt.verify(token, jwtSecret); + await seedDb(_id, teamId); } catch (error) { next(handleError(error, SERVICE_NAME, "seedDb")); } diff --git a/Server/validation/joi.js b/Server/validation/joi.js index d30235048..8f366a113 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -131,7 +131,7 @@ const getMonitorByIdQueryValidation = joi.object({ status: joi.boolean(), sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), - dateRange: joi.string().valid("day", "week", "month"), + dateRange: joi.string().valid("hour", "day", "week", "month", "all"), numToDisplay: joi.number(), normalize: joi.boolean(), }); @@ -166,7 +166,7 @@ const getMonitorStatsByIdQueryValidation = joi.object({ status: joi.string(), limit: joi.number(), sortOrder: joi.string().valid("asc", "desc"), - dateRange: joi.string().valid("day", "week", "month", "all"), + dateRange: joi.string().valid("hour", "day", "week", "month", "all"), numToDisplay: joi.number(), normalize: joi.boolean(), }); @@ -217,7 +217,7 @@ const getHardwareDetailsByIdParamValidation = joi.object({ }); const getHardwareDetailsByIdQueryValidation = joi.object({ - dateRange: joi.string().valid("day", "week", "month"), + dateRange: joi.string().valid("hour", "day", "week", "month", "all"), }); //**************************************** @@ -288,7 +288,7 @@ const getChecksParamValidation = joi.object({ const getChecksQueryValidation = joi.object({ sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), - dateRange: joi.string().valid("day", "week", "month"), + dateRange: joi.string().valid("hour", "day", "week", "month", "all"), filter: joi.string().valid("all", "down", "resolve"), page: joi.number(), rowsPerPage: joi.number(), @@ -301,7 +301,7 @@ const getTeamChecksParamValidation = joi.object({ const getTeamChecksQueryValidation = joi.object({ sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), - dateRange: joi.string().valid("day", "week", "month"), + dateRange: joi.string().valid("hour", "day", "week", "month", "all"), filter: joi.string().valid("all", "down", "resolve"), page: joi.number(), rowsPerPage: joi.number(), From 4268966b44066dfa2d325fdd8870c389e6c683af Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 11:10:09 -0800 Subject: [PATCH 09/11] Add hour filter to incidents page --- .../Pages/Incidents/IncidentTable/index.jsx | 10 +- Client/src/Pages/Incidents/index.jsx | 160 +++++++++++++----- 2 files changed, 121 insertions(+), 49 deletions(-) diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index ab7af33ca..e3ddba24a 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -15,7 +15,8 @@ import { Empty } from "./Empty/Empty"; import { IncidentSkeleton } from "./Skeleton/Skeleton"; import DataTable from "../../../Components/Table"; import Pagination from "../../../Components/Table/TablePagination"; -const IncidentTable = ({ monitors, selectedMonitor, filter }) => { + +const IncidentTable = ({ monitors, selectedMonitor, filter, dateRange }) => { const uiTimezone = useSelector((state) => state.ui.timezone); const theme = useTheme(); @@ -25,7 +26,6 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { const [checksCount, setChecksCount] = useState(0); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); - const [isLoading, setIsLoading] = useState(false); useEffect(() => { @@ -42,7 +42,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { teamId: user.teamId, sortOrder: "desc", limit: null, - dateRange: null, + dateRange, filter: filter, page: page, rowsPerPage: rowsPerPage, @@ -53,7 +53,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { monitorId: selectedMonitor, sortOrder: "desc", limit: null, - dateRange: null, + dateRange, filter: filter, page, rowsPerPage, @@ -69,7 +69,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { } }; fetchPage(); - }, [authToken, user, monitors, selectedMonitor, filter, page, rowsPerPage]); + }, [authToken, user, monitors, selectedMonitor, filter, page, rowsPerPage, dateRange]); const handlePageChange = (_, newPage) => { setPage(newPage); diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index 6e3f513f7..0256b89b3 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -20,6 +20,7 @@ const Incidents = () => { // TODO do something with these filters const [filter, setFilter] = useState("all"); + const [dateRange, setDateRange] = useState("hour"); useEffect(() => { const fetchMonitors = async () => { @@ -67,6 +68,7 @@ const Incidents = () => { return ( @@ -76,62 +78,132 @@ const Incidents = () => { <> - - Incidents for - - + + - - - + + + + + + - Cannot Resolve - - + Limit by: + + + + + + + + )} From 15cd3b23c39701525498272aacf43fe4bad01ff6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 11:13:44 -0800 Subject: [PATCH 10/11] Add daterange prop --- Client/src/Pages/Incidents/IncidentTable/index.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index e3ddba24a..ae9a3ae2a 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -179,6 +179,7 @@ IncidentTable.propTypes = { monitors: PropTypes.object.isRequired, selectedMonitor: PropTypes.string.isRequired, filter: PropTypes.string.isRequired, + dateRange: PropTypes.string.isRequired, }; export default IncidentTable; From 1b31f9ac4cc6adbe6ebd6ecb4c6dfe3b97917e55 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jan 2025 11:36:27 -0800 Subject: [PATCH 11/11] Remove console logs, update UI --- Client/src/Pages/Incidents/index.jsx | 11 +++++------ Server/db/mongo/modules/checkModule.js | 2 -- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index 0256b89b3..8305ec672 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -68,7 +68,6 @@ const Incidents = () => { return ( @@ -144,7 +143,7 @@ const Incidents = () => { filled={(filter === "resolve").toString()} onClick={() => setFilter("resolve")} > - Cannot Resolve + Cannot resolve @@ -158,7 +157,7 @@ const Incidents = () => { component="h1" color={theme.palette.primary.contrastTextSecondary} > - Limit by: + Show: { filled={(dateRange === "hour").toString()} onClick={() => setDateRange("hour")} > - Hour + Last hour