mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-24 02:29:35 -06:00
Merge branch 'develop' into feat/fe/statuspage-3
This commit is contained in:
94
Client/package-lock.json
generated
94
Client/package-lock.json
generated
@@ -11,9 +11,9 @@
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@mui/icons-material": "6.3.0",
|
||||
"@mui/lab": "6.0.0-beta.21",
|
||||
"@mui/material": "6.3.0",
|
||||
"@mui/icons-material": "6.3.1",
|
||||
"@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",
|
||||
@@ -1106,9 +1106,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/core-downloads-tracker": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.3.0.tgz",
|
||||
"integrity": "sha512-/d8NwSuC3rMwCjswmGB3oXC4sdDuhIUJ8inVQAxGrADJhf0eq/kmy+foFKvpYhHl2siOZR+MLdFttw6/Bzqtqg==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.3.1.tgz",
|
||||
"integrity": "sha512-2OmnEyoHpj5//dJJpMuxOeLItCCHdf99pjMFfUFdBteCunAK9jW+PwEo4mtdGcLs7P+IgZ+85ypd52eY4AigoQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -1116,9 +1116,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/icons-material": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.3.0.tgz",
|
||||
"integrity": "sha512-3uWws6DveDn5KxCS34p+sUNMxehuclQY6OmoJeJJ+Sfg9L7LGBpksY/nX5ywKAqickTZnn+sQyVcp963ep9jvw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.3.1.tgz",
|
||||
"integrity": "sha512-nJmWj1PBlwS3t1PnoqcixIsftE+7xrW3Su7f0yrjPw4tVjYrgkhU0hrRp+OlURfZ3ptdSkoBkalee9Bhf1Erfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0"
|
||||
@@ -1131,7 +1131,7 @@
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mui/material": "^6.3.0",
|
||||
"@mui/material": "^6.3.1",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
@@ -1142,16 +1142,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/lab": {
|
||||
"version": "6.0.0-beta.21",
|
||||
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.21.tgz",
|
||||
"integrity": "sha512-hiFZgTwBNhJMUlEhmqfW4+5wy3C8UF9KFuzSOux6x4kgc9hsC0l+motXcF1Vyh+jhJYGeZ6yUoImqCf9RWzEvw==",
|
||||
"version": "6.0.0-beta.22",
|
||||
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.22.tgz",
|
||||
"integrity": "sha512-9nwUfBj+UzoQJOCbqV+JcCSJ74T+gGWrM1FMlXzkahtYUcMN+5Zmh2ArlttW3zv2dZyCzp7K5askcnKF0WzFQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/base": "5.0.0-beta.68",
|
||||
"@mui/system": "^6.3.0",
|
||||
"@mui/types": "^7.2.20",
|
||||
"@mui/utils": "^6.3.0",
|
||||
"@mui/system": "^6.3.1",
|
||||
"@mui/types": "^7.2.21",
|
||||
"@mui/utils": "^6.3.1",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
@@ -1165,8 +1165,8 @@
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@mui/material": "^6.3.0",
|
||||
"@mui/material-pigment-css": "^6.3.0",
|
||||
"@mui/material": "^6.3.1",
|
||||
"@mui/material-pigment-css": "^6.3.1",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
@@ -1187,16 +1187,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.3.0.tgz",
|
||||
"integrity": "sha512-qhlTFyRMxfoVPxUtA5e8IvqxP0dWo2Ij7cvot7Orag+etUlZH+3UwD8gZGt+3irOoy7Ms3UNBflYjwEikUXtAQ==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.3.1.tgz",
|
||||
"integrity": "sha512-ynG9ayhxgCsHJ/dtDcT1v78/r2GwQyP3E0hPz3GdPRl0uFJz/uUTtI5KFYwadXmbC+Uv3bfB8laZ6+Cpzh03gA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/core-downloads-tracker": "^6.3.0",
|
||||
"@mui/system": "^6.3.0",
|
||||
"@mui/types": "^7.2.20",
|
||||
"@mui/utils": "^6.3.0",
|
||||
"@mui/core-downloads-tracker": "^6.3.1",
|
||||
"@mui/system": "^6.3.1",
|
||||
"@mui/types": "^7.2.21",
|
||||
"@mui/utils": "^6.3.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -1215,7 +1215,7 @@
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@mui/material-pigment-css": "^6.3.0",
|
||||
"@mui/material-pigment-css": "^6.3.1",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
@@ -1242,13 +1242,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mui/private-theming": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.3.0.tgz",
|
||||
"integrity": "sha512-tdS8jvqMokltNTXg6ioRCCbVdDmZUJZa/T9VtTnX2Lwww3FTgCakst9tWLZSxm1fEE9Xp0m7onZJmgeUmWQYVw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.3.1.tgz",
|
||||
"integrity": "sha512-g0u7hIUkmXmmrmmf5gdDYv9zdAig0KoxhIQn1JN8IVqApzf/AyRhH3uDGx5mSvs8+a1zb4+0W6LC260SyTTtdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/utils": "^6.3.0",
|
||||
"@mui/utils": "^6.3.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1269,9 +1269,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/styled-engine": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.3.0.tgz",
|
||||
"integrity": "sha512-iWA6eyiPkO07AlHxRUvI7dwVRSc+84zV54kLmjUms67GJeOWVuXlu8ZO+UhCnwJxHacghxnabsMEqet5PYQmHg==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.3.1.tgz",
|
||||
"integrity": "sha512-/7CC0d2fIeiUxN5kCCwYu4AWUDd9cCTxWCyo0v/Rnv6s8uk6hWgJC3VLZBoDENBHf/KjqDZuYJ2CR+7hD6QYww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
@@ -1303,16 +1303,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/system": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.3.0.tgz",
|
||||
"integrity": "sha512-L+8hUHMNlfReKSqcnVslFrVhoNfz/jw7Fe9NfDE85R3KarvZ4O3MU9daF/lZeqEAvnYxEilkkTfDwQ7qCgJdFg==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.3.1.tgz",
|
||||
"integrity": "sha512-AwqQ3EAIT2np85ki+N15fF0lFXX1iFPqenCzVOSl3QXKy2eifZeGd9dGtt7pGMoFw5dzW4dRGGzRpLAq9rkl7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/private-theming": "^6.3.0",
|
||||
"@mui/styled-engine": "^6.3.0",
|
||||
"@mui/types": "^7.2.20",
|
||||
"@mui/utils": "^6.3.0",
|
||||
"@mui/private-theming": "^6.3.1",
|
||||
"@mui/styled-engine": "^6.3.1",
|
||||
"@mui/types": "^7.2.21",
|
||||
"@mui/utils": "^6.3.1",
|
||||
"clsx": "^2.1.1",
|
||||
"csstype": "^3.1.3",
|
||||
"prop-types": "^15.8.1"
|
||||
@@ -1343,9 +1343,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/types": {
|
||||
"version": "7.2.20",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.20.tgz",
|
||||
"integrity": "sha512-straFHD7L8v05l/N5vcWk+y7eL9JF0C2mtph/y4BPm3gn2Eh61dDwDB65pa8DLss3WJfDXYC7Kx5yjP0EmXpgw==",
|
||||
"version": "7.2.21",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz",
|
||||
"integrity": "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
@@ -1357,13 +1357,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/utils": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.3.0.tgz",
|
||||
"integrity": "sha512-MkDBF08OPVwXhAjedyMykRojgvmf0y/jxkBWjystpfI/pasyTYrfdv4jic6s7j3y2+a+SJzS9qrD6X8ZYj/8AQ==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.3.1.tgz",
|
||||
"integrity": "sha512-sjGjXAngoio6lniQZKJ5zGfjm+LD2wvLwco7FbKe1fu8A7VIFmz2SwkLb+MDPLNX1lE7IscvNNyh1pobtZg2tw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mui/types": "^7.2.20",
|
||||
"@mui/types": "^7.2.21",
|
||||
"@types/prop-types": "^15.7.14",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@mui/icons-material": "6.3.0",
|
||||
"@mui/lab": "6.0.0-beta.21",
|
||||
"@mui/material": "6.3.0",
|
||||
"@mui/icons-material": "6.3.1",
|
||||
"@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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import {
|
||||
TableContainer,
|
||||
Paper,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
TablePagination,
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setRowsPerPage } from "../../Features/UI/uiSlice";
|
||||
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 SelectorVertical from "../../assets/icons/selector-vertical.svg?react";
|
||||
import "./index.css";
|
||||
/**
|
||||
* Component for pagination actions (first, previous, next, last).
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props
|
||||
* @param {number} props.count - Total number of items.
|
||||
* @param {number} props.page - Current page number.
|
||||
* @param {number} props.rowsPerPage - Number of rows per page.
|
||||
* @param {function} props.onPageChange - Callback function to handle page change.
|
||||
*
|
||||
* @returns {JSX.Element} Pagination actions component.
|
||||
*/
|
||||
const TablePaginationActions = (props) => {
|
||||
const { count, page, rowsPerPage, onPageChange } = props;
|
||||
|
||||
const handleFirstPageButtonClick = (event) => {
|
||||
onPageChange(event, 0);
|
||||
};
|
||||
const handleBackButtonClick = (event) => {
|
||||
onPageChange(event, page - 1);
|
||||
};
|
||||
const handleNextButtonClick = (event) => {
|
||||
onPageChange(event, page + 1);
|
||||
};
|
||||
const handleLastPageButtonClick = (event) => {
|
||||
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ flexShrink: 0, ml: "24px" }}>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleFirstPageButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="first page"
|
||||
>
|
||||
<LeftArrowDouble />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="previous page"
|
||||
>
|
||||
<LeftArrow />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleNextButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="next page"
|
||||
>
|
||||
<RightArrow />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
>
|
||||
<RightArrowDouble />
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
TablePaginationActions.propTypes = {
|
||||
count: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
rowsPerPage: PropTypes.number.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* BasicTable Component
|
||||
* Renders a table with optional pagination.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - Component props.
|
||||
* @param {Object} props.data - Data for the table including columns and rows.
|
||||
* @param {Array} props.data.cols - Array of objects for column headers.
|
||||
* @param {number} props.data.cols[].id - Unique identifier for the column.
|
||||
* @param {string} props.data.cols[].name - Name of the column to display as header.
|
||||
* @param {Array} props.data.rows - Array of row objects.
|
||||
* @param {number} props.data.rows[].id - Unique identifier for the row.
|
||||
* @param {Array} props.data.rows[].data - Array of cell data objects for the row.
|
||||
* @param {number} props.data.rows[].data[].id - Unique identifier for the cell.
|
||||
* @param {JSX.Element} props.data.rows[].data[].data - The content to display in the cell.
|
||||
* @param {function} props.data.rows.data.handleClick - Function to call when the row is clicked.
|
||||
* @param {boolean} [props.paginated=false] - Flag to enable pagination.
|
||||
* @param {boolean} [props.reversed=false] - Flag to enable reverse order.
|
||||
* @param {number} props.rowsPerPage- Number of rows per page (table).
|
||||
* @param {string} props.emptyMessage - Message to display when there is no data.
|
||||
* @example
|
||||
* const data = {
|
||||
* cols: [
|
||||
* { id: 1, name: "First Col" },
|
||||
* { id: 2, name: "Second Col" },
|
||||
* { id: 3, name: "Third Col" },
|
||||
* { id: 4, name: "Fourth Col" },
|
||||
* ],
|
||||
* rows: [
|
||||
* {
|
||||
* id: 1,
|
||||
* data: [
|
||||
* { id: 1, data: <div>Data for Row 1 Col 1</div> },
|
||||
* { id: 2, data: <div>Data for Row 1 Col 2</div> },
|
||||
* { id: 3, data: <div>Data for Row 1 Col 3</div> },
|
||||
* { id: 4, data: <div>Data for Row 1 Col 4</div> },
|
||||
* ],
|
||||
* },
|
||||
* {
|
||||
* id: 2,
|
||||
* data: [
|
||||
* { id: 5, data: <div>Data for Row 2 Col 1</div> },
|
||||
* { id: 6, data: <div>Data for Row 2 Col 2</div> },
|
||||
* { id: 7, data: <div>Data for Row 2 Col 3</div> },
|
||||
* { id: 8, data: <div>Data for Row 2 Col 4</div> },
|
||||
* ],
|
||||
* },
|
||||
* ],
|
||||
* };
|
||||
*
|
||||
* <BasicTable data={data} rows={rows} paginated={true} />
|
||||
*/
|
||||
|
||||
const BasicTable = ({ data, paginated, reversed, table, emptyMessage = "No data" }) => {
|
||||
const DEFAULT_ROWS_PER_PAGE = 5;
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const uiState = useSelector((state) => state.ui);
|
||||
let rowsPerPage = uiState?.[table]?.rowsPerPage ?? DEFAULT_ROWS_PER_PAGE;
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(0);
|
||||
}, [data]);
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value: parseInt(event.target.value, 10),
|
||||
table: table,
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
let displayData = [];
|
||||
|
||||
if (data && data.rows) {
|
||||
let rows = reversed ? [...data.rows].reverse() : data.rows;
|
||||
displayData = paginated
|
||||
? rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
: rows;
|
||||
}
|
||||
|
||||
if (!data || !data.cols || !data.rows) {
|
||||
return <div>No data</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to calculate the range of displayed rows.
|
||||
* @returns {string}
|
||||
*/
|
||||
const getRange = () => {
|
||||
let start = page * rowsPerPage + 1;
|
||||
let end = Math.min(page * rowsPerPage + rowsPerPage, data.rows.length);
|
||||
return `${start} - ${end}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{data.cols.map((col) => (
|
||||
<TableCell key={col.id}>{col.name}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{displayData.map((row) => {
|
||||
return (
|
||||
<TableRow
|
||||
sx={{
|
||||
cursor: row.handleClick ? "pointer" : "default",
|
||||
"&:hover": {
|
||||
filter: "brightness(.75)",
|
||||
opacity: 0.75,
|
||||
transition: "filter 0.3s ease, opacity 0.3s ease",
|
||||
},
|
||||
}}
|
||||
key={row.id}
|
||||
onClick={row.handleClick ? row.handleClick : null}
|
||||
>
|
||||
{row.data.map((cell) => {
|
||||
return <TableCell key={cell.id}>{cell.data}</TableCell>;
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{displayData.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{ textAlign: "center" }}
|
||||
colSpan={data.cols.length}
|
||||
>
|
||||
{emptyMessage}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
{paginated && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
px={theme.spacing(4)}
|
||||
sx={{
|
||||
"& p": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
px={theme.spacing(2)}
|
||||
fontSize={12}
|
||||
sx={{ opacity: 0.7 }}
|
||||
>
|
||||
Showing {getRange()} of {data.rows.length} monitor(s)
|
||||
</Typography>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={data.rows.length}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
rowsPerPageOptions={[5, 10, 15, 25]}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
ActionsComponent={TablePaginationActions}
|
||||
labelRowsPerPage="Rows per page"
|
||||
labelDisplayedRows={({ page, count }) =>
|
||||
`Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}`
|
||||
}
|
||||
slotProps={{
|
||||
select: {
|
||||
MenuProps: {
|
||||
keepMounted: true,
|
||||
PaperProps: {
|
||||
className: "pagination-dropdown",
|
||||
sx: {
|
||||
mt: 0,
|
||||
mb: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
transformOrigin: { vertical: "bottom", horizontal: "left" },
|
||||
anchorOrigin: { vertical: "top", horizontal: "left" },
|
||||
sx: { mt: theme.spacing(-2) },
|
||||
},
|
||||
inputProps: { id: "pagination-dropdown" },
|
||||
IconComponent: SelectorVertical,
|
||||
sx: {
|
||||
ml: theme.spacing(4),
|
||||
mr: theme.spacing(12),
|
||||
minWidth: theme.spacing(20),
|
||||
textAlign: "left",
|
||||
"&.Mui-focused > div": {
|
||||
backgroundColor: theme.palette.background.main,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
mt: theme.spacing(6),
|
||||
color: theme.palette.text.secondary,
|
||||
"& svg path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
strokeWidth: 1.3,
|
||||
},
|
||||
"& .MuiSelect-select": {
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BasicTable.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
paginated: PropTypes.bool,
|
||||
reversed: PropTypes.bool,
|
||||
rowPerPage: PropTypes.number,
|
||||
table: PropTypes.string,
|
||||
emptyMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
export default BasicTable;
|
||||
@@ -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)}
|
||||
</Typography>
|
||||
<Box mt={theme.spacing(1)}>
|
||||
<Box
|
||||
@@ -102,13 +107,12 @@ CustomToolTip.propTypes = {
|
||||
})
|
||||
),
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
dateRange: PropTypes.string,
|
||||
};
|
||||
const CustomTick = ({ x, y, payload, index }) => {
|
||||
const CustomTick = ({ x, y, payload, index, 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 (
|
||||
<Text
|
||||
x={x}
|
||||
@@ -118,7 +122,7 @@ const CustomTick = ({ x, y, payload, index }) => {
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
|
||||
{formatDateWithTz(payload?.value, format, uiTimezone)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -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 }) => {
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
dataKey="_id"
|
||||
tick={<CustomTick />}
|
||||
minTickGap={0}
|
||||
tick={<CustomTick dateRange={dateRange} />}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
height={20}
|
||||
interval="equidistantPreserveStart"
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ stroke: theme.palette.border.light }}
|
||||
content={<CustomToolTip />}
|
||||
content={<CustomToolTip dateRange={dateRange} />}
|
||||
wrapperStyle={{ pointerEvents: "none" }}
|
||||
/>
|
||||
<Area
|
||||
@@ -211,6 +214,7 @@ const MonitorDetailsAreaChart = ({ checks }) => {
|
||||
|
||||
MonitorDetailsAreaChart.propTypes = {
|
||||
checks: PropTypes.array,
|
||||
dateRange: PropTypes.string,
|
||||
};
|
||||
|
||||
export default MonitorDetailsAreaChart;
|
||||
|
||||
25
Client/src/Components/Charts/Utils/chartUtilFunctions.js
Normal file
25
Client/src/Components/Charts/Utils/chartUtilFunctions.js
Normal file
@@ -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;
|
||||
};
|
||||
@@ -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 (
|
||||
<Text
|
||||
x={x}
|
||||
@@ -27,7 +28,7 @@ export const TzTick = ({ x, y, payload, index }) => {
|
||||
fontSize={11}
|
||||
fontWeight={400}
|
||||
>
|
||||
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
|
||||
{formatDateWithTz(payload?.value, format, uiTimezone)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -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)}
|
||||
</Typography>
|
||||
<Box mt={theme.spacing(1)}>
|
||||
<Box
|
||||
@@ -185,11 +190,20 @@ InfrastructureTooltip.propTypes = {
|
||||
yIdx: PropTypes.number,
|
||||
yLabel: PropTypes.string,
|
||||
dotColor: PropTypes.string,
|
||||
dateRange: PropTypes.string,
|
||||
};
|
||||
|
||||
export const TemperatureTooltip = ({ active, payload, label, keys, dotColor }) => {
|
||||
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)}
|
||||
</Typography>
|
||||
|
||||
<Stack direction="column">
|
||||
@@ -273,4 +287,6 @@ TemperatureTooltip.propTypes = {
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
dotColor: PropTypes.string,
|
||||
dateRange: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -138,6 +138,7 @@ const Search = ({
|
||||
}
|
||||
return filtered;
|
||||
}}
|
||||
getOptionKey={(option) => option.id}
|
||||
renderOption={(props, option) => {
|
||||
const { key, ...optionProps } = props;
|
||||
return (
|
||||
|
||||
@@ -126,8 +126,8 @@ const PasswordPanel = () => {
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(26)}
|
||||
maxWidth={"80ch"}
|
||||
marginInline={"auto"}
|
||||
maxWidth={"80ch"} // Keep maxWidth
|
||||
|
||||
>
|
||||
<TextInput
|
||||
type="text"
|
||||
@@ -166,7 +166,7 @@ const PasswordPanel = () => {
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
alignItems={"flex-start"}
|
||||
gap={SPACING_GAP}
|
||||
flexWrap={"wrap"}
|
||||
>
|
||||
@@ -192,7 +192,7 @@ const PasswordPanel = () => {
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
alignItems={"flex-start"}
|
||||
gap={SPACING_GAP}
|
||||
flexWrap={"wrap"}
|
||||
>
|
||||
@@ -255,4 +255,4 @@ PasswordPanel.propTypes = {
|
||||
// No props are being passed to this component, hence no specific PropTypes are defined.
|
||||
};
|
||||
|
||||
export default PasswordPanel;
|
||||
export default PasswordPanel;
|
||||
@@ -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 (
|
||||
<Stack>
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
{row.firstName + " " + row.lastName}
|
||||
</Typography>
|
||||
<Typography>
|
||||
Created {new Date(row.createdAt).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
},
|
||||
{ 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: (
|
||||
<Stack>
|
||||
<Typography color={theme.palette.text.secondary}>
|
||||
{member.firstName + " " + member.lastName}
|
||||
</Typography>
|
||||
<Typography>
|
||||
Created {new Date(member.createdAt).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{ 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
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
<BasicTable
|
||||
data={tableData}
|
||||
paginated={false}
|
||||
reversed={true}
|
||||
table={"team"}
|
||||
emptyMessage={"There are no team members with this role"}
|
||||
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={data}
|
||||
config={{ emptyView: "There are no team members with this role" }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -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.background.main,
|
||||
},
|
||||
"& .MuiSelect-icon": {
|
||||
// Add this style override
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
113
Client/src/Components/Table/index.jsx
Normal file
113
Client/src/Components/Table/index.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
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 (
|
||||
<TableContainer component={Paper}>
|
||||
<Table stickyHeader>
|
||||
<TableHead sx={{ backgroundColor: theme.palette.background.accent }}>
|
||||
<TableRow>
|
||||
{headers.map((header, index) => (
|
||||
<TableCell
|
||||
key={header.id}
|
||||
align={index === 0 ? "left" : "center"}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
}}
|
||||
>
|
||||
{header.content}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{(data?.length ?? 0) === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={headers.length}
|
||||
align="center"
|
||||
>
|
||||
{config.emptyView}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
data.map((row) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
sx={config?.rowSX ?? {}}
|
||||
onClick={() => config?.onRowClick(row)}
|
||||
>
|
||||
{headers.map((header, index) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={index === 0 ? "left" : "center"}
|
||||
key={header.id}
|
||||
>
|
||||
{header.render(row)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -40,7 +40,7 @@ export const checkEndpointResolution = createAsyncThunk(
|
||||
const res = await networkService.checkEndpointResolution({
|
||||
authToken: authToken,
|
||||
monitorURL: monitorURL,
|
||||
})
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
@@ -53,7 +53,7 @@ export const checkEndpointResolution = createAsyncThunk(
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const getPagespeedMonitorById = createAsyncThunk(
|
||||
"monitors/getMonitorById",
|
||||
@@ -88,7 +88,6 @@ export const getPageSpeedByTeamId = createAsyncThunk(
|
||||
teamId: user.teamId,
|
||||
types: ["pagespeed"],
|
||||
});
|
||||
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
|
||||
@@ -78,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
|
||||
// *****************************************************
|
||||
|
||||
@@ -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 (
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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) => <HttpStatusLabel status={row.statusCode} />,
|
||||
},
|
||||
{ id: "message", content: "Message", render: (row) => row.message },
|
||||
];
|
||||
|
||||
let paginationComponent = <></>;
|
||||
if (checksCount > paginationController.rowsPerPage) {
|
||||
paginationComponent = (
|
||||
@@ -166,47 +194,11 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Monitor Name</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Date & Time</TableCell>
|
||||
<TableCell>Status Code</TableCell>
|
||||
<TableCell>Message</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{checks.map((check) => {
|
||||
const status = check.status === true ? "up" : "down";
|
||||
const formattedDate = formatDateWithTz(
|
||||
check.createdAt,
|
||||
"YYYY-MM-DD HH:mm:ss A",
|
||||
uiTimezone
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRow key={check._id}>
|
||||
<TableCell>{monitors[check.monitorId]?.name}</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{formattedDate}</TableCell>
|
||||
<TableCell>
|
||||
<HttpStatusLabel status={check.statusCode} />
|
||||
</TableCell>
|
||||
<TableCell>{check.message}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={checks}
|
||||
/>
|
||||
|
||||
{paginationComponent}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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: <PercentTick />,
|
||||
xTick: <TzTick />,
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.primary.main}
|
||||
yKey={"avgMemoryUsage"}
|
||||
yLabel={"Memory usage"}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -433,12 +433,13 @@ const InfrastructureDetails = () => {
|
||||
yLabel: "CPU usage",
|
||||
yDomain: [0, 1],
|
||||
yTick: <PercentTick />,
|
||||
xTick: <TzTick />,
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.success.main}
|
||||
yKey={"avgCpuUsage"}
|
||||
yLabel={"CPU usage"}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -450,7 +451,7 @@ const InfrastructureDetails = () => {
|
||||
gradientStartColor: theme.palette.error.main,
|
||||
heading: "CPU Temperature",
|
||||
yLabel: "Temperature",
|
||||
xTick: <TzTick />,
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
yDomain: [
|
||||
0,
|
||||
Math.max(
|
||||
@@ -462,6 +463,7 @@ const InfrastructureDetails = () => {
|
||||
<TemperatureTooltip
|
||||
keys={tempKeys}
|
||||
dotColor={theme.palette.error.main}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -476,13 +478,14 @@ const InfrastructureDetails = () => {
|
||||
yLabel: "Disk Usage",
|
||||
yDomain: [0, 1],
|
||||
yTick: <PercentTick />,
|
||||
xTick: <TzTick />,
|
||||
xTick: <TzTick dateRange={dateRange} />,
|
||||
toolTip: (
|
||||
<InfrastructureTooltip
|
||||
dotColor={theme.palette.warning.main}
|
||||
yKey={`disks.usagePercent`}
|
||||
yLabel={"Disc usage"}
|
||||
yIdx={idx}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
),
|
||||
})) || []),
|
||||
|
||||
@@ -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?.monitors ?? []);
|
||||
setSummary(response?.data?.data?.summary ?? {});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
}, [page, rowsPerPage, authToken, user.teamId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMonitors();
|
||||
}, [page, rowsPerPage]);
|
||||
}, [fetchMonitors]);
|
||||
|
||||
const { determineState } = useUtils();
|
||||
const { monitors, total: totalMonitors } = monitorState;
|
||||
// do it here
|
||||
function openDetails(id) {
|
||||
navigate(`/infrastructure/${id}`);
|
||||
@@ -139,6 +89,71 @@ function Infrastructure() {
|
||||
fetchMonitors();
|
||||
}
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "host",
|
||||
content: "Host",
|
||||
render: (row) => (
|
||||
<Host
|
||||
title={row.name}
|
||||
url={row.url}
|
||||
percentage={row.uptimePercentage}
|
||||
percentageColor={row.percentageColor}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: "Status",
|
||||
render: (row) => (
|
||||
<StatusLabel
|
||||
status={row.status}
|
||||
text={row.status}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "frequency",
|
||||
content: "Frequency",
|
||||
render: (row) => (
|
||||
<Stack
|
||||
direction={"row"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
gap=".25rem"
|
||||
>
|
||||
<CPUChipIcon
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
{row.processor}
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{ id: "cpu", content: "CPU", render: (row) => <CustomGauge progress={row.cpu} /> },
|
||||
{ id: "mem", content: "Mem", render: (row) => <CustomGauge progress={row.mem} /> },
|
||||
{ id: "disk", content: "Disk", render: (row) => <CustomGauge progress={row.disk} /> },
|
||||
{
|
||||
id: "actions",
|
||||
content: "Actions",
|
||||
render: (row) => (
|
||||
<IconButton
|
||||
sx={{
|
||||
"& svg path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<InfrastructureMenu
|
||||
monitor={row}
|
||||
isAdmin={isAdmin}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
/>
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
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 (
|
||||
<Box
|
||||
className="infrastructure-monitor"
|
||||
@@ -189,7 +204,7 @@ function Infrastructure() {
|
||||
>
|
||||
{isActuallyLoading ? (
|
||||
<SkeletonLayout />
|
||||
) : monitorState.monitors?.length !== 0 ? (
|
||||
) : monitors?.length !== 0 ? (
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Box>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
@@ -234,108 +249,25 @@ function Infrastructure() {
|
||||
borderColor={theme.palette.border.light}
|
||||
backgroundColor={theme.palette.background.accent}
|
||||
>
|
||||
{totalMonitors}
|
||||
{summary?.totalMonitors ?? 0}
|
||||
</Box>
|
||||
</Stack>
|
||||
<TableContainer component={Paper}>
|
||||
<Table stickyHeader>
|
||||
<TableHead sx={{ backgroundColor: theme.palette.background.accent }}>
|
||||
<TableRow>
|
||||
{columns.map((column, index) => (
|
||||
<TableCell
|
||||
key={index}
|
||||
align={index === 0 ? "left" : "center"}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
}}
|
||||
>
|
||||
{column.label}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{monitorsAsRows.map((row) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
onClick={() => openDetails(row.id)}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* TODO iterate over column and get column id, applying row[column.id] */}
|
||||
<TableCell>
|
||||
<Host
|
||||
title={row.name}
|
||||
url={row.url}
|
||||
percentage={row.uptimePercentage}
|
||||
percentageColor={row.percentageColor}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<StatusLabel
|
||||
status={row.status}
|
||||
text={row.status}
|
||||
/* Use capitalize inside of Status Label */
|
||||
/* Update component so we don't need to pass text and status separately*/
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Stack
|
||||
direction={"row"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
gap=".25rem"
|
||||
>
|
||||
<CPUChipIcon
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
{row.processor}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<CustomGauge progress={row.cpu} />
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<CustomGauge progress={row.mem} />
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<CustomGauge progress={row.disk} />
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{/* Get ActionsMenu from Monitor Table and create a component */}
|
||||
<IconButton
|
||||
sx={{
|
||||
"& svg path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<InfrastructureMenu
|
||||
monitor={row}
|
||||
isAdmin={isAdmin}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
/>
|
||||
{/* <GearIcon
|
||||
width={20}
|
||||
height={20}
|
||||
/> */}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<DataTable
|
||||
config={{
|
||||
rowSX: {
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => openDetails(row.id),
|
||||
}}
|
||||
headers={headers}
|
||||
data={monitorsAsRows}
|
||||
/>
|
||||
<Pagination
|
||||
monitorCount={totalMonitors}
|
||||
monitorCount={summary?.totalMonitors ?? 0}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
|
||||
@@ -267,7 +267,7 @@ const Card = ({ monitor }) => {
|
||||
sx={{ gridColumnStart: 1, gridColumnEnd: 4 }}
|
||||
>
|
||||
<PagespeedAreaChart
|
||||
data={monitor.checks}
|
||||
data={monitor.checks.slice().reverse()}
|
||||
status={monitorState}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Box, Button, Grid, Stack } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getPageSpeedByTeamId } from "../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import { useSelector } from "react-redux";
|
||||
import Fallback from "../../Components/Fallback";
|
||||
import "./index.css";
|
||||
import { useNavigate } from "react-router";
|
||||
@@ -15,16 +14,12 @@ import { Heading } from "../../Components/Heading";
|
||||
import { useIsAdmin } from "../../Hooks/useIsAdmin";
|
||||
const PageSpeed = () => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const isAdmin = useIsAdmin();
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [monitors, setMonitors] = useState([]);
|
||||
const [monitorCount, setMonitorCount] = useState(0);
|
||||
useEffect(() => {
|
||||
dispatch(getPageSpeedByTeamId(authToken));
|
||||
}, [authToken, dispatch]);
|
||||
const [summary, setSummary] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
@@ -35,9 +30,6 @@ const PageSpeed = () => {
|
||||
teamId: user.teamId,
|
||||
limit: 10,
|
||||
types: ["pagespeed"],
|
||||
status: null,
|
||||
checkOrder: "desc",
|
||||
normalize: true,
|
||||
page: null,
|
||||
rowsPerPage: null,
|
||||
filter: null,
|
||||
@@ -46,7 +38,7 @@ const PageSpeed = () => {
|
||||
});
|
||||
if (res?.data?.data?.monitors) {
|
||||
setMonitors(res.data.data.monitors);
|
||||
setMonitorCount(res.data.data.monitorCount);
|
||||
setSummary(res.data.data.summary);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -118,7 +110,7 @@ const PageSpeed = () => {
|
||||
borderColor={theme.palette.border.light}
|
||||
backgroundColor={theme.palette.background.accent}
|
||||
>
|
||||
{monitorCount}
|
||||
{summary?.totalMonitors ?? 0}
|
||||
</Box>
|
||||
</Stack>
|
||||
<Grid
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
updateUptimeMonitor,
|
||||
pauseUptimeMonitor,
|
||||
getUptimeMonitorById,
|
||||
getUptimeMonitorsByTeamId,
|
||||
deleteUptimeMonitor,
|
||||
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
@@ -157,7 +156,6 @@ const Configure = () => {
|
||||
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." });
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Box className="create-monitor">
|
||||
|
||||
@@ -308,7 +308,9 @@ const DetailsPage = () => {
|
||||
<Typography component="span">
|
||||
{hoveredUptimeData !== null
|
||||
? hoveredUptimeData.totalChecks
|
||||
: (monitor.stats?.upChecksAggregate?.totalChecks ?? 0)}
|
||||
: (monitor.stats?.upChecks?.reduce((count, checkGroup) => {
|
||||
return count + checkGroup.totalChecks;
|
||||
}, 0) ?? 0)}
|
||||
</Typography>
|
||||
{hoveredUptimeData !== null && hoveredUptimeData.time !== null && (
|
||||
<Typography
|
||||
@@ -364,7 +366,9 @@ const DetailsPage = () => {
|
||||
<Typography component="span">
|
||||
{hoveredIncidentsData !== null
|
||||
? hoveredIncidentsData.totalChecks
|
||||
: (monitor.stats?.downChecksAggregate?.totalChecks ?? 0)}
|
||||
: (monitor.stats?.downChecks?.reduce((count, checkGroup) => {
|
||||
return count + checkGroup.totalChecks;
|
||||
}, 0) ?? 0)}
|
||||
</Typography>
|
||||
{hoveredIncidentsData !== null &&
|
||||
hoveredIncidentsData.time !== null && (
|
||||
@@ -410,7 +414,10 @@ const DetailsPage = () => {
|
||||
</IconBox>
|
||||
<Typography component="h2">Response Times</Typography>
|
||||
</Stack>
|
||||
<MonitorDetailsAreaChart checks={monitor?.stats?.groupChecks ?? []} />
|
||||
<MonitorDetailsAreaChart
|
||||
checks={monitor?.stats?.groupChecks ?? []}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
</ChartBox>
|
||||
<ChartBox
|
||||
gap={theme.spacing(8)}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import Search from "../../../../Components/Inputs/Search";
|
||||
import MemoizedMonitorTable from "../UptimeTable";
|
||||
import { useState } from "react";
|
||||
import useDebounce from "../../../../Utils/debounce";
|
||||
import PropTypes from "prop-types";
|
||||
import { Heading } from "../../../../Components/Heading";
|
||||
|
||||
const CurrentMonitoring = ({ totalMonitors, monitors, isAdmin, handlePause }) => {
|
||||
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 (
|
||||
<Box
|
||||
flex={1}
|
||||
py={theme.spacing(8)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Heading component="h2">Uptime monitors</Heading>
|
||||
|
||||
<Box
|
||||
className="current-monitors-counter"
|
||||
color={theme.palette.text.primary}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
backgroundColor={theme.palette.background.accent}
|
||||
>
|
||||
{totalMonitors}
|
||||
</Box>
|
||||
<Box
|
||||
width="25%"
|
||||
minWidth={150}
|
||||
ml="auto"
|
||||
>
|
||||
<Search
|
||||
options={monitors}
|
||||
filteredBy="name"
|
||||
inputValue={search}
|
||||
handleInputChange={handleSearch}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
<MemoizedMonitorTable
|
||||
isAdmin={isAdmin}
|
||||
filter={debouncedFilter}
|
||||
setIsSearching={setIsSearching}
|
||||
isSearching={isSearching}
|
||||
handlePause={handlePause}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
CurrentMonitoring.propTypes = {
|
||||
handlePause: PropTypes.func,
|
||||
totalMonitors: PropTypes.number,
|
||||
monitors: PropTypes.array,
|
||||
isAdmin: PropTypes.bool,
|
||||
};
|
||||
|
||||
export { CurrentMonitoring };
|
||||
@@ -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: () => <Skeleton />,
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: "Status",
|
||||
render: () => <Skeleton />,
|
||||
},
|
||||
{
|
||||
id: "responseTime",
|
||||
content: "Response Time",
|
||||
render: () => <Skeleton />,
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
content: "Type",
|
||||
render: () => <Skeleton />,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
content: "Actions",
|
||||
render: () => <Skeleton />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={ROWS_ARRAY}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { TableSkeleton };
|
||||
315
Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx
Normal file
315
Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx
Normal file
@@ -0,0 +1,315 @@
|
||||
// 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 (
|
||||
<Box
|
||||
width="25%"
|
||||
minWidth={150}
|
||||
ml="auto"
|
||||
>
|
||||
<Search
|
||||
options={monitors}
|
||||
filteredBy="name"
|
||||
inputValue={localSearch}
|
||||
handleInputChange={handleSearch}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
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,
|
||||
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: (
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
alignItems="center"
|
||||
direction="row"
|
||||
onClick={() => handleSort("name")}
|
||||
>
|
||||
Host
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
style={{
|
||||
visibility: sort.field === "name" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
),
|
||||
render: (row) => (
|
||||
<Host
|
||||
key={row._id}
|
||||
url={row.url}
|
||||
title={row.title}
|
||||
percentageColor={row.percentageColor}
|
||||
percentage={row.percentage}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
content: (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
alignItems="center"
|
||||
width="max-content"
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
{" "}
|
||||
Status
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
style={{
|
||||
visibility: sort.field === "status" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
),
|
||||
render: (row) => {
|
||||
const status = determineState(row.monitor);
|
||||
return (
|
||||
<StatusLabel
|
||||
status={status}
|
||||
text={status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "responseTime",
|
||||
content: "Response Time",
|
||||
render: (row) => <BarChart checks={row.monitor.checks.slice().reverse()} />,
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
content: "Type",
|
||||
render: (row) => (
|
||||
<span style={{ textTransform: "uppercase" }}>{row.monitor.type}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
content: "Actions",
|
||||
render: (row) => (
|
||||
<ActionsMenu
|
||||
monitor={row.monitor}
|
||||
isAdmin={isAdmin}
|
||||
updateRowCallback={triggerUpdate}
|
||||
setIsLoading={setIsLoading}
|
||||
pauseCallback={triggerUpdate}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box
|
||||
flex={1}
|
||||
py={theme.spacing(8)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Heading component="h2">Uptime monitors</Heading>
|
||||
|
||||
<Box
|
||||
className="current-monitors-counter"
|
||||
color={theme.palette.text.primary}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
backgroundColor={theme.palette.background.accent}
|
||||
>
|
||||
{monitorCount}
|
||||
</Box>
|
||||
|
||||
<SearchComponent
|
||||
monitors={monitors}
|
||||
debouncedSearch={debouncedSearch}
|
||||
onSearchChange={setSearch}
|
||||
setIsSearching={setIsSearching}
|
||||
/>
|
||||
</Stack>
|
||||
<Box position="relative">
|
||||
{(isSearching || isLoading) && (
|
||||
<>
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="absolute"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.main,
|
||||
opacity: 0.8,
|
||||
zIndex: 100,
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
height="100%"
|
||||
position="absolute"
|
||||
top="50%"
|
||||
left="50%"
|
||||
sx={{
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 101,
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
sx={{
|
||||
color: theme.palette.other.icon,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<DataTable
|
||||
headers={headers}
|
||||
data={monitors}
|
||||
config={{
|
||||
rowSX: {
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => {
|
||||
navigate(`/uptime/${row.id}`);
|
||||
},
|
||||
emptyView: "No monitors found",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const MemoizedUptimeDataTable = memo(UptimeDataTable);
|
||||
export default MemoizedUptimeDataTable;
|
||||
|
||||
UptimeDataTable.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
monitors: PropTypes.array,
|
||||
monitorCount: PropTypes.number,
|
||||
sort: PropTypes.shape({
|
||||
field: PropTypes.string,
|
||||
order: PropTypes.oneOf(["asc", "desc"]),
|
||||
}),
|
||||
setSort: PropTypes.func,
|
||||
debouncedSearch: PropTypes.string,
|
||||
setSearch: PropTypes.func,
|
||||
isSearching: PropTypes.bool,
|
||||
setIsSearching: PropTypes.func,
|
||||
setIsLoading: PropTypes.func,
|
||||
triggerUpdate: PropTypes.func,
|
||||
};
|
||||
@@ -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) => (
|
||||
<TableRow key={row}>
|
||||
<TableCell>
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { TableBodySkeleton };
|
||||
@@ -1,336 +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 (
|
||||
<Box position="relative">
|
||||
{isSearching && (
|
||||
<>
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="absolute"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.main,
|
||||
opacity: 0.8,
|
||||
zIndex: 100,
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
height="100%"
|
||||
position="absolute"
|
||||
top="20%"
|
||||
left="50%"
|
||||
sx={{
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 101,
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
sx={{
|
||||
color: theme.palette.other.icon,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("name")}
|
||||
>
|
||||
<Box>
|
||||
Host
|
||||
<span
|
||||
style={{
|
||||
visibility: sort.field === "name" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
{" "}
|
||||
<Box width="max-content">
|
||||
{" "}
|
||||
Status
|
||||
<span
|
||||
style={{
|
||||
visibility: sort.field === "status" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
)}
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>Response Time</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{/* 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.percentage.uptimeExcellent;
|
||||
|
||||
// Determine uptime percentage and color based on the monitor's uptimePercentage value
|
||||
if (monitor.uptimePercentage !== undefined) {
|
||||
uptimePercentage =
|
||||
monitor.uptimePercentage === 0
|
||||
? "0"
|
||||
: (monitor.uptimePercentage * 100).toFixed(2);
|
||||
|
||||
percentageColor =
|
||||
monitor.uptimePercentage < 0.25
|
||||
? theme.palette.percentage.uptimePoor
|
||||
: monitor.uptimePercentage < 0.5
|
||||
? theme.palette.percentage.uptimeFair
|
||||
: monitor.uptimePercentage < 0.75
|
||||
? theme.palette.percentage.uptimeGood
|
||||
: theme.palette.percentage.uptimeExcellent;
|
||||
}
|
||||
|
||||
const params = {
|
||||
url: monitor.url,
|
||||
title: monitor.name,
|
||||
percentage: uptimePercentage,
|
||||
percentageColor,
|
||||
status: determineState(monitor),
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={monitor._id}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
filter: "brightness(.75)",
|
||||
opacity: 0.75,
|
||||
transition: "filter 0.3s ease, opacity 0.3s ease",
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate(`/uptime/${monitor._id}`);
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<Host
|
||||
key={monitor._id}
|
||||
url={params.url}
|
||||
title={params.title}
|
||||
percentageColor={params.percentageColor}
|
||||
percentage={params.percentage}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={params.status}
|
||||
text={params.status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<BarChart checks={monitor.checks.slice().reverse()} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ActionsMenu
|
||||
monitor={monitor}
|
||||
isAdmin={isAdmin}
|
||||
updateRowCallback={handleRowUpdate}
|
||||
pauseCallback={handlePause}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableBodySkeleton />
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Pagination
|
||||
monitorCount={monitorCount}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -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 })
|
||||
);
|
||||
@@ -169,6 +174,8 @@ const ActionsMenu = ({ monitor, isAdmin, updateRowCallback, pauseCallback }) =>
|
||||
{isAdmin && (
|
||||
<MenuItem
|
||||
onClick={(e) => {
|
||||
closeMenu(e);
|
||||
|
||||
e.stopPropagation();
|
||||
handlePause(e);
|
||||
}}
|
||||
@@ -221,6 +228,7 @@ ActionsMenu.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
updateRowCallback: PropTypes.func,
|
||||
pauseCallback: PropTypes.func,
|
||||
setIsLoading: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ActionsMenu;
|
||||
|
||||
@@ -1,46 +1,144 @@
|
||||
import "./index.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { getUptimeMonitorsByTeamId } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Button, Stack } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
// Components
|
||||
import { Box, Stack, Button } from "@mui/material";
|
||||
import Greeting from "../../../Utils/greeting";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import Fallback from "./fallback";
|
||||
import StatusBox from "./StatusBox";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import Greeting from "../../../Utils/greeting";
|
||||
import { CurrentMonitoring } from "./CurrentMonitoring";
|
||||
import UptimeDataTable from "./UptimeDataTable";
|
||||
import { Pagination } from "../../../Components/Table/TablePagination";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import { setRowsPerPage } from "../../../Features/UI/uiSlice";
|
||||
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import useDebounce from "../../../Utils/debounce";
|
||||
import { networkService } from "../../../main";
|
||||
|
||||
const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }];
|
||||
|
||||
const UptimeMonitors = () => {
|
||||
// Redux state
|
||||
const rowsPerPage = useSelector((state) => state.ui.monitors.rowsPerPage);
|
||||
// Local state
|
||||
const [monitors, setMonitors] = useState([]);
|
||||
const [sort, setSort] = useState({});
|
||||
const [search, setSearch] = useState("");
|
||||
const [page, setPage] = useState(0);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false);
|
||||
const [monitorsSummary, setMonitorsSummary] = useState({});
|
||||
|
||||
// Utils
|
||||
const debouncedFilter = useDebounce(search, 500);
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const isAdmin = useIsAdmin();
|
||||
const uptimeMonitorsState = useSelector((state) => state.uptimeMonitors);
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch({});
|
||||
const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false);
|
||||
|
||||
const handlePause = () => {
|
||||
setMonitorUpdateTrigger((prev) => !prev);
|
||||
};
|
||||
const fetchParams = useMemo(
|
||||
() => ({
|
||||
authToken: authState.authToken,
|
||||
teamId: authState.user.teamId,
|
||||
sort: { field: sort.field, order: sort.order },
|
||||
filter: debouncedFilter,
|
||||
page,
|
||||
rowsPerPage,
|
||||
}),
|
||||
[authState.authToken, authState.user.teamId, sort, debouncedFilter, page, rowsPerPage]
|
||||
);
|
||||
|
||||
const getMonitorWithPercentage = useCallback((monitor, theme) => {
|
||||
let uptimePercentage = "";
|
||||
let percentageColor = theme.palette.percentage.uptimeExcellent;
|
||||
|
||||
if (monitor.uptimePercentage !== undefined) {
|
||||
uptimePercentage =
|
||||
monitor.uptimePercentage === 0
|
||||
? "0"
|
||||
: (monitor.uptimePercentage * 100).toFixed(2);
|
||||
|
||||
percentageColor =
|
||||
monitor.uptimePercentage < 0.25
|
||||
? theme.palette.percentage.uptimePoor
|
||||
: monitor.uptimePercentage < 0.5
|
||||
? theme.palette.percentage.uptimeFair
|
||||
: monitor.uptimePercentage < 0.75
|
||||
? theme.palette.percentage.uptimeGood
|
||||
: theme.palette.percentage.uptimeExcellent;
|
||||
}
|
||||
|
||||
return {
|
||||
id: monitor._id,
|
||||
name: monitor.name,
|
||||
url: monitor.url,
|
||||
title: monitor.name,
|
||||
percentage: uptimePercentage,
|
||||
percentageColor,
|
||||
monitor: monitor,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchMonitors = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const config = fetchParams;
|
||||
const res = await networkService.getMonitorsByTeamId({
|
||||
authToken: config.authToken,
|
||||
teamId: config.teamId,
|
||||
limit: 25,
|
||||
types: ["http", "ping", "docker", "port"],
|
||||
page: config.page,
|
||||
rowsPerPage: config.rowsPerPage,
|
||||
filter: config.filter,
|
||||
field: config.sort.field,
|
||||
order: config.sort.order,
|
||||
});
|
||||
const { monitors, summary } = res.data.data;
|
||||
const mappedMonitors = monitors.map((monitor) =>
|
||||
getMonitorWithPercentage(monitor, theme)
|
||||
);
|
||||
setMonitors(mappedMonitors);
|
||||
setMonitorsSummary(summary);
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: "Error fetching monitors",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsSearching(false);
|
||||
}
|
||||
}, [fetchParams, getMonitorWithPercentage, theme]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
|
||||
}, [authState.authToken, dispatch, monitorUpdateTrigger]);
|
||||
fetchMonitors();
|
||||
}, [fetchMonitors, monitorUpdateTrigger]);
|
||||
|
||||
//TODO bring fetching to this component, like on pageSpeed
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const loading = uptimeMonitorsState?.isLoading;
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value: parseInt(event.target.value, 10),
|
||||
table: "monitors",
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const totalMonitors = uptimeMonitorsState?.monitorsSummary?.monitorCounts?.total;
|
||||
|
||||
const hasMonitors = totalMonitors > 0;
|
||||
const noMonitors = !hasMonitors;
|
||||
const triggerUpdate = useCallback(() => {
|
||||
setMonitorUpdateTrigger((prev) => !prev);
|
||||
}, []);
|
||||
const totalMonitors = monitorsSummary?.totalMonitors ?? 0;
|
||||
const hasMonitors = monitorsSummary?.totalMonitors ?? 0;
|
||||
const canAddMonitor = isAdmin && hasMonitors;
|
||||
|
||||
return (
|
||||
@@ -72,46 +170,60 @@ const UptimeMonitors = () => {
|
||||
</Stack>
|
||||
<Greeting type="uptime" />
|
||||
</Box>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
{
|
||||
<>
|
||||
{noMonitors && <Fallback isAdmin={isAdmin} />}
|
||||
{hasMonitors && (
|
||||
<>
|
||||
<Stack
|
||||
gap={theme.spacing(8)}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<StatusBox
|
||||
title="up"
|
||||
value={uptimeMonitorsState?.monitorsSummary?.monitorCounts?.up ?? 0}
|
||||
{!isLoading && !hasMonitors && <Fallback isAdmin={isAdmin} />}
|
||||
{isLoading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
hasMonitors && (
|
||||
<>
|
||||
<Stack
|
||||
gap={theme.spacing(8)}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<StatusBox
|
||||
title="up"
|
||||
value={monitorsSummary?.upMonitors ?? 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title="down"
|
||||
value={monitorsSummary?.downMonitors ?? 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title="paused"
|
||||
value={monitorsSummary?.pausedMonitors ?? 0}
|
||||
/>
|
||||
</Stack>
|
||||
<UptimeDataTable
|
||||
isAdmin={isAdmin}
|
||||
isLoading={isLoading}
|
||||
monitors={monitors}
|
||||
monitorCount={totalMonitors}
|
||||
sort={sort}
|
||||
setSort={setSort}
|
||||
debouncedSearch={debouncedFilter}
|
||||
setSearch={setSearch}
|
||||
isSearching={isSearching}
|
||||
setIsSearching={setIsSearching}
|
||||
setIsLoading={setIsLoading}
|
||||
triggerUpdate={triggerUpdate}
|
||||
/>
|
||||
<StatusBox
|
||||
title="down"
|
||||
value={uptimeMonitorsState?.monitorsSummary?.monitorCounts?.down ?? 0}
|
||||
<Pagination
|
||||
monitorCount={totalMonitors}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
handleChangePage={handleChangePage}
|
||||
handleChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
<StatusBox
|
||||
title="paused"
|
||||
value={uptimeMonitorsState?.monitorsSummary?.monitorCounts?.paused ?? 0}
|
||||
/>
|
||||
</Stack>
|
||||
<CurrentMonitoring
|
||||
isAdmin={isAdmin}
|
||||
monitors={uptimeMonitorsState.monitorsSummary.monitors}
|
||||
totalMonitors={totalMonitors}
|
||||
handlePause={handlePause}
|
||||
/>
|
||||
</>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
UptimeMonitors.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
};
|
||||
export default UptimeMonitors;
|
||||
|
||||
@@ -49,6 +49,7 @@ const Routes = () => {
|
||||
path="/uptime"
|
||||
element={<Monitors />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/uptime/create/:monitorId?"
|
||||
element={<CreateMonitor />}
|
||||
|
||||
@@ -127,7 +127,7 @@ class NetworkService {
|
||||
* @param {Array<string>} config.types - Array of monitor types
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*/
|
||||
async getMonitorsAndSummaryByTeamId(config) {
|
||||
async getMonitorsSummaryByTeamId(config) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (config.types) {
|
||||
@@ -157,9 +157,6 @@ class NetworkService {
|
||||
* @param {string} config.teamId - The ID of the team whose monitors are to be retrieved.
|
||||
* @param {number} [config.limit] - The maximum number of checks to retrieve. 0 for all, -1 for none
|
||||
* @param {Array<string>} [config.types] - The types of monitors to retrieve.
|
||||
* @param {string} [config.status] - The status of the monitors to retrieve.
|
||||
* @param {string} [config.checkOrder] - The order in which to sort the retrieved monitors.
|
||||
* @param {boolean} [config.normalize] - Whether to normalize the retrieved monitors.
|
||||
* @param {number} [config.page] - The page number for pagination.
|
||||
* @param {number} [config.rowsPerPage] - The number of rows per page for pagination.
|
||||
* @param {string} [config.filter] - The filter to apply to the monitors.
|
||||
@@ -167,21 +164,10 @@ class NetworkService {
|
||||
* @param {string} [config.order] - The order in which to sort the field.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
|
||||
*/
|
||||
|
||||
async getMonitorsByTeamId(config) {
|
||||
const {
|
||||
authToken,
|
||||
teamId,
|
||||
limit,
|
||||
types,
|
||||
status,
|
||||
checkOrder,
|
||||
normalize,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
} = config;
|
||||
const { authToken, teamId, limit, types, page, rowsPerPage, filter, field, order } =
|
||||
config;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
@@ -191,9 +177,6 @@ class NetworkService {
|
||||
params.append("type", type);
|
||||
});
|
||||
}
|
||||
if (status) params.append("status", status);
|
||||
if (checkOrder) params.append("checkOrder", checkOrder);
|
||||
if (normalize) params.append("normalize", normalize);
|
||||
if (page) params.append("page", page);
|
||||
if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
|
||||
if (filter) params.append("filter", filter);
|
||||
|
||||
@@ -95,9 +95,48 @@ const monitorValidation = joi.object({
|
||||
.string()
|
||||
.trim()
|
||||
.custom((value, helpers) => {
|
||||
const urlRegex =
|
||||
/^(https?:\/\/)?(([0-9]{1,3}\.){3}[0-9]{1,3}|[\da-z\.-]+)(\.[a-z\.]{2,6})?(:(\d+))?([\/\w \.-]*)*\/?$/i;
|
||||
|
||||
// Regex from https://gist.github.com/dperini/729294
|
||||
var urlRegex = new RegExp(
|
||||
"^" +
|
||||
// protocol identifier (optional)
|
||||
// short syntax // still required
|
||||
"(?:(?:https?|ftp):\\/\\/)?" +
|
||||
// user:pass BasicAuth (optional)
|
||||
"(?:" +
|
||||
// IP address exclusion
|
||||
// private & local networks
|
||||
"(?!(?:10|127)(?:\\.\\d{1,3}){3})" +
|
||||
"(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" +
|
||||
"(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" +
|
||||
// IP address dotted notation octets
|
||||
// excludes loopback network 0.0.0.0
|
||||
// excludes reserved space >= 224.0.0.0
|
||||
// excludes network & broadcast addresses
|
||||
// (first & last IP address of each class)
|
||||
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
|
||||
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
|
||||
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
|
||||
"|" +
|
||||
// host & domain names, may end with dot
|
||||
// can be replaced by a shortest alternative
|
||||
// (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+
|
||||
"(?:" +
|
||||
"(?:" +
|
||||
"[a-z0-9\\u00a1-\\uffff]" +
|
||||
"[a-z0-9\\u00a1-\\uffff_-]{0,62}" +
|
||||
")?" +
|
||||
"[a-z0-9\\u00a1-\\uffff]\\." +
|
||||
")+" +
|
||||
// TLD identifier name, may end with dot
|
||||
"(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" +
|
||||
")" +
|
||||
// port number (optional)
|
||||
"(?::\\d{2,5})?" +
|
||||
// resource path (optional)
|
||||
"(?:[/?#]\\S*)?" +
|
||||
"$",
|
||||
"i"
|
||||
);
|
||||
if (!urlRegex.test(value)) {
|
||||
return helpers.error("string.invalidUrl");
|
||||
}
|
||||
|
||||
@@ -24,6 +24,12 @@ services:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- ./redis/data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
mongodb:
|
||||
image: uptime_database_mongo:latest
|
||||
restart: always
|
||||
|
||||
6
Docker/dist/docker-compose.yaml
vendored
6
Docker/dist/docker-compose.yaml
vendored
@@ -30,6 +30,12 @@ services:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- ./redis/data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
mongodb:
|
||||
image: bluewaveuptime/uptime_database_mongo:latest
|
||||
restart: always
|
||||
|
||||
@@ -38,6 +38,12 @@ services:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- ./redis/data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
mongodb:
|
||||
image: uptime_database_mongo:latest
|
||||
restart: always
|
||||
|
||||
@@ -38,6 +38,12 @@ services:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- ./redis/data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
mongodb:
|
||||
image: uptime_database_mongo:latest
|
||||
command: ["mongod", "--quiet", "--auth"]
|
||||
|
||||
17
README.md
17
README.md
@@ -1,10 +1,9 @@
|
||||
|
||||
# **We're opening our $5000 grant funding announcement soon, powered by Checkmate and [UpRock](https://uprock.com) - check [our web page](https://checkmate.so) for preliminary details.**
|
||||
# **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.**
|
||||
|
||||
**If you would like to support us, please consider giving it a ⭐, and think about contributing or providing feedback. Need support or have a suggestion? Check our [Discord channel](https://discord.gg/NAb6H3UTjK) or [Discussions](https://github.com/bluewave-labs/checkmate/discussions) forum.**
|
||||
|
||||
<img width="1259" alt="Frame 34" src="https://github.com/user-attachments/assets/d491a734-fd7a-4841-9f84-fb5bef5ad586" />
|
||||
**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.**
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
@@ -13,12 +12,14 @@
|
||||

|
||||

|
||||

|
||||
[](https://www.bestpractices.dev/projects/9901)
|
||||
|
||||
|
||||
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
|
||||
|
||||
<p align="center"><strong>An open source server and infrastructure monitoring application</strong></p>
|
||||
<p align="center"><strong>An open source uptime and infrastructure monitoring application</strong></p>
|
||||
|
||||

|
||||

|
||||
|
||||
Checkmate is an open source monitoring tool used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
|
||||
|
||||
@@ -112,8 +113,8 @@ Here's how you can contribute:
|
||||
|
||||
Also check other developer and contributor-friendly projects of BlueWave:
|
||||
|
||||
- [BlueWave DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
|
||||
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
|
||||
- [BlueWave HRM](https://github.com/bluewave-labs/bluewave-hrm), a complete Human Resource Management platform.
|
||||
- [BlueWave Onboarding](https://github.com/bluewave-labs/bluewave-onboarding), an application that helps new users learn how to use your product via hints, tours, popups and banners.
|
||||
- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners.
|
||||
- [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform.
|
||||
|
||||
|
||||
@@ -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, "getMonitorsForDisplay"));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default MonitorController;
|
||||
|
||||
@@ -175,6 +175,7 @@ const getTeamChecks = async (req) => {
|
||||
case "all":
|
||||
break;
|
||||
case "down":
|
||||
checksQuery.status = false;
|
||||
break;
|
||||
case "resolve":
|
||||
checksQuery.statusCode = 5000;
|
||||
|
||||
@@ -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,272 +340,9 @@ 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 monitorData = await Check.aggregate(
|
||||
buildUptimeDetailsPipeline(monitor, dates, dateString)
|
||||
);
|
||||
|
||||
const normalizedGroupChecks = NormalizeDataUptimeDetails(
|
||||
monitorData[0].groupChecks,
|
||||
@@ -715,261 +456,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 +505,167 @@ const getMonitorById = async (monitorId) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get monitors and Summary by TeamID
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Array<Monitor>>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getMonitorsByTeamId = async (req) => {
|
||||
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
|
||||
|
||||
const 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;
|
||||
// Parse ints
|
||||
limit = parseInt(limit);
|
||||
page = parseInt(page);
|
||||
rowsPerPage = parseInt(rowsPerPage);
|
||||
|
||||
// Build the match stage
|
||||
const matchStage = { teamId: ObjectId.createFromHexString(req.params.teamId) };
|
||||
if (type !== undefined) {
|
||||
matchStage.type = Array.isArray(type) ? { $in: type } : type;
|
||||
}
|
||||
|
||||
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: [
|
||||
...(filter !== undefined
|
||||
? [
|
||||
{
|
||||
$match: {
|
||||
$or: [
|
||||
{ name: { $regex: filter, $options: "i" } },
|
||||
{ url: { $regex: filter, $options: "i" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ $sort: sort },
|
||||
{ $skip: skip },
|
||||
...(rowsPerPage ? [{ $limit: rowsPerPage }] : []),
|
||||
{
|
||||
$lookup: {
|
||||
from: "checks",
|
||||
let: { monitorId: "$_id" },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: { $eq: ["$monitorId", "$$monitorId"] },
|
||||
},
|
||||
},
|
||||
{ $sort: { createdAt: -1 } },
|
||||
...(limit ? [{ $limit: limit }] : []),
|
||||
],
|
||||
as: "standardchecks",
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "pagespeedchecks",
|
||||
let: { monitorId: "$_id" },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: { $eq: ["$monitorId", "$$monitorId"] },
|
||||
},
|
||||
},
|
||||
{ $sort: { createdAt: -1 } },
|
||||
...(limit ? [{ $limit: limit }] : []),
|
||||
],
|
||||
as: "pagespeedchecks",
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "hardwarechecks",
|
||||
let: { monitorId: "$_id" },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: { $eq: ["$monitorId", "$$monitorId"] },
|
||||
},
|
||||
},
|
||||
{ $sort: { createdAt: -1 } },
|
||||
...(limit ? [{ $limit: limit }] : []),
|
||||
],
|
||||
as: "hardwarechecks",
|
||||
},
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
checks: {
|
||||
$switch: {
|
||||
branches: [
|
||||
{
|
||||
case: { $in: ["$type", ["http", "ping", "docker", "port"]] },
|
||||
then: "$standardchecks",
|
||||
},
|
||||
{
|
||||
case: { $eq: ["$type", "pagespeed"] },
|
||||
then: "$pagespeedchecks",
|
||||
},
|
||||
{
|
||||
case: { $eq: ["$type", "hardware"] },
|
||||
then: "$hardwarechecks",
|
||||
},
|
||||
],
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
standardchecks: 0,
|
||||
pagespeedchecks: 0,
|
||||
hardwarechecks: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{ 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] },
|
||||
monitors: 1,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
/**
|
||||
* Get monitors by TeamID
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Array<Monitor>>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getMonitorsByTeamId = async (req, res) => {
|
||||
try {
|
||||
let {
|
||||
limit,
|
||||
type,
|
||||
status,
|
||||
checkOrder,
|
||||
normalize,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
} = req.query;
|
||||
|
||||
const monitorQuery = { teamId: req.params.teamId };
|
||||
|
||||
if (type !== undefined) {
|
||||
monitorQuery.type = Array.isArray(type) ? { $in: type } : type;
|
||||
}
|
||||
// Add filter if provided
|
||||
// $options: "i" makes the search case-insensitive
|
||||
if (filter !== undefined) {
|
||||
monitorQuery.$or = [
|
||||
{ name: { $regex: filter, $options: "i" } },
|
||||
{ url: { $regex: filter, $options: "i" } },
|
||||
];
|
||||
}
|
||||
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;
|
||||
}
|
||||
let { monitors, summary } = results[0];
|
||||
monitors = monitors.map((monitor) => {
|
||||
monitor.checks = NormalizeData(monitor.checks, 10, 100);
|
||||
return monitor;
|
||||
});
|
||||
return { monitors, summary };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1262,9 +797,8 @@ export {
|
||||
getAllMonitorsWithUptimeStats,
|
||||
getMonitorStatsById,
|
||||
getMonitorById,
|
||||
getUptimeDetailsById,
|
||||
getMonitorsAndSummaryByTeamId,
|
||||
getMonitorsByTeamId,
|
||||
getUptimeDetailsById,
|
||||
createMonitor,
|
||||
deleteMonitor,
|
||||
deleteAllMonitors,
|
||||
|
||||
525
Server/db/mongo/modules/monitorModuleQueries.js
Normal file
525
Server/db/mongo/modules/monitorModuleQueries.js
Normal file
@@ -0,0 +1,525 @@
|
||||
const buildUptimeDetailsPipeline = (monitor, dates, dateString) => {
|
||||
return [
|
||||
{
|
||||
$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 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 };
|
||||
186
Server/package-lock.json
generated
186
Server/package-lock.json
generated
@@ -11,9 +11,9 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "5.1.1",
|
||||
"bullmq": "5.34.6",
|
||||
"bullmq": "5.34.8",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "4.0.2",
|
||||
"dockerode": "4.0.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"handlebars": "^4.7.8",
|
||||
@@ -352,6 +352,37 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.5.tgz",
|
||||
"integrity": "sha512-d3iiHxdpg5+ZcJ6jnDSOT8Z0O0VMVGy34jAnYLUX8yd36b1qn8f1TwOA/Lc7TsOh03IkPJ38eGI5qD2EjNkoEA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/proto-loader": {
|
||||
"version": "0.7.13",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz",
|
||||
"integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"long": "^5.0.0",
|
||||
"protobufjs": "^7.2.5",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@hapi/hoek": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
||||
@@ -850,6 +881,16 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@js-sdsl/ordered-map": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
|
||||
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||
@@ -967,6 +1008,70 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@scarf/scarf": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||
@@ -1132,7 +1237,6 @@
|
||||
"version": "22.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.4.tgz",
|
||||
"integrity": "sha512-99l6wv4HEzBQhvaU/UGoeBoCK61SCROQaCCGyQSgX2tEQ3rKkNZ2S7CEWnS/4s1LV+8ODdK21UeyR1fHP2mXug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
@@ -1640,9 +1744,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.8",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.34.8.tgz",
|
||||
"integrity": "sha512-id5mmPg3K8tNXQ9VVlmUxBSeLmliIWUrB8Hd5c62PFrIiHywz4TN1PEqU6OWvYXEvoFCr8/BlnbE4JCrGqPVmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cron-parser": "^4.9.0",
|
||||
@@ -2654,9 +2758,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/docker-modem": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz",
|
||||
"integrity": "sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.5.tgz",
|
||||
"integrity": "sha512-Cxw8uEcvNTRmsQuGqzzfiCnfGgf96tVJItLh8taOX0miTcIBALKH5TckCSuZbpbjP7uhAl81dOL9sxfa6HgCIg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
@@ -2669,19 +2773,36 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dockerode": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz",
|
||||
"integrity": "sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.3.tgz",
|
||||
"integrity": "sha512-QSXJFcBQNaGZO6U3qWW4B7p8yRIJn/dWmvL2AQWfO/bjptBBO6QYdVkYSYFz9qoivP2jsOHZfmXMAfrK0BMKyg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
"docker-modem": "^5.0.3",
|
||||
"tar-fs": "~2.0.1"
|
||||
"@grpc/grpc-js": "^1.11.1",
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"docker-modem": "^5.0.5",
|
||||
"protobufjs": "^7.3.2",
|
||||
"tar-fs": "~2.0.1",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dockerode/node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
@@ -4650,6 +4771,12 @@
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
@@ -4770,6 +4897,12 @@
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
|
||||
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
|
||||
@@ -6958,6 +7091,30 @@
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
|
||||
"integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -8111,7 +8268,6 @@
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unfetch": {
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "5.1.1",
|
||||
"bullmq": "5.34.6",
|
||||
"bullmq": "5.34.8",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "4.0.2",
|
||||
"dockerode": "4.0.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"handlebars": "^4.7.8",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -217,14 +217,51 @@ class NewJobQueue {
|
||||
connection: this.connection,
|
||||
concurrency: 5,
|
||||
});
|
||||
worker.on("failed", (job, err) => {
|
||||
this.logger.error({
|
||||
message: `Worker failed job: ${job.id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker",
|
||||
stack: err.stack,
|
||||
});
|
||||
});
|
||||
|
||||
// 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",
|
||||
// });
|
||||
// });
|
||||
|
||||
// // 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}%`,
|
||||
// });
|
||||
// });
|
||||
|
||||
// // 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;
|
||||
}
|
||||
|
||||
@@ -353,8 +390,9 @@ class NewJobQueue {
|
||||
const jobs = await queue.getJobs();
|
||||
const ret = await Promise.all(
|
||||
jobs.map(async (job) => {
|
||||
console.log(job);
|
||||
const state = await job.getState();
|
||||
return { url: job.data.url, state };
|
||||
return { url: job.data.url, state, progress: job.progress };
|
||||
})
|
||||
);
|
||||
stats[name] = { jobs: ret, workers: this.workers[name].length };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user