Migrate Sidebar to typescript and MUI 5

master
Tudor Stanciu 2024-03-30 12:55:22 +02:00
parent 14f16677c9
commit c981660442
11 changed files with 411 additions and 209 deletions

View File

@ -0,0 +1,19 @@
import React from "react";
import * as MuiIcons from "./list";
import { Icon as MuiIcon, IconProps } from "@mui/material";
interface Props extends IconProps {
code?: string | null;
fallback?: JSX.Element;
}
const DynamicIcon: React.FC<Props> = ({ code, fallback, ...res }) => {
if (code && code in MuiIcons) {
const Icon = MuiIcons[code as keyof typeof MuiIcons] as typeof MuiIcon;
return <Icon {...res} />;
} else {
return <>{fallback ?? ""}</>;
}
};
export default DynamicIcon;

View File

@ -0,0 +1,4 @@
import DynamicIcon from "./DynamicIcon";
export * from "./list";
export { DynamicIcon };

View File

@ -0,0 +1 @@
export { Home, Dashboard, Dns, DeviceHub, Build, Settings, FeaturedPlayList, Info } from "@mui/icons-material";

View File

@ -1,33 +0,0 @@
import React, { useState } from "react";
import { useTheme } from "@mui/material/styles";
import AppRoutes from "./AppRoutes";
import TopBar from "./TopBar";
import Sidebar from "./Sidebar";
import { getStyles } from "./styles";
const AppLayout = () => {
const [open, setOpen] = useState(false);
const theme = useTheme();
const styles = getStyles(theme);
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
return (
<div style={styles.root}>
<TopBar open={open} handleDrawerOpen={handleDrawerOpen} />
<Sidebar open={open} handleDrawerClose={handleDrawerClose} />
<main style={styles.content}>
<div style={styles.toolbar} />
<AppRoutes />
</main>
</div>
);
};
export default AppLayout;

View File

@ -0,0 +1,54 @@
import React, { useState, useMemo } from "react";
import { styled } from "@mui/material/styles";
import AppRoutes from "./AppRoutes";
import TopBar from "./TopBar";
import Sidebar from "./SideBar";
import { drawerWidth } from "./constants";
import { Box } from "@mui/material";
const DrawerHeader = styled("div")(({ theme }) => ({
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar
}));
const AppLayout = () => {
const [open, setOpen] = useState(false);
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
return (
<Box sx={{ display: "flex", minHeight: "100vh" }}>
<TopBar open={open} onDrawerOpen={handleDrawerOpen} />
<Sidebar open={open} onDrawerOpen={handleDrawerOpen} onDrawerClose={handleDrawerClose} />
<Box
component="main"
sx={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
paddingTop: 2,
paddingBottom: 1,
paddingLeft: 1,
paddingRight: 1,
width: `calc(100% - ${drawerWidth}px)`
}}
>
<DrawerHeader />
<AppRoutes />
</Box>
</Box>
);
};
export default AppLayout;

View File

