github.com/filecoin-project/bacalhau@v0.3.23-0.20230228154132-45c989550ace/dashboard/frontend/src/pages/Layout.tsx (about) 1 import React, { FC, useState, useContext, useEffect, useMemo, useCallback } from 'react' 2 import bluebird from 'bluebird' 3 import { navigate } from 'hookrouter' 4 import { styled, useTheme } from '@mui/material/styles' 5 import useMediaQuery from '@mui/material/useMediaQuery' 6 import CssBaseline from '@mui/material/CssBaseline' 7 import MuiDrawer from '@mui/material/Drawer' 8 import Grid from '@mui/material/Grid' 9 import Box from '@mui/material/Box' 10 import Button from '@mui/material/Button' 11 import MuiAppBar, { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar' 12 import Toolbar from '@mui/material/Toolbar' 13 import TextField from '@mui/material/TextField' 14 import Typography from '@mui/material/Typography' 15 import Divider from '@mui/material/Divider' 16 import Container from '@mui/material/Container' 17 import Stack from '@mui/material/Stack' 18 import List from '@mui/material/List' 19 import ListItem from '@mui/material/ListItem' 20 import ListItemButton from '@mui/material/ListItemButton' 21 import ListItemIcon from '@mui/material/ListItemIcon' 22 import ListItemText from '@mui/material/ListItemText' 23 import Link from '@mui/material/Link' 24 import IconButton from '@mui/material/IconButton' 25 26 import DvrIcon from '@mui/icons-material/Dvr' 27 import CategoryIcon from '@mui/icons-material/Category' 28 import AccountTreeIcon from '@mui/icons-material/AccountTree' 29 import MenuIcon from '@mui/icons-material/Menu' 30 import LoginIcon from '@mui/icons-material/Login' 31 import LogoutIcon from '@mui/icons-material/Logout' 32 33 import { RouterContext } from '../contexts/router' 34 import { UserContext } from '../contexts/user' 35 import Snackbar from '../components/system/Snackbar' 36 import Window from '../components/widgets/Window' 37 import GlobalLoading from '../components/system/GlobalLoading' 38 import useSnackbar from '../hooks/useSnackbar' 39 import useLoadingErrorHandler from '../hooks/useLoadingErrorHandler' 40 41 function Copyright(props: any) { 42 return ( 43 <Typography variant="body2" color="text.secondary" align="center" {...props}> 44 {'Copyright © '} 45 <Link color="inherit" href="https://www.bacalhau.org/"> 46 Bacalhau 47 </Link>{' '} 48 {new Date().getFullYear()} 49 {'.'} 50 </Typography> 51 ) 52 } 53 54 const drawerWidth: number = 240 55 56 interface AppBarProps extends MuiAppBarProps { 57 open?: boolean 58 } 59 60 const Logo = styled('img')({ 61 height: '50px', 62 }) 63 64 const AppBar = styled(MuiAppBar, { 65 shouldForwardProp: (prop) => prop !== 'open', 66 })<AppBarProps>(({ theme, open }) => ({ 67 zIndex: theme.zIndex.drawer + 1, 68 transition: theme.transitions.create(['width', 'margin'], { 69 easing: theme.transitions.easing.sharp, 70 duration: theme.transitions.duration.leavingScreen, 71 }), 72 ...(open && { 73 marginLeft: drawerWidth, 74 width: `calc(100% - ${drawerWidth}px)`, 75 transition: theme.transitions.create(['width', 'margin'], { 76 easing: theme.transitions.easing.sharp, 77 duration: theme.transitions.duration.enteringScreen, 78 }), 79 }), 80 })) 81 82 const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })( 83 ({ theme, open }) => ({ 84 '& .MuiDrawer-paper': { 85 position: 'relative', 86 whiteSpace: 'nowrap', 87 width: drawerWidth, 88 transition: theme.transitions.create('width', { 89 easing: theme.transitions.easing.sharp, 90 duration: theme.transitions.duration.enteringScreen, 91 }), 92 boxSizing: 'border-box', 93 ...(!open && { 94 overflowX: 'hidden', 95 transition: theme.transitions.create('width', { 96 easing: theme.transitions.easing.sharp, 97 duration: theme.transitions.duration.leavingScreen, 98 }), 99 width: theme.spacing(7), 100 [theme.breakpoints.up('sm')]: { 101 width: theme.spacing(9), 102 }, 103 }), 104 }, 105 }), 106 ) 107 108 const Layout: FC = () => { 109 const route = useContext(RouterContext) 110 const user = useContext(UserContext) 111 const snackbar = useSnackbar() 112 const [ username, setUsername ] = useState('') 113 const [ password, setPassword ] = useState('') 114 const [ loginOpen, setLoginOpen ] = useState(false) 115 const [ mobileOpen, setMobileOpen ] = useState(false) 116 117 const theme = useTheme() 118 const bigScreen = useMediaQuery(theme.breakpoints.up('md')) 119 120 const handleDrawerToggle = () => { 121 setMobileOpen(!mobileOpen) 122 }; 123 124 const onLogin = useCallback(async () => { 125 const result = await user.login(username, password) 126 if (result) { 127 snackbar.success('Login successful') 128 setLoginOpen(false) 129 } else { 130 snackbar.error('Incorrect details') 131 } 132 }, [ 133 username, 134 password, 135 ]) 136 137 const onLogout = useCallback(async () => { 138 setMobileOpen(false) 139 await user.logout() 140 snackbar.success('Logout successful') 141 }, []) 142 143 const drawer = ( 144 <div> 145 <Toolbar 146 sx={{ 147 display: 'flex', 148 alignItems: 'center', 149 justifyContent: 'flex-start', 150 px: [1], 151 }} 152 > 153 <Logo 154 src="/img/logo.png" 155 /> 156 <Typography variant="h6"> 157 Bacalhau 158 </Typography> 159 </Toolbar> 160 <Divider /> 161 <List> 162 <ListItem 163 disablePadding 164 selected={route.id === 'home'} 165 onClick={ () => { 166 navigate('/') 167 setMobileOpen(false) 168 }} 169 > 170 <ListItemButton> 171 <ListItemIcon> 172 <DvrIcon /> 173 </ListItemIcon> 174 <ListItemText primary="Dashboard" /> 175 </ListItemButton> 176 </ListItem> 177 <ListItem 178 disablePadding 179 selected={route.id === 'network'} 180 onClick={ () => { 181 navigate('/network') 182 setMobileOpen(false) 183 }} 184 > 185 <ListItemButton> 186 <ListItemIcon> 187 <AccountTreeIcon /> 188 </ListItemIcon> 189 <ListItemText primary="Network" /> 190 </ListItemButton> 191 </ListItem> 192 <ListItem 193 disablePadding 194 selected={route.id.indexOf('jobs') === 0} 195 onClick={ () => { 196 navigate('/jobs') 197 setMobileOpen(false) 198 }} 199 > 200 <ListItemButton> 201 <ListItemIcon> 202 <CategoryIcon /> 203 </ListItemIcon> 204 <ListItemText primary="Jobs" /> 205 </ListItemButton> 206 </ListItem> 207 <Divider /> 208 { 209 user.user ? ( 210 <ListItem 211 disablePadding 212 onClick={ onLogout } 213 > 214 <ListItemButton> 215 <ListItemIcon> 216 <LogoutIcon /> 217 </ListItemIcon> 218 <ListItemText primary="Logout" /> 219 </ListItemButton> 220 </ListItem> 221 ) : ( 222 <ListItem 223 disablePadding 224 onClick={ () => { 225 setLoginOpen(true) 226 setMobileOpen(false) 227 }} 228 > 229 <ListItemButton> 230 <ListItemIcon> 231 <LoginIcon /> 232 </ListItemIcon> 233 <ListItemText primary="Login" /> 234 </ListItemButton> 235 </ListItem> 236 ) 237 } 238 239 </List> 240 </div> 241 ) 242 243 const container = window !== undefined ? () => document.body : undefined 244 245 useEffect(() => { 246 user.initialise() 247 }, []) 248 249 return ( 250 <Box sx={{ display: 'flex' }} component="div"> 251 <CssBaseline /> 252 <AppBar 253 elevation={ 1 } 254 position="fixed" 255 open 256 color="default" 257 sx={{ 258 width: { xs: '100%', sm: '100%', md: `calc(100% - ${drawerWidth}px)` }, 259 ml: { xs: '0px', sm: '0px', md: `${drawerWidth}px` }, 260 }} 261 > 262 <Toolbar 263 sx={{ 264 pr: '24px', // keep right padding when drawer closed 265 backgroundColor: '#fff' 266 }} 267 > 268 { 269 !bigScreen && ( 270 <> 271 <IconButton 272 color="inherit" 273 aria-label="open drawer" 274 edge="start" 275 onClick={ handleDrawerToggle } 276 sx={{ 277 mr: 1, 278 ml: 1, 279 }} 280 > 281 <MenuIcon /> 282 </IconButton> 283 <Logo 284 src="/img/logo.png" 285 sx={{ 286 mr: 1, 287 ml: 1, 288 }} 289 /> 290 <Typography 291 variant="h6" 292 sx={{ 293 mr: 1, 294 ml: 1, 295 }} 296 > 297 Bacalhau 298 </Typography> 299 <Typography 300 variant="h6" 301 sx={{ 302 mr: 1, 303 ml: 1, 304 }} 305 > 306 : 307 </Typography> 308 </> 309 310 ) 311 } 312 <Typography 313 component="h1" 314 variant="h6" 315 color="inherit" 316 noWrap 317 sx={{ 318 flexGrow: 1, 319 ml: 1, 320 color: 'text.primary', 321 }} 322 > 323 {route.title || 'Page'} 324 </Typography> 325 { 326 bigScreen && ( 327 <> 328 { 329 user.user ? ( 330 <Stack 331 direction="row" 332 spacing={2} 333 justifyContent="center" 334 alignItems="center" 335 > 336 <Typography variant="body1"> 337 { 338 user.user.username 339 } 340 </Typography> 341 <Button 342 color="primary" 343 variant="outlined" 344 onClick={ onLogout } 345 > 346 Logout 347 </Button> 348 </Stack> 349 350 ) : ( 351 <Button 352 color="primary" 353 variant="outlined" 354 onClick={ () => setLoginOpen(true) } 355 > 356 Login 357 </Button> 358 ) 359 } 360 </> 361 ) 362 } 363 </Toolbar> 364 </AppBar> 365 <MuiDrawer 366 container={container} 367 variant="temporary" 368 open={mobileOpen} 369 onClose={handleDrawerToggle} 370 ModalProps={{ 371 keepMounted: true, // Better open performance on mobile. 372 }} 373 sx={{ 374 display: { sm: 'block', md: 'none' }, 375 '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth }, 376 }} 377 > 378 {drawer} 379 </MuiDrawer> 380 <Drawer 381 variant="permanent" 382 sx={{ 383 display: { xs: 'none', md: 'block' }, 384 '& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth }, 385 }} 386 open 387 > 388 {drawer} 389 </Drawer> 390 <Box 391 component="main" 392 sx={{ 393 backgroundColor: (theme) => 394 theme.palette.mode === 'light' 395 ? theme.palette.grey[100] 396 : theme.palette.grey[900], 397 flexGrow: 1, 398 height: '100vh', 399 overflow: 'auto', 400 display: 'flex', 401 flexDirection: 'column', 402 }} 403 > 404 <Box 405 component="div" 406 sx={{ 407 flexGrow: 0, 408 }} 409 > 410 <Toolbar /> 411 </Box> 412 <Box 413 component="div" 414 sx={{ 415 flexGrow: 1, 416 }} 417 > 418 {route.render()} 419 </Box> 420 <Box 421 component="div" 422 sx={{ 423 flexGrow: 0, 424 }} 425 > 426 <Container maxWidth={'xl'} sx={{ mt: 4, mb: 4 }}> 427 <Copyright sx={{ pt: 4 }} /> 428 </Container> 429 </Box> 430 </Box> 431 { 432 loginOpen && ( 433 <Window 434 open 435 size="md" 436 title="Login" 437 submitTitle="Login" 438 withCancel 439 onCancel={ () => setLoginOpen(false) } 440 onSubmit={ onLogin } 441 > 442 <Box 443 sx={{ 444 p: 2, 445 }} 446 > 447 <Grid container spacing={ 0 }> 448 <Grid item xs={ 12 }> 449 <TextField 450 fullWidth 451 label="Username" 452 name="username" 453 required 454 size="small" 455 variant="outlined" 456 value={ username } 457 onChange={(e) => setUsername(e.target.value)} 458 /> 459 </Grid> 460 <Grid item xs={ 12 }> 461 <TextField 462 fullWidth 463 type="password" 464 label="Password" 465 name="password" 466 required 467 size="small" 468 variant="outlined" 469 sx={{ 470 mt: 2, 471 }} 472 value={ password } 473 onChange={(e) => setPassword(e.target.value)} 474 /> 475 </Grid> 476 </Grid> 477 </Box> 478 </Window> 479 ) 480 } 481 <Snackbar /> 482 <GlobalLoading /> 483 </Box> 484 ) 485 } 486 487 export default Layout