diff options
author | Anton Tananaev <anton.tananaev@gmail.com> | 2021-07-21 23:22:00 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-21 23:22:00 -0700 |
commit | 618c453374c2adc5974c3a5805ffb8c883528bb5 (patch) | |
tree | ca407b084b162b04c56083f877a26c489a41d7fd | |
parent | 16d579d1628bd8aa4a8d1f262ed70face50af907 (diff) | |
parent | 49a4517ec8b1f8ee7c9f38e0d7cd172033d9b170 (diff) | |
download | trackermap-web-618c453374c2adc5974c3a5805ffb8c883528bb5.tar.gz trackermap-web-618c453374c2adc5974c3a5805ffb8c883528bb5.tar.bz2 trackermap-web-618c453374c2adc5974c3a5805ffb8c883528bb5.zip |
Merge pull request #873 from mail2bishnoi/device_list
Map Screen Device list
-rw-r--r-- | modern/public/images/icon/ignition.svg | 3 | ||||
-rw-r--r-- | modern/src/DevicesList.js | 88 | ||||
-rw-r--r-- | modern/src/MainPage.js | 174 | ||||
-rw-r--r-- | modern/src/components/BottomNav.js | 109 | ||||
-rw-r--r-- | modern/src/theme/dimensions.js | 1 |
5 files changed, 322 insertions, 53 deletions
diff --git a/modern/public/images/icon/ignition.svg b/modern/public/images/icon/ignition.svg new file mode 100644 index 00000000..d731c92e --- /dev/null +++ b/modern/public/images/icon/ignition.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M17 8H19.5C19.81 8 20.11 8.15 20.3 8.4L22.6 11.47C22.86 11.82 23 12.24 23 12.67V16C23 16.55 22.55 17 22 17H21C21 18.66 19.66 20 18 20C16.34 20 15 18.66 15 17H9C9 18.66 7.66 20 6 20C4.34 20 3 18.66 3 17C1.9 17 1 16.1 1 15V6C1 4.9 1.9 4 3 4H15C16.1 4 17 4.9 17 6V8ZM5 17C5 17.55 5.45 18 6 18C6.55 18 7 17.55 7 17C7 16.45 6.55 16 6 16C5.45 16 5 16.45 5 17ZM21.46 12L19.5 9.5H17V12H21.46ZM17 17C17 17.55 17.45 18 18 18C18.55 18 19 17.55 19 17C19 16.45 18.55 16 18 16C17.45 16 17 16.45 17 17ZM7.7704 16.1299L8.3613 16.2341C8.3613 16.2341 9.91885 14.1732 13.0446 10.0602L13.1033 9.96899C13.1774 9.86019 13.1742 9.63624 12.8847 9.58519L10.8165 9.22052L12.2178 4.72894L11.6269 4.62475C10.789 5.73304 10.0364 6.72746 9.36924 7.60897C8.4039 8.88444 7.61745 9.92355 7.01063 10.7292C6.99419 10.7534 7.10701 10.5973 6.95538 10.8007C6.80375 11.0041 6.7608 11.2132 7.10352 11.2736L9.17166 11.6383L7.7704 16.1299Z" fill="#F3A813"/> +</svg> diff --git a/modern/src/DevicesList.js b/modern/src/DevicesList.js index 85b936ce..294a9fff 100644 --- a/modern/src/DevicesList.js +++ b/modern/src/DevicesList.js @@ -3,21 +3,24 @@ import { useDispatch, useSelector } from 'react-redux'; import { makeStyles } from '@material-ui/core/styles'; import Avatar from '@material-ui/core/Avatar'; import Divider from '@material-ui/core/Divider'; -import IconButton from '@material-ui/core/IconButton'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; import ListItemAvatar from '@material-ui/core/ListItemAvatar'; import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; +import Grid from '@material-ui/core/Grid'; import ListItemText from '@material-ui/core/ListItemText'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; +import SvgIcon from '@material-ui/core/SvgIcon'; import { FixedSizeList } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; +import BatteryFullIcon from '@material-ui/icons/BatteryFull'; +import { ReactComponent as IgnitionIcon } from '../public/images/icon/ignition.svg'; import { devicesActions } from './store'; import EditCollectionView from './EditCollectionView'; import { useEffectAsync } from './reactHelper'; +import { formatPosition } from './common/formatter'; -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles((theme) => ({ list: { maxHeight: '100%', }, @@ -26,29 +29,92 @@ const useStyles = makeStyles(() => ({ height: '25px', filter: 'brightness(0) invert(1)', }, + listItem: { + backgroundColor: 'white', + '&:hover': { + backgroundColor: 'white', + }, + }, + batteryText: { + fontSize: '0.75rem', + fontWeight: 'normal', + lineHeight: '0.875rem', + }, + green: { + color: theme.palette.common.green, + }, + red: { + color: theme.palette.common.red, + }, + gray: { + color: theme.palette.common.gray, + }, + indicators: { + lineHeight: 1, + }, })); +const getStatusColor = (status) => { + switch (status) { + case 'online': + return 'green'; + case 'offline': + return 'red'; + case 'unknown': + default: + return 'gray'; + } +}; + +const getBatteryStatus = (batteryLevel) => { + if (batteryLevel >= 70) { + return 'green'; + } + if (batteryLevel > 30) { + return 'gray'; + } + return 'red'; +}; + const DeviceRow = ({ data, index, style }) => { const classes = useStyles(); const dispatch = useDispatch(); - const { items, onMenuClick } = data; + const { items } = data; const item = items[index]; + const position = useSelector((state) => state.positions.items[item.id]); + const showIgnition = position?.attributes.hasOwnProperty('ignition') && position.attributes.ignition; return ( <div style={style}> <Fragment key={index}> - <ListItem button key={item.id} onClick={() => dispatch(devicesActions.select(item))}> + <ListItem button key={item.id} className={classes.listItem} onClick={() => dispatch(devicesActions.select(item))}> <ListItemAvatar> <Avatar> <img className={classes.icon} src={`images/icon/${item.category || 'default'}.svg`} alt="" /> </Avatar> </ListItemAvatar> - <ListItemText primary={item.name} secondary={item.uniqueId} /> - <ListItemSecondaryAction> - <IconButton onClick={(event) => onMenuClick(event.currentTarget, item.id)}> - <MoreVertIcon /> - </IconButton> + <ListItemText primary={item.name} secondary={item.status} classes={{ secondary: classes[getStatusColor(item.status)] }} /> + <ListItemSecondaryAction className={classes.indicators}> + {position && ( + <Grid container direction="row" alignItems="center" alignContent="center" spacing={2}> + {showIgnition && ( + <Grid item> + <SvgIcon component={IgnitionIcon} /> + </Grid> + )} + {position.attributes.hasOwnProperty('batteryLevel') && ( + <Grid item container xs alignItems="center" alignContent="center"> + <Grid item> + <span className={classes.batteryText}>{formatPosition(position.attributes.batteryLevel, 'batteryLevel')}</span> + </Grid> + <Grid item> + <BatteryFullIcon className={classes[getBatteryStatus(position.attributes.batteryLevel)]} /> + </Grid> + </Grid> + )} + </Grid> + )} </ListItemSecondaryAction> </ListItem> {index < items.length - 1 ? <Divider /> : null} @@ -90,7 +156,7 @@ const DeviceView = ({ updateTimestamp, onMenuClick }) => { }; const DevicesList = () => ( - <EditCollectionView content={DeviceView} editPath="/device" endpoint="devices" /> + <EditCollectionView content={DeviceView} editPath="/device" endpoint="devices" disableAdd /> ); export default DevicesList; diff --git a/modern/src/MainPage.js b/modern/src/MainPage.js index 88608df7..417cdb03 100644 --- a/modern/src/MainPage.js +++ b/modern/src/MainPage.js @@ -1,74 +1,164 @@ -import React from 'react'; -import { isWidthUp, makeStyles, withWidth } from '@material-ui/core'; -import Drawer from '@material-ui/core/Drawer'; -import ContainerDimensions from 'react-container-dimensions'; +import React, { useState, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { + makeStyles, Paper, Toolbar, TextField, IconButton, Button, +} from '@material-ui/core'; + +import { useTheme } from '@material-ui/core/styles'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; +import AddIcon from '@material-ui/icons/Add'; +import CloseIcon from '@material-ui/icons/Close'; +import ArrowBackIcon from '@material-ui/icons/ArrowBack'; +import ListIcon from '@material-ui/icons/ViewList'; + import DevicesList from './DevicesList'; -import MainToolbar from './MainToolbar'; import Map from './map/Map'; import SelectedDeviceMap from './map/SelectedDeviceMap'; import AccuracyMap from './map/AccuracyMap'; import GeofenceMap from './map/GeofenceMap'; import CurrentPositionsMap from './map/CurrentPositionsMap'; import CurrentLocationMap from './map/CurrentLocationMap'; +import BottomNav from './components/BottomNav'; +import t from './common/localization'; const useStyles = makeStyles((theme) => ({ root: { - height: '100%', + height: '100vh', + }, + sidebar: { display: 'flex', flexDirection: 'column', + position: 'absolute', + left: 0, + top: 0, + margin: theme.spacing(1.5), + width: theme.dimensions.drawerWidthDesktop, + bottom: theme.spacing(8), + zIndex: 1301, + transition: 'transform .5s ease', + [theme.breakpoints.down('md')]: { + width: '100%', + margin: 0, + backgroundColor: 'white', + }, }, - content: { - flexGrow: 1, - overflow: 'hidden', - display: 'flex', - flexDirection: 'row', - [theme.breakpoints.down('xs')]: { - flexDirection: 'column-reverse', + sidebarCollapsed: { + transform: `translateX(-${theme.dimensions.drawerWidthDesktop})`, + marginLeft: 0, + [theme.breakpoints.down('md')]: { + transform: 'translateX(-100vw)', }, }, - drawerPaper: { - position: 'relative', - [theme.breakpoints.up('sm')]: { - width: 350, + paper: { + borderRadius: '0px', + }, + toolbar: { + display: 'flex', + padding: theme.spacing(0, 1), + '& > *': { + margin: theme.spacing(0, 1), }, - [theme.breakpoints.down('xs')]: { - height: 250, + }, + deviceList: { + flex: 1, + overflow: 'auto', + padding: theme.spacing(1.5, 0), + }, + sidebarToggle: { + position: 'absolute', + left: theme.spacing(1.5), + top: theme.spacing(3), + borderRadius: '0px', + minWidth: 0, + [theme.breakpoints.down('md')]: { + left: theme.spacing(0), }, - overflow: 'hidden', }, - mapContainer: { - flexGrow: 1, + sidebarToggleBg: { + backgroundColor: 'white', + color: '#777777', + '&:hover': { + backgroundColor: 'white', + }, }, })); -const MainPage = ({ width }) => { +const MainPage = () => { const classes = useStyles(); + const history = useHistory(); + const theme = useTheme(); + + const isTablet = useMediaQuery(theme.breakpoints.down('md')); + const isPhone = useMediaQuery(theme.breakpoints.down('xs')); + + const [deviceName, setDeviceName] = useState(''); + const [collapsed, setCollapsed] = useState(false); + + const handleClose = () => { + setCollapsed(!collapsed); + }; + + // Collapse sidebar for tablets and phones + useEffect(() => { + setCollapsed(isTablet); + }, [isTablet]); return ( <div className={classes.root}> - <MainToolbar /> - <div className={classes.content}> - <Drawer - anchor={isWidthUp('sm', width) ? 'left' : 'bottom'} - variant="permanent" - classes={{ paper: classes.drawerPaper }} - > + <Map> + <CurrentLocationMap /> + <GeofenceMap /> + <AccuracyMap /> + <CurrentPositionsMap /> + <SelectedDeviceMap /> + </Map> + <Button + variant="contained" + color={isPhone ? 'secondary' : 'primary'} + classes={{ containedPrimary: classes.sidebarToggleBg }} + className={classes.sidebarToggle} + onClick={handleClose} + disableElevation + > + <ListIcon /> + {!isPhone ? t('deviceTitle') : ''} + </Button> + <div className={`${classes.sidebar} ${collapsed && classes.sidebarCollapsed}`}> + <Paper className={classes.paper} elevation={isTablet ? 3 : 1}> + <Toolbar className={classes.toolbar} disableGutters> + {isTablet && ( + <IconButton onClick={handleClose}> + <ArrowBackIcon /> + </IconButton> + )} + <TextField + fullWidth + name="deviceName" + value={deviceName} + autoComplete="deviceName" + autoFocus + onChange={(event) => setDeviceName(event.target.value)} + placeholder="Search Devices" + variant="filled" + /> + <IconButton onClick={() => history.push('/device')}> + <AddIcon /> + </IconButton> + {!isTablet && ( + <IconButton onClick={handleClose}> + <CloseIcon /> + </IconButton> + )} + </Toolbar> + </Paper> + <div className={classes.deviceList}> <DevicesList /> - </Drawer> - <div className={classes.mapContainer}> - <ContainerDimensions> - <Map> - <CurrentLocationMap /> - <GeofenceMap /> - <AccuracyMap /> - <CurrentPositionsMap /> - <SelectedDeviceMap /> - </Map> - </ContainerDimensions> </div> </div> + + <BottomNav showOnDesktop /> </div> ); }; -export default withWidth()(MainPage); +export default MainPage; diff --git a/modern/src/components/BottomNav.js b/modern/src/components/BottomNav.js new file mode 100644 index 00000000..6aad1dd9 --- /dev/null +++ b/modern/src/components/BottomNav.js @@ -0,0 +1,109 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { Link, useHistory } from 'react-router-dom'; +import { + makeStyles, Paper, Toolbar, IconButton, useMediaQuery, useTheme, +} from '@material-ui/core'; + +import ReplayIcon from '@material-ui/icons/Replay'; +import DescriptionIcon from '@material-ui/icons/Description'; +import ShuffleIcon from '@material-ui/icons/Shuffle'; +import MapIcon from '@material-ui/icons/Map'; +import LogoutIcon from '@material-ui/icons/ExitToApp'; + +import { sessionActions } from '../store'; +import t from '../common/localization'; + +const useStyles = makeStyles((theme) => ({ + container: { + bottom: theme.spacing(0), + left: '0px', + width: '100%', + position: 'fixed', + zIndex: 1301, + [theme.breakpoints.up('lg')]: { + left: theme.spacing(1.5), + bottom: theme.spacing(1.5), + width: theme.dimensions.drawerWidthDesktop, + }, + }, + paper: { + borderRadius: '0px', + }, + toolbar: { + padding: theme.spacing(0, 2), + display: 'flex', + justifyContent: 'space-around', + maxWidth: theme.dimensions.bottomNavMaxWidth, + margin: 'auto', + }, + navItem: { + display: 'flex', + flexDirection: 'column', + fontSize: '0.75rem', + fontWeight: 'normal', + }, +})); + +const BottomNav = ({ showOnDesktop }) => { + const classes = useStyles(); + const theme = useTheme(); + const history = useHistory(); + + const isDesktop = useMediaQuery(theme.breakpoints.up('lg')); + const dispatch = useDispatch(); + + const NavLink = ({ children, location }) => ( + <IconButton component={Link} classes={{ label: classes.navItem }} to={location}> + {children} + </IconButton> + ); + + const handleLogout = async () => { + const response = await fetch('/api/session', { method: 'DELETE' }); + if (response.ok) { + dispatch(sessionActions.updateUser(null)); + history.push('/login'); + } + }; + + if (isDesktop && !showOnDesktop) return null; + + return ( + <div className={classes.container}> + <Paper className={classes.paper} elevation={isDesktop ? 1 : 3}> + <Toolbar className={classes.toolbar} disableGutters> + + {isDesktop ? ( + <NavLink location="/replay"> + <ReplayIcon /> + {t('reportReplay')} + </NavLink> + ) : ( + <NavLink location="/"> + <MapIcon /> + {t('mapTitle')} + </NavLink> + )} + + <NavLink location="/reports/route"> + <DescriptionIcon /> + {t('reportTitle')} + </NavLink> + + <NavLink location="/settings/notifications"> + <ShuffleIcon /> + {t('settingsTitle')} + </NavLink> + + <IconButton classes={{ label: classes.navItem }} onClick={handleLogout}> + <LogoutIcon /> + {t('loginLogout')} + </IconButton> + </Toolbar> + </Paper> + </div> + ); +}; + +export default BottomNav; diff --git a/modern/src/theme/dimensions.js b/modern/src/theme/dimensions.js index fcdbaee5..b6edc5e9 100644 --- a/modern/src/theme/dimensions.js +++ b/modern/src/theme/dimensions.js @@ -9,4 +9,5 @@ export default { columnWidthNumber: 130, columnWidthString: 160, columnWidthBoolean: 130, + bottomNavMaxWidth: '400px', }; |