@ -0,0 +1,84 @@
import React from "react";
import { SxProps, Theme } from "@mui/material/styles";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Badge from "@mui/material/Badge";
import { ExpandLess, ExpandMore } from "@mui/icons-material";
import { Collapse, Divider, Tooltip } from "@mui/material";
type MenuItemProps = {
open: boolean;
icon: React.ReactNode;
label: string;
onClick?: () => void;
subMenus?: MenuItemProps[];
sx?: SxProps<Theme>;
};
const MenuItem: React.FC<MenuItemProps> = ({ open, icon, label, onClick, subMenus, sx }) => {
const [openSubMenu, setOpenSubMenu] = React.useState(false);
const handleSubMenuToggle = () => {
setOpenSubMenu(!openSubMenu);
};
return (
<>
<ListItem disablePadding sx={{ display: "block" }}>
<Tooltip title={open ? undefined : label} placement="right" arrow>
<ListItemButton
sx={{
minHeight: 48,
justifyContent: open ? "initial" : "center",
px: 2.5,
...sx
}}
onClick={onClick ? onClick : handleSubMenuToggle}
>
<ListItemIcon
sx={{
minWidth: 0,
mr: open ? 3 : "auto",
justifyContent: "center"
}}
>
{open || !subMenus ? (
icon
) : (
<Badge badgeContent={openSubMenu ? <ExpandLess fontSize="small" /> : <ExpandMore fontSize="small" />}>
{icon}
</Badge>
)}
</ListItemIcon>
<ListItemText
primary={label}
sx={{ opacity: open ? 1 : 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
/>
{subMenus && open && (
<ListItemIcon sx={{ minWidth: 0, ml: "auto" }}>
{openSubMenu ? <ExpandLess /> : <ExpandMore />}
</ListItemIcon>
)}
</ListItemButton>
</Tooltip>
</ListItem>
{subMenus && (
<Collapse in={openSubMenu} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{subMenus.map((subItem, index) => (
<React.Fragment key={index}>
<MenuItem key={index} {...subItem} sx={{ marginLeft: subItem.open ? 2 : 0 }} />
{index === subMenus.length - 1 && <Divider />}
</React.Fragment>
))}
</List>
</Collapse>
)}
</>
);
};
export default MenuItem;

View File

@ -0,0 +1,149 @@
import * as React from "react";
import { styled, Theme, CSSObject } from "@mui/material/styles";
import MuiDrawer from "@mui/material/Drawer";
import List from "@mui/material/List";
import Divider from "@mui/material/Divider";
import IconButton from "@mui/material/IconButton";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import { useNavigate } from "react-router-dom";
import { drawerWidth } from "./constants";
import {
SettingsOutlined,
HelpOutlineOutlined,
PollOutlined,
DashboardOutlined,
TuneOutlined
} from "@mui/icons-material";
import MenuItem from "./MenuItem";
import { useTranslation } from "react-i18next";
import { menu } from "./constants";
const openedMixin = (theme: Theme): CSSObject => ({
width: drawerWidth,
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen
}),
overflowX: "hidden"
});
const closedMixin = (theme: Theme): CSSObject => ({
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
overflowX: "hidden",
width: `calc(${theme.spacing(7)} + 1px)`,
[theme.breakpoints.up("sm")]: {
width: `calc(${theme.spacing(8)} + 1px)`
}
});
const DrawerHeader = styled("div")(({ theme }) => ({
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar
}));
const Drawer = styled(MuiDrawer, { shouldForwardProp: prop => prop !== "open" })(({ theme, open }) => ({
width: drawerWidth,
flexShrink: 0,
whiteSpace: "nowrap",
boxSizing: "border-box",
...(open && {
...openedMixin(theme),
"& .MuiDrawer-paper": openedMixin(theme)
}),
...(!open && {
...closedMixin(theme),
"& .MuiDrawer-paper": closedMixin(theme)
})
}));
type SideBarProps = {
open: boolean;
onDrawerOpen: () => void;
onDrawerClose: () => void;
};
const SideBar: React.FC<SideBarProps> = ({ open, onDrawerOpen, onDrawerClose }) => {
const navigate = useNavigate();
const { t } = useTranslation();
menu.sort((a, b) => (a.order || 0) - (b.order || 0));
return (
<Drawer variant="permanent" open={open}>
<DrawerHeader>
<IconButton onClick={open ? onDrawerClose : onDrawerOpen}>
{open ? <ChevronLeftIcon /> : <ChevronRightIcon />}
</IconButton>
</DrawerHeader>
<Divider />
{menu.map((section, index) => {
const isLast = index === menu.length - 1;
return (
<React.Fragment key={`section-${section.order}`}>
<List>
{section.items
.sort((i1, i2) => i1.order - i2.order)
.map(item => (
<MenuItem
key={`${item.code}-${index}`}
open={open}
icon={item.icon}
label={t(item.name)}
onClick={item.subMenus ? undefined : () => navigate(item.route)}
subMenus={item.subMenus?.map(si => ({
open,
icon: si.icon,
label: t(si.name),
onClick: () => navigate(si.route)
}))}
/>
))}
</List>
{!isLast && <Divider />}
</React.Fragment>
);
})}
<Divider />
<List>
<MenuItem
open={open}
icon={<SettingsOutlined />}
label={"Settings"}
subMenus={[
{
open,
icon: <DashboardOutlined />,
label: "Dashboards",
onClick: () => navigate("/configurator/dashboards")
},
{
open,
icon: <PollOutlined />,
label: "Charts",
onClick: () => navigate("/configurator/charts")
},
{
open,
icon: <TuneOutlined />,
label: "Filters",
onClick: () => navigate("/configurator/filters")
}
]}
/>
<MenuItem open={open} icon={<HelpOutlineOutlined />} label={"Help"} onClick={() => navigate("/help")} />
</List>
</Drawer>
);
};
export default SideBar;

View File

