aboutsummaryrefslogtreecommitdiff
path: root/modern/src
diff options
context:
space:
mode:
Diffstat (limited to 'modern/src')
-rw-r--r--modern/src/DevicesList.js88
-rw-r--r--modern/src/MainPage.js174
-rw-r--r--modern/src/components/BottomNav.js109
-rw-r--r--modern/src/theme/dimensions.js1
4 files changed, 319 insertions, 53 deletions
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',
};