diff --git a/client/src/Components/ArrowLeft/index.jsx b/client/src/Components/ArrowLeft/index.jsx
new file mode 100644
index 000000000..7909441f3
--- /dev/null
+++ b/client/src/Components/ArrowLeft/index.jsx
@@ -0,0 +1,35 @@
+import LeftArrow from "../../assets/icons/left-arrow.svg?react";
+import LeftArrowDouble from "../../assets/icons/left-arrow-double.svg?react";
+import LeftArrowLong from "../../assets/icons/left-arrow-long.svg?react";
+import PropTypes from "prop-types";
+
+const ArrowLeft = ({ type, color = "#667085", ...props }) => {
+ if (type === "double") {
+ return (
+
+ );
+ } else if (type === "long") {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+};
+
+ArrowLeft.propTypes = {
+ color: PropTypes.string,
+ type: PropTypes.oneOf(["double", "long", "default"]),
+};
+export default ArrowLeft;
diff --git a/client/src/Components/ArrowRight/index.jsx b/client/src/Components/ArrowRight/index.jsx
new file mode 100644
index 000000000..335bc19f2
--- /dev/null
+++ b/client/src/Components/ArrowRight/index.jsx
@@ -0,0 +1,28 @@
+import RightArrow from "../../assets/icons/right-arrow.svg?react";
+import RightArrowDouble from "../../assets/icons/right-arrow-double.svg?react";
+import PropTypes from "prop-types";
+
+const ArrowRight = ({ type, color = "#667085", ...props }) => {
+ if (type === "double") {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+};
+
+ArrowRight.propTypes = {
+ type: PropTypes.oneOf(["double", "default"]),
+ color: PropTypes.string,
+};
+
+export default ArrowRight;
diff --git a/client/src/Components/Avatar/index.jsx b/client/src/Components/Avatar/index.jsx
index aa2050827..d089080ff 100644
--- a/client/src/Components/Avatar/index.jsx
+++ b/client/src/Components/Avatar/index.jsx
@@ -15,7 +15,7 @@ import { useTheme } from "@emotion/react";
*
*/
-const Avatar = ({ src, small, sx }) => {
+const Avatar = ({ src, small, sx, onClick = () => {} }) => {
const { user } = useSelector((state) => state.auth);
const theme = useTheme();
@@ -31,6 +31,7 @@ const Avatar = ({ src, small, sx }) => {
return (
div {
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
diff --git a/client/src/Components/Sidebar/components/authFooter.jsx b/client/src/Components/Sidebar/components/authFooter.jsx
new file mode 100644
index 000000000..0e6937913
--- /dev/null
+++ b/client/src/Components/Sidebar/components/authFooter.jsx
@@ -0,0 +1,286 @@
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import Tooltip from "@mui/material/Tooltip";
+import IconButton from "@mui/material/IconButton";
+import Avatar from "../../Avatar";
+import ThemeSwitch from "../../ThemeSwitch";
+import Menu from "@mui/material/Menu";
+import MenuItem from "@mui/material/MenuItem";
+import Divider from "@mui/material/Divider";
+import DotsVertical from "../../../assets/icons/dots-vertical.svg?react";
+import LogoutSvg from "../../../assets/icons/logout.svg?react";
+
+import { useTheme } from "@emotion/react";
+import { useSelector } from "react-redux";
+import { useTranslation } from "react-i18next";
+import { useState } from "react";
+import { useNavigate } from "react-router";
+import { clearAuthState } from "../../../Features/Auth/authSlice";
+import { useDispatch } from "react-redux";
+import PropTypes from "prop-types";
+
+const getFilteredAccountMenuItems = (user, items) => {
+ if (!user) return [];
+
+ let filtered = [...items];
+
+ if (user.role?.includes("demo")) {
+ filtered = filtered.filter((item) => item.name !== "Password");
+ }
+
+ if (!user.role?.includes("superadmin")) {
+ filtered = filtered.filter((item) => item.name !== "Team");
+ }
+
+ return filtered;
+};
+
+const getRoleDisplayText = (user, t) => {
+ if (!user?.role) return "";
+
+ if (user.role.includes("superadmin")) return t("roles.superAdmin");
+ if (user.role.includes("admin")) return t("roles.admin");
+ if (user.role.includes("user")) return t("roles.teamMember");
+ if (user.role.includes("demo")) return t("roles.demoUser");
+
+ return user.role;
+};
+
+const AuthFooter = ({ collapsed, accountMenuItems }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const authState = useSelector((state) => state.auth);
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+
+ const [anchorEl, setAnchorEl] = useState(null);
+
+ const openPopup = (event) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const closePopup = () => {
+ setAnchorEl(null);
+ };
+
+ const logout = async () => {
+ dispatch(clearAuthState());
+ navigate("/login");
+ };
+ const renderAccountMenuItems = (user, items) => {
+ const filteredItems = getFilteredAccountMenuItems(user, items);
+
+ return filteredItems.map((item) => (
+
+ ));
+ };
+ return (
+
+ collapsed && openPopup(e)}
+ sx={{
+ cursor: collapsed ? "pointer" : "default",
+ }}
+ />
+
+
+
+
+ {authState.user?.firstName} {authState.user?.lastName}
+
+
+ {getRoleDisplayText(authState.user, t)}
+
+
+
+
+
+ openPopup(event)}
+ >
+
+
+
+
+
+
+
+ );
+};
+
+AuthFooter.propTypes = {
+ collapsed: PropTypes.bool,
+ accountMenuItems: PropTypes.array,
+};
+
+export default AuthFooter;
diff --git a/client/src/Components/Sidebar/components/collapseButton.jsx b/client/src/Components/Sidebar/components/collapseButton.jsx
new file mode 100644
index 000000000..e7c382e5a
--- /dev/null
+++ b/client/src/Components/Sidebar/components/collapseButton.jsx
@@ -0,0 +1,55 @@
+import IconButton from "@mui/material/IconButton";
+import ArrowRight from "../../ArrowRight";
+import ArrowLeft from "../../ArrowLeft";
+import { useTheme } from "@mui/material/styles";
+import { useDispatch } from "react-redux";
+import { toggleSidebar } from "../../../Features/UI/uiSlice";
+import PropTypes from "prop-types";
+
+const CollapseButton = ({ collapsed }) => {
+ const theme = useTheme();
+ const dispatch = useDispatch();
+ const arrowIcon = collapsed ? (
+
+ ) : (
+
+ );
+ return (
+ {
+ dispatch(toggleSidebar());
+ }}
+ >
+ {arrowIcon}
+
+ );
+};
+
+CollapseButton.propTypes = {
+ collapsed: PropTypes.bool.isRequired,
+};
+export default CollapseButton;
diff --git a/client/src/Components/Sidebar/components/logo.jsx b/client/src/Components/Sidebar/components/logo.jsx
new file mode 100644
index 000000000..7a59f406d
--- /dev/null
+++ b/client/src/Components/Sidebar/components/logo.jsx
@@ -0,0 +1,67 @@
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import { useTheme } from "@mui/material/styles";
+import { useNavigate } from "react-router";
+import { useTranslation } from "react-i18next";
+import PropTypes from "prop-types";
+
+const Logo = ({ collapsed }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const navigate = useNavigate();
+
+ return (
+ navigate("/")}
+ sx={{ cursor: "pointer" }}
+ >
+
+ C
+
+
+ {" "}
+
+ {t("common.appName")}
+
+
+
+ );
+};
+
+Logo.propTypes = {
+ collapsed: PropTypes.bool,
+};
+
+export default Logo;
diff --git a/client/src/Components/Sidebar/components/navItem.jsx b/client/src/Components/Sidebar/components/navItem.jsx
new file mode 100644
index 000000000..2c569edde
--- /dev/null
+++ b/client/src/Components/Sidebar/components/navItem.jsx
@@ -0,0 +1,97 @@
+import Tooltip from "@mui/material/Tooltip";
+import ListItemButton from "@mui/material/ListItemButton";
+import ListItemIcon from "@mui/material/ListItemIcon";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import { useTheme } from "@emotion/react";
+import PropTypes from "prop-types";
+
+const NavItem = ({ item, collapsed, selected, onClick }) => {
+ const theme = useTheme();
+ const iconStroke = selected
+ ? theme.palette.primary.contrastText
+ : theme.palette.primary.contrastTextTertiary;
+
+ const buttonBgColor = selected ? theme.palette.secondary.main : "transparent";
+ const buttonBgHoverColor = selected
+ ? theme.palette.secondary.main
+ : theme.palette.tertiary.main;
+ const fontWeight = selected ? 600 : 400;
+ return (
+
+
+
+ {item.icon}
+
+
+
+ {item.name}
+
+
+
+
+ );
+};
+
+NavItem.propTypes = {
+ item: PropTypes.object,
+ collapsed: PropTypes.bool,
+ selected: PropTypes.bool,
+ onClick: PropTypes.func,
+};
+export default NavItem;
diff --git a/client/src/Components/Sidebar/index.css b/client/src/Components/Sidebar/index.css
deleted file mode 100644
index ef921acca..000000000
--- a/client/src/Components/Sidebar/index.css
+++ /dev/null
@@ -1,106 +0,0 @@
-/* TODO */
-aside .MuiList-root svg {
- width: 20px;
- height: 20px;
- opacity: 0.9;
-}
-aside span.MuiTypography-root {
- font-size: var(--env-var-font-size-medium);
- line-height: 1;
-}
-aside .MuiStack-root + span.MuiTypography-root {
- font-size: var(--env-var-font-size-medium-plus);
-}
-aside .MuiListSubheader-root {
- font-size: var(--env-var-font-size-small);
- font-weight: 500;
- line-height: 1.5;
- text-transform: uppercase;
- margin-bottom: 2px;
- opacity: 0.6;
-}
-aside p.MuiTypography-root {
- font-size: var(--env-var-font-size-small);
- opacity: 0.8;
-}
-aside .MuiListItemButton-root:not(.selected-path) > * {
- opacity: 0.9;
-}
-aside .selected-path > * {
- opacity: 1;
-}
-aside .selected-path span.MuiTypography-root {
- font-weight: 600;
-}
-aside .MuiCollapse-wrapperInner .MuiList-root > .MuiListItemButton-root {
- position: relative;
-}
-aside .MuiCollapse-wrapperInner .MuiList-root svg,
-aside .MuiList-root .MuiListItemText-root + svg {
- width: 18px;
- height: 18px;
-}
-
-.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root) {
- padding-bottom: 0;
-}
-.sidebar-popup svg {
- width: 16px;
- height: 16px;
- opacity: 0.9;
-}
-
-/* TRANSITIONS */
-aside {
- flex: 1;
- transition: max-width 650ms cubic-bezier(0.36, -0.01, 0, 0.77);
-}
-.home-layout aside.collapsed {
- max-width: 64px;
-}
-
-aside.expanded .MuiTypography-root,
-aside.expanded p.MuiTypography-root,
-aside.expanded .MuiListItemText-root + svg,
-aside.expanded .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root {
- visibility: visible;
- animation: fadeIn 1s ease;
-}
-
-aside.collapsed .MuiTypography-root,
-aside.collapsed p.MuiTypography-root,
-aside.collapsed .MuiListItemText-root + svg,
-aside.collapsed .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root {
- opacity: 0;
- visibility: hidden;
-}
-
-aside .MuiListSubheader-root {
- transition: padding 200ms ease;
-}
-
-.sidebar-delay-fade {
- opacity: 0;
- visibility: hidden;
- transition: opacity 0.3s ease;
-}
-
-aside.expanded.sidebar-ready .sidebar-delay-fade {
- opacity: 1;
- visibility: visible;
-}
-
-@keyframes fadeIn {
- 0% {
- opacity: 0;
- visibility: hidden;
- }
- 30% {
- opacity: 0;
- visibility: hidden;
- }
- 100% {
- opacity: 0.9;
- visibility: visible;
- }
-}
diff --git a/client/src/Components/Sidebar/index.jsx b/client/src/Components/Sidebar/index.jsx
index 219ceff1e..6b7b6577c 100644
--- a/client/src/Components/Sidebar/index.jsx
+++ b/client/src/Components/Sidebar/index.jsx
@@ -1,26 +1,16 @@
-import React, { useEffect, useState, useRef } from "react";
-import {
- Box,
- Collapse,
- Divider,
- IconButton,
- List,
- ListItemButton,
- ListItemIcon,
- ListItemText,
- Menu,
- MenuItem,
- Stack,
- Tooltip,
- Typography,
-} from "@mui/material";
-import ThemeSwitch from "../ThemeSwitch";
-import Avatar from "../Avatar";
+import Stack from "@mui/material/Stack";
+
+import List from "@mui/material/List";
+import Logo from "./components/logo";
+import CollapseButton from "./components/collapseButton";
+import Divider from "@mui/material/Divider";
+import NavItem from "./components/navItem";
+import AuthFooter from "./components/authFooter";
+
import StarPrompt from "../StarPrompt";
import LockSvg from "../../assets/icons/lock.svg?react";
import UserSvg from "../../assets/icons/user.svg?react";
import TeamSvg from "../../assets/icons/user-two.svg?react";
-import LogoutSvg from "../../assets/icons/logout.svg?react";
import Support from "../../assets/icons/support.svg?react";
import Maintenance from "../../assets/icons/maintenance.svg?react";
import Monitors from "../../assets/icons/monitors.svg?react";
@@ -28,11 +18,6 @@ import Incidents from "../../assets/icons/incidents.svg?react";
import Integrations from "../../assets/icons/integrations.svg?react";
import PageSpeed from "../../assets/icons/page-speed.svg?react";
import Settings from "../../assets/icons/settings.svg?react";
-import ArrowDown from "../../assets/icons/down-arrow.svg?react";
-import ArrowUp from "../../assets/icons/up-arrow.svg?react";
-import ArrowRight from "../../assets/icons/right-arrow.svg?react";
-import ArrowLeft from "../../assets/icons/left-arrow.svg?react";
-import DotsVertical from "../../assets/icons/dots-vertical.svg?react";
import ChangeLog from "../../assets/icons/changeLog.svg?react";
import Docs from "../../assets/icons/docs.svg?react";
import StatusPages from "../../assets/icons/status-pages.svg?react";
@@ -40,17 +25,18 @@ import Discussions from "../../assets/icons/discussions.svg?react";
import Notifications from "../../assets/icons/notifications.svg?react";
import Logs from "../../assets/icons/logs.svg?react";
-import "./index.css";
-
// Utils
-import { useLocation, useNavigate } from "react-router";
-import { useTheme } from "@emotion/react";
-import { useDispatch, useSelector } from "react-redux";
+import { useTheme } from "@mui/material/styles";
+import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
-import { clearAuthState } from "../../Features/Auth/authSlice";
-import { toggleSidebar } from "../../Features/UI/uiSlice";
-import { TurnedIn } from "@mui/icons-material";
-import { rules } from "eslint-plugin-react-refresh";
+import { useNavigate } from "react-router";
+
+const URL_MAP = {
+ support: "https://discord.com/invite/NAb6H3UTjK",
+ discussions: "https://github.com/bluewave-labs/checkmate/discussions",
+ docs: "https://bluewavelabs.gitbook.io/checkmate",
+ changelog: "https://github.com/bluewave-labs/checkmate/releases",
+};
const getMenu = (t) => [
{ name: t("menu.uptime"), path: "uptime", icon: },
@@ -92,782 +78,94 @@ const getAccountMenuItems = (t) => [
{ name: t("menu.team"), path: "account/team", icon: },
];
-/* TODO this could be a key in nested Path would be the link */
-const URL_MAP = {
- support: "https://discord.com/invite/NAb6H3UTjK",
- discussions: "https://github.com/bluewave-labs/checkmate/discussions",
- docs: "https://bluewavelabs.gitbook.io/checkmate",
- changelog: "https://github.com/bluewave-labs/checkmate/releases",
-};
-
-const PATH_MAP = {
- monitors: "Dashboard",
- pagespeed: "Dashboard",
- infrastructure: "Dashboard",
- account: "Account",
- settings: "Settings",
-};
-
-/**
- * @component
- * Sidebar component serves as a sidebar containing a menu.
- *
- * @returns {JSX.Element} The JSX element representing the Sidebar component.
- */
-
-function Sidebar() {
+const Sidebar = () => {
const theme = useTheme();
- const navigate = useNavigate();
- const location = useLocation();
- const dispatch = useDispatch();
const { t } = useTranslation();
- const authState = useSelector((state) => state.auth);
+ const navigate = useNavigate();
+ // Redux state
+ const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
+ const menu = getMenu(t);
const otherMenuItems = getOtherMenuItems(t);
const accountMenuItems = getAccountMenuItems(t);
- const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
- const [open, setOpen] = useState({ Dashboard: false, Account: false, Other: false });
- const [anchorEl, setAnchorEl] = useState(null);
- const [popup, setPopup] = useState();
- const { user } = useSelector((state) => state.auth);
- const sidebarRef = useRef(null);
- const [sidebarReady, setSidebarReady] = useState(false);
- const TRANSITION_DURATION = 200;
- let menu = getMenu(t);
- menu = menu.filter((item) => {
- if (item.path === "logs") {
- return user.role?.includes("admin") || user.role?.includes("superadmin");
- }
- return true;
- });
-
- useEffect(() => {
- if (!collapsed) {
- setSidebarReady(false);
- const timeout = setTimeout(() => {
- setSidebarReady(true);
- }, TRANSITION_DURATION);
- return () => clearTimeout(timeout);
- } else {
- setSidebarReady(false);
- }
- }, [collapsed]);
-
- const renderAccountMenuItems = () => {
- let filteredAccountMenuItems = [...accountMenuItems];
-
- // If the user is in demo mode, remove the "Password" option
- if (user.role?.includes("demo")) {
- filteredAccountMenuItems = filteredAccountMenuItems.filter(
- (item) => item.name !== "Password"
- );
- }
-
- // If the user is NOT a superadmin, remove the "Team" option
- if (user.role && !user.role.includes("superadmin")) {
- filteredAccountMenuItems = filteredAccountMenuItems.filter(
- (item) => item.name !== "Team"
- );
- }
-
- return filteredAccountMenuItems.map((item) => (
-
- ));
- };
-
- const openPopup = (event, id) => {
- setAnchorEl(event.currentTarget);
- setPopup(id);
- };
- const closePopup = () => {
- setAnchorEl(null);
- };
-
- /**
- * Handles logging out the user
- *
- */
- const logout = async () => {
- // Clear auth state
- dispatch(clearAuthState());
- navigate("/login");
- };
-
- useEffect(() => {
- const matchedKey = Object.keys(PATH_MAP).find((key) =>
- location.pathname.includes(key)
- );
-
- if (matchedKey) {
- setOpen((prev) => ({ ...prev, [PATH_MAP[matchedKey]]: true }));
- }
- }, [location]);
-
- const iconColor = theme.palette.primary.contrastTextTertiary;
- const sidebarClassName = `${collapsed ? "collapsed" : "expanded"} ${sidebarReady ? "sidebar-ready" : ""}`;
-
- /* TODO refactor this, there are a some ternaries and comments in the return */
return (
-
+
+ {
- setOpen((prev) =>
- Object.fromEntries(Object.keys(prev).map((key) => [key, false]))
+ >
+ {menu.map((item) => {
+ const selected = location.pathname.startsWith(`/${item.path}`);
+ return (
+ navigate(`/${item.path}`)}
+ />
);
- dispatch(toggleSidebar());
- }}
- >
- {collapsed ? : }
-
- {/* TODO Alignment done using padding. Use single source of truth to that*/}
-
- {/* TODO Abstract logo into component */}
- {/* TODO Turn logo into a link */}
- navigate("/")}
- sx={{ cursor: "pointer" }}
- >
-
- C
-
-
- {t("common.appName")}
-
-
-
-
-
- {menu.map((item) => {
- return item.path ? (
- /* If item has a path */
-
- navigate(`/${item.path}`)}
- sx={{
- height: "37px",
- gap: theme.spacing(4),
- borderRadius: theme.shape.borderRadius,
- px: theme.spacing(4),
- pl: theme.spacing(5),
- }}
- >
- {item.icon}
- {item.name}
-
-
- ) : collapsed ? (
- /* TODO Do we ever get here? If item does not have a path and collapsed state is true */
-
-
- openPopup(event, item.name)}
- sx={{
- position: "relative",
- gap: theme.spacing(4),
- borderRadius: theme.shape.borderRadius,
- px: theme.spacing(4),
- }}
- >
- {item.icon}
- {item.name}
-
-
-
-
- ) : (
- /* TODO Do we ever get here? If item does not have a path and collapsed state is false */
-
-
- setOpen((prev) => ({
- ...Object.fromEntries(Object.keys(prev).map((key) => [key, false])),
- [item.name]: !prev[item.name],
- }))
- }
- sx={{
- gap: theme.spacing(4),
- borderRadius: theme.shape.borderRadius,
- px: theme.spacing(4),
- }}
- >
- {item.icon}
- {item.name}
- {open[`${item.name}`] ? : }
-
-
-
- {item.nested.map((child) => {
- if (
- child.name === "Team" &&
- authState.user?.role &&
- !authState.user.role.includes("superadmin")
- ) {
- return null;
- }
-
- return (
- {
- const url = URL_MAP[child.path];
- if (url) {
- window.open(url, "_blank", "noreferrer");
- } else {
- navigate(`/${child.path}`);
- }
- }}
- sx={{
- gap: theme.spacing(4),
- borderRadius: theme.shape.borderRadius,
- pl: theme.spacing(4),
- "&::before": {
- content: `""`,
- position: "absolute",
- top: 0,
- left: "-7px",
- height: "100%",
- borderLeft: 1,
- borderLeftColor: theme.palette.primary.lowContrast,
- },
- "&:last-child::before": {
- height: "50%",
- },
- "&::after": {
- content: `""`,
- position: "absolute",
- top: "45%",
- left: "-8px",
- height: "3px",
- width: "3px",
- borderRadius: "50%",
- backgroundColor: theme.palette.primary.lowContrast,
- },
- "&.selected-path::after": {
- /* TODO what is this selector doing? */
- backgroundColor: theme.palette.primary.contrastTextTertiary,
- transform: "scale(1.2)",
- },
- }}
- >
- {child.icon}
- {child.name}
-
- );
- })}
-
-
-
- );
- })}
-
-
-
+ })}
+
{!collapsed && }
-
{otherMenuItems.map((item) => {
- return item.path ? (
-
- {
+ const url = URL_MAP[item.path];
+ if (url) {
+ window.open(url, "_blank", "noreferrer");
+ } else {
+ navigate(`/${item.path}`);
}
- onClick={() => {
- const url = URL_MAP[item.path];
- if (url) {
- window.open(url, "_blank", "noreferrer");
- } else {
- navigate(`/${item.path}`);
- }
- }}
- sx={{
- height: "37px",
- gap: theme.spacing(4),
- borderRadius: theme.shape.borderRadius,
- px: theme.spacing(4),
- pl: theme.spacing(5),
- }}
- >
- {item.icon}
- {item.name} {" "}
-
-
- ) : null;
+ }}
+ />
+ );
})}
-
- {collapsed ? (
- <>
-
- openPopup(event, "logout")}
- sx={{ p: 0, "&:focus": { outline: "none" } }}
- >
-
-
-
- >
- ) : (
- <>
-
-
-
- {authState.user?.firstName} {authState.user?.lastName}
-
-
- {authState.user?.role?.includes("superadmin")
- ? t("roles.superAdmin")
- : authState.user?.role?.includes("admin")
- ? t("roles.admin")
- : authState.user?.role?.includes("user")
- ? t("roles.teamMember")
- : authState.user?.role?.includes("demo")
- ? t("roles.demoUser")
- : authState.user?.role}
-
-
-
-
-
- openPopup(event, "logout")}
- >
-
-
-
-
- >
- )}
-
-
+
);
-}
+};
export default Sidebar;
diff --git a/client/src/Components/Table/TablePagination/Actions/index.jsx b/client/src/Components/Table/TablePagination/Actions/index.jsx
index 1b654fe9b..dec72446b 100644
--- a/client/src/Components/Table/TablePagination/Actions/index.jsx
+++ b/client/src/Components/Table/TablePagination/Actions/index.jsx
@@ -1,9 +1,7 @@
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 LeftArrow from "../../../ArrowLeft";
+import RightArrow from "../../../ArrowRight";
import { useTheme } from "@emotion/react";
TablePaginationActions.propTypes = {
@@ -50,7 +48,7 @@ function TablePaginationActions({ count, page, rowsPerPage, onPageChange }) {
disabled={page === 0}
aria-label="first page"
>
-
+
);
diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx
index b19f017e7..7b20e3d51 100644
--- a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx
+++ b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx
@@ -2,18 +2,22 @@ import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import Background from "../../../../assets/Images/background-grid.svg?react";
-import MonitorHeartOutlinedIcon from "@mui/icons-material/MonitorHeartOutlined";
-import TaskAltOutlinedIcon from "@mui/icons-material/TaskAltOutlined";
-import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined";
-import WarningAmberRoundedIcon from "@mui/icons-material/WarningAmberRounded";
+import AlertIcon from "../../../../assets/icons/alert-icon.svg?react";
+import CheckIcon from "../../../../assets/icons/check-icon.svg?react";
+import CloseIcon from "../../../../assets/icons/close-icon.svg?react";
+import WarningIcon from "../../../../assets/icons/warning-icon.svg?react";
const StatusBox = ({ title, value, status }) => {
const theme = useTheme();
let sharedStyles = {
position: "absolute",
right: 8,
- opacity: 0.5,
- "& svg path": { stroke: theme.palette.primary.contrastTextTertiary },
+ "& svg": {
+ width: 20,
+ height: 20,
+ opacity: 0.9,
+ "& path": { stroke: theme.palette.primary.contrastTextTertiary, strokeWidth: 1.7 },
+ },
};
let color;
@@ -22,28 +26,28 @@ const StatusBox = ({ title, value, status }) => {
color = theme.palette.success.lowContrast;
icon = (
-
+
);
} else if (status === "down") {
color = theme.palette.error.lowContrast;
icon = (
-
+
);
} else if (status === "paused") {
color = theme.palette.warning.lowContrast;
icon = (
-
+
);
} else {
color = theme.palette.accent.main;
icon = (
-
+
);
}
diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx
index cde9315aa..ae95cce33 100644
--- a/client/src/Pages/Infrastructure/Create/index.jsx
+++ b/client/src/Pages/Infrastructure/Create/index.jsx
@@ -1,78 +1,51 @@
-// React, Redux, Router
-import { useTheme } from "@emotion/react";
-import { useParams } from "react-router-dom";
-import { useState, useEffect } from "react";
-import { useSelector } from "react-redux";
-// Utility and Network
-import { infrastructureMonitorValidation } from "../../../Validation/validation";
-import { useFetchHardwareMonitorById } from "../../../Hooks/monitorHooks";
-import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
-import { useTranslation } from "react-i18next";
-import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
-import NotificationsConfig from "../../../Components/NotificationConfig";
-import {
- useUpdateMonitor,
- useCreateMonitor,
- useFetchGlobalSettings,
-} from "../../../Hooks/monitorHooks";
-
-// MUI
-import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
-
//Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
-import Link from "../../../Components/Link";
import ConfigBox from "../../../Components/ConfigBox";
+import Dialog from "../../../Components/Dialog";
+import FieldWrapper from "../../../Components/Inputs/FieldWrapper";
+import Link from "../../../Components/Link";
+import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
+import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
+import PulseDot from "../../../Components/Animated/PulseDot";
+import Select from "../../../Components/Inputs/Select";
import TextInput from "../../../Components/Inputs/TextInput";
+import { Box, Stack, Tooltip, Typography, Button, ButtonGroup } from "@mui/material";
+import { CustomThreshold } from "./Components/CustomThreshold";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import { createToast } from "../../../Utils/toastUtils";
-import Select from "../../../Components/Inputs/Select";
-import { CustomThreshold } from "./Components/CustomThreshold";
-import FieldWrapper from "../../../Components/Inputs/FieldWrapper";
-
-const SELECT_VALUES = [
- { _id: 0.25, name: "15 seconds" },
- { _id: 0.5, name: "30 seconds" },
- { _id: 1, name: "1 minute" },
- { _id: 2, name: "2 minutes" },
- { _id: 5, name: "5 minutes" },
- { _id: 10, name: "10 minutes" },
-];
-
-const METRICS = ["cpu", "memory", "disk", "temperature"];
-const METRIC_PREFIX = "usage_";
-const MS_PER_MINUTE = 60000;
-
-const hasAlertError = (errors) => {
- return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0;
-};
-
-const getAlertError = (errors) => {
- return Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX))
- ? errors[Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX))]
- : null;
-};
+// Utils
+import NotificationsConfig from "../../../Components/NotificationConfig";
+import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
+import { infrastructureMonitorValidation } from "../../../Validation/validation";
+import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
+import { useMonitorUtils } from "../../../Hooks/useMonitorUtils";
+import { useParams } from "react-router-dom";
+import { useSelector } from "react-redux";
+import { useState, useEffect } from "react";
+import { useTheme } from "@emotion/react";
+import { useTranslation } from "react-i18next";
+import {
+ useCreateMonitor,
+ useDeleteMonitor,
+ useFetchGlobalSettings,
+ useFetchHardwareMonitorById,
+ usePauseMonitor,
+ useUpdateMonitor,
+} from "../../../Hooks/monitorHooks";
const CreateInfrastructureMonitor = () => {
- const theme = useTheme();
const { user } = useSelector((state) => state.auth);
const { monitorId } = useParams();
- const { t } = useTranslation();
-
- // Determine if we are creating or editing
const isCreate = typeof monitorId === "undefined";
- // Fetch monitor details if editing
- const [monitor, isLoading, networkError] = useFetchHardwareMonitorById({ monitorId });
- const [notifications, notificationsAreLoading, notificationsError] =
- useGetNotificationsByTeamId();
- const [updateMonitor, isUpdating] = useUpdateMonitor();
- const [createMonitor, isCreating] = useCreateMonitor();
- const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings();
+ const theme = useTheme();
+ const { t } = useTranslation();
// State
const [errors, setErrors] = useState({});
const [https, setHttps] = useState(false);
+ const [isOpen, setIsOpen] = useState(false);
+ const [updateTrigger, setUpdateTrigger] = useState(false);
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
url: "",
name: "",
@@ -90,8 +63,50 @@ const CreateInfrastructureMonitor = () => {
secret: "",
});
- // Populate form fields if editing
+ // Fetch monitor details if editing
+ const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils();
+ const [monitor, isLoading] = useFetchHardwareMonitorById({
+ monitorId,
+ updateTrigger,
+ });
+ const [createMonitor, isCreating] = useCreateMonitor();
+ const [deleteMonitor, isDeleting] = useDeleteMonitor();
+ const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings();
+ const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId();
+ const [pauseMonitor, isPausing] = usePauseMonitor();
+ const [updateMonitor, isUpdating] = useUpdateMonitor();
+ const FREQUENCIES = [
+ { _id: 0.25, name: t("time.fifteenSeconds") },
+ { _id: 0.5, name: t("time.thirtySeconds") },
+ { _id: 1, name: t("time.oneMinute") },
+ { _id: 2, name: t("time.twoMinutes") },
+ { _id: 5, name: t("time.fiveMinutes") },
+ { _id: 10, name: t("time.tenMinutes") },
+ ];
+ const CRUMBS = [
+ { name: "Infrastructure monitors", path: "/infrastructure" },
+ ...(isCreate
+ ? [{ name: "Create", path: "/infrastructure/create" }]
+ : [
+ { name: "Details", path: `/infrastructure/${monitorId}` },
+ { name: "Configure", path: `/infrastructure/configure/${monitorId}` },
+ ]),
+ ];
+ const METRICS = ["cpu", "memory", "disk", "temperature"];
+ const METRIC_PREFIX = "usage_";
+ const MS_PER_MINUTE = 60000;
+
+ const hasAlertError = (errors) => {
+ return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0;
+ };
+
+ const getAlertError = (errors) => {
+ const errorKey = Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX));
+ return errorKey ? errors[errorKey] : null;
+ };
+
+ // Populate form fields if editing
useEffect(() => {
if (isCreate) {
if (globalSettingsLoading) return;
@@ -231,6 +246,10 @@ const CreateInfrastructureMonitor = () => {
: await updateMonitor({ monitor: form, redirect: "/infrastructure" });
};
+ const triggerUpdate = () => {
+ setUpdateTrigger(!updateTrigger);
+ };
+
const onChange = (event) => {
const { value, name } = event.target;
setInfrastructureMonitor({
@@ -257,19 +276,26 @@ const CreateInfrastructureMonitor = () => {
});
};
+ const handlePause = async () => {
+ await pauseMonitor({ monitorId, triggerUpdate });
+ };
+
+ const handleRemove = async (event) => {
+ event.preventDefault();
+ await deleteMonitor({ monitor, redirect: "/infrastructure" });
+ };
+
+ const isBusy =
+ isLoading ||
+ isUpdating ||
+ isCreating ||
+ isDeleting ||
+ isPausing ||
+ notificationsAreLoading;
+
return (
-
+
{
gap={theme.spacing(12)}
mt={theme.spacing(6)}
>
-
-
- {t(isCreate ? "infrastructureCreateYour" : "infrastructureEditYour")}{" "}
-
-
- {t("monitor")}
-
-
+
+
+
+ {!isCreate ? infrastructureMonitor.name : t("createYour") + " "}
+
+ {isCreate ? (
+
+ {t("monitor")}
+
+ ) : (
+ <>>
+ )}
+
+ {!isCreate && (
+
+
+
+
+
+
+
+ {infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."}
+
+
+ {t("editing")}
+
+
+ )}
+
+ {!isCreate && (
+
+
+
+
+ )}
+
{
label="Check frequency"
value={infrastructureMonitor.interval || 15}
onChange={onChange}
- items={SELECT_VALUES}
+ items={FREQUENCIES}
/>
@@ -474,12 +614,23 @@ const CreateInfrastructureMonitor = () => {
type="submit"
variant="contained"
color="accent"
- loading={isLoading || isUpdating || isCreating || notificationsAreLoading}
+ loading={isBusy}
>
{t(isCreate ? "infrastructureCreateMonitor" : "infrastructureEditMonitor")}
+ {!isCreate && (
+
);
};
diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx
index e9f235112..5bb227b7c 100644
--- a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx
+++ b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx
@@ -1,34 +1,80 @@
import Stack from "@mui/material/Stack";
-import Gauge from "../../../../../Components/Charts/CustomGauge";
+import CustomGauge from "../../../../../Components/Charts/CustomGauge";
import Typography from "@mui/material/Typography";
// Utils
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
-import { getPercentage } from "../../utils/utils";
+import { getPercentage, formatBytes } from "../../utils/utils";
import { useTranslation } from "react-i18next";
+import { Box } from "@mui/material";
-const GaugeBox = ({ title, subtitle, children }) => {
- const theme = useTheme();
- return (
-
- {children}
-
- {title}
- {subtitle}
-
+const BaseContainer = ({children}) => {
+ const theme = useTheme()
+ return(
+
+ {children}
+
);
};
-GaugeBox.propTypes = {
- title: PropTypes.string.isRequired,
- subtitle: PropTypes.string.isRequired,
- children: PropTypes.node.isRequired,
+const InfrastructureStyleGauge = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) => {
+ const theme = useTheme();
+
+ const MetricRow = ({ label, value }) => (
+
+ {label}
+
+ {value}
+
+
+ );
+
+ return(
+
+
+
+
+
+ {heading}
+
+
+
+
+ {metricTwo && valueTwo && (
+
+ )}
+
+
+
+ );
};
const Gauges = ({ diagnostics, isLoading }) => {
@@ -53,50 +99,41 @@ const Gauges = ({ diagnostics, isLoading }) => {
return (
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
);
};
@@ -106,4 +143,17 @@ Gauges.propTypes = {
isLoading: PropTypes.bool,
};
+InfrastructureStyleGauge.propTypes = {
+ value: PropTypes.number,
+ heading: PropTypes.string,
+ metricOne: PropTypes.string,
+ valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ metricTwo: PropTypes.string,
+ valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+};
+
+BaseContainer.propTypes = {
+ children: PropTypes.node.isRequired,
+};
+
export default Gauges;
diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx
index 7c5d1f5a9..72ee47f48 100644
--- a/client/src/Pages/Logs/Diagnostics/index.jsx
+++ b/client/src/Pages/Logs/Diagnostics/index.jsx
@@ -1,14 +1,14 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
-import Typography from "@mui/material/Typography";
import Gauges from "./components/gauges";
-import Stats from "./components/stats";
-import Divider from "@mui/material/Divider";
import Button from "@mui/material/Button";
-
+import StatBox from "../../../Components/StatBox";
+import StatusBoxes from "../../../Components/StatusBoxes";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useFetchDiagnostics } from "../../../Hooks/logHooks";
+import { getHumanReadableDuration } from "../../../Utils/timeUtils";
+import { formatBytes, getPercentage } from "./utils/utils";
const Diagnostics = () => {
// Local state
@@ -19,34 +19,57 @@ const Diagnostics = () => {
const [diagnostics, fetchDiagnostics, isLoading, error] = useFetchDiagnostics();
// Setup
return (
-
+
+
+
+
+
+
+
+
+
+
- {t("diagnosticsPage.diagnosticDescription")}
+
-
-
-
-
-
-
-
-
);
};
diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js
index 1a8d5e105..6f2a18e71 100644
--- a/client/src/Utils/Theme/globalTheme.js
+++ b/client/src/Utils/Theme/globalTheme.js
@@ -232,16 +232,16 @@ const baseTheme = (palette) => ({
},
MuiList: {
styleOverrides: {
- root: {
+ root: ({ theme }) => ({
padding: 0,
- },
+ }),
},
},
MuiListItemButton: {
styleOverrides: {
- root: {
- transition: "none",
- },
+ root: ({ theme }) => ({
+ transition: "background-color .3s",
+ }),
},
},
MuiListItemText: {
@@ -278,6 +278,7 @@ const baseTheme = (palette) => ({
}),
},
},
+
MuiTableHead: {
styleOverrides: {
root: ({ theme }) => ({
diff --git a/client/src/assets/icons/alert-icon.svg b/client/src/assets/icons/alert-icon.svg
new file mode 100644
index 000000000..1dc7fc2b7
--- /dev/null
+++ b/client/src/assets/icons/alert-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/client/src/assets/icons/check-icon.svg b/client/src/assets/icons/check-icon.svg
new file mode 100644
index 000000000..9a2cfac5a
--- /dev/null
+++ b/client/src/assets/icons/check-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/client/src/assets/icons/close-icon.svg b/client/src/assets/icons/close-icon.svg
new file mode 100644
index 000000000..e1b6b690a
--- /dev/null
+++ b/client/src/assets/icons/close-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/client/src/assets/icons/left-arrow-double.svg b/client/src/assets/icons/left-arrow-double.svg
index 6001a3ead..4cb2a5186 100644
--- a/client/src/assets/icons/left-arrow-double.svg
+++ b/client/src/assets/icons/left-arrow-double.svg
@@ -1,3 +1,4 @@
+
+
\ No newline at end of file
diff --git a/client/src/assets/icons/left-arrow-long.svg b/client/src/assets/icons/left-arrow-long.svg
index db9b827ed..56a774730 100644
--- a/client/src/assets/icons/left-arrow-long.svg
+++ b/client/src/assets/icons/left-arrow-long.svg
@@ -1,3 +1,4 @@
+
+
\ No newline at end of file
diff --git a/client/src/assets/icons/left-arrow.svg b/client/src/assets/icons/left-arrow.svg
index 8b38c408a..649d1681c 100644
--- a/client/src/assets/icons/left-arrow.svg
+++ b/client/src/assets/icons/left-arrow.svg
@@ -1,3 +1,4 @@
+
+
\ No newline at end of file
diff --git a/client/src/assets/icons/right-arrow-double.svg b/client/src/assets/icons/right-arrow-double.svg
index 4235134d7..6ad4882ad 100644
--- a/client/src/assets/icons/right-arrow-double.svg
+++ b/client/src/assets/icons/right-arrow-double.svg
@@ -1,3 +1,4 @@
+
+
\ No newline at end of file
diff --git a/client/src/assets/icons/right-arrow.svg b/client/src/assets/icons/right-arrow.svg
index 6b02062f2..77d022798 100644
--- a/client/src/assets/icons/right-arrow.svg
+++ b/client/src/assets/icons/right-arrow.svg
@@ -1,3 +1,4 @@
+
+
\ No newline at end of file
diff --git a/client/src/assets/icons/warning-icon.svg b/client/src/assets/icons/warning-icon.svg
new file mode 100644
index 000000000..468611758
--- /dev/null
+++ b/client/src/assets/icons/warning-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/client/src/index.css b/client/src/index.css
index f83cdc461..a5d305d46 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -28,6 +28,8 @@ html {
--env-var-nav-bar-height: 70px;
--env-var-side-bar-width: 250px;
+ --env-var-side-bar-collapsed-width: 64px;
+ --env-var-side-bar-auth-footer-height: 50px;
--env-var-spacing-1: 12px;
--env-var-spacing-1-plus: 16px;
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 21c74773a..b287a4229 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -981,6 +981,7 @@
"testLocale": "testLocale",
"testNotificationsDisabled": "There are no notifications setup for this monitor. You need to add one by clicking 'Configure' button",
"time": {
+ "fifteenSeconds": "15 seconds",
"fiveMinutes": "5 minutes",
"fourMinutes": "4 minutes",
"oneDay": "1 day",
@@ -988,6 +989,7 @@
"oneMinute": "1 minute",
"oneWeek": "1 week",
"tenMinutes": "10 minutes",
+ "thirtySeconds": "30 seconds",
"threeMinutes": "3 minutes",
"twentyMinutes": "20 minutes",
"twoMinutes": "2 minutes"
diff --git a/docker/dev/docker-compose.yaml b/docker/dev/docker-compose.yaml
index 292d0b1d5..f0b193b2d 100755
--- a/docker/dev/docker-compose.yaml
+++ b/docker/dev/docker-compose.yaml
@@ -44,4 +44,4 @@ services:
timeout: 30s
start_period: 0s
start_interval: 1s
- retries: 30
+ retries: 30
\ No newline at end of file
diff --git a/server/src/db/models/HardwareCheck.js b/server/src/db/models/HardwareCheck.js
index bda9f693c..5b7d56213 100755
--- a/server/src/db/models/HardwareCheck.js
+++ b/server/src/db/models/HardwareCheck.js
@@ -40,6 +40,20 @@ const captureSchema = mongoose.Schema({
mode: { type: String, default: "" },
});
+const networkInterfaceSchema = mongoose.Schema({
+ name: { type: String },
+ bytes_sent: { type: Number, default: 0 },
+ bytes_recv: { type: Number, default: 0 },
+ packets_sent: { type: Number, default: 0 },
+ packets_recv: { type: Number, default: 0 },
+ err_in: { type: Number, default: 0 },
+ err_out: { type: Number, default: 0 },
+ drop_in: { type: Number, default: 0 },
+ drop_out: { type: Number, default: 0 },
+ fifo_in: { type: Number, default: 0 },
+ fifo_out: { type: Number, default: 0 },
+});
+
const HardwareCheckSchema = mongoose.Schema(
{
...BaseCheckSchema.obj,
@@ -69,6 +83,12 @@ const HardwareCheckSchema = mongoose.Schema(
type: captureSchema,
default: () => ({}),
},
+
+ net: {
+ type: [networkInterfaceSchema],
+ default: () => [],
+ required: false,
+ },
},
{ timestamps: true }
);
diff --git a/server/src/db/models/StatusPage.js b/server/src/db/models/StatusPage.js
index 92cbab1cd..621749fb8 100755
--- a/server/src/db/models/StatusPage.js
+++ b/server/src/db/models/StatusPage.js
@@ -74,6 +74,10 @@ const StatusPageSchema = mongoose.Schema(
type: Boolean,
default: false,
},
+ customCSS: {
+ type: String,
+ default: "",
+ },
},
{ timestamps: true }
);
diff --git a/server/src/db/mongo/modules/monitorModuleQueries.js b/server/src/db/mongo/modules/monitorModuleQueries.js
index d96e128fa..f7283c8e9 100755
--- a/server/src/db/mongo/modules/monitorModuleQueries.js
+++ b/server/src/db/mongo/modules/monitorModuleQueries.js
@@ -220,6 +220,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
diskCount: {
$size: "$disk",
},
+ netCount: { $size: "$net" },
},
},
{
@@ -227,6 +228,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
from: "hardwarechecks",
let: {
diskCount: "$diskCount",
+ netCount: "$netCount",
},
pipeline: [
{
@@ -258,6 +260,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
disks: {
$push: "$disk",
},
+ net: {
+ $push: "$net",
+ },
},
},
{
@@ -371,6 +376,92 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
},
},
},
+ net: {
+ $map: {
+ input: { $range: [0, "$$netCount"] },
+ as: "netIndex",
+ in: {
+ name: {
+ $arrayElemAt: [
+ {
+ $map: {
+ input: "$net",
+ as: "netArray",
+ in: { $arrayElemAt: ["$$netArray.name", "$$netIndex"] },
+ },
+ },
+ 0,
+ ],
+ },
+ avgBytesSent: {
+ $avg: {
+ $map: {
+ input: "$net",
+ as: "netArray",
+ in: {
+ $arrayElemAt: ["$$netArray.bytes_sent", "$$netIndex"],
+ },
+ },
+ },
+ },
+ avgBytesRecv: {
+ $avg: {
+ $map: {
+ input: "$net",
+ as: "netArray",
+ in: {
+ $arrayElemAt: ["$$netArray.bytes_recv", "$$netIndex"],
+ },
+ },
+ },
+ },
+ avgPacketsSent: {
+ $avg: {
+ $map: {
+ input: "$net",
+ as: "netArray",
+ in: {
+ $arrayElemAt: ["$$netArray.packets_sent", "$$netIndex"],
+ },
+ },
+ },
+ },
+ avgPacketsRecv: {
+ $avg: {
+ $map: {
+ input: "$net",
+ as: "netArray",
+ in: {
+ $arrayElemAt: ["$$netArray.packets_recv", "$$netIndex"],
+ },
+ },
+ },
+ },
+ avgErrIn: {
+ $avg: {
+ $map: {
+ input: "$net",
+ as: "netArray",
+ in: {
+ $arrayElemAt: ["$$netArray.err_in", "$$netIndex"],
+ },
+ },
+ },
+ },
+ avgErrOut: {
+ $avg: {
+ $map: {
+ input: "$net",
+ as: "netArray",
+ in: {
+ $arrayElemAt: ["$$netArray.err_out", "$$netIndex"],
+ },
+ },
+ },
+ },
+ },
+ },
+ },
},
},
],
diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js
index 672eb55fc..72916a0ce 100755
--- a/server/src/service/infrastructure/statusService.js
+++ b/server/src/service/infrastructure/statusService.js
@@ -236,7 +236,7 @@ class StatusService {
}
if (type === "hardware") {
- const { cpu, memory, disk, host } = payload?.data ?? {};
+ const { cpu, memory, disk, host, net } = payload?.data ?? {};
const { errors } = payload?.errors ?? [];
check.cpu = cpu ?? {};
check.memory = memory ?? {};
@@ -244,6 +244,7 @@ class StatusService {
check.host = host ?? {};
check.errors = errors ?? [];
check.capture = payload?.capture ?? {};
+ check.net = net ?? {};
}
return check;
};