@ -1,143 +0,0 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import clsx from "clsx";
import { useTheme } from "@mui/material/styles";
import { Drawer, List, Divider, IconButton, ListItemIcon, ListItemText } from "@mui/material";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import ListItem from "@mui/material/ListItem";
import BuildIcon from "@mui/icons-material/Build";
import DnsIcon from "@mui/icons-material/Dns";
import DeviceHubIcon from "@mui/icons-material/DeviceHub";
import SettingsIcon from "@mui/icons-material/Settings";
import DashboardIcon from "@mui/icons-material/Dashboard";
import FeaturedPlayListIcon from "@mui/icons-material/FeaturedPlayList";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { getStyles } from "./styles";
const menu = [
{
order: 0,
items: [
{
code: "dashboard",
name: "Menu.Dashboard",
route: "/dashboard",
icon: <DashboardIcon />,
order: 0
},
{
code: "machines",
name: "Menu.Machines",
route: "/machines",
icon: <DnsIcon />,
order: 1
},
{
code: "system",
name: "Menu.System",
route: "/system",
icon: <DeviceHubIcon />,
order: 2
}
]
},
{
order: 1,
items: [
{
code: "administration",
name: "Menu.Administration",
route: "/administration",
icon: <BuildIcon />,
order: 0
},
{
code: "settings",
name: "Menu.Settings",
route: "/settings",
icon: <SettingsIcon />,
order: 1
}
]
},
{
order: 2,
items: [
{
code: "about",
name: "Menu.About",
route: "/about",
icon: <FeaturedPlayListIcon />,
order: 0
}
]
}
];
const sortedMenu = menu.sort((i1, i2) => i1 - i2);
const Sidebar = ({ open, handleDrawerClose }) => {
const [selected, setSelected] = useState(null);
const theme = useTheme();
const styles = getStyles(theme);
const navigate = useNavigate();
const { t } = useTranslation();
const handleClick = route => () => {
setSelected(route);
navigate(route);
};
const isSelected = key => selected === key;
return (
<Drawer
variant="permanent"
sx={clsx(styles.drawer, {
[styles.drawerOpen]: open,
[styles.drawerClose]: !open
})}
classes={{
paper: clsx({
[styles.drawerOpen]: open,
[styles.drawerClose]: !open
})
}}
>
<div style={styles.toolbar}>
<IconButton onClick={handleDrawerClose}>
{theme.direction === "rtl" ? <ChevronRightIcon /> : <ChevronLeftIcon />}
</IconButton>
</div>
<Divider />
{sortedMenu.map((menu, index) => {
const isLast = index === sortedMenu.length - 1;
return (
<React.Fragment key={`menu-${menu.order}`}>
<List>
{menu.items
.sort((i1, i2) => i1 - i2)
.map(item => (
<ListItem button key={item.code} onClick={handleClick(item.route)} selected={isSelected(item.route)}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={t(item.name)} />
</ListItem>
))}
</List>
{!isLast && <Divider />}
</React.Fragment>
);
})}
</Drawer>
);
};
Sidebar.propTypes = {
open: PropTypes.bool.isRequired,
handleDrawerClose: PropTypes.func.isRequired
};
export default Sidebar;

View File

@ -1 +1,3 @@
export * from "./menu";
export const drawerWidth = 240; export const drawerWidth = 240;

View File

@ -0,0 +1,98 @@
import React from "react";
import { Dashboard, Dns, DeviceHub, Build, Settings, Info } from "../../icons";
type MenuItem = {
code: string;
name: string;
route: string;
icon: JSX.Element;
order: number;
subMenus?: MenuItem[];
};
type MenuSection = {
order: number;
items: MenuItem[];
};
type Menu = MenuSection[];
const menu: Menu = [
{
order: 0,
items: [
{
code: "dashboard",
name: "Menu.Dashboard",
route: "/dashboard",
icon: <Dashboard />,
order: 0
},
{
code: "machines",
name: "Menu.Machines",
route: "/machines",
icon: <Dns />,
order: 1
},
{
code: "system",
name: "Menu.System",
route: "/system",
icon: <DeviceHub />,
order: 2
}
]
},
{
order: 1,
items: [
{
code: "administration",
name: "Menu.Administration",
route: "/administration",
icon: <Build />,
order: 0,
subMenus: [
{
code: "machines",
name: "Menu.Machines",
route: "/administration/machines",
icon: <Build />,
order: 0
},
{
code: "agents",
name: "Menu.Agents",
route: "/administration/agents",
icon: <Build />,
order: 1
}
]
},
{
code: "settings",
name: "Menu.Settings",
route: "/settings",
icon: <Settings />,
order: 1
}
]
},
{
order: 2,
items: [
{
code: "about",
name: "Menu.About",
route: "/about",
icon: <Info />,
order: 0
}
]
}
];
export type { MenuItem, MenuSection, Menu };
export { menu };
export default menu;

View File

@ -1,9 +1,6 @@
const drawerWidth = 240; const drawerWidth = 240;
const getStyles = theme => ({ const getStyles = theme => ({
root: {
display: "flex"
},
appBar: { appBar: {
zIndex: theme.zIndex.drawer + 1, zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(["width", "margin"], { transition: theme.transitions.create(["width", "margin"], {
@ -30,36 +27,6 @@ const getStyles = theme => ({
flexShrink: 0, flexShrink: 0,
whiteSpace: "nowrap" whiteSpace: "nowrap"
}, },
drawerOpen: {
width: drawerWidth,
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen
})
},
drawerClose: {
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
overflowX: "hidden",
width: theme.spacing(7) + 1,
[theme.breakpoints.up("sm")]: {
width: theme.spacing(9) + 1
}
},
toolbar: {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar
},
content: {
flexGrow: 1,
padding: theme.spacing(2)
},
menuItemIcon: { menuItemIcon: {
minWidth: "26px" minWidth: "26px"
} }