diff --git a/Client/package-lock.json b/Client/package-lock.json
index 843e964f6..d14da647e 100644
--- a/Client/package-lock.json
+++ b/Client/package-lock.json
@@ -15,8 +15,8 @@
"@mui/lab": "6.0.0-beta.22",
"@mui/material": "6.3.1",
"@mui/x-charts": "^7.5.1",
- "@mui/x-data-grid": "7.23.5",
- "@mui/x-date-pickers": "7.23.3",
+ "@mui/x-data-grid": "7.23.6",
+ "@mui/x-date-pickers": "7.23.6",
"@reduxjs/toolkit": "2.5.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
@@ -1390,15 +1390,15 @@
"license": "MIT"
},
"node_modules/@mui/x-charts": {
- "version": "7.23.2",
- "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.23.2.tgz",
- "integrity": "sha512-wLeogvQZZtyrAOdG06mDzIQSHBSAB09Uy16AYRUcMxVObi7Fs0i3TJUMpQHMYz1/1DvE1u8zstDgVpVfk8/iCA==",
+ "version": "7.23.6",
+ "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.23.6.tgz",
+ "integrity": "sha512-25kH01gwE5I0d8kWYZEpXZ9zHVHKSAVDiGwhteqagHtO6x/dEfqbMnjGuaNruCWppJR6wIylLKi/tuiOSpIy2A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0",
"@mui/x-charts-vendor": "7.20.0",
- "@mui/x-internals": "7.23.0",
+ "@mui/x-internals": "7.23.6",
"@react-spring/rafz": "^9.7.5",
"@react-spring/web": "^9.7.5",
"clsx": "^2.1.1",
@@ -1448,14 +1448,14 @@
}
},
"node_modules/@mui/x-data-grid": {
- "version": "7.23.5",
- "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.23.5.tgz",
- "integrity": "sha512-JmwdfaegpwO9Ei3PYCKy1FFip9AcdMGzZ0VTqzWE93pvDBVGxs/MZKT0g/8PYHJ6yzA5sBHHBxFN8sKfs7kVsg==",
+ "version": "7.23.6",
+ "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.23.6.tgz",
+ "integrity": "sha512-NcCZH99ZBLRlCcfLwwhCkVowNZHgjy0XZ/c6EuTRMSZl1UqF8ouwitP1ZfAa1idDIWCHFhxo446U/93aGMqOyQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0",
- "@mui/x-internals": "7.23.5",
+ "@mui/x-internals": "7.23.6",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"reselect": "^5.1.1"
@@ -1484,35 +1484,15 @@
}
}
},
- "node_modules/@mui/x-data-grid/node_modules/@mui/x-internals": {
- "version": "7.23.5",
- "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.23.5.tgz",
- "integrity": "sha512-PS6p9qL7otbQ2edSF83GgTicssE0Q84Ta+X/5tSwoCnToEKClka1Wc/cXlsjhRVLmoqz8uTqaiNcZAgnyQWNYQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.25.7",
- "@mui/utils": "^5.16.6 || ^6.0.0"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
"node_modules/@mui/x-date-pickers": {
- "version": "7.23.3",
- "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.3.tgz",
- "integrity": "sha512-bjTYX/QzD5ZhVZNNnastMUS3j2Hy4p4IXmJgPJ0vKvQBvUdfEO+ZF42r3PJNNde0FVT1MmTzkmdTlz0JZ6ukdw==",
+ "version": "7.23.6",
+ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.6.tgz",
+ "integrity": "sha512-jt6rEAYLju3NZe3y2S+I5KcTiSHV79FW0jeNUEUTceg1qsPzseHbND66k3zVF0hO3N2oZtLtPywof6vN5Doe+Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0",
- "@mui/x-internals": "7.23.0",
+ "@mui/x-internals": "7.23.6",
"@types/react-transition-group": "^4.4.11",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
@@ -1531,7 +1511,7 @@
"@mui/material": "^5.15.14 || ^6.0.0",
"@mui/system": "^5.15.14 || ^6.0.0",
"date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
- "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0",
+ "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
"dayjs": "^1.10.7",
"luxon": "^3.0.2",
"moment": "^2.29.4",
@@ -1571,9 +1551,9 @@
}
},
"node_modules/@mui/x-internals": {
- "version": "7.23.0",
- "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.23.0.tgz",
- "integrity": "sha512-bPclKpqUiJYIHqmTxSzMVZi6MH51cQsn5U+8jskaTlo3J4QiMeCYJn/gn7YbeR9GOZFp8hetyHjoQoVHKRXCig==",
+ "version": "7.23.6",
+ "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.23.6.tgz",
+ "integrity": "sha512-hT1Pa4PNCnxwiauPbYMC3p4DiEF1x05Iu4C1MtC/jMJ1LtthymLmTuQ6ZQ53/R9FeqK6sYd6A6noR+vNMjp5DA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
@@ -3639,9 +3619,9 @@
}
},
"node_modules/eslint-plugin-react-refresh": {
- "version": "0.4.16",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.16.tgz",
- "integrity": "sha512-slterMlxAhov/DZO8NScf6mEeMBBXodFUolijDvrtTxyezyLoTQaa73FyYus/VbTdftd8wBgBxPMRk3poleXNQ==",
+ "version": "0.4.18",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.18.tgz",
+ "integrity": "sha512-IRGEoFn3OKalm3hjfolEWGqoF/jPqeEYFp+C8B0WMzwGwBMvlRDQd06kghDhF0C61uJ6WfSDhEZE/sAQjduKgw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
diff --git a/Client/package.json b/Client/package.json
index e4853715a..d3ed1741d 100644
--- a/Client/package.json
+++ b/Client/package.json
@@ -18,8 +18,8 @@
"@mui/lab": "6.0.0-beta.22",
"@mui/material": "6.3.1",
"@mui/x-charts": "^7.5.1",
- "@mui/x-data-grid": "7.23.5",
- "@mui/x-date-pickers": "7.23.3",
+ "@mui/x-data-grid": "7.23.6",
+ "@mui/x-date-pickers": "7.23.6",
"@reduxjs/toolkit": "2.5.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
diff --git a/Client/src/Components/BasicTable/index.css b/Client/src/Components/BasicTable/index.css
deleted file mode 100644
index 3c1b0f2cd..000000000
--- a/Client/src/Components/BasicTable/index.css
+++ /dev/null
@@ -1,130 +0,0 @@
-.MuiTable-root .host {
- width: fit-content;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
-}
-.MuiTable-root .host span {
- font-size: 11px;
-}
-
-.MuiTable-root .label {
- line-height: 1;
- border-radius: var(--env-var-radius-2);
- padding: 7px;
- font-size: var(--env-var-font-size-small-plus);
-}
-
-.MuiPaper-root:has(table.MuiTable-root) {
- box-shadow: none;
-}
-.MuiTable-root .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root {
- border: none;
-}
-
-.MuiTable-root .MuiTableHead-root .MuiTableCell-root,
-.MuiTable-root .MuiTableBody-root .MuiTableCell-root {
- font-size: var(--env-var-font-size-medium);
-}
-.MuiTable-root .MuiTableHead-root .MuiTableCell-root {
- padding: var(--env-var-spacing-1);
- font-weight: 500;
-}
-.MuiTable-root .MuiTableHead-root span {
- display: inline-block;
- height: 17px;
- width: 20px;
- overflow: hidden;
- margin-bottom: -3px;
- margin-left: 3px;
-}
-.MuiTable-root .MuiTableHead-root span svg {
- width: 20px;
- height: 20px;
-}
-.MuiTable-root .MuiTableBody-root .MuiTableCell-root {
- padding: var(--env-var-spacing-1);
-}
-.MuiTable-root .MuiTableBody-root .MuiTableRow-root {
- height: 50px;
-}
-
-.MuiPaper-root + .MuiPagination-root {
- border-radius: var(--env-var-radius-1);
- padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
-}
-.MuiPaper-root + .MuiPagination-root ul {
- justify-content: center;
-}
-.MuiPaper-root + .MuiPagination-root button {
- font-size: var(--env-var-font-size-medium);
- font-weight: 500;
-}
-.MuiPaper-root + .MuiPagination-root ul li:first-child {
- margin-right: auto;
-}
-.MuiPaper-root + .MuiPagination-root ul li:last-child {
- margin-left: auto;
-}
-.MuiPaper-root + .MuiPagination-root ul li:first-child button {
- padding: 0 var(--env-var-spacing-1) 0 var(--env-var-spacing-1-plus);
-}
-.MuiPaper-root + .MuiPagination-root ul li:last-child button {
- padding: 0 var(--env-var-spacing-1-plus) 0 var(--env-var-spacing-1);
-}
-.MuiPaper-root + .MuiPagination-root ul li:first-child button::after,
-.MuiPaper-root + .MuiPagination-root ul li:last-child button::before {
- position: relative;
- display: inline-block;
-}
-.MuiPaper-root + .MuiPagination-root ul li:first-child button::after {
- content: "Previous";
- margin-left: 15px;
-}
-.MuiPaper-root + .MuiPagination-root ul li:last-child button::before {
- content: "Next";
- margin-right: 15px;
-}
-.MuiPaper-root + .MuiPagination-root div.MuiPaginationItem-root {
- user-select: none;
-}
-
-.MuiTablePagination-root p {
- font-weight: 500;
- font-size: var(--env-var-font-size-small-plus);
-}
-.MuiTablePagination-root .MuiTablePagination-select.MuiSelect-select {
- text-align: left;
- text-align-last: left;
-}
-.MuiTablePagination-root button {
- min-width: 0;
- padding: 4px;
- margin-left: 5px;
-}
-.MuiTablePagination-root svg {
- width: 22px;
- height: 22px;
-}
-.MuiTablePagination-root .MuiSelect-icon {
- width: 16px;
- height: 16px;
- top: 50%;
- right: 8%;
- transform: translateY(-50%);
-}
-.MuiTablePagination-root button.Mui-disabled {
- opacity: 0.4;
-}
-
-.table-container .MuiTable-root .MuiTableHead-root .MuiTableCell-root {
- text-transform: uppercase;
- opacity: 0.8;
- font-size: var(--env-var-font-size-small-plus);
- font-weight: 400;
-}
-.monitors .MuiTableCell-root:not(:first-of-type):not(:last-of-type),
-.monitors .MuiTableCell-root:not(:first-of-type):not(:last-of-type) {
- padding-left: var(--env-var-spacing-1);
- padding-right: var(--env-var-spacing-1);
-}
diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx
index 2c6579bc3..10648a3f3 100644
--- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx
+++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx
@@ -13,9 +13,14 @@ import { useTheme } from "@emotion/react";
import { useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { formatDateWithTz } from "../../../Utils/timeUtils";
-import "./index.css";
+import {
+ tooltipDateFormatLookup,
+ tickDateFormatLookup,
+} from "../Utils/chartUtilFunctions";
-const CustomToolTip = ({ active, payload, label }) => {
+import "./index.css";
+const CustomToolTip = ({ active, payload, label, dateRange }) => {
+ const format = tooltipDateFormatLookup(dateRange);
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
if (active && payload && payload.length) {
@@ -41,7 +46,7 @@ const CustomToolTip = ({ active, payload, label }) => {
fontWeight: 500,
}}
>
- {formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
+ {formatDateWithTz(label, format, uiTimezone)}
{
+const CustomTick = ({ x, y, payload, dateRange }) => {
+ const format = tickDateFormatLookup(dateRange);
const theme = useTheme();
-
const uiTimezone = useSelector((state) => state.ui.timezone);
- // Render nothing for the first tick
- if (index === 0) return null;
return (
{
fontSize={11}
fontWeight={400}
>
- {formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
+ {formatDateWithTz(payload?.value, format, uiTimezone)}
);
};
@@ -128,9 +132,10 @@ CustomTick.propTypes = {
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
+ dateRange: PropTypes.string,
};
-const MonitorDetailsAreaChart = ({ checks }) => {
+const MonitorDetailsAreaChart = ({ checks, dateRange }) => {
const theme = useTheme();
const memoizedChecks = useMemo(() => checks, [checks[0]]);
const [isHovered, setIsHovered] = useState(false);
@@ -184,16 +189,14 @@ const MonitorDetailsAreaChart = ({ checks }) => {
}
- minTickGap={0}
+ tick={}
axisLine={false}
tickLine={false}
height={20}
- interval="equidistantPreserveStart"
/>
}
+ content={}
wrapperStyle={{ pointerEvents: "none" }}
/>
{
MonitorDetailsAreaChart.propTypes = {
checks: PropTypes.array,
+ dateRange: PropTypes.string,
};
export default MonitorDetailsAreaChart;
diff --git a/Client/src/Components/Charts/Utils/chartUtilFunctions.js b/Client/src/Components/Charts/Utils/chartUtilFunctions.js
new file mode 100644
index 000000000..70f5ac9c3
--- /dev/null
+++ b/Client/src/Components/Charts/Utils/chartUtilFunctions.js
@@ -0,0 +1,25 @@
+export const tooltipDateFormatLookup = (dateRange) => {
+ const dateFormatLookup = {
+ day: "ddd. MMMM D, YYYY, hh:mm A",
+ week: "ddd. MMMM D, YYYY, hh:mm A",
+ month: "ddd. MMMM D, YYYY",
+ };
+ const format = dateFormatLookup[dateRange];
+ if (format === undefined) {
+ return "";
+ }
+ return format;
+};
+
+export const tickDateFormatLookup = (dateRange) => {
+ const tickFormatLookup = {
+ day: "h:mm A",
+ week: "MM/D, h:mm A",
+ month: "ddd. M/D",
+ };
+ const format = tickFormatLookup[dateRange];
+ if (format === undefined) {
+ return "";
+ }
+ return format;
+};
diff --git a/Client/src/Components/Charts/Utils/chartUtils.jsx b/Client/src/Components/Charts/Utils/chartUtils.jsx
index ced9fadbc..414e1ff9c 100644
--- a/Client/src/Components/Charts/Utils/chartUtils.jsx
+++ b/Client/src/Components/Charts/Utils/chartUtils.jsx
@@ -4,7 +4,7 @@ import { useTheme } from "@mui/material";
import { Text } from "recharts";
import { formatDateWithTz } from "../../../Utils/timeUtils";
import { Box, Stack, Typography } from "@mui/material";
-
+import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions";
/**
* Custom tick component for rendering time with timezone.
*
@@ -15,9 +15,10 @@ import { Box, Stack, Typography } from "@mui/material";
* @param {number} props.index - The index of the tick.
* @returns {JSX.Element} The rendered tick component.
*/
-export const TzTick = ({ x, y, payload, index }) => {
+export const TzTick = ({ x, y, payload, index, dateRange }) => {
const theme = useTheme();
const uiTimezone = useSelector((state) => state.ui.timezone);
+ const format = tickDateFormatLookup(dateRange);
return (
{
fontSize={11}
fontWeight={400}
>
- {formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
+ {formatDateWithTz(payload?.value, format, uiTimezone)}
);
};
@@ -37,6 +38,7 @@ TzTick.propTypes = {
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
+ dateRange: PropTypes.string,
};
/**
@@ -109,9 +111,12 @@ export const InfrastructureTooltip = ({
yIdx = -1,
yLabel,
dotColor,
+ dateRange,
}) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
+
+ const format = tooltipDateFormatLookup(dateRange);
if (active && payload && payload.length) {
const [hardwareType, metric] = yKey.split(".");
return (
@@ -133,7 +138,7 @@ export const InfrastructureTooltip = ({
fontWeight: 500,
}}
>
- {formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
+ {formatDateWithTz(label, format, uiTimezone)}
{
+export const TemperatureTooltip = ({
+ active,
+ payload,
+ label,
+ keys,
+ dotColor,
+ dateRange,
+}) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
+ const format = tooltipDateFormatLookup(dateRange);
const formatCoreKey = (key) => {
return key.replace(/^core(\d+)$/, "Core $1");
};
@@ -213,7 +227,7 @@ export const TemperatureTooltip = ({ active, payload, label, keys, dotColor }) =
fontWeight: 500,
}}
>
- {formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
+ {formatDateWithTz(label, format, uiTimezone)}
@@ -273,4 +287,6 @@ TemperatureTooltip.propTypes = {
PropTypes.string,
PropTypes.number,
]),
+ dotColor: PropTypes.string,
+ dateRange: PropTypes.string,
};
diff --git a/Client/src/Components/Inputs/Search/index.jsx b/Client/src/Components/Inputs/Search/index.jsx
index 2749d7f74..bbc1ab614 100644
--- a/Client/src/Components/Inputs/Search/index.jsx
+++ b/Client/src/Components/Inputs/Search/index.jsx
@@ -56,7 +56,6 @@ const Search = ({
disabled,
}) => {
const theme = useTheme();
-
return (
option.id}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
return (
diff --git a/Client/src/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx
index ad4dd1f9f..51c6fa2d6 100644
--- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx
+++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx
@@ -7,11 +7,10 @@ import { credentials } from "../../../Validation/validation";
import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import { useSelector } from "react-redux";
-import BasicTable from "../../BasicTable";
import Select from "../../Inputs/Select";
import LoadingButton from "@mui/lab/LoadingButton";
import { GenericDialog } from "../../Dialog/genericDialog";
-
+import DataTable from "../../Table/";
/**
* TeamPanel component manages the organization and team members,
* providing functionalities like renaming the organization, managing team members,
@@ -21,34 +20,47 @@ import { GenericDialog } from "../../Dialog/genericDialog";
*/
const TeamPanel = () => {
- const roleMap = {
- superadmin: "Super admin",
- admin: "Admin",
- user: "Team member",
- demo: "Demo User",
- };
-
const theme = useTheme();
const SPACING_GAP = theme.spacing(12);
- const { authToken /* , user */ } = useSelector((state) => state.auth);
- //TODO
- // const [orgStates, setOrgStates] = useState({
- // name: "Bluewave Labs",
- // isEdit: false,
- // });
+ const { authToken } = useSelector((state) => state.auth);
const [toInvite, setToInvite] = useState({
email: "",
role: ["0"],
});
- const [tableData, setTableData] = useState({});
+ const [data, setData] = useState([]);
const [members, setMembers] = useState([]);
const [filter, setFilter] = useState("all");
const [isDisabled, setIsDisabled] = useState(true);
const [errors, setErrors] = useState({});
const [isSendingInvite, setIsSendingInvite] = useState(false);
+ const headers = [
+ {
+ id: "name",
+ content: "Name",
+ render: (row) => {
+ return (
+
+
+ {row.firstName + " " + row.lastName}
+
+
+ Created {new Date(row.createdAt).toLocaleDateString()}
+
+
+ );
+ },
+ },
+ { id: "email", content: "Email", render: (row) => row.email },
+ {
+ id: "role",
+ content: "Role",
+ render: (row) => row.role,
+ },
+ ];
+
useEffect(() => {
const fetchTeam = async () => {
try {
@@ -67,6 +79,12 @@ const TeamPanel = () => {
}, [authToken]);
useEffect(() => {
+ const ROLE_MAP = {
+ superadmin: "Super admin",
+ admin: "Admin",
+ user: "Team member",
+ demo: "Demo User",
+ };
let team = members;
if (filter !== "all")
team = members.filter((member) => {
@@ -76,42 +94,14 @@ const TeamPanel = () => {
return member.role.includes(filter);
});
- const data = {
- cols: [
- { id: 1, name: "NAME" },
- { id: 2, name: "EMAIL" },
- { id: 3, name: "ROLE" },
- ],
- rows: team?.map((member, idx) => {
- const roles = member.role.map((role) => roleMap[role]).join(",");
- return {
- id: member._id,
- data: [
- {
- id: idx,
- data: (
-
-
- {member.firstName + " " + member.lastName}
-
-
- Created {new Date(member.createdAt).toLocaleDateString()}
-
-
- ),
- },
- { id: idx + 1, data: member.email },
- {
- id: idx + 2,
- data: roles,
- },
- ],
- };
- }),
- };
+ team = team.map((member) => ({
+ ...member,
+ id: member._id,
+ role: member.role.map((role) => ROLE_MAP[role]).join(","),
+ }));
+ setData(team);
+ }, [filter, members]);
- setTableData(data);
- }, [filter, members, roleMap, theme]);
useEffect(() => {
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
}, [errors, toInvite.email]);
@@ -248,12 +238,11 @@ const TeamPanel = () => {
Invite a team member
-
diff --git a/Client/src/Pages/Infrastructure/components/TablePagination/Actions/index.jsx b/Client/src/Components/Table/TablePagination/Actions/index.jsx
similarity index 85%
rename from Client/src/Pages/Infrastructure/components/TablePagination/Actions/index.jsx
rename to Client/src/Components/Table/TablePagination/Actions/index.jsx
index c684da145..c607fcb8b 100644
--- a/Client/src/Pages/Infrastructure/components/TablePagination/Actions/index.jsx
+++ b/Client/src/Components/Table/TablePagination/Actions/index.jsx
@@ -1,9 +1,9 @@
import PropTypes from "prop-types";
import { Box, Button } from "@mui/material";
-import LeftArrowDouble from "../../../../../assets/icons/left-arrow-double.svg?react";
-import RightArrowDouble from "../../../../../assets/icons/right-arrow-double.svg?react";
-import LeftArrow from "../../../../../assets/icons/left-arrow.svg?react";
-import RightArrow from "../../../../../assets/icons/right-arrow.svg?react";
+import LeftArrowDouble from "../../../../assets/icons/left-arrow-double.svg?react";
+import RightArrowDouble from "../../../../assets/icons/right-arrow-double.svg?react";
+import LeftArrow from "../../../../assets/icons/left-arrow.svg?react";
+import RightArrow from "../../../../assets/icons/right-arrow.svg?react";
TablePaginationActions.propTypes = {
count: PropTypes.number.isRequired,
diff --git a/Client/src/Pages/Infrastructure/components/TablePagination/index.jsx b/Client/src/Components/Table/TablePagination/index.jsx
similarity index 92%
rename from Client/src/Pages/Infrastructure/components/TablePagination/index.jsx
rename to Client/src/Components/Table/TablePagination/index.jsx
index 63a2567e6..66707494a 100644
--- a/Client/src/Pages/Infrastructure/components/TablePagination/index.jsx
+++ b/Client/src/Components/Table/TablePagination/index.jsx
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Stack, TablePagination, Typography } from "@mui/material";
import { TablePaginationActions } from "./Actions";
-import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react";
+import SelectorVertical from "../../../assets/icons/selector-vertical.svg?react";
Pagination.propTypes = {
monitorCount: PropTypes.number.isRequired, // Total number of items for pagination.
@@ -94,6 +94,13 @@ function Pagination({
"&.Mui-focused > div": {
backgroundColor: theme.palette.primary.main,
},
+ "& .MuiSelect-icon": {
+ // Add this style override
+ position: "absolute",
+ right: 0,
+ top: "50%",
+ transform: "translateY(-50%)",
+ },
},
},
}}
diff --git a/Client/src/Components/Table/index.jsx b/Client/src/Components/Table/index.jsx
new file mode 100644
index 000000000..9f5dbc5cb
--- /dev/null
+++ b/Client/src/Components/Table/index.jsx
@@ -0,0 +1,116 @@
+import {
+ Paper,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+} from "@mui/material";
+import PropTypes from "prop-types";
+import { useTheme } from "@emotion/react";
+
+/**
+ * @typedef {Object} Header
+ * @property {number|string} id - The unique identifier for the header.
+ * @property {React.ReactNode} content - The content to display in the header cell.
+ * @property {Function} render - A function to render the cell content for a given row.
+ */
+
+/**
+ * @typedef {Object} Config
+ * @property {Function} onRowClick - A function to be called when a row is clicked, receiving the row data as an argument.
+ * @property {Object} rowSX - Style object for the table row.
+ */
+
+/**
+ * DataTable component renders a table with headers and data.
+ *
+ * @param {Object} props - The component props.
+ * @param {Header[]} props.headers - An array of header objects, each containing an `id`, `content`, and `render` function.
+ * @param {Array} props.data - An array of data objects, each representing a row.
+ * @returns {JSX.Element} The rendered table component.
+ */
+
+const DataTable = ({ headers, data, config = { emptyView: "No data" } }) => {
+ const theme = useTheme();
+ if ((headers?.length ?? 0) === 0) {
+ return "No data";
+ }
+
+ return (
+
+
+
+
+ {headers.map((header, index) => (
+
+ {header.content}
+
+ ))}
+
+
+
+ {(data?.length ?? 0) === 0 ? (
+
+
+ {config.emptyView}
+
+
+ ) : (
+ data.map((row) => {
+ return (
+ config?.onRowClick(row)}
+ >
+ {headers.map((header, index) => {
+ return (
+
+ {header.render(row)}
+
+ );
+ })}
+
+ );
+ })
+ )}
+
+
+
+ );
+};
+
+DataTable.propTypes = {
+ headers: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
+ content: PropTypes.node.isRequired,
+ render: PropTypes.func.isRequired,
+ })
+ ).isRequired,
+ data: PropTypes.array.isRequired,
+ config: PropTypes.shape({
+ onRowClick: PropTypes.func.isRequired,
+ rowSX: PropTypes.object,
+ emptyView: PropTypes.node,
+ }),
+};
+
+export default DataTable;
diff --git a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
index 6cd91ac28..b2da2cbe6 100644
--- a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
+++ b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
@@ -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) {
diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
index 78250c19a..7ea34b9ea 100644
--- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
+++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
@@ -78,30 +78,6 @@ export const getUptimeMonitorById = createAsyncThunk(
}
);
-export const getUptimeMonitorsByTeamId = createAsyncThunk(
- "monitors/getMonitorsByTeamId",
- async (token, thunkApi) => {
- const user = jwtDecode(token);
- try {
- const res = await networkService.getMonitorsAndSummaryByTeamId({
- 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 updateUptimeMonitor = createAsyncThunk(
"monitors/updateMonitor",
async (data, thunkApi) => {
@@ -257,26 +233,6 @@ const uptimeMonitorsSlice = createSlice({
},
extraReducers: (builder) => {
builder
- // *****************************************************
- // Monitors by teamId
- // *****************************************************
-
- .addCase(getUptimeMonitorsByTeamId.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(getUptimeMonitorsByTeamId.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.msg;
- state.monitorsSummary = action.payload.data;
- })
- .addCase(getUptimeMonitorsByTeamId.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Getting uptime monitors failed";
- })
-
// *****************************************************
// Create Monitor
// *****************************************************
diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx
index 59273a5af..b89246cb8 100644
--- a/Client/src/Pages/Incidents/IncidentTable/index.jsx
+++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx
@@ -1,17 +1,5 @@
import PropTypes from "prop-types";
-import {
- TableContainer,
- Table,
- TableHead,
- TableRow,
- TableCell,
- TableBody,
- Pagination,
- PaginationItem,
- Paper,
- Typography,
- Box,
-} from "@mui/material";
+import { Pagination, PaginationItem, Typography, Box } from "@mui/material";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
@@ -27,7 +15,7 @@ import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?re
import { HttpStatusLabel } from "../../../Components/HttpStatusLabel";
import { Empty } from "./Empty/Empty";
import { IncidentSkeleton } from "./Skeleton/Skeleton";
-
+import DataTable from "../../../Components/Table";
const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
@@ -106,6 +94,46 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
});
};
+ const headers = [
+ {
+ id: "monitorName",
+ content: "Monitor Name",
+ render: (row) => monitors[row.monitorId]?.name ?? "N/A",
+ },
+ {
+ id: "status",
+ content: "Status",
+ render: (row) => {
+ const status = row.status === true ? "up" : "down";
+ return (
+
+ );
+ },
+ },
+ {
+ id: "dateTime",
+ content: "Date & Time",
+ render: (row) => {
+ const formattedDate = formatDateWithTz(
+ row.createdAt,
+ "YYYY-MM-DD HH:mm:ss A",
+ uiTimezone
+ );
+ return formattedDate;
+ },
+ },
+ {
+ id: "statusCode",
+ content: "Status Code",
+ render: (row) => ,
+ },
+ { id: "message", content: "Message", render: (row) => row.message },
+ ];
+
let paginationComponent = <>>;
if (checksCount > paginationController.rowsPerPage) {
paginationComponent = (
@@ -166,52 +194,11 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
) : (
<>
-
-
-
-
- Monitor Name
- Status
- Date & Time
- Status Code
- Message
-
-
-
- {checks.map((check) => {
- const status = check.status === true ? "up" : "down";
- const formattedDate = formatDateWithTz(
- check.createdAt,
- "YYYY-MM-DD HH:mm:ss A",
- uiTimezone
- );
+
- return (
-
- {monitors[check.monitorId]?.name}
-
-
-
- {formattedDate}
-
-
-
- {check.message}
-
- );
- })}
-
-
-
{paginationComponent}
>
)}
diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx
index baac880f1..86f5afc5f 100644
--- a/Client/src/Pages/Incidents/index.jsx
+++ b/Client/src/Pages/Incidents/index.jsx
@@ -29,7 +29,7 @@ const Incidents = () => {
const res = await networkService.getMonitorsByTeamId({
authToken: authState.authToken,
teamId: authState.user.teamId,
- limit: -1,
+ limit: null,
types: null,
status: null,
checkOrder: null,
@@ -40,7 +40,6 @@ const Incidents = () => {
field: null,
order: null,
});
- // Reduce to a lookup object for 0(1) lookup
if (res?.data?.data?.monitors?.length > 0) {
const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => {
acc[monitor._id] = monitor;
@@ -56,7 +55,7 @@ const Incidents = () => {
}
};
fetchMonitors();
- }, [authState]);
+ }, [authState, monitorId]);
useEffect(() => {}, [monitors]);
diff --git a/Client/src/Pages/Infrastructure/Details/index.jsx b/Client/src/Pages/Infrastructure/Details/index.jsx
index af7586d0b..d38be8098 100644
--- a/Client/src/Pages/Infrastructure/Details/index.jsx
+++ b/Client/src/Pages/Infrastructure/Details/index.jsx
@@ -13,9 +13,8 @@ import { useNavigate } from "react-router-dom";
import Empty from "./empty";
import { logger } from "../../../Utils/Logger";
import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils";
+import { TzTick, PercentTick } from "../../../Components/Charts/Utils/chartUtils";
import {
- TzTick,
- PercentTick,
InfrastructureTooltip,
TemperatureTooltip,
} from "../../../Components/Charts/Utils/chartUtils";
@@ -414,12 +413,13 @@ const InfrastructureDetails = () => {
yLabel: "Memory usage",
yDomain: [0, 1],
yTick: ,
- xTick: ,
+ xTick: ,
toolTip: (
),
},
@@ -433,12 +433,13 @@ const InfrastructureDetails = () => {
yLabel: "CPU usage",
yDomain: [0, 1],
yTick: ,
- xTick: ,
+ xTick: ,
toolTip: (
),
},
@@ -450,7 +451,7 @@ const InfrastructureDetails = () => {
gradientStartColor: theme.palette.error.main,
heading: "CPU Temperature",
yLabel: "Temperature",
- xTick: ,
+ xTick: ,
yDomain: [
0,
Math.max(
@@ -462,6 +463,7 @@ const InfrastructureDetails = () => {
),
},
@@ -476,13 +478,14 @@ const InfrastructureDetails = () => {
yLabel: "Disk Usage",
yDomain: [0, 1],
yTick: ,
- xTick: ,
+ xTick: ,
toolTip: (
),
})) || []),
diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx
index 99ac77afa..8ffc01685 100644
--- a/Client/src/Pages/Infrastructure/index.jsx
+++ b/Client/src/Pages/Infrastructure/index.jsx
@@ -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";
@@ -8,23 +8,12 @@ import SkeletonLayout from "./skeleton";
import Fallback from "../../Components/Fallback";
// import GearIcon from "../../Assets/icons/settings-bold.svg?react";
import CPUChipIcon from "../../assets/icons/cpu-chip.svg?react";
-import {
- Box,
- Button,
- IconButton,
- Paper,
- Stack,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
-} from "@mui/material";
+import DataTable from "../../Components/Table";
+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/TablePagination";
+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";
@@ -32,42 +21,8 @@ import Host from "../Uptime/Home/host.jsx";
import { useIsAdmin } from "../../Hooks/useIsAdmin.js";
import { InfrastructureMenu } from "./components/Menu";
-const columns = [
- { label: "Host" },
- { label: "Status" },
- { label: "Frequency" },
- { label: "CPU" },
- { label: "Mem" },
- { label: "Disk" },
- { label: "Actions" },
-];
-
const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }];
-/* TODO
-Create reusable table component.
-It should receive as a parameter the following object:
-tableData = [
- columns = [
- {
- id: example,
- label: Example Extendable,
- align: "center" | "left" (default)
- }
- ],
- rows: [
- {
- **Number of keys will be equal to number of columns**
- key1: string,
- key2: number,
- key3: Component
- }
- ]
-]
-Apply to Monitor Table, and Account/Team.
-Analyze existing BasicTable
-*/
-
/**
* This is the Infrastructure monitoring page. This is a work in progress
*
@@ -95,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({
@@ -108,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?.filteredMonitors ?? []);
+ 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}`);
@@ -139,6 +89,71 @@ function Infrastructure() {
fetchMonitors();
}
+ const headers = [
+ {
+ id: "host",
+ content: "Host",
+ render: (row) => (
+
+ ),
+ },
+ {
+ id: "status",
+ content: "Status",
+ render: (row) => (
+
+ ),
+ },
+ {
+ id: "frequency",
+ content: "Frequency",
+ render: (row) => (
+
+
+ {row.processor}
+
+ ),
+ },
+ { id: "cpu", content: "CPU", render: (row) => },
+ { id: "mem", content: "Mem", render: (row) => },
+ { id: "disk", content: "Disk", render: (row) => },
+ {
+ id: "actions",
+ content: "Actions",
+ render: (row) => (
+
+
+
+ ),
+ },
+ ];
+
const monitorsAsRows = monitors.map((monitor) => {
const processor =
((monitor.checks[0]?.cpu?.usage_frequency ?? 0) / 1000).toFixed(2) + " GHz";
@@ -171,7 +186,7 @@ function Infrastructure() {
};
});
- let isActuallyLoading = isLoading && monitorState.monitors?.length === 0;
+ let isActuallyLoading = isLoading && monitors?.length === 0;
return (
{isActuallyLoading ? (
- ) : monitorState.monitors?.length !== 0 ? (
+ ) : monitors?.length !== 0 ? (
@@ -234,108 +249,26 @@ function Infrastructure() {
borderColor={theme.palette.primary.lowContrast}
backgroundColor={theme.palette.tertiary.main}
>
- {totalMonitors}
+ {summary?.totalMonitors ?? 0}
-
-
-
-
- {columns.map((column, index) => (
-
- {column.label}
-
- ))}
-
-
-
- {monitorsAsRows.map((row) => {
- return (
- openDetails(row.id)}
- sx={{
- cursor: "pointer",
- "&:hover": {
- backgroundColor: theme.palette.tertiary.main,
- },
- }}
- >
- {/* TODO iterate over column and get column id, applying row[column.id] */}
-
-
-
-
-
-
-
-
-
- {row.processor}
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Get ActionsMenu from Monitor Table and create a component */}
-
-
- {/* */}
-
-
-
- );
- })}
-
-
-
+
+ openDetails(row.id),
+ }}
+ headers={headers}
+ data={monitorsAsRows}
+ />
+
{
sx={{ gridColumnStart: 1, gridColumnEnd: 4 }}
>
diff --git a/Client/src/Pages/PageSpeed/index.jsx b/Client/src/Pages/PageSpeed/index.jsx
index da07aed70..c15ed5296 100644
--- a/Client/src/Pages/PageSpeed/index.jsx
+++ b/Client/src/Pages/PageSpeed/index.jsx
@@ -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,18 +30,15 @@ const PageSpeed = () => {
teamId: user.teamId,
limit: 10,
types: ["pagespeed"],
- status: null,
- checkOrder: "desc",
- normalize: true,
page: null,
rowsPerPage: null,
filter: null,
field: null,
order: null,
});
- if (res?.data?.data?.monitors) {
- setMonitors(res.data.data.monitors);
- setMonitorCount(res.data.data.monitorCount);
+ if (res?.data?.data?.filteredMonitors) {
+ setMonitors(res.data.data.filteredMonitors);
+ setSummary(res.data.data.summary);
}
} catch (error) {
console.log(error);
@@ -118,7 +110,7 @@ const PageSpeed = () => {
borderColor={theme.palette.primary.lowContrast}
backgroundColor={theme.palette.tertiary.main}
>
- {monitorCount}
+ {summary?.totalMonitors ?? 0}
{
const action = await dispatch(updateUptimeMonitor({ authToken, monitor: monitor }));
if (action.meta.requestStatus === "fulfilled") {
createToast({ body: "Monitor updated successfully!" });
- dispatch(getUptimeMonitorsByTeamId(authToken));
} else {
createToast({ body: "Failed to update monitor." });
}
diff --git a/Client/src/Pages/Uptime/CreateUptime/index.jsx b/Client/src/Pages/Uptime/CreateUptime/index.jsx
index c5a9df770..3499a9c73 100644
--- a/Client/src/Pages/Uptime/CreateUptime/index.jsx
+++ b/Client/src/Pages/Uptime/CreateUptime/index.jsx
@@ -58,7 +58,7 @@ const CreateMonitor = () => {
};
const { user, authToken } = useSelector((state) => state.auth);
- const { monitors, isLoading } = useSelector((state) => state.uptimeMonitors);
+ const { isLoading } = useSelector((state) => state.uptimeMonitors);
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
@@ -205,7 +205,7 @@ const CreateMonitor = () => {
}
};
fetchMonitor();
- }, [monitorId, authToken, monitors, dispatch, navigate]);
+ }, [monitorId, authToken, dispatch, navigate]);
return (
diff --git a/Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx b/Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx
index f7f864205..28b33d61a 100644
--- a/Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx
+++ b/Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx
@@ -4,7 +4,7 @@ import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
import PropTypes from "prop-types";
import CustomLabels from "./CustomLabels";
-const DownBarChart = memo(({ stats, type, onBarHover }) => {
+const DownBarChart = memo(({ monitor, type, onBarHover }) => {
const theme = useTheme();
const [chartHovered, setChartHovered] = useState(false);
@@ -19,7 +19,7 @@ const DownBarChart = memo(({ stats, type, onBarHover }) => {
{
setChartHovered(true);
onBarHover({ time: null, totalChecks: 0 });
@@ -40,8 +40,10 @@ const DownBarChart = memo(({ stats, type, onBarHover }) => {
y={0}
width="100%"
height="100%"
- firstDataPoint={stats.downChecks?.[0] ?? {}}
- lastDataPoint={stats.downChecks?.[stats.downChecks.length - 1] ?? {}}
+ firstDataPoint={monitor.groupedDownChecks?.[0] ?? {}}
+ lastDataPoint={
+ monitor.groupedDownChecks?.[monitor.groupedDownChecks.length - 1] ?? {}
+ }
type={type}
/>
}
@@ -51,7 +53,7 @@ const DownBarChart = memo(({ stats, type, onBarHover }) => {
maxBarSize={7}
background={{ fill: "transparent" }}
>
- {stats.downChecks.map((entry, index) => (
+ {monitor.groupedDownChecks.map((entry, index) => (
{
DownBarChart.displayName = "DownBarChart";
DownBarChart.propTypes = {
- stats: PropTypes.shape({
- downChecks: PropTypes.arrayOf(PropTypes.object),
- downChecksAggregate: PropTypes.object,
+ monitor: PropTypes.shape({
+ groupedDownChecks: PropTypes.arrayOf(PropTypes.object),
}),
type: PropTypes.string,
onBarHover: PropTypes.func,
diff --git a/Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx b/Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx
index d3bb3aca2..472aec31f 100644
--- a/Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx
+++ b/Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx
@@ -14,7 +14,7 @@ const getThemeColor = (responseTime) => {
}
};
-const UpBarChart = memo(({ stats, type, onBarHover }) => {
+const UpBarChart = memo(({ monitor, type, onBarHover }) => {
const theme = useTheme();
const [chartHovered, setChartHovered] = useState(false);
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
@@ -36,7 +36,7 @@ const UpBarChart = memo(({ stats, type, onBarHover }) => {
{
setChartHovered(true);
onBarHover({ time: null, totalChecks: 0, avgResponseTime: 0 });
@@ -57,8 +57,8 @@ const UpBarChart = memo(({ stats, type, onBarHover }) => {
y={0}
width="100%"
height="100%"
- firstDataPoint={stats.upChecks[0]}
- lastDataPoint={stats.upChecks[stats.upChecks.length - 1]}
+ firstDataPoint={monitor.groupedUpChecks[0]}
+ lastDataPoint={monitor.groupedUpChecks[monitor.groupedUpChecks.length - 1]}
type={type}
/>
}
@@ -68,10 +68,8 @@ const UpBarChart = memo(({ stats, type, onBarHover }) => {
maxBarSize={7}
background={{ fill: "transparent" }}
>
- {stats.upChecks.map((entry, index) => {
- /* let { main, light } = getColorRange(entry.avgResponseTime); */
+ {monitor.groupedUpChecks.map((entry, index) => {
const themeColor = getThemeColor(entry.avgResponseTime);
- console.log(themeColor);
return (
| {
gradient={true}
status={determineState(monitor)}
heading={"active for"}
- subHeading={splitDuration(monitor?.stats?.timeSinceLastFalseCheck)}
+ subHeading={splitDuration(monitor?.uptimeStreak)}
/>
- {monitor?.stats?.latestResponseTime}
+ {monitor?.latestResponseTime}
{"ms"}
>
}
@@ -328,7 +328,9 @@ const DetailsPage = () => {
{hoveredUptimeData !== null
? hoveredUptimeData.totalChecks
- : (monitor.stats?.upChecksAggregate?.totalChecks ?? 0)}
+ : (monitor?.groupedUpChecks?.reduce((count, checkGroup) => {
+ return count + checkGroup.totalChecks;
+ }, 0) ?? 0)}
{hoveredUptimeData !== null && hoveredUptimeData.time !== null && (
{
{hoveredUptimeData !== null
? Math.floor(hoveredUptimeData?.avgResponseTime ?? 0)
: Math.floor(
- ((monitor?.stats?.upChecksAggregate?.totalChecks ?? 0) /
- (monitor?.stats?.totalChecks ?? 1)) *
+ ((monitor?.upChecks?.totalChecks ?? 0) /
+ (monitor?.totalChecks ?? 1)) *
100
)}
@@ -367,7 +369,7 @@ const DetailsPage = () => {
| |
@@ -384,7 +386,9 @@ const DetailsPage = () => {
{hoveredIncidentsData !== null
? hoveredIncidentsData.totalChecks
- : (monitor.stats?.downChecksAggregate?.totalChecks ?? 0)}
+ : (monitor?.groupedDownChecks?.reduce((count, checkGroup) => {
+ return count + checkGroup.totalChecks;
+ }, 0) ?? 0)}
{hoveredIncidentsData !== null &&
hoveredIncidentsData.time !== null && (
@@ -404,7 +408,7 @@ const DetailsPage = () => {
)}
@@ -416,9 +420,7 @@ const DetailsPage = () => {
Average Response Time
-
+
{
Response Times
-
+
{
- const theme = useTheme();
- const [search, setSearch] = useState("");
- const [isSearching, setIsSearching] = useState(false);
- const debouncedFilter = useDebounce(search, 500);
- const handleSearch = (value) => {
- setIsSearching(true);
- setSearch(value);
- };
- return (
-
-
-
- Uptime monitors
-
-
-
- {totalMonitors}
-
-
-
-
-
-
-
- );
-};
-
-CurrentMonitoring.propTypes = {
- handlePause: PropTypes.func,
- totalMonitors: PropTypes.number,
- monitors: PropTypes.array,
- isAdmin: PropTypes.bool,
-};
-
-export { CurrentMonitoring };
diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx b/Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx
new file mode 100644
index 000000000..c17f6d74b
--- /dev/null
+++ b/Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx
@@ -0,0 +1,47 @@
+import { Skeleton } from "@mui/material";
+import DataTable from "../../../../../Components/Table";
+const ROWS_NUMBER = 7;
+const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i);
+
+const TableSkeleton = () => {
+ /* TODO Skeleton does not follow light and dark theme */
+
+ const headers = [
+ {
+ id: "name",
+
+ content: "Host",
+
+ render: () => ,
+ },
+ {
+ id: "status",
+ content: "Status",
+ render: () => ,
+ },
+ {
+ id: "responseTime",
+ content: "Response Time",
+ render: () => ,
+ },
+ {
+ id: "type",
+ content: "Type",
+ render: () => ,
+ },
+ {
+ id: "actions",
+ content: "Actions",
+ render: () => ,
+ },
+ ];
+
+ return (
+
+ );
+};
+
+export { TableSkeleton };
diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx
new file mode 100644
index 000000000..a184eb09f
--- /dev/null
+++ b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx
@@ -0,0 +1,317 @@
+// Components
+import { Box, Stack, CircularProgress } from "@mui/material";
+import Search from "../../../../Components/Inputs/Search";
+import { Heading } from "../../../../Components/Heading";
+import DataTable from "../../../../Components/Table";
+import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
+import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
+import Host from "../host";
+import { StatusLabel } from "../../../../Components/Label";
+import BarChart from "../../../../Components/Charts/BarChart";
+import ActionsMenu from "../actionsMenu";
+
+// Utils
+import { useTheme } from "@emotion/react";
+import useUtils from "../../utils";
+import { useState, memo, useCallback } from "react";
+import { useNavigate } from "react-router-dom";
+import "../index.css";
+import PropTypes from "prop-types";
+
+const SearchComponent = memo(
+ ({ monitors, debouncedSearch, onSearchChange, setIsSearching }) => {
+ const [localSearch, setLocalSearch] = useState(debouncedSearch);
+ const handleSearch = useCallback(
+ (value) => {
+ setIsSearching(true);
+ setLocalSearch(value);
+ onSearchChange(value);
+ },
+ [onSearchChange, setIsSearching]
+ );
+
+ return (
+
+
+
+ );
+ }
+);
+SearchComponent.displayName = "SearchComponent";
+SearchComponent.propTypes = {
+ monitors: PropTypes.array,
+ debouncedSearch: PropTypes.string,
+ onSearchChange: PropTypes.func,
+ setIsSearching: PropTypes.func,
+};
+
+/**
+ * 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,
+ monitors,
+ filteredMonitors,
+ monitorCount,
+ sort,
+ setSort,
+ debouncedSearch,
+ setSearch,
+ isSearching,
+ setIsSearching,
+ setIsLoading,
+ triggerUpdate,
+}) => {
+ const { determineState } = useUtils();
+
+ const theme = useTheme();
+ const navigate = useNavigate();
+
+ const handleSort = (field) => {
+ let order = "";
+ if (sort.field !== field) {
+ order = "desc";
+ } else {
+ order = sort.order === "asc" ? "desc" : "asc";
+ }
+ setSort({ field, order });
+ };
+
+ const headers = [
+ {
+ id: "name",
+ content: (
+ handleSort("name")}
+ >
+ Host
+
+ {sort.order === "asc" ? (
+
+ ) : (
+
+ )}
+
+
+ ),
+ render: (row) => (
+
+ ),
+ },
+ {
+ id: "status",
+ content: (
+ handleSort("status")}
+ >
+ {" "}
+ Status
+
+ {sort.order === "asc" ? (
+
+ ) : (
+
+ )}
+
+
+ ),
+ render: (row) => {
+ const status = determineState(row.monitor);
+ return (
+
+ );
+ },
+ },
+ {
+ id: "responseTime",
+ content: "Response Time",
+ render: (row) => ,
+ },
+ {
+ id: "type",
+ content: "Type",
+ render: (row) => (
+ {row.monitor.type}
+ ),
+ },
+ {
+ id: "actions",
+ content: "Actions",
+ render: (row) => (
+
+ ),
+ },
+ ];
+
+ return (
+
+
+ Uptime monitors
+
+
+ {monitorCount}
+
+
+
+
+
+ {(isSearching || isLoading) && (
+ <>
+
+
+
+
+ >
+ )}
+ {
+ navigate(`/uptime/${row.id}`);
+ },
+ emptyView: "No monitors found",
+ }}
+ />
+
+
+ );
+};
+
+const MemoizedUptimeDataTable = memo(UptimeDataTable);
+export default MemoizedUptimeDataTable;
+
+UptimeDataTable.propTypes = {
+ isAdmin: PropTypes.bool,
+ isLoading: PropTypes.bool,
+ monitors: PropTypes.array,
+ filteredMonitors: PropTypes.array,
+ monitorCount: PropTypes.number,
+ sort: PropTypes.shape({
+ field: PropTypes.string,
+ order: PropTypes.oneOf(["asc", "desc"]),
+ }),
+ setSort: PropTypes.func,
+ debouncedSearch: PropTypes.string,
+ setSearch: PropTypes.func,
+ isSearching: PropTypes.bool,
+ setIsSearching: PropTypes.func,
+ setIsLoading: PropTypes.func,
+ triggerUpdate: PropTypes.func,
+};
diff --git a/Client/src/Pages/Uptime/Home/UptimeTable/Skeleton/index.jsx b/Client/src/Pages/Uptime/Home/UptimeTable/Skeleton/index.jsx
deleted file mode 100644
index 22d885c97..000000000
--- a/Client/src/Pages/Uptime/Home/UptimeTable/Skeleton/index.jsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Skeleton, TableCell, TableRow } from "@mui/material";
-const ROWS_NUMBER = 7;
-const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i);
-
-const TableBodySkeleton = () => {
- /* TODO Skeleton does not follow light and dark theme */
- return (
- <>
- {ROWS_ARRAY.map((row) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
- >
- );
-};
-
-export { TableBodySkeleton };
diff --git a/Client/src/Pages/Uptime/Home/UptimeTable/index.jsx b/Client/src/Pages/Uptime/Home/UptimeTable/index.jsx
deleted file mode 100644
index ea64bdcc9..000000000
--- a/Client/src/Pages/Uptime/Home/UptimeTable/index.jsx
+++ /dev/null
@@ -1,344 +0,0 @@
-import PropTypes from "prop-types";
-import { useState, useEffect, memo, useCallback, useRef } from "react";
-import { useNavigate } from "react-router";
-import { useDispatch, useSelector } from "react-redux";
-import { useTheme } from "@emotion/react";
-import useUtils from "../../utils";
-
-import { setRowsPerPage } from "../../../../Features/UI/uiSlice";
-import { logger } from "../../../../Utils/Logger";
-import { jwtDecode } from "jwt-decode";
-import { networkService } from "../../../../main";
-
-import {
- TableContainer,
- Table,
- TableHead,
- TableRow,
- TableCell,
- TableBody,
- Paper,
- Box,
- CircularProgress,
-} from "@mui/material";
-import ActionsMenu from "../actionsMenu";
-import Host from "../host";
-import { StatusLabel } from "../../../../Components/Label";
-import { TableBodySkeleton } from "./Skeleton";
-import BarChart from "../../../../Components/Charts/BarChart";
-
-import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
-import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
-
-import { Pagination } from "../../../Infrastructure/components/TablePagination";
-
-const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching, handlePause }) => {
- const theme = useTheme();
- const navigate = useNavigate();
- const dispatch = useDispatch();
- const { determineState } = useUtils();
-
- const { rowsPerPage } = useSelector((state) => state.ui.monitors);
- const authState = useSelector((state) => state.auth);
-
- const [page, setPage] = useState(0);
- const [monitors, setMonitors] = useState([]);
- const [monitorCount, setMonitorCount] = useState(0);
- const [updateTrigger, setUpdateTrigger] = useState(false);
- const [sort, setSort] = useState({});
- const prevFilter = useRef(filter);
-
- const handleRowUpdate = () => {
- setUpdateTrigger((prev) => !prev);
- };
-
- const handleChangePage = (event, newPage) => {
- setPage(newPage);
- };
-
- const handleChangeRowsPerPage = (event) => {
- dispatch(
- setRowsPerPage({
- value: parseInt(event.target.value, 10),
- table: "monitors",
- })
- );
- setPage(0);
- };
-
- const fetchPage = useCallback(async () => {
- try {
- const { authToken } = authState;
- const user = jwtDecode(authToken);
- const res = await networkService.getMonitorsByTeamId({
- authToken,
- teamId: user.teamId,
- limit: 25,
- types: ["http", "ping", "docker", "port"],
- status: null,
- checkOrder: "desc",
- normalize: true,
- page: page,
- rowsPerPage: rowsPerPage,
- filter: filter,
- field: sort.field,
- order: sort.order,
- });
- setMonitors(res?.data?.data?.monitors ?? []);
- setMonitorCount(res?.data?.data?.monitorCount ?? 0);
- } catch (error) {
- logger.error(error);
- } finally {
- setIsSearching(false);
- }
- }, [authState, page, rowsPerPage, filter, sort, setIsSearching]);
-
- useEffect(() => {
- fetchPage();
- }, [
- updateTrigger,
- authState,
- page,
- rowsPerPage,
- filter,
- sort,
- setIsSearching,
- fetchPage,
- ]);
-
- // Listen for changes in filter, if new value reset the page
- useEffect(() => {
- if (prevFilter.current !== filter) {
- setPage(0);
- fetchPage();
- }
- prevFilter.current = filter;
- }, [filter, fetchPage]);
-
- const handleSort = async (field) => {
- let order = "";
- if (sort.field !== field) {
- order = "desc";
- } else {
- order = sort.order === "asc" ? "desc" : "asc";
- }
- setSort({ field, order });
-
- const { authToken } = authState;
- const user = jwtDecode(authToken);
-
- const res = await networkService.getMonitorsByTeamId({
- authToken,
- teamId: user.teamId,
- limit: 25,
- types: ["http", "ping"],
- status: null,
- checkOrder: "desc",
- normalize: true,
- page: page,
- rowsPerPage: rowsPerPage,
- filter: null,
- field: field,
- order: order,
- });
- setMonitors(res?.data?.data?.monitors ?? []);
- setMonitorCount(res?.data?.data?.monitorCount ?? 0);
- };
- /* TODO Apply component basic table? */
- return (
-
- {isSearching && (
- <>
-
-
-
-
- >
- )}
-
-
-
-
- handleSort("name")}
- >
-
- Host
-
- {sort.order === "asc" ? (
-
- ) : (
-
- )}
-
-
-
- handleSort("status")}
- >
- {" "}
-
- {" "}
- Status
-
- {sort.order === "asc" ? (
-
- ) : (
-
- )}
-
-
-
- Response Time
- Type
- Actions
-
-
-
- {/* TODO add empty state. Check if is searching, and empty => skeleton. Is empty, not searching => skeleton */}
- {monitors.length > 0 ? (
- monitors.map((monitor) => {
- let uptimePercentage = "";
- let percentageColor = theme.palette.success.lowContrast;
-
- // Determine uptime percentage and color based on the monitor's uptimePercentage value
- if (monitor.uptimePercentage !== undefined) {
- uptimePercentage =
- monitor.uptimePercentage === 0
- ? "0"
- : (monitor.uptimePercentage * 100).toFixed(2);
-
- percentageColor =
- /* TODO colors update */
- monitor.uptimePercentage < 0.25
- ? theme.palette.error.main
- : monitor.uptimePercentage < 0.5
- ? theme.palette.percentage.uptimeFair
- : monitor.uptimePercentage < 0.75
- ? theme.palette.percentage.uptimeGood
- : theme.palette.success.main;
- }
-
- const params = {
- url: monitor.url,
- title: monitor.name,
- percentage: uptimePercentage,
- percentageColor,
- status: determineState(monitor),
- };
-
- return (
- {
- navigate(`/uptime/${monitor._id}`);
- }}
- >
-
-
-
-
-
-
-
-
-
-
- {monitor.type}
-
-
-
-
-
- );
- })
- ) : (
-
- )}
-
-
-
-
-
- );
-};
-
-MonitorTable.propTypes = {
- isAdmin: PropTypes.bool,
- filter: PropTypes.string,
- setIsSearching: PropTypes.func,
- isSearching: PropTypes.bool,
- setMonitorUpdateTrigger: PropTypes.func,
- handlePause: PropTypes.func,
-};
-
-const MemoizedMonitorTable = memo(MonitorTable);
-export default MemoizedMonitorTable;
diff --git a/Client/src/Pages/Uptime/Home/actionsMenu.jsx b/Client/src/Pages/Uptime/Home/actionsMenu.jsx
index d1dd334ec..493c5f25e 100644
--- a/Client/src/Pages/Uptime/Home/actionsMenu.jsx
+++ b/Client/src/Pages/Uptime/Home/actionsMenu.jsx
@@ -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,8 +38,7 @@ const ActionsMenu = ({ monitor, isAdmin, updateRowCallback, pauseCallback }) =>
);
if (action.meta.requestStatus === "fulfilled") {
setIsOpen(false); // close modal
- dispatch(getUptimeMonitorsByTeamId(authState.authToken));
- updateCallback();
+ updateRowCallback();
createToast({ body: "Monitor deleted successfully." });
} else {
createToast({ body: "Failed to delete monitor." });
@@ -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 })
);
@@ -174,6 +179,8 @@ const ActionsMenu = ({ monitor, isAdmin, updateRowCallback, pauseCallback }) =>
{isAdmin && (
- {loading ? (
-
- ) : (
+ {
<>
- {noMonitors && }
- {hasMonitors && (
- <>
-
- }
+ {isLoading ? (
+
+ ) : (
+ hasMonitors && (
+ <>
+
+
+
+
+
+
-
-
-
-
- >
+ >
+ )
)}
>
- )}
+ }
);
};
-UptimeMonitors.propTypes = {
- isAdmin: PropTypes.bool,
-};
export default UptimeMonitors;
diff --git a/Client/src/Routes/index.jsx b/Client/src/Routes/index.jsx
index 7d444f2ab..b5f4dd376 100644
--- a/Client/src/Routes/index.jsx
+++ b/Client/src/Routes/index.jsx
@@ -48,6 +48,7 @@ const Routes = () => {
path="/uptime"
element={}
/>
+
}
diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js
index 4812947d0..a47ed5221 100644
--- a/Client/src/Utils/NetworkService.js
+++ b/Client/src/Utils/NetworkService.js
@@ -3,6 +3,8 @@ const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
const FALLBACK_BASE_URL = "http://localhost:5000/api/v1";
import { clearAuthState } from "../Features/Auth/authSlice";
import { clearUptimeMonitorState } from "../Features/UptimeMonitors/uptimeMonitorsSlice";
+import { createToast } from "./toastUtils";
+
class NetworkService {
constructor(store, dispatch, navigate) {
this.store = store;
@@ -25,11 +27,7 @@ class NetworkService {
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
- if (error.response && error.response.status === 401) {
- dispatch(clearAuthState());
- dispatch(clearUptimeMonitorState());
- navigate("/login");
- }
+ this.handleError(error);
return Promise.reject(error);
}
);
@@ -39,6 +37,30 @@ class NetworkService {
this.axiosInstance.defaults.baseURL = url;
};
+ handleError(error) {
+ if (error.response) {
+ const status = error.response.status;
+ if (status === 401) {
+ this.dispatch(clearAuthState());
+ this.dispatch(clearUptimeMonitorState());
+ this.navigate("/login");
+ } else if (status >= 500) {
+ // Display toast for server errors to all users
+ createToast({
+ variant: "error",
+ body: "Checkmate server is not running or has issues. Please check.",
+ });
+ }
+ } else if (error.request) {
+ // Show a toast informing the user the server didn't respond
+ createToast({
+ variant: "error",
+ body: "The server did not respond. Please check your network or try again later.",
+ });
+ }
+ }
+
+
cleanup() {
if (this.unsubscribe) {
this.unsubscribe();
@@ -127,7 +149,7 @@ class NetworkService {
* @param {Array} config.types - Array of monitor types
* @returns {Promise} The response from the axios POST request.
*/
- async getMonitorsAndSummaryByTeamId(config) {
+ async getMonitorsSummaryByTeamId(config) {
const params = new URLSearchParams();
if (config.types) {
@@ -157,9 +179,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} [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 +186,10 @@ class NetworkService {
* @param {string} [config.order] - The order in which to sort the field.
* @returns {Promise} 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 +199,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);
diff --git a/README.md b/README.md
index b0f158451..0927290b1 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,9 @@
-# **We're opening our $5000 grant funding announcement soon, in partnership with [UpRock](https://uprock.com) - check [our web page](https://checkmate.so) for preliminary details.**
+# **Announcing $5000 grant funding in partnership with [UpRock](https://uprock.com) - check [our web page](https://checkmate.so) for funding ideas**
-**If you would like to support us, please consider giving it a ⭐ and click on "watch" so you can latest news from us. Need support or have a suggestion? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.**
+- If you would like to support us, please consider giving it a ⭐ and click on "watch".
+- Need support or have a suggestion? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.
+- Need a ping when there's a new release? Use [Newreleases](https://newreleases.io/), a free service to track releases.

@@ -12,6 +14,7 @@



+[](https://www.bestpractices.dev/projects/9901)
@@ -81,11 +84,12 @@ If you have any questions, suggestions or comments, please use our [Discord chan
- [MUI (React framework)](https://mui.com/)
- [Node.js](https://nodejs.org/en)
- [MongoDB](https://mongodb.com)
+- [Recharts](https://recharts.org)
- Lots of other open source components!
## 🤝 Contributing
-We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 2700 stars and attracted almost 40 contributors from around the globe. So, don’t hold back — jump in, contribute and learn with us!
+We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 3300 stars and attracted 40+ contributors from around the globe. So, don’t hold back — jump in, contribute and learn with us!
Here's how you can contribute:
diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js
index 5e16b3b18..839f2bb13 100644
--- a/Server/controllers/monitorController.js
+++ b/Server/controllers/monitorController.js
@@ -1,13 +1,11 @@
import {
getMonitorByIdParamValidation,
getMonitorByIdQueryValidation,
- getMonitorsByTeamIdValidation,
+ getMonitorsByTeamIdParamValidation,
+ getMonitorsByTeamIdQueryValidation,
createMonitorBodyValidation,
getMonitorURLByQueryValidation,
editMonitorBodyValidation,
- getMonitorsAndSummaryByTeamIdParamValidation,
- getMonitorsAndSummaryByTeamIdQueryValidation,
- 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).
- */
- getMonitorsAndSummaryByTeamId = async (req, res, next) => {
- try {
- await getMonitorsAndSummaryByTeamIdParamValidation.validateAsync(req.params);
- await getMonitorsAndSummaryByTeamIdQueryValidation.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.getMonitorsAndSummaryByTeamId(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, "getMonitorsByTeamId"));
+ }
+ };
}
export default MonitorController;
diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js
index 773d6ef1e..0a552012d 100644
--- a/Server/db/mongo/modules/checkModule.js
+++ b/Server/db/mongo/modules/checkModule.js
@@ -175,6 +175,7 @@ const getTeamChecks = async (req) => {
case "all":
break;
case "down":
+ checksQuery.status = false;
break;
case "resolve":
checksQuery.statusCode = 5000;
diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js
index 1143b9eeb..4c60e0f22 100644
--- a/Server/db/mongo/modules/monitorModule.js
+++ b/Server/db/mongo/modules/monitorModule.js
@@ -8,7 +8,11 @@ import { NormalizeData, NormalizeDataUptimeDetails } from "../../../utils/dataUt
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
-
+import {
+ buildUptimeDetailsPipeline,
+ buildHardwareDetailsPipeline,
+} from "./monitorModuleQueries.js";
+import { ObjectId } from "mongodb";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -336,294 +340,21 @@ const getUptimeDetailsById = async (req) => {
};
const dateString = formatLookup[dateRange];
- const monitorData = await Check.aggregate([
- {
- $match: {
- monitorId: monitor._id,
- },
- },
- {
- $sort: {
- createdAt: 1,
- },
- },
- {
- $facet: {
- aggregateData: [
- {
- $group: {
- _id: null,
- avgResponseTime: {
- $avg: "$responseTime",
- },
- firstCheck: {
- $first: "$$ROOT",
- },
- lastCheck: {
- $last: "$$ROOT",
- },
- totalChecks: {
- $sum: 1,
- },
- },
- },
- ],
- uptimeDuration: [
- {
- $match: {
- status: false,
- },
- },
- {
- $sort: {
- createdAt: 1,
- },
- },
- {
- $group: {
- _id: null,
- lastFalseCheck: {
- $last: "$$ROOT",
- },
- },
- },
- ],
- groupChecks: [
- {
- $match: {
- createdAt: { $gte: dates.start, $lte: dates.end },
- },
- },
- {
- $group: {
- _id: {
- $dateToString: {
- format: dateString,
- date: "$createdAt",
- },
- },
- avgResponseTime: {
- $avg: "$responseTime",
- },
- totalChecks: {
- $sum: 1,
- },
- },
- },
- {
- $sort: {
- _id: 1,
- },
- },
- ],
- groupAggregate: [
- {
- $match: {
- createdAt: { $gte: dates.start, $lte: dates.end },
- },
- },
- {
- $group: {
- _id: null,
- avgResponseTime: {
- $avg: "$responseTime",
- },
- },
- },
- ],
- upChecksAggregate: [
- {
- $match: {
- status: true,
- },
- },
- {
- $group: {
- _id: null,
- avgResponseTime: {
- $avg: "$responseTime",
- },
- totalChecks: {
- $sum: 1,
- },
- },
- },
- ],
- upChecks: [
- {
- $match: {
- status: true,
- createdAt: { $gte: dates.start, $lte: dates.end },
- },
- },
- {
- $group: {
- _id: {
- $dateToString: {
- format: dateString,
- date: "$createdAt",
- },
- },
- totalChecks: {
- $sum: 1,
- },
- avgResponseTime: {
- $avg: "$responseTime",
- },
- },
- },
- {
- $sort: { _id: 1 },
- },
- ],
- downChecksAggregate: [
- {
- $match: {
- status: false,
- },
- },
- {
- $group: {
- _id: null,
- avgResponseTime: {
- $avg: "$responseTime",
- },
- totalChecks: {
- $sum: 1,
- },
- },
- },
- ],
- downChecks: [
- {
- $match: {
- status: false,
- createdAt: { $gte: dates.start, $lte: dates.end },
- },
- },
- {
- $group: {
- _id: {
- $dateToString: {
- format: dateString,
- date: "$createdAt",
- },
- },
- totalChecks: {
- $sum: 1,
- },
- avgResponseTime: {
- $avg: "$responseTime",
- },
- },
- },
- {
- $sort: { _id: 1 },
- },
- ],
- },
- },
- {
- $project: {
- avgResponseTime: {
- $arrayElemAt: ["$aggregateData.avgResponseTime", 0],
- },
- totalChecks: {
- $arrayElemAt: ["$aggregateData.totalChecks", 0],
- },
- latestResponseTime: {
- $arrayElemAt: ["$aggregateData.lastCheck.responseTime", 0],
- },
- timeSinceLastCheck: {
- $let: {
- vars: {
- lastCheck: {
- $arrayElemAt: ["$aggregateData.lastCheck", 0],
- },
- },
- in: {
- $cond: [
- {
- $ifNull: ["$$lastCheck", false],
- },
- {
- $subtract: [new Date(), "$$lastCheck.createdAt"],
- },
- 0,
- ],
- },
- },
- },
- timeSinceLastFalseCheck: {
- $let: {
- vars: {
- lastFalseCheck: {
- $arrayElemAt: ["$uptimeDuration.lastFalseCheck", 0],
- },
- firstCheck: {
- $arrayElemAt: ["$aggregateData.firstCheck", 0],
- },
- },
- in: {
- $cond: [
- {
- $ifNull: ["$$lastFalseCheck", false],
- },
- {
- $subtract: [new Date(), "$$lastFalseCheck.createdAt"],
- },
- {
- $cond: [
- {
- $ifNull: ["$$firstCheck", false],
- },
- {
- $subtract: [new Date(), "$$firstCheck.createdAt"],
- },
- 0,
- ],
- },
- ],
- },
- },
- },
- groupChecks: "$groupChecks",
- groupAggregate: {
- $arrayElemAt: ["$groupAggregate", 0],
- },
- upChecksAggregate: {
- $arrayElemAt: ["$upChecksAggregate", 0],
- },
- upChecks: "$upChecks",
- downChecksAggregate: {
- $arrayElemAt: ["$downChecksAggregate", 0],
- },
- downChecks: "$downChecks",
- },
- },
- ]);
+ const results = await Check.aggregate(
+ buildUptimeDetailsPipeline(monitor, dates, dateString)
+ );
+ const monitorData = results[0];
const normalizedGroupChecks = NormalizeDataUptimeDetails(
- monitorData[0].groupChecks,
+ monitorData.groupedChecks,
10,
100
);
const monitorStats = {
...monitor.toObject(),
- stats: {
- avgResponseTime: monitorData[0].avgResponseTime,
- totalChecks: monitorData[0].totalChecks,
- timeSinceLastCheck: monitorData[0].timeSinceLastCheck,
- timeSinceLastFalseCheck: monitorData[0].timeSinceLastFalseCheck,
- latestResponseTime: monitorData[0].latestResponseTime,
- groupChecks: normalizedGroupChecks,
- groupAggregate: monitorData[0].groupAggregate,
- upChecksAggregate: monitorData[0].upChecksAggregate,
- upChecks: monitorData[0].upChecks,
- downChecksAggregate: monitorData[0].downChecksAggregate,
- downChecks: monitorData[0].downChecks,
- },
+ ...monitorData,
+ groupedChecks: normalizedGroupChecks,
};
return monitorStats;
@@ -715,261 +446,9 @@ const getHardwareDetailsById = async (req) => {
month: "%Y-%m-%dT00:00:00Z",
};
const dateString = formatLookup[dateRange];
- const hardwareStats = await HardwareCheck.aggregate([
- {
- $match: {
- monitorId: monitor._id,
- createdAt: { $gte: dates.start, $lte: dates.end },
- },
- },
- {
- $sort: {
- createdAt: 1,
- },
- },
- {
- $facet: {
- aggregateData: [
- {
- $group: {
- _id: null,
- latestCheck: {
- $last: "$$ROOT",
- },
- totalChecks: {
- $sum: 1,
- },
- },
- },
- ],
- upChecks: [
- {
- $match: {
- status: true,
- },
- },
- {
- $group: {
- _id: null,
- totalChecks: {
- $sum: 1,
- },
- },
- },
- ],
- checks: [
- {
- $limit: 1,
- },
- {
- $project: {
- diskCount: {
- $size: "$disk",
- },
- },
- },
- {
- $lookup: {
- from: "hardwarechecks",
- let: {
- diskCount: "$diskCount",
- },
- pipeline: [
- {
- $match: {
- $expr: {
- $and: [
- { $eq: ["$monitorId", monitor._id] },
- { $gte: ["$createdAt", dates.start] },
- { $lte: ["$createdAt", dates.end] },
- ],
- },
- },
- },
- {
- $group: {
- _id: {
- $dateToString: {
- format: dateString,
- date: "$createdAt",
- },
- },
- avgCpuUsage: {
- $avg: "$cpu.usage_percent",
- },
- avgMemoryUsage: {
- $avg: "$memory.usage_percent",
- },
- avgTemperatures: {
- $push: {
- $ifNull: ["$cpu.temperature", [0]],
- },
- },
- disks: {
- $push: "$disk",
- },
- },
- },
- {
- $project: {
- _id: 1,
- avgCpuUsage: 1,
- avgMemoryUsage: 1,
- avgTemperature: {
- $map: {
- input: {
- $range: [
- 0,
- {
- $size: {
- // Handle null temperatures array
- $ifNull: [
- { $arrayElemAt: ["$avgTemperatures", 0] },
- [0], // Default to single-element array if null
- ],
- },
- },
- ],
- },
- as: "index",
- in: {
- $avg: {
- $map: {
- input: "$avgTemperatures",
- as: "tempArray",
- in: {
- $ifNull: [
- { $arrayElemAt: ["$$tempArray", "$$index"] },
- 0, // Default to 0 if element is null
- ],
- },
- },
- },
- },
- },
- },
- disks: {
- $map: {
- input: {
- $range: [0, "$$diskCount"],
- },
- as: "diskIndex",
- in: {
- name: {
- $concat: [
- "disk",
- {
- $toString: "$$diskIndex",
- },
- ],
- },
- readSpeed: {
- $avg: {
- $map: {
- input: "$disks",
- as: "diskArray",
- in: {
- $arrayElemAt: [
- "$$diskArray.read_speed_bytes",
- "$$diskIndex",
- ],
- },
- },
- },
- },
- writeSpeed: {
- $avg: {
- $map: {
- input: "$disks",
- as: "diskArray",
- in: {
- $arrayElemAt: [
- "$$diskArray.write_speed_bytes",
- "$$diskIndex",
- ],
- },
- },
- },
- },
- totalBytes: {
- $avg: {
- $map: {
- input: "$disks",
- as: "diskArray",
- in: {
- $arrayElemAt: [
- "$$diskArray.total_bytes",
- "$$diskIndex",
- ],
- },
- },
- },
- },
- freeBytes: {
- $avg: {
- $map: {
- input: "$disks",
- as: "diskArray",
- in: {
- $arrayElemAt: [
- "$$diskArray.free_bytes",
- "$$diskIndex",
- ],
- },
- },
- },
- },
- usagePercent: {
- $avg: {
- $map: {
- input: "$disks",
- as: "diskArray",
- in: {
- $arrayElemAt: [
- "$$diskArray.usage_percent",
- "$$diskIndex",
- ],
- },
- },
- },
- },
- },
- },
- },
- },
- },
- ],
- as: "hourlyStats",
- },
- },
- {
- $unwind: "$hourlyStats",
- },
- {
- $replaceRoot: {
- newRoot: "$hourlyStats",
- },
- },
- ],
- },
- },
- {
- $project: {
- aggregateData: {
- $arrayElemAt: ["$aggregateData", 0],
- },
- upChecks: {
- $arrayElemAt: ["$upChecks", 0],
- },
- checks: {
- $sortArray: {
- input: "$checks",
- sortBy: { _id: 1 },
- },
- },
- },
- },
- ]);
+ const hardwareStats = await HardwareCheck.aggregate(
+ buildHardwareDetailsPipeline(monitor, dates, dateString)
+ );
const monitorStats = {
...monitor.toObject(),
@@ -1016,121 +495,194 @@ const getMonitorById = async (monitorId) => {
}
};
-/**
- * Get monitors and Summary by TeamID
- * @async
- * @param {Express.Request} req
- * @param {Express.Response} res
- * @returns {Promise>}
- * @throws {Error}
- */
+const getMonitorsByTeamId = async (req) => {
+ let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
-const getMonitorsAndSummaryByTeamId = async (teamId, type) => {
- try {
- const monitors = await Monitor.find({ teamId, type });
- const monitorCounts = monitors.reduce(
- (acc, monitor) => {
- if (monitor.status === true) {
- acc.up += 1;
- } else if (monitor.status === false) {
- acc.down += 1;
- } else if (monitor.isActive === false) {
- acc.paused += 1;
- }
- return acc;
+ limit = parseInt(limit);
+ page = parseInt(page);
+ rowsPerPage = parseInt(rowsPerPage);
+ if (field === undefined) {
+ field = "name";
+ order = "asc";
+ }
+ // Build the match stage
+ const matchStage = { teamId: ObjectId.createFromHexString(req.params.teamId) };
+ if (type !== undefined) {
+ matchStage.type = Array.isArray(type) ? { $in: type } : type;
+ }
+
+ const skip = page && rowsPerPage ? page * rowsPerPage : 0;
+
+ const sort = { [field]: order === "asc" ? 1 : -1 };
+ const results = await Monitor.aggregate([
+ { $match: matchStage },
+ {
+ $facet: {
+ summary: [
+ {
+ $group: {
+ _id: null,
+ totalMonitors: { $sum: 1 },
+ upMonitors: {
+ $sum: {
+ $cond: [{ $eq: ["$status", true] }, 1, 0],
+ },
+ },
+ downMonitors: {
+ $sum: {
+ $cond: [{ $eq: ["$status", false] }, 1, 0],
+ },
+ },
+ pausedMonitors: {
+ $sum: {
+ $cond: [{ $eq: ["$isActive", false] }, 1, 0],
+ },
+ },
+ },
+ },
+ {
+ $project: {
+ _id: 0,
+ },
+ },
+ ],
+ monitors: [
+ { $sort: sort },
+ {
+ $project: {
+ _id: 1,
+ name: 1,
+ },
+ },
+ ],
+ filteredMonitors: [
+ ...(filter !== undefined
+ ? [
+ {
+ $match: {
+ $or: [
+ { name: { $regex: filter, $options: "i" } },
+ { url: { $regex: filter, $options: "i" } },
+ ],
+ },
+ },
+ ]
+ : []),
+ { $sort: sort },
+ { $skip: skip },
+ ...(rowsPerPage ? [{ $limit: rowsPerPage }] : []),
+ ...(limit
+ ? [
+ {
+ $lookup: {
+ from: "checks",
+ let: { monitorId: "$_id" },
+ pipeline: [
+ {
+ $match: {
+ $expr: { $eq: ["$monitorId", "$$monitorId"] },
+ },
+ },
+ { $sort: { createdAt: -1 } },
+ ...(limit ? [{ $limit: limit }] : []),
+ ],
+ as: "standardchecks",
+ },
+ },
+ ]
+ : []),
+ ...(limit
+ ? [
+ {
+ $lookup: {
+ from: "pagespeedchecks",
+ let: { monitorId: "$_id" },
+ pipeline: [
+ {
+ $match: {
+ $expr: { $eq: ["$monitorId", "$$monitorId"] },
+ },
+ },
+ { $sort: { createdAt: -1 } },
+ ...(limit ? [{ $limit: limit }] : []),
+ ],
+ as: "pagespeedchecks",
+ },
+ },
+ ]
+ : []),
+ ...(limit
+ ? [
+ {
+ $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,
+ },
+ },
+ ],
},
- { up: 0, down: 0, paused: 0 }
- );
- monitorCounts.total = monitors.length;
- return { monitors, monitorCounts };
- } catch (error) {
- error.service = SERVICE_NAME;
- error.method = "getMonitorsAndSummaryByTeamId";
- throw error;
- }
-};
+ },
+ {
+ $project: {
+ summary: { $arrayElemAt: ["$summary", 0] },
+ filteredMonitors: 1,
+ monitors: 1,
+ },
+ },
+ ]);
-/**
- * Get monitors by TeamID
- * @async
- * @param {Express.Request} req
- * @param {Express.Response} res
- * @returns {Promise>}
- * @throws {Error}
- */
-const getMonitorsByTeamId = async (req, res) => {
- try {
- let {
- limit,
- type,
- status,
- checkOrder,
- normalize,
- page,
- rowsPerPage,
- filter,
- field,
- order,
- } = req.query;
-
- const monitorQuery = { teamId: req.params.teamId };
-
- if (type !== undefined) {
- monitorQuery.type = Array.isArray(type) ? { $in: type } : type;
+ let { monitors, filteredMonitors, summary } = results[0];
+ filteredMonitors = filteredMonitors.map((monitor) => {
+ if (!monitor.checks) {
+ return monitor;
}
- // 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" } },
- ];
- }
- const monitorCount = await Monitor.countDocuments(monitorQuery);
-
- // Pagination
- const skip = page && rowsPerPage ? page * rowsPerPage : 0;
-
- // Build Sort option
- const sort = field ? { [field]: order === "asc" ? 1 : -1 } : {};
-
- const monitors = await Monitor.find(monitorQuery)
- .skip(skip)
- .limit(rowsPerPage)
- .sort(sort);
-
- // Early return if limit is set to -1, indicating we don't want any checks
- if (limit === "-1") {
- return { monitors, monitorCount };
- }
- // Map each monitor to include its associated checks
- const monitorsWithChecks = await Promise.all(
- monitors.map(async (monitor) => {
- let model = CHECK_MODEL_LOOKUP[monitor.type];
-
- // Checks are order newest -> oldest
- let checks = await model
- .find({
- monitorId: monitor._id,
- ...(status && { status }),
- })
- .sort({ createdAt: checkOrder === "asc" ? 1 : -1 })
-
- .limit(limit || 0);
-
- //Normalize checks if requested
- if (normalize !== undefined) {
- checks = NormalizeData(checks, 10, 100);
- }
- return { ...monitor.toObject(), checks };
- })
- );
- return { monitors: monitorsWithChecks, monitorCount };
- } catch (error) {
- error.service = SERVICE_NAME;
- error.method = "getMonitorsByTeamId";
- throw error;
- }
+ monitor.checks = NormalizeData(monitor.checks, 10, 100);
+ return monitor;
+ });
+ return { monitors, filteredMonitors, summary };
};
/**
@@ -1262,9 +814,8 @@ export {
getAllMonitorsWithUptimeStats,
getMonitorStatsById,
getMonitorById,
- getUptimeDetailsById,
- getMonitorsAndSummaryByTeamId,
getMonitorsByTeamId,
+ getUptimeDetailsById,
createMonitor,
deleteMonitor,
deleteAllMonitors,
diff --git a/Server/db/mongo/modules/monitorModuleQueries.js b/Server/db/mongo/modules/monitorModuleQueries.js
new file mode 100644
index 000000000..cdbcd51d8
--- /dev/null
+++ b/Server/db/mongo/modules/monitorModuleQueries.js
@@ -0,0 +1,535 @@
+const buildUptimeDetailsPipeline = (monitor, dates, dateString) => {
+ return [
+ {
+ $match: {
+ monitorId: monitor._id,
+ },
+ },
+ {
+ $sort: {
+ createdAt: 1,
+ },
+ },
+ {
+ $facet: {
+ aggregateData: [
+ {
+ $group: {
+ _id: null,
+ avgResponseTime: {
+ $avg: "$responseTime",
+ },
+ lastCheck: {
+ $last: "$$ROOT",
+ },
+ totalChecks: {
+ $sum: 1,
+ },
+ },
+ },
+ ],
+ uptimeStreak: [
+ {
+ $sort: {
+ createdAt: -1,
+ },
+ },
+ {
+ $group: {
+ _id: null,
+ checks: { $push: "$$ROOT" },
+ },
+ },
+ {
+ $project: {
+ streak: {
+ $reduce: {
+ input: "$checks",
+ initialValue: { checks: [], foundFalse: false },
+ in: {
+ $cond: [
+ {
+ $and: [
+ { $not: "$$value.foundFalse" }, // stop reducing if a false check has been found
+ { $eq: ["$$this.status", true] }, // continue reducing if current check true
+ ],
+ },
+ // true case
+ {
+ checks: { $concatArrays: ["$$value.checks", ["$$this"]] },
+ foundFalse: false, // Add the check to the streak
+ },
+ // false case
+ {
+ checks: "$$value.checks",
+ foundFalse: true, // Mark that we found a false
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ ],
+ // For the response time chart, should return checks for date window
+ // Grouped by: {day: hour}, {week: day}, {month: day}
+ groupedChecks: [
+ {
+ $match: {
+ createdAt: { $gte: dates.start, $lte: dates.end },
+ },
+ },
+ {
+ $group: {
+ _id: {
+ $dateToString: {
+ format: dateString,
+ date: "$createdAt",
+ },
+ },
+ avgResponseTime: {
+ $avg: "$responseTime",
+ },
+ totalChecks: {
+ $sum: 1,
+ },
+ },
+ },
+ {
+ $sort: {
+ _id: 1,
+ },
+ },
+ ],
+ // Average response time for the date window
+ groupAvgResponseTime: [
+ {
+ $match: {
+ createdAt: { $gte: dates.start, $lte: dates.end },
+ },
+ },
+ {
+ $group: {
+ _id: null,
+ avgResponseTime: {
+ $avg: "$responseTime",
+ },
+ },
+ },
+ ],
+ // All UpChecks for the date window
+ upChecks: [
+ {
+ $match: {
+ status: true,
+ createdAt: { $gte: dates.start, $lte: dates.end },
+ },
+ },
+ {
+ $group: {
+ _id: null,
+ avgResponseTime: {
+ $avg: "$responseTime",
+ },
+ totalChecks: {
+ $sum: 1,
+ },
+ },
+ },
+ ],
+ // Up checks grouped by: {day: hour}, {week: day}, {month: day}
+ groupedUpChecks: [
+ {
+ $match: {
+ status: true,
+ createdAt: { $gte: dates.start, $lte: dates.end },
+ },
+ },
+ {
+ $group: {
+ _id: {
+ $dateToString: {
+ format: dateString,
+ date: "$createdAt",
+ },
+ },
+ totalChecks: {
+ $sum: 1,
+ },
+ avgResponseTime: {
+ $avg: "$responseTime",
+ },
+ },
+ },
+ {
+ $sort: { _id: 1 },
+ },
+ ],
+ // All down checks for the date window
+ downChecks: [
+ {
+ $match: {
+ status: false,
+ createdAt: { $gte: dates.start, $lte: dates.end },
+ },
+ },
+ {
+ $group: {
+ _id: null,
+ avgResponseTime: {
+ $avg: "$responseTime",
+ },
+ totalChecks: {
+ $sum: 1,
+ },
+ },
+ },
+ ],
+ // Down checks grouped by: {day: hour}, {week: day}, {month: day} for the date window
+ groupedDownChecks: [
+ {
+ $match: {
+ status: false,
+ createdAt: { $gte: dates.start, $lte: dates.end },
+ },
+ },
+ {
+ $group: {
+ _id: {
+ $dateToString: {
+ format: dateString,
+ date: "$createdAt",
+ },
+ },
+ totalChecks: {
+ $sum: 1,
+ },
+ avgResponseTime: {
+ $avg: "$responseTime",
+ },
+ },
+ },
+ {
+ $sort: { _id: 1 },
+ },
+ ],
+ },
+ },
+ {
+ $project: {
+ uptimeStreak: {
+ $cond: [
+ { $eq: [{ $size: { $first: "$uptimeStreak.streak.checks" } }, 0] },
+ 0,
+ {
+ $subtract: [
+ new Date(),
+ {
+ $last: { $first: "$uptimeStreak.streak.checks.createdAt" },
+ },
+ ],
+ },
+ ],
+ },
+ avgResponseTime: {
+ $arrayElemAt: ["$aggregateData.avgResponseTime", 0],
+ },
+ totalChecks: {
+ $arrayElemAt: ["$aggregateData.totalChecks", 0],
+ },
+ latestResponseTime: {
+ $arrayElemAt: ["$aggregateData.lastCheck.responseTime", 0],
+ },
+ timeSinceLastCheck: {
+ $let: {
+ vars: {
+ lastCheck: {
+ $arrayElemAt: ["$aggregateData.lastCheck", 0],
+ },
+ },
+ in: {
+ $cond: [
+ {
+ $ifNull: ["$$lastCheck", false],
+ },
+ {
+ $subtract: [new Date(), "$$lastCheck.createdAt"],
+ },
+ 0,
+ ],
+ },
+ },
+ },
+ groupedChecks: "$groupedChecks",
+ groupedAvgResponseTime: {
+ $arrayElemAt: ["$groupAvgResponseTime", 0],
+ },
+ upChecks: {
+ $arrayElemAt: ["$upChecks", 0],
+ },
+ groupedUpChecks: "$groupedUpChecks",
+ downChecks: {
+ $arrayElemAt: ["$downChecks", 0],
+ },
+ groupedDownChecks: "$groupedDownChecks",
+ },
+ },
+ ];
+};
+
+const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
+ return [
+ {
+ $match: {
+ monitorId: monitor._id,
+ createdAt: { $gte: dates.start, $lte: dates.end },
+ },
+ },
+ {
+ $sort: {
+ createdAt: 1,
+ },
+ },
+ {
+ $facet: {
+ aggregateData: [
+ {
+ $group: {
+ _id: null,
+ latestCheck: {
+ $last: "$$ROOT",
+ },
+ totalChecks: {
+ $sum: 1,
+ },
+ },
+ },
+ ],
+ upChecks: [
+ {
+ $match: {
+ status: true,
+ },
+ },
+ {
+ $group: {
+ _id: null,
+ totalChecks: {
+ $sum: 1,
+ },
+ },
+ },
+ ],
+ checks: [
+ {
+ $limit: 1,
+ },
+ {
+ $project: {
+ diskCount: {
+ $size: "$disk",
+ },
+ },
+ },
+ {
+ $lookup: {
+ from: "hardwarechecks",
+ let: {
+ diskCount: "$diskCount",
+ },
+ pipeline: [
+ {
+ $match: {
+ $expr: {
+ $and: [
+ { $eq: ["$monitorId", monitor._id] },
+ { $gte: ["$createdAt", dates.start] },
+ { $lte: ["$createdAt", dates.end] },
+ ],
+ },
+ },
+ },
+ {
+ $group: {
+ _id: {
+ $dateToString: {
+ format: dateString,
+ date: "$createdAt",
+ },
+ },
+ avgCpuUsage: {
+ $avg: "$cpu.usage_percent",
+ },
+ avgMemoryUsage: {
+ $avg: "$memory.usage_percent",
+ },
+ avgTemperatures: {
+ $push: {
+ $ifNull: ["$cpu.temperature", [0]],
+ },
+ },
+ disks: {
+ $push: "$disk",
+ },
+ },
+ },
+ {
+ $project: {
+ _id: 1,
+ avgCpuUsage: 1,
+ avgMemoryUsage: 1,
+ avgTemperature: {
+ $map: {
+ input: {
+ $range: [
+ 0,
+ {
+ $size: {
+ // Handle null temperatures array
+ $ifNull: [
+ { $arrayElemAt: ["$avgTemperatures", 0] },
+ [0], // Default to single-element array if null
+ ],
+ },
+ },
+ ],
+ },
+ as: "index",
+ in: {
+ $avg: {
+ $map: {
+ input: "$avgTemperatures",
+ as: "tempArray",
+ in: {
+ $ifNull: [
+ { $arrayElemAt: ["$$tempArray", "$$index"] },
+ 0, // Default to 0 if element is null
+ ],
+ },
+ },
+ },
+ },
+ },
+ },
+ disks: {
+ $map: {
+ input: {
+ $range: [0, "$$diskCount"],
+ },
+ as: "diskIndex",
+ in: {
+ name: {
+ $concat: [
+ "disk",
+ {
+ $toString: "$$diskIndex",
+ },
+ ],
+ },
+ readSpeed: {
+ $avg: {
+ $map: {
+ input: "$disks",
+ as: "diskArray",
+ in: {
+ $arrayElemAt: [
+ "$$diskArray.read_speed_bytes",
+ "$$diskIndex",
+ ],
+ },
+ },
+ },
+ },
+ writeSpeed: {
+ $avg: {
+ $map: {
+ input: "$disks",
+ as: "diskArray",
+ in: {
+ $arrayElemAt: [
+ "$$diskArray.write_speed_bytes",
+ "$$diskIndex",
+ ],
+ },
+ },
+ },
+ },
+ totalBytes: {
+ $avg: {
+ $map: {
+ input: "$disks",
+ as: "diskArray",
+ in: {
+ $arrayElemAt: [
+ "$$diskArray.total_bytes",
+ "$$diskIndex",
+ ],
+ },
+ },
+ },
+ },
+ freeBytes: {
+ $avg: {
+ $map: {
+ input: "$disks",
+ as: "diskArray",
+ in: {
+ $arrayElemAt: ["$$diskArray.free_bytes", "$$diskIndex"],
+ },
+ },
+ },
+ },
+ usagePercent: {
+ $avg: {
+ $map: {
+ input: "$disks",
+ as: "diskArray",
+ in: {
+ $arrayElemAt: [
+ "$$diskArray.usage_percent",
+ "$$diskIndex",
+ ],
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ as: "hourlyStats",
+ },
+ },
+ {
+ $unwind: "$hourlyStats",
+ },
+ {
+ $replaceRoot: {
+ newRoot: "$hourlyStats",
+ },
+ },
+ ],
+ },
+ },
+ {
+ $project: {
+ aggregateData: {
+ $arrayElemAt: ["$aggregateData", 0],
+ },
+ upChecks: {
+ $arrayElemAt: ["$upChecks", 0],
+ },
+ checks: {
+ $sortArray: {
+ input: "$checks",
+ sortBy: { _id: 1 },
+ },
+ },
+ },
+ },
+ ];
+};
+
+export { buildUptimeDetailsPipeline, buildHardwareDetailsPipeline };
diff --git a/Server/package-lock.json b/Server/package-lock.json
index 4baed7570..5a226821f 100644
--- a/Server/package-lock.json
+++ b/Server/package-lock.json
@@ -11,7 +11,7 @@
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
- "bullmq": "5.34.6",
+ "bullmq": "5.34.9",
"cors": "^2.8.5",
"dockerode": "4.0.3",
"dotenv": "^16.4.5",
@@ -229,10 +229,11 @@
"dev": true
},
"node_modules/@eslint/core": {
- "version": "0.9.1",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz",
- "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==",
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz",
+ "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
@@ -323,10 +324,11 @@
"dev": true
},
"node_modules/@eslint/js": {
- "version": "9.17.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz",
- "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==",
+ "version": "9.18.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz",
+ "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
@@ -341,11 +343,13 @@
}
},
"node_modules/@eslint/plugin-kit": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz",
- "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==",
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz",
+ "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==",
"dev": true,
+ "license": "Apache-2.0",
"dependencies": {
+ "@eslint/core": "^0.10.0",
"levn": "^0.4.1"
},
"engines": {
@@ -1744,9 +1748,9 @@
}
},
"node_modules/bullmq": {
- "version": "5.34.6",
- "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.6.tgz",
- "integrity": "sha512-pRCYyO9RlkQWxdmKlrNnUthyFwurYXRYLVXD1YIx+nCCdhAOiHatD8FDHbsT/w2I31c0NWoMcfZiIGuipiF7Lg==",
+ "version": "5.34.9",
+ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.9.tgz",
+ "integrity": "sha512-olCiFMy9o+pUpBcb/RguS5uUwbma+E80cYgDcG5p7ksvWpoyoEmxq5FOUQcobeTQwO7R9PEIvVsel5yewt0mlw==",
"license": "MIT",
"dependencies": {
"cron-parser": "^4.9.0",
@@ -3067,18 +3071,19 @@
}
},
"node_modules/eslint": {
- "version": "9.17.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz",
- "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==",
+ "version": "9.18.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz",
+ "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.19.0",
- "@eslint/core": "^0.9.0",
+ "@eslint/core": "^0.10.0",
"@eslint/eslintrc": "^3.2.0",
- "@eslint/js": "9.17.0",
- "@eslint/plugin-kit": "^0.2.3",
+ "@eslint/js": "9.18.0",
+ "@eslint/plugin-kit": "^0.2.5",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.1",
@@ -3212,21 +3217,6 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
"node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -3238,74 +3228,12 @@
"concat-map": "0.0.1"
}
},
- "node_modules/eslint/node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/eslint/node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "dependencies": {
- "color-name": "~1.1.4"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/eslint/node_modules/color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
- },
- "node_modules/eslint/node_modules/debug": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
- "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
- "dev": true,
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/eslint/node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/eslint/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
},
@@ -3313,15 +3241,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/eslint/node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/eslint/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -3335,24 +3254,6 @@
"node": "*"
}
},
- "node_modules/eslint/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true
- },
- "node_modules/eslint/node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/esm": {
"version": "3.2.25",
"resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
@@ -5774,9 +5675,9 @@
}
},
"node_modules/mongoose": {
- "version": "8.9.3",
- "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.3.tgz",
- "integrity": "sha512-G50GNPdMqhoiRAJ/24GYAzg13yxXDD3FOOFeYiFwtHmHpAJem3hxbYIxAhLJGWbYEiUZL0qFMu2LXYkgGAmo+Q==",
+ "version": "8.9.4",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.4.tgz",
+ "integrity": "sha512-DndoI01aV/q40P9DiYDXsYjhj8vZjmmuFwcC3Tro5wFznoE1z6Fe2JgMnbLR6ghglym5ziYizSfAJykp+UPZWg==",
"license": "MIT",
"dependencies": {
"bson": "^6.10.1",
diff --git a/Server/package.json b/Server/package.json
index 5ee5e08ad..1937f9ca1 100644
--- a/Server/package.json
+++ b/Server/package.json
@@ -18,7 +18,7 @@
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
- "bullmq": "5.34.6",
+ "bullmq": "5.34.9",
"cors": "^2.8.5",
"dockerode": "4.0.3",
"dotenv": "^16.4.5",
diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js
index d47351486..92b529417 100644
--- a/Server/routes/monitorRoute.js
+++ b/Server/routes/monitorRoute.js
@@ -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.getMonitorsAndSummaryByTeamId
- );
+
this.router.get("/team/:teamId", this.monitorController.getMonitorsByTeamId);
this.router.get(
diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js
index fb5c306b1..5a9ab335c 100644
--- a/Server/service/jobQueue.js
+++ b/Server/service/jobQueue.js
@@ -8,6 +8,7 @@ const QUEUE_LOOKUP = {
docker: "uptime",
pagespeed: "pagespeed",
};
+const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`;
import { successMessages, errorMessages } from "../utils/messages.js";
class NewJobQueue {
@@ -218,38 +219,50 @@ class NewJobQueue {
concurrency: 5,
});
- worker.on("active", (job) => {
- this.logger.info({
- message: `Worker started processing job: ${job.id}`,
- service: SERVICE_NAME,
- method: "createWorker",
- });
- });
+ // worker.on("active", (job) => {
+ // this.logger.info({
+ // message: `Worker started processing job: ${job.id}`,
+ // service: SERVICE_NAME,
+ // method: "createWorker",
+ // });
+ // });
- worker.on("completed", (job) => {
- this.logger.info({
- message: `Worker completed job: ${job.id}`,
- service: SERVICE_NAME,
- method: "createWorker",
- });
- });
+ // worker.on("completed", (job) => {
+ // this.logger.info({
+ // message: `Worker completed job: ${job.id}`,
+ // service: SERVICE_NAME,
+ // method: "createWorker",
+ // });
+ // });
- worker.on("failed", (job, err) => {
- this.logger.error({
- message: `Worker failed job: ${job.id}`,
- service: SERVICE_NAME,
- method: "createWorker",
- stack: err.stack,
- });
- });
+ // // Log job progress updates
+ // worker.on("progress", (job, progress) => {
+ // this.logger.info({
+ // message: `Job progress: ${job.id}`,
+ // service: SERVICE_NAME,
+ // method: "createWorker",
+ // details: `Progress: ${progress}%`,
+ // });
+ // });
- worker.on("stalled", (jobId) => {
- this.logger.warn({
- message: `Worker stalled job: ${jobId}`,
- service: SERVICE_NAME,
- method: "createWorker",
- });
- });
+ // // Log when a job fails
+ // worker.on("failed", (job, err) => {
+ // this.logger.error({
+ // message: `Worker failed job: ${job.id}`,
+ // service: SERVICE_NAME,
+ // method: "createWorker",
+ // details: `Error: ${err.message}`,
+ // stack: err.stack,
+ // });
+ // });
+
+ // worker.on("stalled", (jobId) => {
+ // this.logger.warn({
+ // message: `Worker stalled job: ${jobId}`,
+ // service: SERVICE_NAME,
+ // method: "createWorker",
+ // });
+ // });
return worker;
}
@@ -428,27 +441,27 @@ class NewJobQueue {
throw new Error(`Queue for ${monitor.type} not found`);
}
- // build job options
- const jobOptions = {
- attempts: 1,
- backoff: {
- type: "exponential",
- delay: 1000,
+ const jobTemplate = {
+ name: jobName,
+ data: monitor,
+ opts: {
+ attempts: 1,
+ backoff: {
+ type: "exponential",
+ delay: 1000,
+ },
+ removeOnComplete: true,
+ removeOnFail: false,
+ timeout: 1 * 60 * 1000,
},
- removeOnComplete: true,
- removeOnFail: false,
- timeout: 1 * 60 * 1000,
};
- // Execute job immediately
- await queue.add(jobName, monitor, jobOptions);
- await queue.add(jobName, monitor, {
- ...jobOptions,
- repeat: {
- every: monitor?.interval ?? 60000,
- immediately: false,
- },
- });
+ const schedulerId = getSchedulerId(monitor);
+ await queue.upsertJobScheduler(
+ schedulerId,
+ { every: monitor?.interval ?? 60000 },
+ jobTemplate
+ );
const workerStats = await this.getWorkerStats(queue);
await this.scaleWorkers(workerStats, queue);
@@ -478,9 +491,9 @@ class NewJobQueue {
async deleteJob(monitor) {
try {
const queue = this.queues[QUEUE_LOOKUP[monitor.type]];
- const wasDeleted = await queue.removeRepeatable(monitor._id, {
- every: monitor.interval,
- });
+ const schedulerId = getSchedulerId(monitor);
+ const wasDeleted = await queue.removeJobScheduler(schedulerId);
+
if (wasDeleted === true) {
this.logger.info({
message: successMessages.JOB_QUEUE_DELETE_JOB,
diff --git a/Server/service/settingsService.js b/Server/service/settingsService.js
index fb85939ca..24931f6ce 100644
--- a/Server/service/settingsService.js
+++ b/Server/service/settingsService.js
@@ -2,7 +2,7 @@ const SERVICE_NAME = "SettingsService";
import dotenv from "dotenv";
dotenv.config();
const envConfig = {
- logLevel: undefined,
+ logLevel: process.env.LOG_LEVEL,
apiBaseUrl: undefined,
clientHost: process.env.CLIENT_HOST,
jwtSecret: process.env.JWT_SECRET,
diff --git a/Server/utils/dataUtils.js b/Server/utils/dataUtils.js
index fd10a4257..ca6ca66df 100644
--- a/Server/utils/dataUtils.js
+++ b/Server/utils/dataUtils.js
@@ -38,7 +38,7 @@ const NormalizeData = (checks, rangeMin, rangeMax) => {
Math.min(rangeMax, normalizedResponseTime)
);
return {
- ...check._doc,
+ ...check,
responseTime: normalizedResponseTime,
originalResponseTime: originalResponseTime,
};
@@ -47,7 +47,7 @@ const NormalizeData = (checks, rangeMin, rangeMax) => {
return normalizedChecks;
} else {
return checks.map((check) => {
- return { ...check._doc, originalResponseTime: check.responseTime };
+ return { ...check, originalResponseTime: check.responseTime };
});
}
};
diff --git a/Server/validation/joi.js b/Server/validation/joi.js
index dbffef937..d30235048 100644
--- a/Server/validation/joi.js
+++ b/Server/validation/joi.js
@@ -136,32 +136,12 @@ const getMonitorByIdQueryValidation = joi.object({
normalize: joi.boolean(),
});
-const getMonitorsAndSummaryByTeamIdParamValidation = joi.object({
- teamId: joi.string().required(),
-});
-
-const getMonitorsAndSummaryByTeamIdQueryValidation = 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,
- getMonitorsAndSummaryByTeamIdParamValidation,
- getMonitorsAndSummaryByTeamIdQueryValidation,
- getMonitorsByTeamIdValidation,
+ getMonitorsByTeamIdParamValidation,
getMonitorsByTeamIdQueryValidation,
getMonitorStatsByIdParamValidation,
getMonitorStatsByIdQueryValidation,
diff --git a/grant_funding.md b/grant_funding.md
deleted file mode 100644
index e53698b4a..000000000
--- a/grant_funding.md
+++ /dev/null
@@ -1,83 +0,0 @@
-Announcing USD 5K Grant Prize for Distributed Uptime Management Development
-
-### January 9, 2025, [Checkmate](https://github.com/bluewave-labs/checkmate) & [UpRock](https://uprock.com/)
-
-
-**Checkmate** and **UpRock** are excited to announce a USD 5,000 grant prize funding opportunity to support the development of the next-generation distributed uptime manager. UpRock, a DePIN platform with over 2 million devices worldwide, provides the foundation for Checkmate to monitor server uptime from across the globe effectively.
-
-This grant funding aims to empower developers to contribute innovative solutions to enhance Checkmate's functionality and usability for distributed uptime management, powered by **UpRock**.
-
-## Program details
-
-* **Submission start date:** January 9, 2025
-* **Submission deadline:** January 20, 2025
-* **Announcement of prewinners:** January 25, 2025
-* **Total prize pool:** USD 5,000 (subject to change depending on the quality or number submissions)
-* **Completion timeline:** A maximum of 30 days after pre-selection
-
-## What we’re looking for
-
-* Participants are encouraged to submit ideas and features that:
-* Enhance Checkmate & UpRock's distributed uptime monitoring capabilities.
-* Simplify deployment for businesses and scale effectively.
-* Deliver innovative value-adds to Checkmate and UpRock platform.
-* Improve UI/UX consistency, performance, and monitoring reliability.
-
-Here are some of the sample ideas you can select from, or come up with your own.
-
-* **Project 1 (DePIN sourced intelligence):** Build a global internet speed survey using 3M devices. This dashboard will implement the underlying Checkmate & UpRock infrastructure and build maps and tables for: 1. Upload speed, 2. Download speed, 3. Ping numbers.
-* **Project 2:** Build a notification system that will cover Discord, Slack, Telegram and webhooks.
-* **Project 3:** Implement a i18n library for Checkmate. The translation framework will include modifying the source code and using an open source translation manager online so other translators can also contribute. This will help reach out to more developers to build DePIN features on top of Checkmate.
-* **Project 4:** A plugin system and implementation for Checkmate to help add more UpRock platform functionality. This plugin system will also be used to add more plugins/features on top of Checkmate easily.
-* **Project 5 (Checkmate CLI):** When installed via brew or curl, this CLI will help admins manage the monitoring setup directly from the command line. Admin will be able to list, check, trigger the monitors from the command line easily or change password of the users, or create/invite a new user.
-* **Project 6:** Ability to tag or group monitors. Each monitor will have its own tagging, and the admin will be able to filter monitors in the table.
-* **Project 7:** Deploy Checkmate and write a blog post about it on dev.to or Hackernoon. Bonus points if you market it on social media.
-
-## How to participate
-
-1. **Submit your ideas:** Fill out our online form to propose enhancements and features for Checkmate powered by UpRock, until January 20, 2025. You can submit a project based on the ideas above, or you can also come up with your own enhancement projects.
-2. S**election criteria:** Prewinners will be selected based on functionality, design and impact.
-3. **Implementation phase:** Prewinners will have 21 days to develop and submit their code contributions starting from January 25, 2025.
-4. **Quality requirements:** Submitted code must:
- - Be high quality and free from bugs.
- - Pass extensive QA testing
- - Adhere to Checkmate’s UI design and development standards.
-5. **Grant disbursement:** Upon code acceptance via pull, winners will receive USD 1,000 each, sent to their bank account or Paypal.
-
-## Timeline
-
-- **January 10, 2025:** Grant announcement and form open for submissions.
-- **January 20, 2025:** Submission deadline
-- **January 25, 2025:** Winners announced.
-- **21 days post-selection:** Code development and submission period.
-- **Grant payment:** Issued in 14 days upon PR acceptance.
-
-## Rules
-
-- **Cap enforcement:** All projects submitted to the Checkmate & UpRock grant program will be subject to a strict funding cap of $1000 per project. This rule ensures that funding is distributed fairly among contributors, encouraging a diverse range of submissions. Applicants are encouraged to scope their projects accordingly to stay within this limit. Proposals exceeding this amount may either be revised or considered for partial funding.
-- **Multiple submissions:** Contributors are welcome to submit multiple projects, provided each submission adheres to the specified funding cap and size criteria. This approach allows developers to showcase various ideas, features, and improvements without being constrained to a single proposal. Each project will be evaluated independently, ensuring fair consideration regardless of other submissions by the same contributor.
-- **Partial funding:** In cases where a proposal exceeds the budget limit or includes additional features that extend beyond the core requirements, partial funding may be offered. Contributors will receive feedback and recommendations to adjust their project scope to align with the funding guidelines. This flexibility ensures that high-value features and enhancements can still be supported, even if full funding is not feasible.
-- **Feedback process:** To support continuous improvement, contributors will receive constructive feedback on their submissions, whether accepted or not. Feedback will focus on areas such as code quality, design patterns, documentation and usability. Developers can use this input to refine their proposals, submit follow-ups, or enhance their future contributions.
-- **Final review and recognition:** A portion of the total funding, $500 (10%), will be reserved for recognizing outstanding contributions across all categories. This award, labeled the "Best Overall Contribution" prize, will be given to a project that demonstrates exceptional innovation, technical quality, and impact. The selection process will be based on the same evaluation criteria as other submissions, with additional emphasis on originality and long-term value to the Checkmate & UpRock ecosystem.
-
-## Funding tiers
-- **4-5 small size projects**
- - Examples: Usability fixes, UI enhancements, updating test cases, writing command line interfaces or documentation updates.
- - Reward: $100–$300
-- **4-5 medium size projects**
- - Examples: New monitoring modules/additions, API enhancements, performance improvements or feature additions.
- - Reward: $300–$600
-- **1-2 large projects**
- - Examples: Integrations with 3rd party / external tools, major scalability enhancements, advanced analytics features, complex monitoring capabilities, better graphics/charts.
- - Reward: $600–$1000
-
-## Evaluation criteria
-
-- **Innovation (30%):** Uniqueness and creativity of the solution.
-- **Contribution value (30%):** Impact on usability, performance, or scalability.
-- **Code quality & design (40%):** Clean, documented and tested code with working examples showing screenshots and/or videos.
-
-## Ready to innovate for distributed uptime?
-
-Don’t miss this opportunity to shape the future of distributed uptime monitoring with Checkmate and UpRock! Submit your ideas today and join us in building cutting-edge solutions for global server management.
-Visit Link to submit your entry.