Merge pull request #1533 from bluewave-labs/feat/be/unified-uptime-query

feat: be/unified uptime query
This commit is contained in:
Alexander Holliday
2025-01-08 13:53:29 -08:00
committed by GitHub
13 changed files with 285 additions and 465 deletions
@@ -40,7 +40,7 @@ export const checkEndpointResolution = createAsyncThunk(
const res = await networkService.checkEndpointResolution({
authToken: authToken,
monitorURL: monitorURL,
})
});
return res.data;
} catch (error) {
if (error.response && error.response.data) {
@@ -53,7 +53,7 @@ export const checkEndpointResolution = createAsyncThunk(
return thunkApi.rejectWithValue(payload);
}
}
)
);
export const getPagespeedMonitorById = createAsyncThunk(
"monitors/getMonitorById",
@@ -88,7 +88,6 @@ export const getPageSpeedByTeamId = createAsyncThunk(
teamId: user.teamId,
types: ["pagespeed"],
});
return res.data;
} catch (error) {
if (error.response && error.response.data) {
@@ -78,62 +78,6 @@ export const getUptimeMonitorById = createAsyncThunk(
}
);
export const getUptimeSummaryByTeamId = createAsyncThunk(
"monitors/getSummaryByTeamId",
async (token, thunkApi) => {
const user = jwtDecode(token);
try {
const res = await networkService.getMonitorsSummaryByTeamId({
authToken: token,
teamId: user.teamId,
types: ["http", "ping", "docker", "port"],
});
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const getUptimeMonitorsByTeamId = createAsyncThunk(
"monitors/getMonitorsByTeamId",
async (config, thunkApi) => {
try {
const res = await networkService.getMonitorsByTeamId({
authToken: config.authToken,
teamId: config.teamId,
limit: 25,
types: ["http", "ping", "docker", "port"],
status: null,
checkOrder: "desc",
normalize: true,
page: config.page,
rowsPerPage: config.rowsPerPage,
filter: config.filter,
field: config.sort.field,
order: config.sort.order,
});
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const updateUptimeMonitor = createAsyncThunk(
"monitors/updateMonitor",
async (data, thunkApi) => {
@@ -289,42 +233,6 @@ const uptimeMonitorsSlice = createSlice({
},
extraReducers: (builder) => {
builder
// *****************************************************
// Summary by teamId
// *****************************************************
.addCase(getUptimeSummaryByTeamId.pending, (state) => {
state.isLoading = true;
})
.addCase(getUptimeSummaryByTeamId.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.msg;
state.monitorsSummary = action.payload.data;
})
.addCase(getUptimeSummaryByTeamId.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload ? action.payload.msg : "Getting uptime summary failed";
})
// *****************************************************
// Monitors by teamId
// *****************************************************
.addCase(getUptimeMonitorsByTeamId.pending, (state) => {
state.isLoading = true;
})
.addCase(getUptimeMonitorsByTeamId.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
})
.addCase(getUptimeMonitorsByTeamId.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload
? action.payload.msg
: "Getting uptime monitors failed";
})
// *****************************************************
// Create Monitor
// *****************************************************
+12 -17
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useState, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { /* useDispatch, */ useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
@@ -50,12 +50,13 @@ function Infrastructure() {
setRowsPerPage(parseInt(event.target.value));
setPage(0);
};
const [monitorState, setMonitorState] = useState({ monitors: [], total: 0 });
const [monitors, setMonitors] = useState([]);
const [summary, setSummary] = useState({});
const { authToken } = useSelector((state) => state.auth);
const user = jwtDecode(authToken);
const fetchMonitors = async () => {
const fetchMonitors = useCallback(async () => {
try {
setIsLoading(true);
const response = await networkService.getMonitorsByTeamId({
@@ -63,29 +64,23 @@ function Infrastructure() {
teamId: user.teamId,
limit: 1,
types: ["hardware"],
status: null,
checkOrder: "desc",
normalize: true,
page: page,
rowsPerPage: rowsPerPage,
});
setMonitorState({
monitors: response?.data?.data?.monitors ?? [],
total: response?.data?.data?.monitorCount ?? 0,
});
setMonitors(response?.data?.data?.monitors ?? []);
setSummary(response?.data?.data?.summary ?? {});
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
}, [page, rowsPerPage, authToken, user.teamId]);
useEffect(() => {
fetchMonitors();
}, [page, rowsPerPage]);
}, [fetchMonitors]);
const { determineState } = useUtils();
const { monitors, total: totalMonitors } = monitorState;
// do it here
function openDetails(id) {
navigate(`/infrastructure/${id}`);
@@ -191,7 +186,7 @@ function Infrastructure() {
};
});
let isActuallyLoading = isLoading && monitorState.monitors?.length === 0;
let isActuallyLoading = isLoading && monitors?.length === 0;
return (
<Box
className="infrastructure-monitor"
@@ -209,7 +204,7 @@ function Infrastructure() {
>
{isActuallyLoading ? (
<SkeletonLayout />
) : monitorState.monitors?.length !== 0 ? (
) : monitors?.length !== 0 ? (
<Stack gap={theme.spacing(8)}>
<Box>
<Breadcrumbs list={BREADCRUMBS} />
@@ -254,7 +249,7 @@ function Infrastructure() {
borderColor={theme.palette.border.light}
backgroundColor={theme.palette.background.accent}
>
{totalMonitors}
{summary?.totalMonitors ?? 0}
</Box>
</Stack>
@@ -272,7 +267,7 @@ function Infrastructure() {
data={monitorsAsRows}
/>
<Pagination
monitorCount={totalMonitors}
monitorCount={summary?.totalMonitors ?? 0}
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
+1 -1
View File
@@ -267,7 +267,7 @@ const Card = ({ monitor }) => {
sx={{ gridColumnStart: 1, gridColumnEnd: 4 }}
>
<PagespeedAreaChart
data={monitor.checks}
data={monitor.checks.slice().reverse()}
status={monitorState}
/>
</Box>
+4 -12
View File
@@ -1,8 +1,7 @@
import { Box, Button, Grid, Stack } from "@mui/material";
import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
import { useDispatch, useSelector } from "react-redux";
import { getPageSpeedByTeamId } from "../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
import { useSelector } from "react-redux";
import Fallback from "../../Components/Fallback";
import "./index.css";
import { useNavigate } from "react-router";
@@ -15,16 +14,12 @@ import { Heading } from "../../Components/Heading";
import { useIsAdmin } from "../../Hooks/useIsAdmin";
const PageSpeed = () => {
const theme = useTheme();
const dispatch = useDispatch();
const navigate = useNavigate();
const isAdmin = useIsAdmin();
const { user, authToken } = useSelector((state) => state.auth);
const [isLoading, setIsLoading] = useState(true);
const [monitors, setMonitors] = useState([]);
const [monitorCount, setMonitorCount] = useState(0);
useEffect(() => {
dispatch(getPageSpeedByTeamId(authToken));
}, [authToken, dispatch]);
const [summary, setSummary] = useState({});
useEffect(() => {
const fetchMonitors = async () => {
@@ -35,9 +30,6 @@ const PageSpeed = () => {
teamId: user.teamId,
limit: 10,
types: ["pagespeed"],
status: null,
checkOrder: "desc",
normalize: true,
page: null,
rowsPerPage: null,
filter: null,
@@ -46,7 +38,7 @@ const PageSpeed = () => {
});
if (res?.data?.data?.monitors) {
setMonitors(res.data.data.monitors);
setMonitorCount(res.data.data.monitorCount);
setSummary(res.data.data.summary);
}
} catch (error) {
console.log(error);
@@ -118,7 +110,7 @@ const PageSpeed = () => {
borderColor={theme.palette.border.light}
backgroundColor={theme.palette.background.accent}
>
{monitorCount}
{summary?.totalMonitors ?? 0}
</Box>
</Stack>
<Grid
@@ -12,13 +12,42 @@ import ActionsMenu from "../actionsMenu";
// Utils
import { useTheme } from "@emotion/react";
import useDebounce from "../../../../Utils/debounce";
import { useState } from "react";
import useUtils from "../../utils";
import { memo } from "react";
import { useNavigate } from "react-router-dom";
import "../index.css";
import PropTypes from "prop-types";
/**
* UptimeDataTable displays a table of uptime monitors with sorting, searching, and action capabilities
* @param {Object} props - Component props
* @param {boolean} props.isAdmin - Whether the current user has admin privileges
* @param {boolean} props.isLoading - Loading state of the table
* @param {Array<{
* _id: string,
* url: string,
* title: string,
* percentage: number,
* percentageColor: string,
* monitor: {
* _id: string,
* type: string,
* checks: Array
* }
* }>} props.monitors - Array of monitor objects to display
* @param {number} props.monitorCount - Total count of monitors
* @param {Object} props.sort - Current sort configuration
* @param {string} props.sort.field - Field to sort by
* @param {'asc'|'desc'} props.sort.order - Sort direction
* @param {Function} props.setSort - Callback to update sort configuration
* @param {string} props.search - Current search query
* @param {Function} props.setSearch - Callback to update search query
* @param {boolean} props.isSearching - Whether a search is in progress
* @param {Function} props.setIsSearching - Callback to update search state
* @param {Function} props.setIsLoading - Callback to update loading state
* @param {Function} props.triggerUpdate - Callback to trigger a data refresh
* @returns {JSX.Element} Rendered component
*/
const UptimeDataTable = ({
isAdmin,
isLoading,
@@ -30,6 +59,7 @@ const UptimeDataTable = ({
setSearch,
isSearching,
setIsSearching,
setIsLoading,
triggerUpdate,
}) => {
const { determineState } = useUtils();
@@ -37,8 +67,6 @@ const UptimeDataTable = ({
const theme = useTheme();
const navigate = useNavigate();
//Utils
const debouncedFilter = useDebounce(search, 500);
const handleSearch = (value) => {
setIsSearching(true);
setSearch(value);
@@ -146,6 +174,7 @@ const UptimeDataTable = ({
monitor={row.monitor}
isAdmin={isAdmin}
updateRowCallback={triggerUpdate}
setIsLoading={setIsLoading}
pauseCallback={triggerUpdate}
/>
),
@@ -187,7 +216,7 @@ const UptimeDataTable = ({
</Box>
</Stack>
<Box position="relative">
{isSearching && (
{(isSearching || isLoading) && (
<>
<Box
width="100%"
@@ -240,3 +269,21 @@ const UptimeDataTable = ({
const MemoizedUptimeDataTable = memo(UptimeDataTable);
export default MemoizedUptimeDataTable;
UptimeDataTable.propTypes = {
isAdmin: PropTypes.bool,
isLoading: PropTypes.bool,
monitors: PropTypes.array,
monitorCount: PropTypes.number,
sort: PropTypes.shape({
field: PropTypes.string,
order: PropTypes.oneOf(["asc", "desc"]),
}),
setSort: PropTypes.func,
search: PropTypes.string,
setSearch: PropTypes.func,
isSearching: PropTypes.bool,
setIsSearching: PropTypes.func,
setIsLoading: PropTypes.func,
triggerUpdate: PropTypes.func,
};
+9 -3
View File
@@ -8,13 +8,18 @@ import { IconButton, Menu, MenuItem } from "@mui/material";
import {
deleteUptimeMonitor,
pauseUptimeMonitor,
getUptimeMonitorsByTeamId,
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import Settings from "../../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
import Dialog from "../../../Components/Dialog";
const ActionsMenu = ({ monitor, isAdmin, updateRowCallback, pauseCallback }) => {
const ActionsMenu = ({
monitor,
isAdmin,
updateRowCallback,
pauseCallback,
setIsLoading,
}) => {
const [anchorEl, setAnchorEl] = useState(null);
const [actions, setActions] = useState({});
const [isOpen, setIsOpen] = useState(false);
@@ -33,7 +38,6 @@ const ActionsMenu = ({ monitor, isAdmin, updateRowCallback, pauseCallback }) =>
);
if (action.meta.requestStatus === "fulfilled") {
setIsOpen(false); // close modal
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
updateRowCallback();
createToast({ body: "Monitor deleted successfully." });
} else {
@@ -43,6 +47,7 @@ const ActionsMenu = ({ monitor, isAdmin, updateRowCallback, pauseCallback }) =>
const handlePause = async () => {
try {
setIsLoading(true);
const action = await dispatch(
pauseUptimeMonitor({ authToken, monitorId: monitor._id })
);
@@ -223,6 +228,7 @@ ActionsMenu.propTypes = {
isAdmin: PropTypes.bool,
updateRowCallback: PropTypes.func,
pauseCallback: PropTypes.func,
setIsLoading: PropTypes.func,
};
export default ActionsMenu;
+32 -24
View File
@@ -9,11 +9,7 @@ import { Pagination } from "../../../Components/Table/TablePagination";
// Utils
import { useTheme } from "@emotion/react";
import { useEffect, useState, useCallback, useMemo, useRef } from "react";
import {
getUptimeSummaryByTeamId,
getUptimeMonitorsByTeamId,
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { useEffect, useState, useCallback, useMemo } from "react";
import { setRowsPerPage } from "../../../Features/UI/uiSlice";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useSelector, useDispatch } from "react-redux";
@@ -21,6 +17,7 @@ import { useNavigate } from "react-router-dom";
import { createToast } from "../../../Utils/toastUtils";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import useDebounce from "../../../Utils/debounce";
import { networkService } from "../../../main";
const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }];
@@ -33,7 +30,9 @@ const UptimeMonitors = () => {
const [search, setSearch] = useState("");
const [page, setPage] = useState(0);
const [isSearching, setIsSearching] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false);
const [monitorsSummary, setMonitorsSummary] = useState({});
// Utils
const debouncedFilter = useDebounce(search, 500);
@@ -41,7 +40,6 @@ const UptimeMonitors = () => {
const theme = useTheme();
const navigate = useNavigate();
const isAdmin = useIsAdmin();
const { isLoading, monitorsSummary } = useSelector((state) => state.uptimeMonitors);
const authState = useSelector((state) => state.auth);
const fetchParams = useMemo(
@@ -89,30 +87,38 @@ const UptimeMonitors = () => {
const fetchMonitors = useCallback(async () => {
try {
const action = await dispatch(getUptimeMonitorsByTeamId(fetchParams));
if (action.payload.success) {
const { monitors } = action.payload.data;
const mappedMonitors = monitors.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
setMonitors(mappedMonitors);
} else {
// TODO: Check for other errors?
throw new Error("Error fetching monitors");
}
setIsLoading(true);
const config = fetchParams;
const res = await networkService.getMonitorsByTeamId({
authToken: config.authToken,
teamId: config.teamId,
limit: 25,
types: ["http", "ping", "docker", "port"],
page: config.page,
rowsPerPage: config.rowsPerPage,
filter: config.filter,
field: config.sort.field,
order: config.sort.order,
});
const { monitors, summary } = res.data.data;
const mappedMonitors = monitors.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
setMonitors(mappedMonitors);
setMonitorsSummary(summary);
} catch (error) {
createToast({
body: "Error fetching monitors",
});
} finally {
setIsLoading(false);
setIsSearching(false);
}
}, [fetchParams, dispatch, getMonitorWithPercentage, theme]);
}, [fetchParams, getMonitorWithPercentage, theme]);
useEffect(() => {
dispatch(getUptimeSummaryByTeamId(authState.authToken));
fetchMonitors();
}, [fetchMonitors, monitorUpdateTrigger, authState.authToken, dispatch]);
}, [fetchMonitors, monitorUpdateTrigger]);
const handleChangePage = (event, newPage) => {
setPage(newPage);
@@ -131,7 +137,7 @@ const UptimeMonitors = () => {
const triggerUpdate = () => {
setMonitorUpdateTrigger((prev) => !prev);
};
const totalMonitors = monitorsSummary?.monitorCounts?.total;
const totalMonitors = monitorsSummary.totalMonitors;
const hasMonitors = totalMonitors > 0;
const canAddMonitor = isAdmin && hasMonitors;
@@ -176,19 +182,20 @@ const UptimeMonitors = () => {
>
<StatusBox
title="up"
value={monitorsSummary?.monitorCounts?.up ?? 0}
value={monitorsSummary?.upMonitors ?? 0}
/>
<StatusBox
title="down"
value={monitorsSummary?.monitorCounts?.down ?? 0}
value={monitorsSummary?.downMonitors ?? 0}
/>
<StatusBox
title="paused"
value={monitorsSummary?.monitorCounts?.paused ?? 0}
value={monitorsSummary?.pausedMonitors ?? 0}
/>
</Stack>
<UptimeDataTable
isAdmin={isAdmin}
isLoading={isLoading}
monitors={monitors}
monitorCount={totalMonitors}
sort={sort}
@@ -197,6 +204,7 @@ const UptimeMonitors = () => {
setSearch={setSearch}
isSearching={isSearching}
setIsSearching={setIsSearching}
setIsLoading={setIsLoading}
triggerUpdate={triggerUpdate}
/>
<Pagination
+3 -20
View File
@@ -157,9 +157,6 @@ class NetworkService {
* @param {string} config.teamId - The ID of the team whose monitors are to be retrieved.
* @param {number} [config.limit] - The maximum number of checks to retrieve. 0 for all, -1 for none
* @param {Array<string>} [config.types] - The types of monitors to retrieve.
* @param {string} [config.status] - The status of the monitors to retrieve.
* @param {string} [config.checkOrder] - The order in which to sort the retrieved monitors.
* @param {boolean} [config.normalize] - Whether to normalize the retrieved monitors.
* @param {number} [config.page] - The page number for pagination.
* @param {number} [config.rowsPerPage] - The number of rows per page for pagination.
* @param {string} [config.filter] - The filter to apply to the monitors.
@@ -167,21 +164,10 @@ class NetworkService {
* @param {string} [config.order] - The order in which to sort the field.
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
*/
async getMonitorsByTeamId(config) {
const {
authToken,
teamId,
limit,
types,
status,
checkOrder,
normalize,
page,
rowsPerPage,
filter,
field,
order,
} = config;
const { authToken, teamId, limit, types, page, rowsPerPage, filter, field, order } =
config;
const params = new URLSearchParams();
@@ -191,9 +177,6 @@ class NetworkService {
params.append("type", type);
});
}
if (status) params.append("status", status);
if (checkOrder) params.append("checkOrder", checkOrder);
if (normalize) params.append("normalize", normalize);
if (page) params.append("page", page);
if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
if (filter) params.append("filter", filter);
+22 -75
View File
@@ -1,13 +1,11 @@
import {
getMonitorByIdParamValidation,
getMonitorByIdQueryValidation,
getMonitorsByTeamIdValidation,
getMonitorsByTeamIdParamValidation,
getMonitorsByTeamIdQueryValidation,
createMonitorBodyValidation,
getMonitorURLByQueryValidation,
editMonitorBodyValidation,
getMonitorsSummaryByTeamIdParamValidation,
getMonitorsSummaryByTeamIdQueryValidation,
getMonitorsByTeamIdQueryValidation,
pauseMonitorParamValidation,
getMonitorStatsByIdParamValidation,
getMonitorStatsByIdQueryValidation,
@@ -204,77 +202,6 @@ class MonitorController {
}
};
/**
* Retrieves all monitors and a summary for a team based on the team ID.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.teamId - The ID of the team.
* @property {Object} req.query - The query parameters of the request.
* @property {string} req.query.type - The type of the request.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message, and the data containing the monitors and summary for the team.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
getMonitorsSummaryByTeamId = async (req, res, next) => {
try {
await getMonitorsSummaryByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsSummaryByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { teamId } = req.params;
const { type } = req.query;
const monitorsSummary = await this.db.getMonitorsSummaryByTeamId(teamId, type);
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_GET_BY_USER_ID(teamId),
data: monitorsSummary,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorsAndSummaryByTeamId"));
}
};
/**
* Retrieves all monitors associated with a team by the team's ID.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.teamId - The ID of the team.
* @property {Object} req.query - The query parameters of the request.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message, and the data containing the monitors for the team.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
getMonitorsByTeamId = async (req, res, next) => {
try {
await getMonitorsByTeamIdValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const teamId = req.params.teamId;
const monitors = await this.db.getMonitorsByTeamId(req, res);
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_GET_BY_USER_ID(teamId),
data: monitors,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId"));
next(error);
}
};
/**
* Creates a new monitor and adds it to the job queue.
* @async
@@ -564,6 +491,26 @@ class MonitorController {
next(handleError(error, SERVICE_NAME, "addDemoMonitors"));
}
};
getMonitorsByTeamId = async (req, res, next) => {
try {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
}
try {
const monitors = await this.db.getMonitorsByTeamId(req);
return res.status(200).json({
success: true,
msg: "good",
data: monitors,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorsForDisplay"));
}
};
}
export default MonitorController;
+144 -185
View File
@@ -505,202 +505,162 @@ const getMonitorById = async (monitorId) => {
}
};
/**
* Get monitors and Summary by TeamID
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Array<Monitor>>}
* @throws {Error}
*/
const getMonitorsByTeamId = async (req) => {
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
const getMonitorsSummaryByTeamId = async (teamId, type) => {
try {
const monitorCounts = await Monitor.aggregate([
{
$match: {
type: { $in: type },
},
},
{
$facet: {
total: [{ $count: "count" }],
up: [{ $match: { status: true } }, { $count: "count" }],
down: [{ $match: { status: false } }, { $count: "count" }],
paused: [{ $match: { isActive: false } }, { $count: "count" }],
},
},
{
$project: {
total: { $arrayElemAt: ["$total.count", 0] },
up: { $arrayElemAt: ["$up.count", 0] },
down: { $arrayElemAt: ["$down.count", 0] },
paused: { $arrayElemAt: ["$paused.count", 0] },
},
},
]);
return { monitorCounts: monitorCounts[0] };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorsAndSummaryByTeamId";
throw error;
// Parse ints
limit = parseInt(limit);
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
// Build the match stage
const matchStage = { teamId: ObjectId.createFromHexString(req.params.teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
};
/**
* Get monitors by TeamID
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @returns {Promise<Array<Monitor>>}
* @throws {Error}
*/
const getMonitorsByTeamId = async (req, res) => {
try {
let {
limit,
type,
status,
checkOrder,
normalize,
page,
rowsPerPage,
filter,
field,
order,
} = req.query;
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
const monitorQuery = { teamId: req.params.teamId };
const monitorCount = await Monitor.countDocuments(monitorQuery);
const sort = { [field]: order === "asc" ? 1 : -1 };
if (type !== undefined) {
monitorQuery.type = Array.isArray(type) ? { $in: type } : type;
}
// Add filter if provided
// $options: "i" makes the search case-insensitive
if (filter !== undefined) {
monitorQuery.$or = [
{ name: { $regex: filter, $options: "i" } },
{ url: { $regex: filter, $options: "i" } },
];
}
if (filter !== undefined) {
matchStage.$or = [
{ name: { $regex: filter, $options: "i" } },
{ url: { $regex: filter, $options: "i" } },
];
}
// Pagination
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
// Build Sort option
const sort = { [field]: order === "asc" ? 1 : -1 };
const matchStage = { teamId: new ObjectId(req.params.teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
if (filter !== undefined) {
matchStage.$or = [
{ name: { $regex: filter, $options: "i" } },
{ url: { $regex: filter, $options: "i" } },
];
}
let result = await Monitor.aggregate([
{ $match: matchStage },
{ $skip: parseInt(skip) },
...(rowsPerPage ? [{ $limit: parseInt(rowsPerPage) }] : []),
{ $sort: sort },
{
$lookup: {
from: "checks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
...(status && { status }),
},
},
{ $sort: { createdAt: checkOrder === "asc" ? 1 : -1 } },
{ $limit: parseInt(limit) || 0 },
],
as: "standardchecks",
},
},
{
$lookup: {
from: "pagespeedchecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
...(status && { status }),
},
},
{ $sort: { createdAt: checkOrder === "asc" ? 1 : -1 } },
{ $limit: parseInt(limit) || 0 },
],
as: "pagespeedchecks",
},
},
{
$lookup: {
from: "hardwarechecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
...(status && { status }),
},
},
{ $sort: { createdAt: checkOrder === "asc" ? 1 : -1 } },
{ $limit: parseInt(limit) || 0 },
],
as: "hardwarechecks",
},
},
{
$addFields: {
checks: {
$switch: {
branches: [
{
case: { $in: ["$type", ["http", "ping", "docker", "port"]] },
then: "$standardchecks",
const results = await Monitor.aggregate([
{ $match: matchStage },
{
$facet: {
summary: [
{
$group: {
_id: null,
totalMonitors: { $sum: 1 },
upMonitors: {
$sum: {
$cond: [{ $eq: ["$status", true] }, 1, 0],
},
{
case: { $eq: ["$type", "pagespeed"] },
then: "$pagespeedchecks",
},
downMonitors: {
$sum: {
$cond: [{ $eq: ["$status", false] }, 1, 0],
},
{
case: { $eq: ["$type", "hardware"] },
then: "$hardwarechecks",
},
pausedMonitors: {
$sum: {
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
},
],
default: [],
},
},
},
},
{
$project: {
_id: 0,
},
},
],
monitors: [
{ $sort: sort },
{ $skip: skip },
...(rowsPerPage ? [{ $limit: rowsPerPage }] : []),
{
$lookup: {
from: "checks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "standardchecks",
},
},
{
$lookup: {
from: "pagespeedchecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "pagespeedchecks",
},
},
{
$lookup: {
from: "hardwarechecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "hardwarechecks",
},
},
{
$addFields: {
checks: {
$switch: {
branches: [
{
case: { $in: ["$type", ["http", "ping", "docker", "port"]] },
then: "$standardchecks",
},
{
case: { $eq: ["$type", "pagespeed"] },
then: "$pagespeedchecks",
},
{
case: { $eq: ["$type", "hardware"] },
then: "$hardwarechecks",
},
],
default: [],
},
},
},
},
{
$project: {
standardchecks: 0,
pagespeedchecks: 0,
hardwarechecks: 0,
},
},
],
},
{
$project: {
standardchecks: 0,
pagespeedchecks: 0,
hardwarechecks: 0,
},
},
{
$project: {
summary: { $arrayElemAt: ["$summary", 0] },
monitors: 1,
},
]);
if (normalize) {
result = result.map((monitor) => {
monitor.checks = NormalizeData(monitor.checks, 10, 100);
return monitor;
});
}
return { monitors: result, monitorCount };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorsByTeamId";
throw error;
}
},
]);
let { monitors, summary } = results[0];
monitors = monitors.map((monitor) => {
monitor.checks = NormalizeData(monitor.checks, 10, 100);
return monitor;
});
return { monitors, summary };
};
/**
@@ -832,9 +792,8 @@ export {
getAllMonitorsWithUptimeStats,
getMonitorStatsById,
getMonitorById,
getUptimeDetailsById,
getMonitorsSummaryByTeamId,
getMonitorsByTeamId,
getUptimeDetailsById,
createMonitor,
deleteMonitor,
deleteAllMonitors,
+2 -4
View File
@@ -17,6 +17,7 @@ class MonitorRoutes {
"/hardware/details/:monitorId",
this.monitorController.getHardwareDetailsById
);
this.router.get(
"/uptime/details/:monitorId",
this.monitorController.getUptimeDetailsById
@@ -30,10 +31,7 @@ class MonitorRoutes {
);
});
this.router.get("/:monitorId", this.monitorController.getMonitorById);
this.router.get(
"/team/summary/:teamId",
this.monitorController.getMonitorsSummaryByTeamId
);
this.router.get("/team/:teamId", this.monitorController.getMonitorsByTeamId);
this.router.get(
+2 -24
View File
@@ -136,32 +136,12 @@ const getMonitorByIdQueryValidation = joi.object({
normalize: joi.boolean(),
});
const getMonitorsSummaryByTeamIdParamValidation = joi.object({
teamId: joi.string().required(),
});
const getMonitorsSummaryByTeamIdQueryValidation = joi.object({
type: joi
.alternatives()
.try(
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port"),
joi
.array()
.items(
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port")
)
),
});
const getMonitorsByTeamIdValidation = joi.object({
const getMonitorsByTeamIdParamValidation = joi.object({
teamId: joi.string().required(),
});
const getMonitorsByTeamIdQueryValidation = joi.object({
status: joi.boolean(),
checkOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
normalize: joi.boolean(),
type: joi
.alternatives()
.try(
@@ -467,9 +447,7 @@ export {
createMonitorBodyValidation,
getMonitorByIdParamValidation,
getMonitorByIdQueryValidation,
getMonitorsSummaryByTeamIdParamValidation,
getMonitorsSummaryByTeamIdQueryValidation,
getMonitorsByTeamIdValidation,
getMonitorsByTeamIdParamValidation,
getMonitorsByTeamIdQueryValidation,
getMonitorStatsByIdParamValidation,
getMonitorStatsByIdQueryValidation,