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