From c981660442b12efd9e5d4be83d3cccfa5ba7fd94 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 30 Mar 2024 12:55:22 +0200 Subject: [PATCH] Migrate Sidebar to typescript and MUI 5 --- frontend/src/components/icons/DynamicIcon.tsx | 19 +++ frontend/src/components/icons/index.ts | 4 + frontend/src/components/icons/list.ts | 1 + frontend/src/components/layout/AppLayout.js | 33 ---- frontend/src/components/layout/AppLayout.tsx | 54 +++++++ frontend/src/components/layout/MenuItem.tsx | 84 ++++++++++ frontend/src/components/layout/SideBar.tsx | 149 ++++++++++++++++++ frontend/src/components/layout/Sidebar.js | 143 ----------------- .../{constants.ts => constants/index.ts} | 2 + .../src/components/layout/constants/menu.tsx | 98 ++++++++++++ frontend/src/components/layout/styles.js | 33 ---- 11 files changed, 411 insertions(+), 209 deletions(-) create mode 100644 frontend/src/components/icons/DynamicIcon.tsx create mode 100644 frontend/src/components/icons/index.ts create mode 100644 frontend/src/components/icons/list.ts delete mode 100644 frontend/src/components/layout/AppLayout.js create mode 100644 frontend/src/components/layout/AppLayout.tsx create mode 100644 frontend/src/components/layout/MenuItem.tsx create mode 100644 frontend/src/components/layout/SideBar.tsx delete mode 100644 frontend/src/components/layout/Sidebar.js rename frontend/src/components/layout/{constants.ts => constants/index.ts} (56%) create mode 100644 frontend/src/components/layout/constants/menu.tsx diff --git a/frontend/src/components/icons/DynamicIcon.tsx b/frontend/src/components/icons/DynamicIcon.tsx new file mode 100644 index 0000000..8b871fc --- /dev/null +++ b/frontend/src/components/icons/DynamicIcon.tsx @@ -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 = ({ code, fallback, ...res }) => { + if (code && code in MuiIcons) { + const Icon = MuiIcons[code as keyof typeof MuiIcons] as typeof MuiIcon; + return ; + } else { + return <>{fallback ?? ""}; + } +}; + +export default DynamicIcon; diff --git a/frontend/src/components/icons/index.ts b/frontend/src/components/icons/index.ts new file mode 100644 index 0000000..af1e0da --- /dev/null +++ b/frontend/src/components/icons/index.ts @@ -0,0 +1,4 @@ +import DynamicIcon from "./DynamicIcon"; + +export * from "./list"; +export { DynamicIcon }; diff --git a/frontend/src/components/icons/list.ts b/frontend/src/components/icons/list.ts new file mode 100644 index 0000000..6b8ba13 --- /dev/null +++ b/frontend/src/components/icons/list.ts @@ -0,0 +1 @@ +export { Home, Dashboard, Dns, DeviceHub, Build, Settings, FeaturedPlayList, Info } from "@mui/icons-material"; diff --git a/frontend/src/components/layout/AppLayout.js b/frontend/src/components/layout/AppLayout.js deleted file mode 100644 index 81ba03f..0000000 --- a/frontend/src/components/layout/AppLayout.js +++ /dev/null @@ -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 ( -
- - -
-
- -
-
- ); -}; - -export default AppLayout; diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..de32f5b --- /dev/null +++ b/frontend/src/components/layout/AppLayout.tsx @@ -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 ( + + + + + + + + + ); +}; + +export default AppLayout; diff --git a/frontend/src/components/layout/MenuItem.tsx b/frontend/src/components/layout/MenuItem.tsx new file mode 100644 index 0000000..f3c5a2f --- /dev/null +++ b/frontend/src/components/layout/MenuItem.tsx @@ -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; +}; + +const MenuItem: React.FC = ({ open, icon, label, onClick, subMenus, sx }) => { + const [openSubMenu, setOpenSubMenu] = React.useState(false); + + const handleSubMenuToggle = () => { + setOpenSubMenu(!openSubMenu); + }; + + return ( + <> + + + + + {open || !subMenus ? ( + icon + ) : ( + : }> + {icon} + + )} + + + {subMenus && open && ( + + {openSubMenu ? : } + + )} + + + + {subMenus && ( + + + {subMenus.map((subItem, index) => ( + + + {index === subMenus.length - 1 && } + + ))} + + + )} + + ); +}; + +export default MenuItem; diff --git a/frontend/src/components/layout/SideBar.tsx b/frontend/src/components/layout/SideBar.tsx new file mode 100644 index 0000000..4dd81b2 --- /dev/null +++ b/frontend/src/components/layout/SideBar.tsx @@ -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 = ({ open, onDrawerOpen, onDrawerClose }) => { + const navigate = useNavigate(); + const { t } = useTranslation(); + + menu.sort((a, b) => (a.order || 0) - (b.order || 0)); + + return ( + + + + {open ? : } + + + + + {menu.map((section, index) => { + const isLast = index === menu.length - 1; + return ( + + + {section.items + .sort((i1, i2) => i1.order - i2.order) + .map(item => ( + navigate(item.route)} + subMenus={item.subMenus?.map(si => ({ + open, + icon: si.icon, + label: t(si.name), + onClick: () => navigate(si.route) + }))} + /> + ))} + + {!isLast && } + + ); + })} + + + + } + label={"Settings"} + subMenus={[ + { + open, + icon: , + label: "Dashboards", + onClick: () => navigate("/configurator/dashboards") + }, + { + open, + icon: , + label: "Charts", + onClick: () => navigate("/configurator/charts") + }, + { + open, + icon: , + label: "Filters", + onClick: () => navigate("/configurator/filters") + } + ]} + /> + } label={"Help"} onClick={() => navigate("/help")} /> + + + ); +}; + +export default SideBar; diff --git a/frontend/src/components/layout/Sidebar.js b/frontend/src/components/layout/Sidebar.js deleted file mode 100644 index e206e66..0000000 --- a/frontend/src/components/layout/Sidebar.js +++ /dev/null @@ -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: , - order: 0 - }, - { - code: "machines", - name: "Menu.Machines", - route: "/machines", - icon: , - order: 1 - }, - { - code: "system", - name: "Menu.System", - route: "/system", - icon: , - order: 2 - } - ] - }, - { - order: 1, - items: [ - { - code: "administration", - name: "Menu.Administration", - route: "/administration", - icon: , - order: 0 - }, - { - code: "settings", - name: "Menu.Settings", - route: "/settings", - icon: , - order: 1 - } - ] - }, - { - order: 2, - items: [ - { - code: "about", - name: "Menu.About", - route: "/about", - icon: , - 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 ( - -
- - {theme.direction === "rtl" ? : } - -
- - - {sortedMenu.map((menu, index) => { - const isLast = index === sortedMenu.length - 1; - return ( - - - {menu.items - .sort((i1, i2) => i1 - i2) - .map(item => ( - - {item.icon} - - - ))} - - {!isLast && } - - ); - })} -
- ); -}; - -Sidebar.propTypes = { - open: PropTypes.bool.isRequired, - handleDrawerClose: PropTypes.func.isRequired -}; - -export default Sidebar; diff --git a/frontend/src/components/layout/constants.ts b/frontend/src/components/layout/constants/index.ts similarity index 56% rename from frontend/src/components/layout/constants.ts rename to frontend/src/components/layout/constants/index.ts index eee33d4..2e4ad4c 100644 --- a/frontend/src/components/layout/constants.ts +++ b/frontend/src/components/layout/constants/index.ts @@ -1 +1,3 @@ +export * from "./menu"; + export const drawerWidth = 240; diff --git a/frontend/src/components/layout/constants/menu.tsx b/frontend/src/components/layout/constants/menu.tsx new file mode 100644 index 0000000..e943526 --- /dev/null +++ b/frontend/src/components/layout/constants/menu.tsx @@ -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: , + order: 0 + }, + { + code: "machines", + name: "Menu.Machines", + route: "/machines", + icon: , + order: 1 + }, + { + code: "system", + name: "Menu.System", + route: "/system", + icon: , + order: 2 + } + ] + }, + { + order: 1, + items: [ + { + code: "administration", + name: "Menu.Administration", + route: "/administration", + icon: , + order: 0, + subMenus: [ + { + code: "machines", + name: "Menu.Machines", + route: "/administration/machines", + icon: , + order: 0 + }, + { + code: "agents", + name: "Menu.Agents", + route: "/administration/agents", + icon: , + order: 1 + } + ] + }, + { + code: "settings", + name: "Menu.Settings", + route: "/settings", + icon: , + order: 1 + } + ] + }, + { + order: 2, + items: [ + { + code: "about", + name: "Menu.About", + route: "/about", + icon: , + order: 0 + } + ] + } +]; + +export type { MenuItem, MenuSection, Menu }; +export { menu }; +export default menu; diff --git a/frontend/src/components/layout/styles.js b/frontend/src/components/layout/styles.js index 0aaa9f6..4c9d070 100644 --- a/frontend/src/components/layout/styles.js +++ b/frontend/src/components/layout/styles.js @@ -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" }