Migrate Sidebar to typescript and MUI 5
parent
14f16677c9
commit
c981660442
|
@ -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;
|
|
@ -0,0 +1,4 @@
|
|||
import DynamicIcon from "./DynamicIcon";
|
||||
|
||||
export * from "./list";
|
||||
export { DynamicIcon };
|
|
@ -0,0 +1 @@
|
|||
export { Home, Dashboard, Dns, DeviceHub, Build, Settings, FeaturedPlayList, Info } from "@mui/icons-material";
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -1 +1,3 @@
|
|||
export * from "./menu";
|
||||
|
||||
export const drawerWidth = 240;
|
|
@ -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;
|
|
@ -1,9 +1,6 @@
|
|||
const drawerWidth = 240;
|
||||
|
||||
const getStyles = theme => ({
|
||||
root: {
|
||||
display: "flex"
|
||||
},
|
||||
appBar: {
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
transition: theme.transitions.create(["width", "margin"], {
|
||||
|
@ -30,36 +27,6 @@ const getStyles = theme => ({
|
|||
flexShrink: 0,
|
||||
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: {
|
||||
minWidth: "26px"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue