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 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"; 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"; 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 { 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"; const getMenu = (t) => [ { name: t("menu.uptime"), path: "uptime", icon: }, { name: t("menu.pagespeed"), path: "pagespeed", icon: }, { name: t("menu.infrastructure"), path: "infrastructure", icon: }, { name: t("menu.notifications"), path: "notifications", icon: , }, { name: t("menu.incidents"), path: "incidents", icon: }, { name: t("menu.statusPages"), path: "status", icon: }, { name: t("menu.maintenance"), path: "maintenance", icon: }, { name: t("menu.logs"), path: "logs", icon: }, { name: t("menu.settings"), icon: , path: "settings", }, ]; const getOtherMenuItems = (t) => [ { name: t("menu.support"), path: "support", icon: }, { name: t("menu.discussions"), path: "discussions", icon: , }, { name: t("menu.docs"), path: "docs", icon: }, { name: t("menu.changelog"), path: "changelog", icon: }, ]; const getAccountMenuItems = (t) => [ { name: t("menu.profile"), path: "account/profile", icon: }, { name: t("menu.password"), path: "account/password", icon: }, { 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 theme = useTheme(); const navigate = useNavigate(); const location = useLocation(); const dispatch = useDispatch(); const { t } = useTranslation(); const authState = useSelector((state) => state.auth); 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])) ); 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}`); } }} 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;