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) => ( + { + closePopup(); + navigate(item.path); + }} + sx={{ + gap: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + pl: theme.spacing(4), + }} + > + {item.icon} + {item.name} + + )); + }; + return ( + + collapsed && openPopup(e)} + sx={{ + cursor: collapsed ? "pointer" : "default", + }} + /> + + + + + {authState.user?.firstName} {authState.user?.lastName} + + + {getRoleDisplayText(authState.user, t)} + + + + + + openPopup(event)} + > + + + + + + + {collapsed && ( + + + + {authState.user?.firstName} {authState.user?.lastName} + + + {authState.user?.role} + + + + )} + {/* TODO Do we need two dividers? */} + {collapsed && } + {/* */} + {renderAccountMenuItems(authState.user, accountMenuItems)} + + + {t("menu.logOut", "Log out")} + + + + ); +}; + +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) => ( - { - closePopup(); - navigate(item.path); - }} - sx={{ - gap: theme.spacing(2), - borderRadius: theme.shape.borderRadius, - pl: theme.spacing(4), - }} - > - {item.icon} - {item.name} - - )); - }; - - 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} - - - - {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}`); - } - closePopup(); - }} - sx={{ - gap: theme.spacing(4), - opacity: 0.9, - /* TODO this has no effect? */ - "& svg": { - "& path": { - stroke: theme.palette.primary.contrastTextTertiary, - strokeWidth: 1.1, - }, - }, - }} - > - {child.icon} - {child.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")} - > - - - - - - )} - - {collapsed && ( - - - - {authState.user?.firstName} {authState.user?.lastName} - - - {authState.user?.role} - - - - )} - {/* TODO Do we need two dividers? */} - {collapsed && } - {/* */} - {renderAccountMenuItems()} - - - {t("menu.logOut", "Log out")} - - - + ); -} +}; 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 && ( + setIsOpen(false)} + confirmationButtonLabel={t("delete")} + onConfirm={handleRemove} + /> + )} ); }; 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; };