No files matched your search
@@ -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 (
|
||||
<LeftArrowDouble
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else if (type === "long") {
|
||||
return (
|
||||
<LeftArrowLong
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<LeftArrow
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ArrowLeft.propTypes = {
|
||||
color: PropTypes.string,
|
||||
type: PropTypes.oneOf(["double", "long", "default"]),
|
||||
};
|
||||
export default ArrowLeft;
|
||||
@@ -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 (
|
||||
<RightArrowDouble
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<RightArrow
|
||||
style={{ color }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ArrowRight.propTypes = {
|
||||
type: PropTypes.oneOf(["double", "default"]),
|
||||
color: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ArrowRight;
|
||||
@@ -15,7 +15,7 @@ import { useTheme } from "@emotion/react";
|
||||
* <Avatar src="assets/img" first="Alex" last="Holliday" small />
|
||||
*/
|
||||
|
||||
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 (
|
||||
<MuiAvatar
|
||||
onClick={onClick}
|
||||
alt={`${user?.firstName} ${user?.lastName}`}
|
||||
/* TODO What is the /static/images/avatar/2.jpg ?*/
|
||||
src={src ? src : user?.avatarImage ? image : "/static/images/avatar/2.jpg"}
|
||||
@@ -66,6 +67,7 @@ Avatar.propTypes = {
|
||||
src: PropTypes.string,
|
||||
small: PropTypes.bool,
|
||||
sx: PropTypes.object,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
@@ -2,8 +2,7 @@ import PropTypes from "prop-types";
|
||||
import { Box, Breadcrumbs as MUIBreadcrumbs } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import ArrowRight from "../../assets/icons/right-arrow.svg?react";
|
||||
|
||||
import ArrowRight from "../ArrowRight";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
}
|
||||
} */
|
||||
|
||||
.home-layout aside {
|
||||
/* .home-layout aside {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
max-width: var(--env-var-side-bar-width);
|
||||
}
|
||||
} */
|
||||
|
||||
.home-layout > div {
|
||||
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
|
||||
|
||||
@@ -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) => (
|
||||
<MenuItem
|
||||
key={item.name}
|
||||
onClick={() => {
|
||||
closePopup();
|
||||
navigate(item.path);
|
||||
}}
|
||||
sx={{
|
||||
gap: theme.spacing(2),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
pl: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
));
|
||||
};
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
height="var(--env-var-side-bar-auth-footer-height)"
|
||||
alignItems="center"
|
||||
py={theme.spacing(4)}
|
||||
px={theme.spacing(8)}
|
||||
gap={theme.spacing(2)}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
boxSizing={"border-box"}
|
||||
>
|
||||
<Avatar
|
||||
small={true}
|
||||
onClick={(e) => collapsed && openPopup(e)}
|
||||
sx={{
|
||||
cursor: collapsed ? "pointer" : "default",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack
|
||||
direction={"row"}
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(2)}
|
||||
minWidth={0}
|
||||
maxWidth={collapsed ? 0 : "100%"}
|
||||
sx={{
|
||||
opacity: collapsed ? 0 : 1,
|
||||
transition: "opacity 300ms ease, max-width 300ms ease",
|
||||
transitionDelay: collapsed ? "0ms" : "300ms",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
ml={theme.spacing(2)}
|
||||
sx={{
|
||||
maxWidth: "50%",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
color={theme.palette.primary.contrastText}
|
||||
fontWeight={500}
|
||||
lineHeight={1}
|
||||
fontSize={"var(--env-var-font-size-medium)"}
|
||||
sx={{
|
||||
display: "block",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{authState.user?.firstName} {authState.user?.lastName}
|
||||
</Typography>
|
||||
<Typography
|
||||
color={theme.palette.primary.contrastText}
|
||||
fontSize={"var(--env-var-font-size-small)"}
|
||||
textOverflow="ellipsis"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
sx={{ textTransform: "capitalize", opacity: 0.8 }}
|
||||
>
|
||||
{getRoleDisplayText(authState.user, t)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
columnGap={theme.spacing(2)}
|
||||
>
|
||||
<ThemeSwitch color={theme.palette.primary.contrastTextTertiary} />
|
||||
<Tooltip
|
||||
title={t("navControls")}
|
||||
disableInteractive
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
ml: "auto",
|
||||
mr: "-8px",
|
||||
"&:focus": { outline: "none" },
|
||||
alignSelf: "center",
|
||||
padding: "10px",
|
||||
|
||||
"& svg": {
|
||||
width: "22px",
|
||||
height: "22px",
|
||||
},
|
||||
"& svg path": {
|
||||
/* Vertical three dots */
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
onClick={(event) => openPopup(event)}
|
||||
>
|
||||
<DotsVertical />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Menu
|
||||
className="sidebar-popup"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={closePopup}
|
||||
disableScrollLock
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
marginTop: theme.spacing(-4),
|
||||
marginLeft: collapsed ? theme.spacing(2) : 0,
|
||||
},
|
||||
},
|
||||
}}
|
||||
MenuListProps={{
|
||||
sx: {
|
||||
p: 2,
|
||||
"& li": { m: 0 },
|
||||
"& li:has(.MuiBox-root):hover": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
ml: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
{collapsed && (
|
||||
<MenuItem sx={{ cursor: "default", minWidth: "50%" }}>
|
||||
<Box
|
||||
mb={theme.spacing(2)}
|
||||
sx={{
|
||||
minWidth: "50%",
|
||||
maxWidth: "max-content",
|
||||
overflow: "visible",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontWeight={500}
|
||||
fontSize={13}
|
||||
sx={{
|
||||
display: "block",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "visible",
|
||||
// wordBreak: "break-word",
|
||||
textOverflow: "clip",
|
||||
}}
|
||||
>
|
||||
{authState.user?.firstName} {authState.user?.lastName}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
textTransform: "capitalize",
|
||||
fontSize: 12,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "visible",
|
||||
// wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{authState.user?.role}
|
||||
</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* TODO Do we need two dividers? */}
|
||||
{collapsed && <Divider />}
|
||||
{/* <Divider /> */}
|
||||
{renderAccountMenuItems(authState.user, accountMenuItems)}
|
||||
<MenuItem
|
||||
onClick={logout}
|
||||
sx={{
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
pl: theme.spacing(4),
|
||||
"& svg path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LogoutSvg />
|
||||
{t("menu.logOut", "Log out")}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
AuthFooter.propTypes = {
|
||||
collapsed: PropTypes.bool,
|
||||
accountMenuItems: PropTypes.array,
|
||||
};
|
||||
|
||||
export default AuthFooter;
|
||||
@@ -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 ? (
|
||||
<ArrowRight
|
||||
height={theme.spacing(8)}
|
||||
width={theme.spacing(8)}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
/>
|
||||
) : (
|
||||
<ArrowLeft
|
||||
height={theme.spacing(8)}
|
||||
width={theme.spacing(8)}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<IconButton
|
||||
sx={{
|
||||
position: "absolute",
|
||||
/* TODO 60 is a magic number. if logo chnges size this might break */
|
||||
top: 60,
|
||||
right: 0,
|
||||
transform: `translate(50%, 0)`,
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
border: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
p: theme.spacing(2.5),
|
||||
|
||||
"&:focus": { outline: "none" },
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
dispatch(toggleSidebar());
|
||||
}}
|
||||
>
|
||||
{arrowIcon}
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
CollapseButton.propTypes = {
|
||||
collapsed: PropTypes.bool.isRequired,
|
||||
};
|
||||
export default CollapseButton;
|
||||
@@ -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 (
|
||||
<Stack
|
||||
pt={theme.spacing(6)}
|
||||
pb={theme.spacing(12)}
|
||||
pl={theme.spacing(8)}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
onClick={() => navigate("/")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
<Typography
|
||||
pl={theme.spacing("1px")}
|
||||
minWidth={theme.spacing(16)}
|
||||
minHeight={theme.spacing(16)}
|
||||
display={"flex"}
|
||||
justifyContent={"center"}
|
||||
alignItems={"center"}
|
||||
backgroundColor={theme.palette.accent.main}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
color={theme.palette.accent.contrastText}
|
||||
fontSize={18}
|
||||
>
|
||||
C
|
||||
</Typography>
|
||||
<Box
|
||||
overflow={"hidden"}
|
||||
sx={{
|
||||
transition: "opacity 900ms ease, width 900ms ease",
|
||||
opacity: collapsed ? 0 : 1,
|
||||
whiteSpace: "nowrap",
|
||||
width: collapsed ? 0 : "100%",
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
<Typography
|
||||
lineHeight={1}
|
||||
mt={theme.spacing(2)}
|
||||
color={theme.palette.accent.contrastText}
|
||||
fontSize={"var(--env-var-font-size-medium-plus)"}
|
||||
sx={{ opacity: 0.8, fontWeight: 500 }}
|
||||
>
|
||||
{t("common.appName")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Logo.propTypes = {
|
||||
collapsed: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Logo;
|
||||
@@ -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 (
|
||||
<Tooltip
|
||||
placement="right"
|
||||
title={collapsed ? item.name : ""}
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -16],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
disableInteractive
|
||||
>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
backgroundColor: buttonBgColor,
|
||||
"&:hover": {
|
||||
backgroundColor: buttonBgHoverColor,
|
||||
},
|
||||
height: 37,
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
px: theme.spacing(4),
|
||||
pl: theme.spacing(5),
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
"& svg": {
|
||||
height: 20,
|
||||
width: 20,
|
||||
opacity: 0.81,
|
||||
},
|
||||
"& svg path": {
|
||||
stroke: iconStroke,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</ListItemIcon>
|
||||
<Box
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
transition: "opacity 900ms ease",
|
||||
opacity: collapsed ? 0 : 1,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color={theme.palette.primary.contrastText}
|
||||
sx={{
|
||||
fontWeight: fontWeight,
|
||||
opacity: 0.9,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
NavItem.propTypes = {
|
||||
item: PropTypes.object,
|
||||
collapsed: PropTypes.bool,
|
||||
selected: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
export default NavItem;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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: <Monitors /> },
|
||||
@@ -92,782 +78,94 @@ const getAccountMenuItems = (t) => [
|
||||
{ name: t("menu.team"), path: "account/team", icon: <TeamSvg /> },
|
||||
];
|
||||
|
||||
/* 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) => (
|
||||
<MenuItem
|
||||
key={item.name}
|
||||
onClick={() => {
|
||||
closePopup();
|
||||
navigate(item.path);
|
||||
}}
|
||||
sx={{
|
||||
gap: theme.spacing(2),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
pl: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
));
|
||||
};
|
||||
|
||||
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 (
|
||||
<Stack
|
||||
height="100vh"
|
||||
width={
|
||||
collapsed
|
||||
? "var(--env-var-side-bar-collapsed-width)"
|
||||
: "var(--env-var-side-bar-width)"
|
||||
}
|
||||
component="aside"
|
||||
ref={sidebarRef}
|
||||
className={sidebarClassName}
|
||||
/* TODO general padding should be here */
|
||||
py={theme.spacing(6)}
|
||||
position="sticky"
|
||||
top={0}
|
||||
borderRight={`1px solid ${theme.palette.primary.lowContrast}`}
|
||||
paddingTop={theme.spacing(6)}
|
||||
paddingBottom={theme.spacing(6)}
|
||||
gap={theme.spacing(6)}
|
||||
/* TODO set all style in this sx if possible (when general)
|
||||
This is the top lever for styles
|
||||
*/
|
||||
sx={{
|
||||
position: "relative",
|
||||
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: 0,
|
||||
"& :is(p, span, .MuiListSubheader-root)": {
|
||||
/*
|
||||
Text color for unselected menu items and menu headings
|
||||
Secondary contrast text against main background
|
||||
*/
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
},
|
||||
"& .MuiList-root svg path": {
|
||||
/* Menu Icons */
|
||||
stroke: iconColor,
|
||||
},
|
||||
"& .selected-path": {
|
||||
/* Selected menu item */
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
},
|
||||
"& .MuiListItemIcon-root svg path": {
|
||||
/* Selected menu item icon */
|
||||
stroke: theme.palette.secondary.contrastText,
|
||||
},
|
||||
"& .MuiListItemText-root :is(p, span)": {
|
||||
/* Selected menu item text */
|
||||
color: theme.palette.secondary.contrastText,
|
||||
},
|
||||
},
|
||||
"& .MuiListItemButton-root:not(.selected-path)": {
|
||||
transition: "background-color .3s",
|
||||
" &:hover": {
|
||||
/* Hovered menu item bg color */
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
"& :is(p, span)": {
|
||||
/* Hovered menu item text color */
|
||||
color: theme.palette.tertiary.contrastText,
|
||||
},
|
||||
},
|
||||
},
|
||||
transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
<CollapseButton collapsed={collapsed} />
|
||||
<Logo collapsed={collapsed} />
|
||||
<List
|
||||
component="nav"
|
||||
aria-labelledby="nested-menu-subheader"
|
||||
disablePadding
|
||||
sx={{
|
||||
position: "absolute",
|
||||
/* TODO 60 is a magic number. if logo chnges size this might break */
|
||||
top: 60,
|
||||
right: 0,
|
||||
transform: `translate(50%, 0)`,
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
p: theme.spacing(2.5),
|
||||
"& svg": {
|
||||
width: theme.spacing(8),
|
||||
height: theme.spacing(8),
|
||||
"& path": {
|
||||
/* TODO this should be set at the top level if possible */
|
||||
stroke: theme.palette.primary.contrastTextSecondary,
|
||||
},
|
||||
},
|
||||
"&:focus": { outline: "none" },
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
px: theme.spacing(6),
|
||||
height: "100%",
|
||||
}}
|
||||
onClick={() => {
|
||||
setOpen((prev) =>
|
||||
Object.fromEntries(Object.keys(prev).map((key) => [key, false]))
|
||||
>
|
||||
{menu.map((item) => {
|
||||
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||
return (
|
||||
<NavItem
|
||||
key={item.path}
|
||||
item={item}
|
||||
collapsed={collapsed}
|
||||
selected={selected}
|
||||
onClick={() => navigate(`/${item.path}`)}
|
||||
/>
|
||||
);
|
||||
dispatch(toggleSidebar());
|
||||
}}
|
||||
>
|
||||
{collapsed ? <ArrowRight /> : <ArrowLeft />}
|
||||
</IconButton>
|
||||
{/* TODO Alignment done using padding. Use single source of truth to that*/}
|
||||
<Stack
|
||||
pt={theme.spacing(6)}
|
||||
pb={theme.spacing(12)}
|
||||
pl={theme.spacing(8)}
|
||||
>
|
||||
{/* TODO Abstract logo into component */}
|
||||
{/* TODO Turn logo into a link */}
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
onClick={() => navigate("/")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
minWidth={theme.spacing(16)}
|
||||
minHeight={theme.spacing(16)}
|
||||
pl="1px"
|
||||
fontSize={18}
|
||||
color={theme.palette.accent.contrastText}
|
||||
sx={{
|
||||
position: "relative",
|
||||
backgroundColor: theme.palette.accent.main,
|
||||
color: theme.palette.accent.contrastText,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
C
|
||||
</Stack>
|
||||
<Typography
|
||||
component="span"
|
||||
mt={theme.spacing(2)}
|
||||
sx={{ opacity: 0.8, fontWeight: 500 }}
|
||||
>
|
||||
{t("common.appName")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
overflow: "auto",
|
||||
overflowX: "hidden",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: theme.spacing(2),
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.spacing(4),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<List
|
||||
component="nav"
|
||||
aria-labelledby="nested-menu-subheader"
|
||||
disablePadding
|
||||
sx={{
|
||||
px: theme.spacing(6),
|
||||
height: "100%",
|
||||
/* overflow: "hidden", */
|
||||
}}
|
||||
>
|
||||
{menu.map((item) => {
|
||||
return item.path ? (
|
||||
/* If item has a path */
|
||||
<Tooltip
|
||||
key={item.path}
|
||||
placement="right"
|
||||
title={collapsed ? item.name : ""}
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -16],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
disableInteractive
|
||||
>
|
||||
<ListItemButton
|
||||
className={
|
||||
location.pathname.startsWith(`/${item.path}`) ? "selected-path" : ""
|
||||
}
|
||||
onClick={() => navigate(`/${item.path}`)}
|
||||
sx={{
|
||||
height: "37px",
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
px: theme.spacing(4),
|
||||
pl: theme.spacing(5),
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
|
||||
<ListItemText>{item.name}</ListItemText>
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
) : collapsed ? (
|
||||
/* TODO Do we ever get here? If item does not have a path and collapsed state is true */
|
||||
<React.Fragment key={item.name}>
|
||||
<Tooltip
|
||||
placement="right"
|
||||
title={collapsed ? item.name : ""}
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -16],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
disableInteractive
|
||||
>
|
||||
<ListItemButton
|
||||
className={
|
||||
Boolean(anchorEl) && popup === item.name ? "selected-path" : ""
|
||||
}
|
||||
onClick={(event) => openPopup(event, item.name)}
|
||||
sx={{
|
||||
position: "relative",
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
px: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
|
||||
<ListItemText>{item.name}</ListItemText>
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
className="sidebar-popup"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl) && popup === item.name}
|
||||
onClose={closePopup}
|
||||
disableScrollLock
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
mt: theme.spacing(-2),
|
||||
ml: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}}
|
||||
MenuListProps={{ sx: { px: 1, py: 2 } }}
|
||||
sx={{
|
||||
ml: theme.spacing(8),
|
||||
/* TODO what is this selection? */
|
||||
"& .selected-path": {
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.nested.map((child) => {
|
||||
if (
|
||||
child.name === "Team" &&
|
||||
authState.user?.role &&
|
||||
!authState.user.role.includes("superadmin")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
className={
|
||||
location.pathname.includes(child.path) ? "selected-path" : ""
|
||||
}
|
||||
key={child.path}
|
||||
onClick={() => {
|
||||
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}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
/* TODO Do we ever get here? If item does not have a path and collapsed state is false */
|
||||
<React.Fragment key={item.name}>
|
||||
<ListItemButton
|
||||
onClick={() =>
|
||||
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),
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
|
||||
<ListItemText>{item.name}</ListItemText>
|
||||
{open[`${item.name}`] ? <ArrowUp /> : <ArrowDown />}
|
||||
</ListItemButton>
|
||||
<Collapse
|
||||
in={open[`${item.name}`]}
|
||||
timeout="auto"
|
||||
>
|
||||
<List
|
||||
component="div"
|
||||
disablePadding
|
||||
sx={{ pl: theme.spacing(12) }}
|
||||
>
|
||||
{item.nested.map((child) => {
|
||||
if (
|
||||
child.name === "Team" &&
|
||||
authState.user?.role &&
|
||||
!authState.user.role.includes("superadmin")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
className={
|
||||
location.pathname.includes(child.path) ? "selected-path" : ""
|
||||
}
|
||||
key={child.path}
|
||||
onClick={() => {
|
||||
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)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 0 }}>{child.icon}</ListItemIcon>
|
||||
<ListItemText>{child.name}</ListItemText>
|
||||
</ListItemButton>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Collapse>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
|
||||
})}
|
||||
</List>
|
||||
{!collapsed && <StarPrompt />}
|
||||
|
||||
<List
|
||||
component="nav"
|
||||
disablePadding
|
||||
sx={{ px: theme.spacing(6) }}
|
||||
>
|
||||
{otherMenuItems.map((item) => {
|
||||
return item.path ? (
|
||||
<Tooltip
|
||||
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||
|
||||
return (
|
||||
<NavItem
|
||||
key={item.path}
|
||||
placement="right"
|
||||
title={collapsed ? item.name : ""}
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -16],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
disableInteractive
|
||||
>
|
||||
<ListItemButton
|
||||
className={
|
||||
location.pathname.startsWith(`/${item.path}`) ? "selected-path" : ""
|
||||
item={item}
|
||||
collapsed={collapsed}
|
||||
selected={selected}
|
||||
onClick={() => {
|
||||
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),
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon} </ListItemIcon>
|
||||
<ListItemText>{item.name} </ListItemText>{" "}
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Divider sx={{ mt: "auto", borderColor: theme.palette.primary.lowContrast }} />
|
||||
<Stack
|
||||
direction="row"
|
||||
height="50px"
|
||||
alignItems="center"
|
||||
py={theme.spacing(4)}
|
||||
px={theme.spacing(8)}
|
||||
gap={theme.spacing(2)}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
>
|
||||
{collapsed ? (
|
||||
<>
|
||||
<Tooltip
|
||||
title="Options"
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -10],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
disableInteractive
|
||||
>
|
||||
<IconButton
|
||||
onClick={(event) => openPopup(event, "logout")}
|
||||
sx={{ p: 0, "&:focus": { outline: "none" } }}
|
||||
>
|
||||
<Avatar small={true} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Avatar small={true} />
|
||||
<Box
|
||||
ml={theme.spacing(2)}
|
||||
sx={{ maxWidth: "50%", overflow: "hidden" }}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontWeight={500}
|
||||
sx={{
|
||||
display: "block",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{authState.user?.firstName} {authState.user?.lastName}
|
||||
</Typography>
|
||||
<Typography sx={{ textTransform: "capitalize" }}>
|
||||
{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}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
className="sidebar-delay-fade"
|
||||
flexDirection={"row"}
|
||||
marginLeft={"auto"}
|
||||
columnGap={theme.spacing(2)}
|
||||
>
|
||||
<ThemeSwitch color={iconColor} />
|
||||
<Tooltip
|
||||
title={t("navControls")}
|
||||
disableInteractive
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
ml: "auto",
|
||||
mr: "-8px",
|
||||
"&:focus": { outline: "none" },
|
||||
alignSelf: "center",
|
||||
padding: "10px",
|
||||
|
||||
"& svg": {
|
||||
width: "22px",
|
||||
height: "22px",
|
||||
},
|
||||
"& svg path": {
|
||||
/* Vertical three dots */
|
||||
stroke: iconColor,
|
||||
},
|
||||
}}
|
||||
onClick={(event) => openPopup(event, "logout")}
|
||||
>
|
||||
<DotsVertical />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
<Menu
|
||||
className="sidebar-popup"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl) && popup === "logout"}
|
||||
onClose={closePopup}
|
||||
disableScrollLock
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
marginTop: theme.spacing(-4),
|
||||
marginLeft: collapsed ? theme.spacing(2) : 0,
|
||||
},
|
||||
},
|
||||
}}
|
||||
MenuListProps={{
|
||||
sx: {
|
||||
p: 2,
|
||||
"& li": { m: 0 },
|
||||
"& li:has(.MuiBox-root):hover": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
ml: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
{collapsed && (
|
||||
<MenuItem sx={{ cursor: "default", minWidth: "50%" }}>
|
||||
<Box
|
||||
mb={theme.spacing(2)}
|
||||
sx={{
|
||||
minWidth: "50%",
|
||||
maxWidth: "max-content",
|
||||
overflow: "visible",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontWeight={500}
|
||||
fontSize={13}
|
||||
sx={{
|
||||
display: "block",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "visible",
|
||||
// wordBreak: "break-word",
|
||||
textOverflow: "clip",
|
||||
}}
|
||||
>
|
||||
{authState.user?.firstName} {authState.user?.lastName}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
textTransform: "capitalize",
|
||||
fontSize: 12,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "visible",
|
||||
// wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{authState.user?.role}
|
||||
</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* TODO Do we need two dividers? */}
|
||||
{collapsed && <Divider />}
|
||||
{/* <Divider /> */}
|
||||
{renderAccountMenuItems()}
|
||||
<MenuItem
|
||||
onClick={logout}
|
||||
sx={{
|
||||
gap: theme.spacing(4),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
pl: theme.spacing(4),
|
||||
"& svg path": {
|
||||
stroke: iconColor,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<LogoutSvg />
|
||||
{t("menu.logOut", "Log out")}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
<AuthFooter
|
||||
collapsed={collapsed}
|
||||
accountMenuItems={accountMenuItems}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -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"
|
||||
>
|
||||
<LeftArrowDouble />
|
||||
<LeftArrow type="double" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
@@ -74,7 +72,7 @@ function TablePaginationActions({ count, page, rowsPerPage, onPageChange }) {
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
>
|
||||
<RightArrowDouble />
|
||||
<RightArrow type="double" />
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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 }) => ({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 17L13 12L18 7M11 17L6 12L11 7" stroke="#667085" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<path d="M18 17L13 12L18 7M11 17L6 12L11 7" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 232 B After Width: | Height: | Size: 249 B |
@@ -1,3 +1,4 @@
|
||||
<svg width="18" height="14" viewBox="0 0 18 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 7H1M1 7L7 13M1 7L7 1" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<path d="M17 7H1M1 7L7 13M1 7L7 1" stroke="currentColor" stroke-width="1.5"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 223 B After Width: | Height: | Size: 240 B |
@@ -1,3 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 18L9 12L15 6" stroke="#667085" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 215 B After Width: | Height: | Size: 232 B |
@@ -1,3 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 17L11 12L6 7M13 17L18 12L13 7" stroke="#667085" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<path d="M6 17L11 12L6 7M13 17L18 12L13 7" stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 231 B After Width: | Height: | Size: 248 B |
@@ -1,3 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 18L15 12L9 6" stroke="#667085" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<path d="M9 18L15 12L9 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 214 B After Width: | Height: | Size: 231 B |
@@ -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;
|
||||
|
||||