diff --git a/Client/src/Components/BasicTable/index.css b/Client/src/Components/BasicTable/index.css
index 1d4a354e9..3c1b0f2cd 100644
--- a/Client/src/Components/BasicTable/index.css
+++ b/Client/src/Components/BasicTable/index.css
@@ -27,7 +27,7 @@
font-size: var(--env-var-font-size-medium);
}
.MuiTable-root .MuiTableHead-root .MuiTableCell-root {
- padding: var(--env-var-spacing-1) var(--env-var-spacing-2);
+ padding: var(--env-var-spacing-1);
font-weight: 500;
}
.MuiTable-root .MuiTableHead-root span {
@@ -43,7 +43,7 @@
height: 20px;
}
.MuiTable-root .MuiTableBody-root .MuiTableCell-root {
- padding: 6px var(--env-var-spacing-2);
+ padding: var(--env-var-spacing-1);
}
.MuiTable-root .MuiTableBody-root .MuiTableRow-root {
height: 50px;
diff --git a/Client/src/Components/Charts/CustomGauge/index.jsx b/Client/src/Components/Charts/CustomGauge/index.jsx
index f3ad15b0b..44512aa60 100644
--- a/Client/src/Components/Charts/CustomGauge/index.jsx
+++ b/Client/src/Components/Charts/CustomGauge/index.jsx
@@ -4,6 +4,9 @@ import { Box, Typography } from "@mui/material";
import PropTypes from "prop-types";
import "./index.css";
+const MINIMUM_VALUE = 0;
+const MAXIMUM_VALUE = 100;
+
/**
* A Performant SVG based circular gauge
*
@@ -11,25 +14,21 @@ import "./index.css";
* @param {Object} props - Component properties
* @param {number} [props.progress=0] - Progress percentage (0-100)
* @param {number} [props.radius=60] - Radius of the gauge circle
- * @param {string} [props.color="#000000"] - Color of the progress stroke
* @param {number} [props.strokeWidth=15] - Width of the gauge stroke
+ * @param {number} [props.threshold=50] - Threshold for color change
*
* @example
*
*
* @returns {React.ReactElement} Rendered CustomGauge component
*/
-const CustomGauge = ({
- progress = 0,
- radius = 60,
- color = "#000000",
- strokeWidth = 15,
-}) => {
+const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = 50 }) => {
+ const theme = useTheme();
// Calculate the length of the stroke for the circle
const { circumference, totalSize, strokeLength } = useMemo(
() => ({
@@ -39,10 +38,9 @@ const CustomGauge = ({
}),
[radius, strokeWidth, progress]
);
- const [offset, setOffset] = useState(circumference);
- const theme = useTheme();
// Handle initial animation
+ const [offset, setOffset] = useState(circumference);
useEffect(() => {
setOffset(circumference);
const timer = setTimeout(() => {
@@ -52,6 +50,13 @@ const CustomGauge = ({
return () => clearTimeout(timer);
}, [progress, circumference, strokeLength]);
+ const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
+
+ const fillColor =
+ progressWithinRange > threshold
+ ? theme.palette.percentage.uptimePoor
+ : theme.palette.primary.main;
+
return (
- {`${progress.toFixed(2)}%`}
+ {`${progressWithinRange.toFixed(1)}%`}
);
@@ -108,6 +113,6 @@ export default CustomGauge;
CustomGauge.propTypes = {
progress: PropTypes.number,
radius: PropTypes.number,
- color: PropTypes.string,
strokeWidth: PropTypes.number,
+ threshold: PropTypes.number,
};
diff --git a/Client/src/Components/Charts/Gauge/index.jsx b/Client/src/Components/Charts/Gauge/index.jsx
index 0ae872e3f..562753dc4 100644
--- a/Client/src/Components/Charts/Gauge/index.jsx
+++ b/Client/src/Components/Charts/Gauge/index.jsx
@@ -2,41 +2,37 @@ import { RadialBarChart, RadialBar, ResponsiveContainer } from "recharts";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
+/* TODO delete component */
+
const MINIMUM_VALUE = 0;
const MAXIMUM_VALUE = 100;
-const DATA = [{ value: MAXIMUM_VALUE }];
+const CHART_MAXIMUM_DATA = {
+ value: MAXIMUM_VALUE,
+ fill: "transparent",
+};
const PROGRESS_THRESHOLD = 50;
-const DEFAULT_CONTAINER_HEIGHT = 160;
-const TEXT_POSITIONS = {
- value: { x: "50%", y: "45%" },
- label: { x: "50%", y: "55%" },
-};
-const COMMON_RADIAL_BAR_PROPS = {
- minAngle: 15,
- clockWise: true,
- dataKey: "value",
- cornerRadius: 0,
-};
+const DEFAULT_WIDTH = 60;
+const RADIUS_SIZE = "90%";
+const START_ANGLE = 90;
+const CHART_RANGE = 360;
const RADIAL_BAR_CHART_PROPS = {
- cx: "50%",
- cy: "50%",
- innerRadius: "45%",
- outerRadius: "100%",
- startAngle: 90,
- endAngle: -270,
+ innerRadius: RADIUS_SIZE,
+ outerRadius: RADIUS_SIZE,
+ barSize: 6,
+ startAngle: START_ANGLE,
+ endAngle: START_ANGLE - CHART_RANGE,
+ margin: { top: 0, right: 0, bottom: 0, left: 0 },
};
-const RADIAL_BAR_OVERLAY_PROPS = {
- transform: "rotate(180deg)",
- position: "absolute",
- top: "0",
- left: "0",
+const RADIAL_BAR_PROPS = {
+ dataKey: "value",
+ cornerRadius: 8,
};
-const COMMON_HEADER_PROPS = {
- textAnchor: "middle",
- dominantBaseline: "middle",
+Gauge.propTypes = {
+ progressValue: PropTypes.number.isRequired,
+ width: PropTypes.number,
};
/**
@@ -46,79 +42,70 @@ const COMMON_HEADER_PROPS = {
* @component
* @param {Object} props - Component props
* @param {number} props.progressValue - The value to display in the gauge (0-100)
- * @param {string} props.displayText - The text to display below the progress value
- * @param {number} [props.containerHeight=160] - Height of the gauge container in pixels
- * @param {Object} [props.gaugeHeader] - Custom styles to apply to the progress value text
- * @param {Object} [props.gaugeSubheader] - Custom styles to apply to the display text
- *
+ * @param {number} props.width - Width of the gauge container in pixels
* @returns {JSX.Element} A circular gauge chart with progress value and text
*
* @example
*
*/
-const Gauge = ({
- progressValue,
- displayText,
- containerHeight,
- gaugeHeader,
- gaugeSubheader,
-}) => {
+
+function Gauge({ progressValue, width = DEFAULT_WIDTH }) {
const theme = useTheme();
const myProgressValue = Math.max(MINIMUM_VALUE, Math.min(progressValue, MAXIMUM_VALUE));
+ const chartColor =
+ progressValue > PROGRESS_THRESHOLD
+ ? theme.palette.primary.main
+ : theme.palette.percentage.uptimePoor;
+ const chartData = [
+ CHART_MAXIMUM_DATA,
+ {
+ value: myProgressValue,
+ fill: chartColor,
+ },
+ ];
return (
-
+
PROGRESS_THRESHOLD
? theme.palette.primary.main
: theme.palette.percentage.uptimePoor
}
background={{ fill: theme.palette.background.fill }}
- data={[{ value: myProgressValue }]}
+ label={{
+ position: "center",
+ content: () => (
+
+ {`${myProgressValue}%`}
+
+ ),
+ }}
/>
-
-
- {`${myProgressValue}%`}
-
-
- {`${displayText}`}
-
);
-};
+}
-Gauge.propTypes = {
- progressValue: PropTypes.number.isRequired,
- displayText: PropTypes.string.isRequired,
- containerHeight: PropTypes.number,
- gaugeHeader: PropTypes.object,
- gaugeSubheader: PropTypes.object,
-};
-export default Gauge;
+export { Gauge };
diff --git a/Client/src/Components/Inputs/Search/index.jsx b/Client/src/Components/Inputs/Search/index.jsx
index 9044f7c0d..100b826b4 100644
--- a/Client/src/Components/Inputs/Search/index.jsx
+++ b/Client/src/Components/Inputs/Search/index.jsx
@@ -161,7 +161,7 @@ const Search = ({
},
}}
sx={{
- height: 34,
+ /* height: 34,*/
"&.MuiAutocomplete-root .MuiAutocomplete-input": { p: 0 },
...sx,
}}
diff --git a/Client/src/Components/Layouts/HomeLayout/index.css b/Client/src/Components/Layouts/HomeLayout/index.css
index 1a7ae4ceb..e08904f8e 100644
--- a/Client/src/Components/Layouts/HomeLayout/index.css
+++ b/Client/src/Components/Layouts/HomeLayout/index.css
@@ -6,6 +6,13 @@
padding: var(--env-var-spacing-2);
}
+/* TODO go for this approach for responsiveness. The aside needs to be taken care of */
+/* @media (max-width: 1000px) {
+ .home-layout {
+ flex-direction: column !important;
+ }
+} */
+
.home-layout aside {
position: sticky;
top: var(--env-var-spacing-2);
diff --git a/Client/src/Features/InfrastructureMonitors/infrastructureMonitorsSlice.js b/Client/src/Features/InfrastructureMonitors/infrastructureMonitorsSlice.js
index b593edff3..f0282b28b 100644
--- a/Client/src/Features/InfrastructureMonitors/infrastructureMonitorsSlice.js
+++ b/Client/src/Features/InfrastructureMonitors/infrastructureMonitorsSlice.js
@@ -40,7 +40,7 @@ export const checkInfrastructureEndpointResolution = 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 checkInfrastructureEndpointResolution = createAsyncThunk(
return thunkApi.rejectWithValue(payload);
}
}
-)
+);
export const getInfrastructureMonitorById = createAsyncThunk(
"infrastructureMonitors/getMonitorById",
@@ -87,6 +87,8 @@ export const getInfrastructureMonitorsByTeamId = createAsyncThunk(
authToken: token,
teamId: user.teamId,
types: ["hardware"],
+ limit: 1,
+ rowsPerPage: 0,
});
return res.data;
} catch (error) {
@@ -112,7 +114,7 @@ export const updateInfrastructureMonitor = createAsyncThunk(
description: monitor.description,
interval: monitor.interval,
notifications: monitor.notifications,
- threshold: monitor.threshold
+ threshold: monitor.threshold,
};
const res = await networkService.updateMonitor({
authToken: authToken,
@@ -307,7 +309,9 @@ const infrastructureMonitorsSlice = createSlice({
.addCase(getInfrastructureMonitorById.rejected, (state, action) => {
state.isLoading = false;
state.success = false;
- state.msg = action.payload ? action.payload.msg : "Failed to get infrastructure monitor";
+ state.msg = action.payload
+ ? action.payload.msg
+ : "Failed to get infrastructure monitor";
})
// *****************************************************
// update Monitor
@@ -401,6 +405,7 @@ const infrastructureMonitorsSlice = createSlice({
},
});
-export const { setInfrastructureMonitors, clearInfrastructureMonitorState } = infrastructureMonitorsSlice.actions;
+export const { setInfrastructureMonitors, clearInfrastructureMonitorState } =
+ infrastructureMonitorsSlice.actions;
export default infrastructureMonitorsSlice.reducer;
diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
index d472a673a..00ef87bc0 100644
--- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
+++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
@@ -221,6 +221,7 @@ export const addDemoMonitors = createAsyncThunk(
}
}
);
+
export const deleteAllMonitors = createAsyncThunk(
"monitors/deleteAllMonitors",
async (data, thunkApi) => {
diff --git a/Client/src/Hooks/useIsAdmin.js b/Client/src/Hooks/useIsAdmin.js
new file mode 100644
index 000000000..7d593b874
--- /dev/null
+++ b/Client/src/Hooks/useIsAdmin.js
@@ -0,0 +1,11 @@
+import { useSelector } from "react-redux";
+
+const useIsAdmin = () => {
+ const { user } = useSelector((state) => state.auth);
+ const isAdmin =
+ (user?.role?.includes("admin") ?? false) ||
+ (user?.role?.includes("superadmin") ?? false);
+ return isAdmin;
+};
+
+export { useIsAdmin };
diff --git a/Client/src/Pages/Infrastructure/CreateMonitor/index.jsx b/Client/src/Pages/Infrastructure/CreateMonitor/index.jsx
index 2af4f0f2f..cffc34a1a 100644
--- a/Client/src/Pages/Infrastructure/CreateMonitor/index.jsx
+++ b/Client/src/Pages/Infrastructure/CreateMonitor/index.jsx
@@ -265,7 +265,8 @@ const CreateInfrastructureMonitor = () => {
{
+ const anchor = useRef(null);
+ const [isOpen, setIsOpen] = useState(false);
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
+ const theme = useTheme();
+ const authState = useSelector((state) => state.auth);
+ const authToken = authState.authToken;
+ const { isLoading } = useSelector((state) => state.uptimeMonitors);
+
+ const openMenu = (e) => {
+ e.stopPropagation();
+ setIsOpen(true);
+ };
+
+ const closeMenu = (e) => {
+ e.stopPropagation();
+ setIsOpen(false);
+ };
+
+ const openRemove = (e) => {
+ closeMenu(e);
+ setIsDialogOpen(true);
+ };
+ const cancelRemove = () => {
+ setIsDialogOpen(false);
+ };
+
+ const navigate = useNavigate();
+
+ function openDetails(id) {
+ navigate(`/infrastructure/${id}`);
+ }
+
+ const handleRemove = async () => {
+ try {
+ await networkService.deleteMonitorById({
+ authToken,
+ monitorId: monitor.id,
+ });
+ createToast({ body: "Monitor deleted successfully." });
+ } catch (error) {
+ createToast({ body: "Failed to delete monitor." });
+ } finally {
+ setIsDialogOpen(false);
+ updateCallback();
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+};
+
+InfrastructureMenu.propTypes = {
+ monitor: PropTypes.shape({
+ id: PropTypes.string,
+ url: PropTypes.string,
+ type: PropTypes.string,
+ isActive: PropTypes.bool,
+ }).isRequired,
+ isAdmin: PropTypes.bool,
+ updateCallback: PropTypes.func,
+};
+
+export { InfrastructureMenu };
diff --git a/Client/src/Pages/Infrastructure/components/TablePagination/Actions/index.jsx b/Client/src/Pages/Infrastructure/components/TablePagination/Actions/index.jsx
new file mode 100644
index 000000000..c684da145
--- /dev/null
+++ b/Client/src/Pages/Infrastructure/components/TablePagination/Actions/index.jsx
@@ -0,0 +1,80 @@
+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";
+
+TablePaginationActions.propTypes = {
+ count: PropTypes.number.isRequired,
+ page: PropTypes.number.isRequired,
+ rowsPerPage: PropTypes.number.isRequired,
+ onPageChange: PropTypes.func.isRequired,
+};
+
+/**
+ * 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.
+ */
+
+function TablePaginationActions({ count, page, rowsPerPage, onPageChange }) {
+ 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 (
+
+
+
+
+
+
+ );
+}
+
+export { TablePaginationActions };
diff --git a/Client/src/Pages/Infrastructure/components/TablePagination/index.jsx b/Client/src/Pages/Infrastructure/components/TablePagination/index.jsx
new file mode 100644
index 000000000..bb618add8
--- /dev/null
+++ b/Client/src/Pages/Infrastructure/components/TablePagination/index.jsx
@@ -0,0 +1,117 @@
+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";
+
+Pagination.propTypes = {
+ monitorCount: PropTypes.number.isRequired, // Total number of items for pagination.
+ page: PropTypes.number.isRequired, // Current page index.
+ rowsPerPage: PropTypes.number.isRequired, // Number of rows displayed per page.
+ handleChangePage: PropTypes.func.isRequired, // Function to handle page changes.
+ handleChangeRowsPerPage: PropTypes.func.isRequired, // Function to handle changes in rows per page.
+};
+
+const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25];
+
+/**
+ * Pagination component for table navigation with customized styling and behavior.
+ *
+ * @param {object} props - Component properties.
+ * @param {number} props.monitorCount - Total number of monitors to paginate.
+ * @param {number} props.page - Current page index (0-based).
+ * @param {number} props.rowsPerPage - Number of rows to display per page.
+ * @param {function} props.handleChangePage - Callback for handling page changes.
+ * @param {function} props.handleChangeRowsPerPage - Callback for handling changes to rows per page.
+ * @returns {JSX.Element} The Pagination component.
+ */
+function Pagination({
+ monitorCount,
+ page,
+ rowsPerPage,
+ handleChangePage,
+ handleChangeRowsPerPage,
+}) {
+ const theme = useTheme();
+
+ const start = page * rowsPerPage + 1;
+ const end = Math.min(page * rowsPerPage + rowsPerPage, monitorCount);
+ const range = `${start} - ${end}`;
+
+ return (
+
+
+ Showing {range} of {monitorCount} monitor(s)
+
+
+ `Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}`
+ }
+ slotProps={{
+ select: {
+ MenuProps: {
+ keepMounted: true,
+ disableScrollLock: 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={{
+ 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,
+ },
+ }}
+ />
+
+ );
+}
+
+export { Pagination };
diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx
index 0cca80940..af24bf861 100644
--- a/Client/src/Pages/Infrastructure/index.jsx
+++ b/Client/src/Pages/Infrastructure/index.jsx
@@ -1,6 +1,70 @@
-import { Box, Button } from "@mui/material";
-import { Heading } from "../../Components/Heading";
+import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
+import { /* useDispatch, */ useSelector } from "react-redux";
+import { useTheme } from "@emotion/react";
+import useUtils from "../Monitors/utils";
+import { jwtDecode } from "jwt-decode";
+// 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 Breadcrumbs from "../../Components/Breadcrumbs";
+import { StatusLabel } from "../../Components/Label";
+import { Heading } from "../../Components/Heading";
+import { Pagination } from "./components/TablePagination";
+// import { getInfrastructureMonitorsByTeamId } from "../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
+import { networkService } from "../../Utils/NetworkService.js";
+import CustomGauge from "../../Components/Charts/CustomGauge/index.jsx";
+import Host from "../Monitors/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
@@ -10,21 +74,259 @@ import { useNavigate } from "react-router-dom";
*/
function Infrastructure() {
- const navigate = useNavigate();
+ /* Adding this custom hook so we can avoid using the HOC approach that can lower performance (we are calling the admin logic N times on initializing the project. using a custom hook will cal it ass needed ) */
+ const isAdmin = useIsAdmin();
+ const theme = useTheme();
+
+ const navigate = useNavigate();
+ const navigateToCreate = () => navigate("/infrastructure/create");
+
+ const [page, setPage] = useState(0);
+ /* TODO refactor this, so it is not aware of the MUI implementation. First argument only exists because of MUI. This should require onlu the new page. Adapting for MUI should happen inside of table pagination component */
+ const handleChangePage = (_, newPage) => {
+ setPage(newPage);
+ };
+
+ const [rowsPerPage, setRowsPerPage] = useState(5);
+ const handleChangeRowsPerPage = (event) => {
+ setRowsPerPage(parseInt(event.target.value));
+ setPage(0);
+ };
+ const [monitorState, setMonitorState] = useState({ monitors: [], total: 0 });
+
+ const { authToken } = useSelector((state) => state.auth);
+ const user = jwtDecode(authToken);
+
+ const fetchMonitors = async () => {
+ try {
+ const response = await networkService.getMonitorsByTeamId({
+ authToken,
+ 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,
+ });
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ useEffect(() => {
+ fetchMonitors();
+ }, [page, rowsPerPage]);
+
+ const { determineState } = useUtils();
+ const { monitors, total: totalMonitors } = monitorState;
+ // do it here
+ function openDetails(id) {
+ navigate(`/infrastructure/${id}`);
+ }
+ function handleActionMenuDelete() {
+ fetchMonitors();
+ }
+
+ const monitorsAsRows = monitors.map((monitor) => {
+ const processor =
+ ((monitor.checks[0]?.cpu?.usage_frequency ?? 0) / 1000).toFixed(2) + " GHz";
+ const cpu = (monitor?.checks[0]?.cpu.usage_percent ?? 0) * 100;
+ const mem = (monitor?.checks[0]?.memory.usage_percent ?? 0) * 100;
+ const disk = (monitor?.checks[0]?.disk[0]?.usage_percent ?? 0) * 100;
+ const status = determineState(monitor);
+ const uptimePercentage = ((monitor?.uptimePercentage ?? 0) * 100)
+ .toFixed(2)
+ .toString();
+ const 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,
+ processor,
+ cpu,
+ mem,
+ disk,
+ status,
+ uptimePercentage,
+ percentageColor,
+ };
+ });
+
return (
-
-
- Infrastructure monitors
-
+ {/*
+ This will be removed from here, but keeping the commented code to remind me to add a max width to the greeting component
+
+
+ */}
+
+
+
+
+ Infrastructure monitors
+ {/* TODO Correct the class current-monitors-counter, there are some unnecessary things there */}
+
+ {totalMonitors}
+
+
+
+
+
+
+ {columns.map((column, index) => (
+
+ {column.label}
+
+ ))}
+
+
+
+ {monitorsAsRows.map((row) => {
+ return (
+ openDetails(row.id)}
+ sx={{
+ cursor: "pointer",
+ "&:hover": {
+ backgroundColor: theme.palette.background.accent,
+ },
+ }}
+ >
+ {/* TODO iterate over column and get column id, applying row[column.id] */}
+
+
+
+
+
+
+
+
+
+ {row.processor}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Get ActionsMenu from Monitor Table and create a component */}
+
+
+ {/* */}
+
+
+
+ );
+ })}
+
+
+
+
+
+
);
}
diff --git a/Client/src/Pages/Monitors/Home/MonitorTable/Skeleton/index.jsx b/Client/src/Pages/Monitors/Home/MonitorTable/Skeleton/index.jsx
index e30e0a255..22d885c97 100644
--- a/Client/src/Pages/Monitors/Home/MonitorTable/Skeleton/index.jsx
+++ b/Client/src/Pages/Monitors/Home/MonitorTable/Skeleton/index.jsx
@@ -3,6 +3,7 @@ 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) => (
diff --git a/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx b/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx
index 5970a5c54..9a73bd543 100644
--- a/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx
+++ b/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx
@@ -1,4 +1,15 @@
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,
@@ -8,106 +19,18 @@ import {
TableBody,
Paper,
Box,
- TablePagination,
- Stack,
- Typography,
- Button,
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 { setRowsPerPage } from "../../../../Features/UI/uiSlice";
-import { useState, useEffect, memo, useCallback, useRef } from "react";
-import { useNavigate } from "react-router";
-import { logger } from "../../../../Utils/Logger";
-import Host from "../host";
-import { StatusLabel } from "../../../../Components/Label";
-import { jwtDecode } from "jwt-decode";
-import { useDispatch, useSelector } from "react-redux";
-import { networkService } from "../../../../main";
-import { useTheme } from "@emotion/react";
-import BarChart from "../../../../Components/Charts/BarChart";
-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 ActionsMenu from "../actionsMenu";
-import useUtils from "../../utils";
-import { TableBodySkeleton } from "./Skeleton";
-
-/**
- * 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 (
-
-
-
-
-
-
- );
-};
-
-TablePaginationActions.propTypes = {
- count: PropTypes.number.isRequired,
- page: PropTypes.number.isRequired,
- rowsPerPage: PropTypes.number.isRequired,
- onPageChange: PropTypes.func.isRequired,
-};
+import { Pagination } from "../../../Infrastructure/components/TablePagination";
const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
const theme = useTheme();
@@ -117,6 +40,7 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
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);
@@ -191,16 +115,6 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
prevFilter.current = filter;
}, [filter, fetchPage]);
- /**
- * 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, monitorCount);
- return `${start} - ${end}`;
- };
-
const handleSort = async (field) => {
let order = "";
if (sort.field !== field) {
@@ -360,7 +274,10 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
@@ -392,76 +309,13 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
-
-
- Showing {getRange()} of {monitorCount} monitor(s)
-
-
- `Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}`
- }
- slotProps={{
- select: {
- MenuProps: {
- keepMounted: true,
- disableScrollLock: 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,
- },
- }}
- />
-
+
);
};
diff --git a/Client/src/Pages/Monitors/Home/host.jsx b/Client/src/Pages/Monitors/Home/host.jsx
index 104eb29e7..68871cfe7 100644
--- a/Client/src/Pages/Monitors/Home/host.jsx
+++ b/Client/src/Pages/Monitors/Home/host.jsx
@@ -6,12 +6,14 @@ import PropTypes from "prop-types";
*
* @component
* @param {Object} params - An object containing the following properties:
+ * @param {string} params.url - The URL of the host.
* @param {string} params.title - The name of the host.
* @param {string} params.percentageColor - The color of the percentage text.
* @param {number} params.percentage - The percentage to display.
* @returns {React.ElementType} Returns a div element with the host details.
*/
-const Host = ({ params }) => {
+const Host = ({ url, title, percentageColor, percentage }) => {
+ const noTitle = title === undefined || title === url;
return (
{
},
}}
>
- {params.title}
+ {title}
-
- {params.percentage}%
-
- {params.url}
+ {percentageColor && percentage && (
+
+ {percentage}%
+
+ )}
+ {!noTitle && {url}}
);
};
Host.propTypes = {
- params: PropTypes.shape({
- title: PropTypes.string,
- percentageColor: PropTypes.string,
- percentage: PropTypes.string,
- url: PropTypes.string,
- }).isRequired,
+ title: PropTypes.string,
+ percentageColor: PropTypes.string,
+ percentage: PropTypes.string,
+ url: PropTypes.string,
};
export default Host;
diff --git a/Client/src/Pages/Monitors/Home/index.jsx b/Client/src/Pages/Monitors/Home/index.jsx
index 345a0a88e..59655af92 100644
--- a/Client/src/Pages/Monitors/Home/index.jsx
+++ b/Client/src/Pages/Monitors/Home/index.jsx
@@ -20,6 +20,8 @@ const Monitors = ({ isAdmin }) => {
const authState = useSelector((state) => state.auth);
const dispatch = useDispatch({});
+ console.log({ monitorState });
+
useEffect(() => {
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
}, [authState.authToken, dispatch]);
diff --git a/Client/src/Pages/Monitors/utils.jsx b/Client/src/Pages/Monitors/utils.jsx
index 4dc4260cd..1c351d4ad 100644
--- a/Client/src/Pages/Monitors/utils.jsx
+++ b/Client/src/Pages/Monitors/utils.jsx
@@ -1,13 +1,15 @@
import { useTheme } from "@mui/material";
const useUtils = () => {
- const theme = useTheme();
const determineState = (monitor) => {
if (monitor.isActive === false) return "paused";
if (monitor?.status === undefined) return "pending";
return monitor?.status == true ? "up" : "down";
};
+ /* TODO Refactor: from here on shouldn't live in a custom hook, but on theme, or constants */
+ const theme = useTheme();
+
const statusColor = {
up: theme.palette.success.main,
down: theme.palette.error.main,
diff --git a/Client/src/Utils/Theme/constants.js b/Client/src/Utils/Theme/constants.js
index ed3719278..6f71c5d1b 100644
--- a/Client/src/Utils/Theme/constants.js
+++ b/Client/src/Utils/Theme/constants.js
@@ -169,7 +169,7 @@ const semanticColors = {
background: {
main: {
light: paletteColors.white,
- dark: paletteColors.gray850,
+ dark: paletteColors.gray890,
},
alt: {
light: paletteColors.gray90,
diff --git a/Client/src/Utils/Theme/globalTheme.js b/Client/src/Utils/Theme/globalTheme.js
index 641e22582..f0cfceea0 100644
--- a/Client/src/Utils/Theme/globalTheme.js
+++ b/Client/src/Utils/Theme/globalTheme.js
@@ -32,19 +32,8 @@ const baseTheme = (palette) => ({
fontWeight: 400,
},
},
- /* TODO take chart from here, it should live inside of the gauge, and get info from the theme */
- chart: {
- header: {
- fontWeight: 400,
- fill: palette.text.tertiary,
- fontSize: typographyLevels.m,
- },
- subheader: {
- fontWeight: 400,
- fill: palette.text.tertiary,
- fontSize: typographyLevels.xs,
- },
- },
+ /* TODO we can skip using the callback functions on the next lines since we are already accessing it on line 10. That was the last thing I managed to do, so we are sort of doing it twice*/
+
spacing: 2,
/* TODO All these should live inside of a component*/
components: {
@@ -217,7 +206,8 @@ const baseTheme = (palette) => ({
borderRadius: theme.shape.borderRadius,
},
"& .MuiInputBase-input": {
- height: "var(--env-var-height-2)",
+ padding: ".75em",
+ minHeight: "var(--env-var-height-2)",
fontSize: "var(--env-var-font-size-medium)",
fontWeight: 400,
color: palette.text.secondary,
diff --git a/Client/src/assets/icons/cpu-chip.svg b/Client/src/assets/icons/cpu-chip.svg
new file mode 100644
index 000000000..5645840b1
--- /dev/null
+++ b/Client/src/assets/icons/cpu-chip.svg
@@ -0,0 +1,3 @@
+
diff --git a/Server/db/mongo/modules/hardwareCheckModule.js b/Server/db/mongo/modules/hardwareCheckModule.js
index de46828e5..00b2c1c07 100644
--- a/Server/db/mongo/modules/hardwareCheckModule.js
+++ b/Server/db/mongo/modules/hardwareCheckModule.js
@@ -1,7 +1,29 @@
import HardwareCheck from "../../models/HardwareCheck.js";
+import Monitor from "../../models/Monitor.js";
+
const SERVICE_NAME = "hardwareCheckModule";
const createHardwareCheck = async (hardwareCheckData) => {
try {
+ const { monitorId, status } = hardwareCheckData;
+ const n = (await HardwareCheck.countDocuments({ monitorId })) + 1;
+ const monitor = await Monitor.findById(monitorId);
+
+ if (!monitor) {
+ logger.error({
+ message: "Monitor not found",
+ service: SERVICE_NAME,
+ method: "createHardwareCheck",
+ details: `monitor ID: ${monitorId}`,
+ });
+ }
+
+ if (monitor.uptimePercentage === undefined) {
+ monitor.uptimePercentage = status === true ? 1 : 0;
+ } else {
+ monitor.uptimePercentage =
+ (monitor.uptimePercentage * (n - 1) + (status === true ? 1 : 0)) / n;
+ }
+ await monitor.save();
const hardwareCheck = await new HardwareCheck({
...hardwareCheckData,
}).save();