mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 08:19:52 +03:00
Merge remote-tracking branch 'origin/v3.4' into dev
This commit is contained in:
36
interface/src/components/layout/Layout.tsx
Normal file
36
interface/src/components/layout/Layout.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { FC, useState, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { Box, Toolbar } from '@mui/material';
|
||||
|
||||
import { PROJECT_NAME } from '../../api/env';
|
||||
import LayoutDrawer from './LayoutDrawer';
|
||||
import LayoutAppBar from './LayoutAppBar';
|
||||
import { LayoutContext } from './context';
|
||||
|
||||
export const DRAWER_WIDTH = 240;
|
||||
|
||||
const Layout: FC = ({ children }) => {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [title, setTitle] = useState(PROJECT_NAME);
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
setMobileOpen(!mobileOpen);
|
||||
};
|
||||
|
||||
useEffect(() => setMobileOpen(false), [pathname]);
|
||||
|
||||
return (
|
||||
<LayoutContext.Provider value={{ title, setTitle }}>
|
||||
<LayoutAppBar title={title} onToggleDrawer={handleDrawerToggle} />
|
||||
<LayoutDrawer mobileOpen={mobileOpen} onClose={handleDrawerToggle} />
|
||||
<Box component="main" sx={{ marginLeft: { md: `${DRAWER_WIDTH}px` } }}>
|
||||
<Toolbar />
|
||||
{children}
|
||||
</Box>
|
||||
</LayoutContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
51
interface/src/components/layout/LayoutAppBar.tsx
Normal file
51
interface/src/components/layout/LayoutAppBar.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { FC, useContext } from 'react';
|
||||
|
||||
import { AppBar, Box, IconButton, Toolbar, Typography } from '@mui/material';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
|
||||
import LayoutAuthMenu from './LayoutAuthMenu';
|
||||
|
||||
import { FeaturesContext } from '../../contexts/features';
|
||||
|
||||
export const DRAWER_WIDTH = 240;
|
||||
|
||||
interface LayoutAppBarProps {
|
||||
title: string;
|
||||
onToggleDrawer: () => void;
|
||||
}
|
||||
|
||||
const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => {
|
||||
const { features } = useContext(FeaturesContext);
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
position="fixed"
|
||||
sx={{
|
||||
width: { md: `calc(100% - ${DRAWER_WIDTH}px)` },
|
||||
ml: { md: `${DRAWER_WIDTH}px` },
|
||||
boxShadow: 'none',
|
||||
backgroundColor: '#2e586a'
|
||||
// color: "#2196f3",
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
edge="start"
|
||||
onClick={onToggleDrawer}
|
||||
sx={{ mr: 2, display: { md: 'none' } }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" noWrap component="div">
|
||||
{title}
|
||||
</Typography>
|
||||
<Box flexGrow={1} />
|
||||
{features.security && <LayoutAuthMenu />}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutAppBar;
|
||||
73
interface/src/components/layout/LayoutAuthMenu.tsx
Normal file
73
interface/src/components/layout/LayoutAuthMenu.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { FC, useState, useContext } from 'react';
|
||||
|
||||
import { Box, Button, Divider, IconButton, Popover, Typography, Avatar, styled, TypographyProps } from '@mui/material';
|
||||
|
||||
import PersonIcon from '@mui/icons-material/Person';
|
||||
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
|
||||
|
||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||
|
||||
const ItemTypography = styled(Typography)<TypographyProps>({
|
||||
maxWidth: '250px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
});
|
||||
|
||||
const LayoutAuthMenu: FC = () => {
|
||||
const { me, signOut } = useContext(AuthenticatedContext);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const id = anchorEl ? 'app-menu-popover' : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton id="open-auth-menu" sx={{ padding: 0 }} aria-describedby={id} color="inherit" onClick={handleClick}>
|
||||
<AccountCircleIcon />
|
||||
</IconButton>
|
||||
<Popover
|
||||
id="app-menu-popover"
|
||||
sx={{ mt: 1 }}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center'
|
||||
}}
|
||||
>
|
||||
<Box display="flex" flexDirection="row" alignItems="center" p={2}>
|
||||
<Avatar sx={{ width: 80, height: 80 }}>
|
||||
<PersonIcon fontSize="large" />
|
||||
</Avatar>
|
||||
<Box pl={2}>
|
||||
<ItemTypography variant="h6">{me.username}</ItemTypography>
|
||||
<ItemTypography variant="body1">{me.admin ? 'Admin User' : 'Guest User'}</ItemTypography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider />
|
||||
<Box p={1.5}>
|
||||
<Button variant="outlined" fullWidth color="primary" onClick={() => signOut(true)}>
|
||||
Sign Out
|
||||
</Button>
|
||||
</Box>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutAuthMenu;
|
||||
73
interface/src/components/layout/LayoutDrawer.tsx
Normal file
73
interface/src/components/layout/LayoutDrawer.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
|
||||
|
||||
import { PROJECT_NAME } from '../../api/env';
|
||||
|
||||
import LayoutMenu from './LayoutMenu';
|
||||
import { DRAWER_WIDTH } from './Layout';
|
||||
|
||||
const LayoutDrawerLogo = styled('img')(({ theme }) => ({
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
height: 24,
|
||||
marginRight: theme.spacing(2)
|
||||
},
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
height: 36,
|
||||
marginRight: theme.spacing(2)
|
||||
}
|
||||
}));
|
||||
|
||||
interface LayoutDrawerProps {
|
||||
mobileOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const LayoutDrawer: FC<LayoutDrawerProps> = ({ mobileOpen, onClose }) => {
|
||||
const drawer = (
|
||||
<>
|
||||
<Toolbar disableGutters>
|
||||
<Box display="flex" alignItems="center" px={2}>
|
||||
<LayoutDrawerLogo src="/app/icon.png" alt={PROJECT_NAME} />
|
||||
<Typography variant="h6" color="textPrimary">
|
||||
{PROJECT_NAME}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider absolute />
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
<LayoutMenu />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box component="nav" sx={{ width: { md: DRAWER_WIDTH }, flexShrink: { md: 0 } }}>
|
||||
<Drawer
|
||||
variant="temporary"
|
||||
open={mobileOpen}
|
||||
onClose={onClose}
|
||||
ModalProps={{
|
||||
keepMounted: true // Better open performance on mobile.
|
||||
}}
|
||||
sx={{
|
||||
display: { xs: 'block', md: 'none' },
|
||||
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: DRAWER_WIDTH }
|
||||
}}
|
||||
>
|
||||
{drawer}
|
||||
</Drawer>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
display: { xs: 'none', md: 'block' },
|
||||
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: DRAWER_WIDTH }
|
||||
}}
|
||||
open
|
||||
>
|
||||
{drawer}
|
||||
</Drawer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutDrawer;
|
||||
42
interface/src/components/layout/LayoutMenu.tsx
Normal file
42
interface/src/components/layout/LayoutMenu.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { FC, useContext } from 'react';
|
||||
|
||||
import { Divider, List } from '@mui/material';
|
||||
|
||||
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
|
||||
|
||||
import { FeaturesContext } from '../../contexts/features';
|
||||
import ProjectMenu from '../../project/ProjectMenu';
|
||||
|
||||
import LayoutMenuItem from './LayoutMenuItem';
|
||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||
|
||||
const LayoutMenu: FC = () => {
|
||||
const { features } = useContext(FeaturesContext);
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
{features.project && (
|
||||
<List disablePadding component="nav">
|
||||
<ProjectMenu />
|
||||
<Divider />
|
||||
</List>
|
||||
)}
|
||||
<List disablePadding component="nav">
|
||||
<LayoutMenuItem icon={SettingsEthernetIcon} label="Network Connection" to="/network" />
|
||||
<LayoutMenuItem icon={SettingsInputAntennaIcon} label="Access Point" to="/ap" />
|
||||
{features.ntp && <LayoutMenuItem icon={AccessTimeIcon} label="Network Time" to="/ntp" />}
|
||||
{features.mqtt && <LayoutMenuItem icon={DeviceHubIcon} label="MQTT" to="/mqtt" />}
|
||||
<LayoutMenuItem icon={LockIcon} label="Security" to="/security" disabled={!authenticatedContext.me.admin} />
|
||||
<LayoutMenuItem icon={SettingsIcon} label="System" to="/system" />
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutMenu;
|
||||
32
interface/src/components/layout/LayoutMenuItem.tsx
Normal file
32
interface/src/components/layout/LayoutMenuItem.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { FC } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
import { ListItem, ListItemButton, ListItemIcon, ListItemText, SvgIconProps } from '@mui/material';
|
||||
|
||||
import { grey } from '@mui/material/colors';
|
||||
|
||||
import { routeMatches } from '../../utils';
|
||||
|
||||
interface LayoutMenuItemProps {
|
||||
icon: React.ComponentType<SvgIconProps>;
|
||||
label: string;
|
||||
to: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const LayoutMenuItem: FC<LayoutMenuItemProps> = ({ icon: Icon, label, to, disabled }) => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<ListItem disablePadding selected={routeMatches(to, pathname)}>
|
||||
<ListItemButton component={Link} to={to} disabled={disabled}>
|
||||
<ListItemIcon sx={{ color: grey[500] }}>
|
||||
<Icon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{label}</ListItemText>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutMenuItem;
|
||||
25
interface/src/components/layout/context.ts
Normal file
25
interface/src/components/layout/context.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useRef, useEffect, createContext, useContext } from 'react';
|
||||
|
||||
export interface LayoutContextValue {
|
||||
title: string;
|
||||
setTitle: (title: string) => void;
|
||||
}
|
||||
|
||||
const LayoutContextDefaultValue = {} as LayoutContextValue;
|
||||
export const LayoutContext = createContext(LayoutContextDefaultValue);
|
||||
|
||||
export const useLayoutTitle = (myTitle: string) => {
|
||||
const { title, setTitle } = useContext(LayoutContext);
|
||||
const previousTitle = useRef(title);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(myTitle);
|
||||
}, [setTitle, myTitle]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
setTitle(previousTitle.current);
|
||||
},
|
||||
[setTitle]
|
||||
);
|
||||
};
|
||||
2
interface/src/components/layout/index.ts
Normal file
2
interface/src/components/layout/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './context';
|
||||
export { default as Layout } from './Layout';
|
||||
Reference in New Issue
Block a user