UX left sidebar menu (#1356)

This commit is contained in:
Prakash Senthil Vel
2022-01-04 02:26:32 +00:00
committed by GitHub
parent dd781dc6da
commit 9b12f5a41e
6 changed files with 385 additions and 283 deletions

View File

@@ -14,34 +14,24 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Suspense } from "react";
import React from "react";
import { connect } from "react-redux";
import { NavLink } from "react-router-dom";
import { Divider, Drawer, IconButton, Tooltip } from "@mui/material";
import { ChevronLeft } from "@mui/icons-material";
import { Drawer } from "@mui/material";
import withStyles from "@mui/styles/withStyles";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import ListItem from "@mui/material/ListItem";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import List from "@mui/material/List";
import clsx from "clsx";
import { AppState } from "../../../store";
import { setMenuOpen, userLoggedIn } from "../../../actions";
import { menuGroups } from "./utils";
import { IMenuItem } from "./types";
import { ErrorResponseHandler } from "../../../common/types";
import { clearSession } from "../../../common/utils";
import OperatorLogo from "../../../icons/OperatorLogo";
import ConsoleLogo from "../../../icons/ConsoleLogo";
import history from "../../../history";
import api from "../../../common/api";
import MenuIcon from "@mui/icons-material/Menu";
import LogoutIcon from "../../../icons/LogoutIcon";
import { resetSession } from "../actions";
import {
AccountIcon,
@@ -58,7 +48,6 @@ import {
TiersIcon,
ToolsIcon,
UsersIcon,
VersionIcon,
} from "../../../icons";
import {
CONSOLE_UI_RESOURCE,
@@ -68,98 +57,13 @@ import {
IAM_SCOPES,
} from "../../../common/SecureComponent/permissions";
import { hasPermission } from "../../../common/SecureComponent/SecureComponent";
import MenuList from "./MenuList";
import MenuToggle from "./MenuToggle";
const drawerWidth = 245;
const styles = (theme: Theme) =>
createStyles({
logo: {
paddingTop: 25,
height: 80,
marginBottom: 30,
paddingLeft: 45,
borderBottom: "#1C3B64 1px solid",
transition: theme.transitions.create("paddingLeft", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"& img": {
width: 120,
},
"& .MuiIconButton-root": {
color: "#ffffff",
float: "right",
},
},
logoClosed: {
paddingTop: 25,
marginBottom: 0,
paddingLeft: 34,
transition: theme.transitions.create("paddingLeft", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"& .MuiIconButton-root": {
color: "#ffffff",
},
},
menuList: {
"& .active": {
color: "#fff",
backgroundBlendMode: "multiply",
background:
"transparent linear-gradient(90deg, rgba(0, 0, 0, 0.14) 0%, #00000000 100%) 0% 0% no-repeat padding-box",
"& .min-icon": {
color: "white",
},
"& .MuiTypography-root": {
color: "#fff",
fontWeight: "bold",
},
},
"& .min-icon": {
width: 16,
height: 16,
fill: "rgba(255, 255, 255, 0.8)",
},
"& .MuiListItemIcon-root": {
minWidth: 36,
},
"& .MuiTypography-root": {
fontSize: 15,
color: "rgba(255, 255, 255, 0.8)",
},
"& .MuiListItem-gutters": {
paddingRight: 0,
fontWeight: 300,
},
"& .MuiListItem-root": {
padding: "2px 0 2px 16px",
marginLeft: 36,
height: 50,
width: "calc(100% - 30px)",
},
},
menuDivider: {
backgroundColor: "#1C3B64",
marginRight: 36,
marginLeft: 36,
marginBottom: 0,
height: 1,
},
extraMargin: {
"&.MuiListItem-gutters": {
marginLeft: 5,
},
},
subTitleMenu: {
fontWeight: 700,
marginLeft: 10,
"&.MuiTypography-root": {
fontSize: 13,
color: "#fff",
},
},
drawer: {
width: drawerWidth,
flexShrink: 0,
@@ -184,63 +88,27 @@ const styles = (theme: Theme) =>
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: "hidden",
width: 115,
[theme.breakpoints.up("sm")]: {
width: 115,
},
"& .logo": {
background: "red",
},
"& .MuiListItem-root": {
padding: "2px 0 2px 16px",
marginLeft: 36,
height: 50,
width: "48px",
transition: theme.transitions.create("marginLeft", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"& .MuiListItemText-root": {
display: "none",
},
},
},
logoIcon: {
float: "left",
"& svg": {
fill: "white",
width: 120,
},
},
logoIconClosed: {
color: "white",
"& .min-icon": {
marginLeft: 11,
width: 24,
fill: "rgba(255, 255, 255, 0.8)",
[theme.breakpoints.up("xs")]: {
width: 75,
},
},
});
interface IMenuProps {
classes: any;
classes?: any;
userLoggedIn: typeof userLoggedIn;
operatorMode: boolean;
distributedSetup: boolean;
operatorMode?: boolean;
sidebarOpen: boolean;
setMenuOpen: typeof setMenuOpen;
resetSession: typeof resetSession;
features: string[] | null;
features?: string[] | null;
}
const Menu = ({
userLoggedIn,
classes,
operatorMode,
distributedSetup,
operatorMode = false,
sidebarOpen,
setMenuOpen,
resetSession,
features,
}: IMenuProps) => {
const logout = () => {
@@ -441,138 +309,44 @@ const Menu = ({
);
return (
<React.Fragment>
<Drawer
variant="permanent"
className={clsx(classes.drawer, {
<Drawer
variant="permanent"
className={clsx(classes.drawer, {
[classes.drawerOpen]: sidebarOpen,
[classes.drawerClose]: !sidebarOpen,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: sidebarOpen,
[classes.drawerClose]: !sidebarOpen,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: sidebarOpen,
[classes.drawerClose]: !sidebarOpen,
}),
}),
}}
>
<MenuToggle
onToggle={(nextState) => {
setMenuOpen(nextState);
}}
>
<div
className={clsx({
[classes.logo]: sidebarOpen,
[classes.logoClosed]: !sidebarOpen,
})}
>
{sidebarOpen && (
<span className={classes.logoIcon}>
{operatorMode ? <OperatorLogo /> : <ConsoleLogo />}
</span>
)}
{!sidebarOpen && (
<div className={classes.logoIconClosed}>
<Suspense fallback={<div>Loading...</div>}>
<VersionIcon />
</Suspense>
</div>
)}
<IconButton
onClick={() => {
if (sidebarOpen) {
setMenuOpen(false);
} else {
setMenuOpen(true);
}
}}
size="large"
>
{sidebarOpen ? <ChevronLeft /> : <MenuIcon />}
</IconButton>
</div>
<List className={classes.menuList}>
{menuGroups.map((groupMember, index) => {
const filterByGroup = (allowedItems || []).filter(
(item: any) => item.group === groupMember.group
);
isOperatorMode={operatorMode}
isOpen={sidebarOpen}
/>
const countableElements = filterByGroup.filter(
(menuItem: any) => menuItem.type !== "title"
);
if (countableElements.length === 0) {
return null;
}
return (
<React.Fragment key={`menuElem-${index.toString()}`}>
{index !== 0 && <Divider className={classes.menuDivider} />}
{filterByGroup.map((page: IMenuItem) => {
switch (page.type) {
case "item": {
return (
<ListItem
key={page.to}
button
onClick={page.onClick}
component={page.component}
to={page.to}
className={
page.extraMargin ? classes.extraMargin : null
}
>
{page.icon && (
<Tooltip title={page.name}>
<div>
<ListItemIcon>
<Suspense fallback={<div>Loading...</div>}>
<page.icon />
</Suspense>
</ListItemIcon>
</div>
</Tooltip>
)}
{page.name && <ListItemText primary={page.name} />}
</ListItem>
);
}
case "title": {
return (
<ListItem
key={page.name}
component={page.component}
className={classes.subTitleMenu}
>
{page.name}
</ListItem>
);
}
default:
return null;
}
})}
</React.Fragment>
);
})}
<Divider className={classes.menuDivider} />
<ListItem button onClick={logout}>
<ListItemIcon>
<LogoutIcon />
</ListItemIcon>
<ListItemText primary="Logout" />
</ListItem>
</List>
</Drawer>
</React.Fragment>
<MenuList
menuItems={allowedItems}
isOpen={sidebarOpen}
onLogoutClick={logout}
/>
</Drawer>
);
};
const mapState = (state: AppState) => ({
sidebarOpen: state.system.sidebarOpen,
operatorMode: state.system.operatorMode,
distributedSetup: state.system.distributedSetup,
features: state.console.session.features,
});
const connector = connect(mapState, {
userLoggedIn,
setMenuOpen,
resetSession,
});
export default connector(withStyles(styles)(Menu));

View File

@@ -0,0 +1,62 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Suspense } from "react";
import { ListItem, ListItemIcon, ListItemText, Tooltip } from "@mui/material";
import {
menuItemContainerStyles,
menuItemIconStyles,
menuItemTextStyles,
} from "./MenuStyleUtils";
const MenuItem = ({
page,
stateClsName = "",
}: {
page: any;
stateClsName?: string;
}) => {
return (
<ListItem
key={page.to}
button
onClick={page.onClick}
component={page.component}
to={page.to}
disableRipple
sx={{ ...menuItemContainerStyles }}
>
{page.icon && (
<Tooltip title={`${page.name}`} placement="right">
<ListItemIcon sx={{ ...menuItemIconStyles }}>
<Suspense fallback={<div>...</div>}>
<page.icon />
</Suspense>
</ListItemIcon>
</Tooltip>
)}
{page.name && (
<ListItemText
className={stateClsName}
sx={{ ...menuItemTextStyles }}
primary={page.name}
/>
)}
</ListItem>
);
};
export default MenuItem;

View File

@@ -0,0 +1,114 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import MenuItem from "./MenuItem";
import { Box } from "@mui/material";
import ListItem from "@mui/material/ListItem";
import ListItemIcon from "@mui/material/ListItemIcon";
import LogoutIcon from "../../../icons/LogoutIcon";
import ListItemText from "@mui/material/ListItemText";
import List from "@mui/material/List";
import { IMenuItem } from "./types";
import {
menuItemContainerStyles,
menuItemIconStyles,
menuItemTextStyles,
} from "./MenuStyleUtils";
const MenuList = ({
menuItems,
onLogoutClick,
isOpen,
}: {
menuItems: IMenuItem[];
isOpen: boolean;
onLogoutClick: () => void;
}) => {
const stateClsName = isOpen ? "wide" : "mini";
return (
<Box
className={`${stateClsName}`}
sx={{
display: "flex",
flexFlow: "column",
justifyContent: "space-between",
height: "100%",
flex: 1,
marginLeft: "26px",
marginTop: "35px",
paddingRight: "8px",
"&.mini": {
marginLeft: "18px",
marginTop: "10px",
xs: {
marginTop: "30px",
},
},
}}
>
<List
sx={{
flex: 1,
paddingTop: 0,
}}
>
<React.Fragment>
{(menuItems || []).map((page: IMenuItem) => {
switch (page.type) {
case "item": {
return (
<MenuItem
stateClsName={stateClsName}
page={page}
key={page.to}
/>
);
}
default:
return null;
}
})}
</React.Fragment>
</List>
<List
sx={{
paddingTop: 0,
}}
>
<ListItem
button
onClick={onLogoutClick}
disableRipple
sx={{ ...menuItemContainerStyles }}
>
<ListItemIcon sx={{ ...menuItemIconStyles }}>
<LogoutIcon />
</ListItemIcon>
<ListItemText
primary="Logout"
sx={{ ...menuItemTextStyles }}
className={stateClsName}
/>
</ListItem>
</List>
</Box>
);
};
export default MenuList;

View File

@@ -0,0 +1,61 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export const menuItemContainerStyles: any = {
paddingLeft: 0,
paddingBottom: "18px",
"&.active div:nth-of-type(1)": {
border: "2px solid #ffffff",
},
"&:hover": {
background: "none",
"& div:nth-of-type(1)": {
background: "#073052",
"& svg": {
fill: "#ffffff",
},
},
},
};
export const menuItemIconStyles: any = {
width: 37,
minWidth: 37,
height: 37,
background: "#00274D",
border: "2px solid #002148",
display: "flex",
alignItems: "center",
borderRadius: "50%",
justifyContent: "center",
"& svg": {
width: 16,
height: 16,
fill: "#8399AB",
},
};
export const menuItemTextStyles: any = {
color: "#BCC7D1",
fontSize: "14px",
marginLeft: "11px",
"& span": {
fontSize: "14px",
},
"&.mini": {
display: "none",
},
};

View File

@@ -0,0 +1,115 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Suspense } from "react";
import OperatorLogo from "../../../icons/OperatorLogo";
import ConsoleLogo from "../../../icons/ConsoleLogo";
import { VersionIcon } from "../../../icons";
import { Box, IconButton } from "@mui/material";
import { ChevronLeft } from "@mui/icons-material";
import MenuIcon from "@mui/icons-material/Menu";
type MenuToggleProps = {
isOpen: boolean;
isOperatorMode: boolean;
onToggle: (nextState: boolean) => void;
};
const MenuToggle = ({ isOpen, isOperatorMode, onToggle }: MenuToggleProps) => {
const stateClsName = isOpen ? "wide" : "mini";
return (
<Box
className={`${stateClsName}`}
sx={{
marginLeft: "26px",
marginTop: "28px",
marginRight: "8px",
display: "flex",
minHeight: "36px",
"&.mini": {
flexFlow: "column",
alignItems: "center",
margin: "auto",
marginTop: "28px",
},
"& .logo": {
background: "transparent",
"&.wide": {
flex: "1",
"& svg": {
fill: "white",
width: 120,
},
},
"&.mini": {
marginBottom: "5px",
flex: "1",
color: "#ffffff",
"& svg": {
width: 24,
fill: "rgba(255, 255, 255, 0.8)",
},
},
},
}}
>
{isOpen ? (
<div className={`logo ${stateClsName}`}>
{isOperatorMode ? <OperatorLogo /> : <ConsoleLogo />}
</div>
) : (
<div className={`logo ${stateClsName}`}>
<Suspense fallback={<div>...</div>}>
<VersionIcon />
</Suspense>
</div>
)}
<IconButton
className={`${stateClsName}`}
sx={{
"&.mini": {
marginBottom: "10px",
"&:hover": {
background: "#081C42",
},
},
"&:hover": {
borderRadius: "50%",
background: "#073052",
},
"& svg": {
fill: "#ffffff",
},
}}
onClick={() => {
if (isOpen) {
onToggle(false);
} else {
onToggle(true);
}
}}
size="small"
>
{isOpen ? <ChevronLeft /> : <MenuIcon />}
</IconButton>
</Box>
);
};
export default MenuToggle;

View File

@@ -1,24 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export const menuGroups = [
{ label: "", group: "common", collapsible: false },
{ label: "User", group: "User", collapsible: true },
{ label: "Admin", group: "Admin", collapsible: true },
{ label: "Tools", group: "Tools", collapsible: true },
{ label: "Operator", group: "Operator", collapsible: false },
{ label: "", group: "License", collapsible: false },
];