aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAshutosh Bishnoi <41992346+mail2bishnoi@users.noreply.github.com>2021-07-22 11:17:22 +0530
committerGitHub <noreply@github.com>2021-07-22 11:17:22 +0530
commitbdb3d3714d746d10b56b6db1f35dcf9cdaf4c844 (patch)
treed011e72f9609444f71eced195a6394c1d481e80b
parent4a6ed2462ed5ed2960fc8245ac3c5bae967e685b (diff)
parentb0c4891c4b2687e7a08f05e779c84847a0f4f46c (diff)
downloadetbsa-traccar-web-bdb3d3714d746d10b56b6db1f35dcf9cdaf4c844.tar.gz
etbsa-traccar-web-bdb3d3714d746d10b56b6db1f35dcf9cdaf4c844.tar.bz2
etbsa-traccar-web-bdb3d3714d746d10b56b6db1f35dcf9cdaf4c844.zip
Merge pull request #1 from dkyeremeh/device_list
Thank you. Merged.
-rw-r--r--modern/.eslintrc.js35
-rw-r--r--modern/package.json3
-rw-r--r--modern/public/images/icon/ignition.svg3
-rw-r--r--modern/public/styles.css4
-rw-r--r--modern/src/DevicesList.js15
-rw-r--r--modern/src/EditItemView.js7
-rw-r--r--modern/src/GeofencesPage.js24
-rw-r--r--modern/src/MainPage.js248
-rw-r--r--modern/src/MainToolbar.js97
-rw-r--r--modern/src/StartPage.js2
-rw-r--r--modern/src/admin/ServerPage.js12
-rw-r--r--modern/src/admin/StatisticsPage.js7
-rw-r--r--modern/src/admin/UsersPage.js7
-rw-r--r--modern/src/attributes/EditAttributesView.js2
-rw-r--r--modern/src/common/selectors.js3
-rw-r--r--modern/src/components/BottomNav.js109
-rw-r--r--modern/src/components/NavBar.js (renamed from modern/src/components/reports/ReportNavbar.js)9
-rw-r--r--modern/src/components/SideNav.js34
-rw-r--r--modern/src/components/registration/LoginForm.js5
-rw-r--r--modern/src/components/reports/ReportSidebar.js31
-rw-r--r--modern/src/map/GeofenceEditMap.js1
-rw-r--r--modern/src/reports/ChartReportPage.js6
-rw-r--r--modern/src/reports/EventReportPage.js6
-rw-r--r--modern/src/reports/ReportFilter.js2
-rw-r--r--modern/src/reports/ReportLayout.js (renamed from modern/src/reports/ReportLayoutPage.js)52
-rw-r--r--modern/src/reports/RouteReportPage.js6
-rw-r--r--modern/src/reports/StopReportPage.js6
-rw-r--r--modern/src/reports/SummaryReportPage.js6
-rw-r--r--modern/src/reports/TripReportPage.js6
-rw-r--r--modern/src/settings/ComputedAttributesPage.js8
-rw-r--r--modern/src/settings/DriversPage.js7
-rw-r--r--modern/src/settings/GroupsPage.js7
-rw-r--r--modern/src/settings/MaintenancesPage.js7
-rw-r--r--modern/src/settings/NotificationsPage.js7
-rw-r--r--modern/src/settings/OptionsLayout/index.js106
-rw-r--r--modern/src/settings/OptionsLayout/useRoutes.js86
-rw-r--r--modern/src/theme/dimensions.js1
-rw-r--r--web/app/store/MapTypes.js6
-rw-r--r--web/app/view/map/BaseMap.js30
39 files changed, 608 insertions, 405 deletions
diff --git a/modern/.eslintrc.js b/modern/.eslintrc.js
index a108980..a4cf305 100644
--- a/modern/.eslintrc.js
+++ b/modern/.eslintrc.js
@@ -1,18 +1,21 @@
module.exports = {
- "extends": "airbnb",
- "plugins": [
- "react"
- ],
- "ignorePatterns": ["serviceWorker.js", "localization.js", "switcher.js"],
- "rules": {
- "max-len": [0],
- "no-shadow": [0],
- "no-return-assign": [0],
- "no-param-reassign": [0],
- "no-prototype-builtins": [0],
- "react/jsx-filename-extension": [1, { "extensions": [".js"] }],
- "react/prop-types": [0],
- "react/jsx-props-no-spreading": [0],
- "jsx-a11y/anchor-is-valid": [0]
- }
+ extends: 'airbnb',
+ parserOptions: {
+ ecmaVersion: 2020,
+ },
+ plugins: [
+ 'react',
+ ],
+ ignorePatterns: ['serviceWorker.js', 'localization.js', 'switcher.js'],
+ rules: {
+ 'max-len': [0],
+ 'no-shadow': [0],
+ 'no-return-assign': [0],
+ 'no-param-reassign': [0],
+ 'no-prototype-builtins': [0],
+ 'react/jsx-filename-extension': [1, { extensions: ['.js'] }],
+ 'react/prop-types': [0],
+ 'react/jsx-props-no-spreading': [0],
+ 'jsx-a11y/anchor-is-valid': [0],
+ },
};
diff --git a/modern/package.json b/modern/package.json
index 365d986..6606649 100644
--- a/modern/package.json
+++ b/modern/package.json
@@ -13,6 +13,7 @@
"@turf/circle": "^6.5.0",
"@turf/turf": "^6.4.0",
"canvas-tint-image": "^2.0.1",
+ "mapbox-gl": "^1.13.1",
"maplibre-gl": "^1.15.0",
"moment": "^2.29.1",
"react": "^17.0.2",
@@ -49,7 +50,7 @@
]
},
"devDependencies": {
- "eslint": "^6.8.0",
+ "eslint": "^7.30.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-react": "^7.24.0",
diff --git a/modern/public/images/icon/ignition.svg b/modern/public/images/icon/ignition.svg
new file mode 100644
index 0000000..d731c92
--- /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/public/styles.css b/modern/public/styles.css
index 7e9de65..e7aee86 100644
--- a/modern/public/styles.css
+++ b/modern/public/styles.css
@@ -6,3 +6,7 @@ canvas {
.maplibregl-popup-content {
padding: 10px !important;
}
+
+.mapboxgl-ctrl {
+ margin: 10px;
+}
diff --git a/modern/src/DevicesList.js b/modern/src/DevicesList.js
index b06f7f7..aa85630 100644
--- a/modern/src/DevicesList.js
+++ b/modern/src/DevicesList.js
@@ -9,10 +9,11 @@ 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 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 VpnKeyIcon from '@material-ui/icons/VpnKey';
+import { ReactComponent as IgnitionIcon } from '../public/images/icon/ignition.svg';
import { devicesActions } from './store';
import EditCollectionView from './EditCollectionView';
@@ -48,6 +49,9 @@ const useStyles = makeStyles((theme) => ({
gray: {
color: theme.palette.common.gray,
},
+ indicators: {
+ lineHeight: 1,
+ },
}));
const getStatusColor = (status) => {
@@ -79,6 +83,7 @@ const DeviceRow = ({ data, index, style }) => {
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}>
@@ -90,12 +95,12 @@ const DeviceRow = ({ data, index, style }) => {
</Avatar>
</ListItemAvatar>
<ListItemText primary={item.name} secondary={item.status} classes={{ secondary: classes[getStatusColor(item.status)] }} />
- <ListItemSecondaryAction>
+ <ListItemSecondaryAction className={classes.indicators}>
{position && (
- <Grid container direction="row" alignItems="center" alignContent="center" spacing={1}>
- {position.attributes.hasOwnProperty('ignition') && (
+ <Grid container direction="row" alignItems="center" alignContent="center" spacing={2}>
+ {showIgnition && (
<Grid item>
- <VpnKeyIcon className={`${position.attributes.ignition ? classes.green : classes.gray}`} />
+ <SvgIcon component={IgnitionIcon} />
</Grid>
)}
{position.attributes.hasOwnProperty('batteryLevel') && (
diff --git a/modern/src/EditItemView.js b/modern/src/EditItemView.js
index 9bf91a6..e067a39 100644
--- a/modern/src/EditItemView.js
+++ b/modern/src/EditItemView.js
@@ -4,10 +4,10 @@ import { makeStyles } from '@material-ui/core/styles';
import Container from '@material-ui/core/Container';
import Button from '@material-ui/core/Button';
import FormControl from '@material-ui/core/FormControl';
-import MainToolbar from './MainToolbar';
import t from './common/localization';
import { useEffectAsync } from './reactHelper';
+import OptionsLayout from './settings/OptionsLayout';
const useStyles = makeStyles((theme) => ({
container: {
@@ -58,8 +58,7 @@ const EditItemView = ({
};
return (
- <>
- <MainToolbar />
+ <OptionsLayout>
<Container maxWidth="xs" className={classes.container}>
{children}
<FormControl fullWidth margin="normal">
@@ -73,7 +72,7 @@ const EditItemView = ({
</div>
</FormControl>
</Container>
- </>
+ </OptionsLayout>
);
};
diff --git a/modern/src/GeofencesPage.js b/modern/src/GeofencesPage.js
index 95c7151..71219c1 100644
--- a/modern/src/GeofencesPage.js
+++ b/modern/src/GeofencesPage.js
@@ -1,13 +1,17 @@
import React from 'react';
-import { isWidthUp, makeStyles, withWidth } from '@material-ui/core';
+import {
+ Divider, isWidthUp, makeStyles, withWidth, Typography, IconButton,
+} from '@material-ui/core';
import Drawer from '@material-ui/core/Drawer';
import ContainerDimensions from 'react-container-dimensions';
-import MainToolbar from './MainToolbar';
+import ArrowBackIcon from '@material-ui/icons/ArrowBack';
import Map from './map/Map';
import CurrentLocationMap from './map/CurrentLocationMap';
import GeofenceEditMap from './map/GeofenceEditMap';
import GeofencesList from './GeofencesList';
+import t from './common/localization';
+
const useStyles = makeStyles((theme) => ({
root: {
height: '100%',
@@ -32,6 +36,12 @@ const useStyles = makeStyles((theme) => ({
height: 250,
},
},
+ drawerHeader: {
+ ...theme.mixins.toolbar,
+ display: 'flex',
+ alignItems: 'center',
+ padding: theme.spacing(0, 1),
+ },
mapContainer: {
flexGrow: 1,
},
@@ -42,13 +52,21 @@ const GeofencesPage = ({ width }) => {
return (
<div className={classes.root}>
- <MainToolbar />
<div className={classes.content}>
<Drawer
anchor={isWidthUp('sm', width) ? 'left' : 'bottom'}
variant="permanent"
classes={{ paper: classes.drawerPaper }}
>
+ <div className={classes.drawerHeader}>
+ <IconButton component="a" href="/">
+ <ArrowBackIcon />
+ </IconButton>
+ <Typography variant="h6" color="inherit" noWrap>
+ {t('sharedGeofences')}
+ </Typography>
+ </div>
+ <Divider />
<GeofencesList />
</Drawer>
<div className={classes.mapContainer}>
diff --git a/modern/src/MainPage.js b/modern/src/MainPage.js
index d5447e5..417cdb0 100644
--- a/modern/src/MainPage.js
+++ b/modern/src/MainPage.js
@@ -1,7 +1,7 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import {
- makeStyles, Paper, Toolbar, Grid, TextField, IconButton, Button,
+ makeStyles, Paper, Toolbar, TextField, IconButton, Button,
} from '@material-ui/core';
import { useTheme } from '@material-ui/core/styles';
@@ -9,117 +9,78 @@ 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/List';
-import ReplayIcon from '@material-ui/icons/Replay';
-import DescriptionIcon from '@material-ui/icons/Description';
-import ShuffleIcon from '@material-ui/icons/Shuffle';
-import PersonIcon from '@material-ui/icons/Person';
+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: '100vh',
+ },
+ sidebar: {
display: 'flex',
flexDirection: 'column',
- },
- drawerPaper: {
- position: 'relative',
- [theme.breakpoints.up('sm')]: {
- width: 350,
- },
- [theme.breakpoints.down('xs')]: {
- height: 250,
- },
- overflow: 'hidden',
- },
- listContainer: {
position: 'absolute',
- left: theme.spacing(1.5),
- top: theme.spacing(10.5),
+ left: 0,
+ top: 0,
+ margin: theme.spacing(1.5),
width: theme.dimensions.drawerWidthDesktop,
+ bottom: theme.spacing(8),
zIndex: 1301,
- height: '100%',
- maxHeight: `calc(100vh - ${theme.spacing(20)}px)`,
+ transition: 'transform .5s ease',
[theme.breakpoints.down('md')]: {
- top: theme.spacing(7),
- left: '0px',
width: '100%',
- maxHeight: `calc(100vh - ${theme.spacing(14)}px)`,
- },
- },
- paper: {
- borderRadius: '0px',
- },
- toolbar: {
- paddingLeft: theme.spacing(2),
- paddingRight: theme.spacing(2),
- },
- deviceList: {
- height: '100%',
- backgroundColor: 'transparent',
- [theme.breakpoints.down('md')]: {
+ margin: 0,
backgroundColor: 'white',
},
},
- collapsed: {
- transform: `translateX(-${360 + 16}px)`,
- transition: 'transform .5s ease',
+ sidebarCollapsed: {
+ transform: `translateX(-${theme.dimensions.drawerWidthDesktop})`,
+ marginLeft: 0,
[theme.breakpoints.down('md')]: {
transform: 'translateX(-100vw)',
},
},
- uncollapsed: {
- transform: `translateX(${theme.spacing(1.5)})`,
- transition: 'transform .5s ease',
- [theme.breakpoints.down('md')]: {
- transform: 'translateX(0)',
+ paper: {
+ borderRadius: '0px',
+ },
+ toolbar: {
+ display: 'flex',
+ padding: theme.spacing(0, 1),
+ '& > *': {
+ margin: theme.spacing(0, 1),
},
},
- deviceButton: {
+ deviceList: {
+ flex: 1,
+ overflow: 'auto',
+ padding: theme.spacing(1.5, 0),
+ },
+ sidebarToggle: {
position: 'absolute',
- left: theme.spacing(1),
- top: theme.spacing(10.5),
+ left: theme.spacing(1.5),
+ top: theme.spacing(3),
borderRadius: '0px',
+ minWidth: 0,
[theme.breakpoints.down('md')]: {
left: theme.spacing(0),
- top: theme.spacing(7),
},
},
- deviceButtonBackground: {
+ sidebarToggleBg: {
backgroundColor: 'white',
color: '#777777',
'&:hover': {
backgroundColor: 'white',
},
},
- bottomMenuContainer: {
- position: 'absolute',
- left: theme.spacing(1.5),
- bottom: theme.spacing(1.5),
- width: theme.dimensions.drawerWidthDesktop,
- zIndex: 1301,
- [theme.breakpoints.down('md')]: {
- bottom: theme.spacing(0),
- left: '0px',
- width: '100%',
- },
- },
- menuButton: {
- display: 'flex',
- flexDirection: 'column',
- fontSize: '0.75rem',
- fontWeight: 'normal',
- color: '#222222',
- },
}));
const MainPage = () => {
@@ -127,19 +88,23 @@ const MainPage = () => {
const history = useHistory();
const theme = useTheme();
- const [deviceName, setDeviceName] = useState();
- const [collapsed, setCollapsed] = useState(false);
-
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 />
<Map>
<CurrentLocationMap />
<GeofenceMap />
@@ -147,100 +112,51 @@ const MainPage = () => {
<CurrentPositionsMap />
<SelectedDeviceMap />
</Map>
- {collapsed
- && (
- <Button
- variant="contained"
- color={isPhone ? 'secondary' : 'primary'}
- classes={{ containedPrimary: classes.deviceButtonBackground }}
- className={classes.deviceButton}
- onClick={handleClose}
- startIcon={<ListIcon />}
- disableElevation
- >
- {!isPhone ? t('deviceTitle') : ''}
- </Button>
- )}
- <div className={`${classes.listContainer} ${collapsed ? classes.collapsed : classes.uncollapsed}`}>
- <Grid container direction="column" spacing={isTablet ? 0 : 2} style={{ height: '100%' }}>
- <Grid item>
- <Paper className={classes.paper}>
- <Toolbar className={classes.toolbar} disableGutters>
- <Grid container direction="row" alignItems="center" spacing={2}>
- {isTablet && (
- <Grid item>
- <IconButton onClick={handleClose}>
- <ArrowBackIcon />
- </IconButton>
- </Grid>
- )}
- <Grid item xs>
- <TextField
- fullWidth
- name="deviceName"
- value={deviceName || ''}
- autoComplete="deviceName"
- autoFocus
- onChange={(event) => setDeviceName(event.target.value)}
- placeholder="Search Devices"
- variant="filled"
- />
- </Grid>
- <Grid item>
- <IconButton onClick={() => history.push('/device')}>
- <AddIcon />
- </IconButton>
- </Grid>
- {!isTablet && (
- <Grid item>
- <IconButton onClick={handleClose}>
- <CloseIcon />
- </IconButton>
- </Grid>
- )}
- </Grid>
- </Toolbar>
- </Paper>
- </Grid>
- <Grid item xs>
- <div className={classes.deviceList}>
- <DevicesList />
- </div>
- </Grid>
- </Grid>
- </div>
- <div className={classes.bottomMenuContainer}>
- <Paper className={classes.paper}>
+ <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>
- <Grid container justify="space-around">
- <Grid item>
- <IconButton classes={{ label: classes.menuButton }}>
- <ReplayIcon />
- {t('reportReplay')}
- </IconButton>
- </Grid>
- <Grid item>
- <IconButton classes={{ label: classes.menuButton }}>
- <DescriptionIcon />
- {t('reportTitle')}
- </IconButton>
- </Grid>
- <Grid item>
- <IconButton classes={{ label: classes.menuButton }}>
- <ShuffleIcon />
- Options
- </IconButton>
- </Grid>
- <Grid item>
- <IconButton classes={{ label: classes.menuButton }}>
- <PersonIcon />
- {t('settingsUser')}
- </IconButton>
- </Grid>
- </Grid>
+ {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 />
+ </div>
</div>
+
+ <BottomNav showOnDesktop />
</div>
);
};
diff --git a/modern/src/MainToolbar.js b/modern/src/MainToolbar.js
index da7c42c..59c2eca 100644
--- a/modern/src/MainToolbar.js
+++ b/modern/src/MainToolbar.js
@@ -10,24 +10,16 @@ import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import Drawer from '@material-ui/core/Drawer';
import List from '@material-ui/core/List';
-import ListSubheader from '@material-ui/core/ListSubheader';
-import Divider from '@material-ui/core/Divider';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import MapIcon from '@material-ui/icons/Map';
-import BarChartIcon from '@material-ui/icons/BarChart';
-import PeopleIcon from '@material-ui/icons/People';
-import StorageIcon from '@material-ui/icons/Storage';
import PersonIcon from '@material-ui/icons/Person';
-import NotificationsIcon from '@material-ui/icons/Notifications';
import DescriptionIcon from '@material-ui/icons/Description';
-import FolderIcon from '@material-ui/icons/Folder';
-import CreateIcon from '@material-ui/icons/Create';
import ReplayIcon from '@material-ui/icons/Replay';
-import BuildIcon from '@material-ui/icons/Build';
import { sessionActions } from './store';
import t from './common/localization';
+import * as selectors from './common/selectors';
const useStyles = makeStyles((theme) => ({
flex: {
@@ -50,8 +42,7 @@ const MainToolbar = () => {
const [drawer, setDrawer] = useState(false);
const classes = useStyles();
const history = useHistory();
- const adminEnabled = useSelector((state) => state.session.user && state.session.user.administrator);
- const userId = useSelector((state) => state.session.user && state.session.user.id);
+ const userId = useSelector(selectors.getUserId);
const openDrawer = () => { setDrawer(true); };
const closeDrawer = () => { setDrawer(false); };
@@ -108,89 +99,17 @@ const MainToolbar = () => {
</ListItemIcon>
<ListItemText primary={t('reportTitle')} />
</ListItem>
- </List>
- <Divider />
- <List
- subheader={(
- <ListSubheader>
- {t('settingsTitle')}
- </ListSubheader>
- )}
- >
- <ListItem button disabled={!userId} onClick={() => history.push(`/user/${userId}`)}>
+ <ListItem
+ button
+ disabled={!userId}
+ onClick={() => history.push('/settings/notifications')}
+ >
<ListItemIcon>
<PersonIcon />
</ListItemIcon>
- <ListItemText primary={t('settingsUser')} />
- </ListItem>
- <ListItem button onClick={() => history.push('/geofences')}>
- <ListItemIcon>
- <CreateIcon />
- </ListItemIcon>
- <ListItemText primary={t('sharedGeofences')} />
- </ListItem>
- <ListItem button onClick={() => history.push('/settings/notifications')}>
- <ListItemIcon>
- <NotificationsIcon />
- </ListItemIcon>
- <ListItemText primary={t('sharedNotifications')} />
- </ListItem>
- <ListItem button onClick={() => history.push('/settings/groups')}>
- <ListItemIcon>
- <FolderIcon />
- </ListItemIcon>
- <ListItemText primary={t('settingsGroups')} />
- </ListItem>
- <ListItem button onClick={() => history.push('/settings/drivers')}>
- <ListItemIcon>
- <PersonIcon />
- </ListItemIcon>
- <ListItemText primary={t('sharedDrivers')} />
- </ListItem>
- <ListItem button onClick={() => history.push('/settings/attributes')}>
- <ListItemIcon>
- <StorageIcon />
- </ListItemIcon>
- <ListItemText primary={t('sharedComputedAttributes')} />
- </ListItem>
- <ListItem button onClick={() => history.push('/settings/maintenances')}>
- <ListItemIcon>
- <BuildIcon />
- </ListItemIcon>
- <ListItemText primary={t('sharedMaintenance')} />
+ <ListItemText primary={t('settingsTitle')} />
</ListItem>
</List>
- {adminEnabled && (
- <>
- <Divider />
- <List
- subheader={(
- <ListSubheader>
- {t('userAdmin')}
- </ListSubheader>
- )}
- >
- <ListItem button onClick={() => history.push('/admin/server')}>
- <ListItemIcon>
- <StorageIcon />
- </ListItemIcon>
- <ListItemText primary={t('settingsServer')} />
- </ListItem>
- <ListItem button onClick={() => history.push('/admin/users')}>
- <ListItemIcon>
- <PeopleIcon />
- </ListItemIcon>
- <ListItemText primary={t('settingsUsers')} />
- </ListItem>
- <ListItem button onClick={() => history.push('/admin/statistics')}>
- <ListItemIcon>
- <BarChartIcon />
- </ListItemIcon>
- <ListItemText primary={t('statisticsTitle')} />
- </ListItem>
- </List>
- </>
- )}
</div>
</Drawer>
</>
diff --git a/modern/src/StartPage.js b/modern/src/StartPage.js
index 3edc5b3..1472f50 100644
--- a/modern/src/StartPage.js
+++ b/modern/src/StartPage.js
@@ -16,7 +16,7 @@ const useStyles = makeStyles((theme) => ({
paddingBottom: theme.spacing(5),
width: theme.dimensions.sidebarWidth,
[theme.breakpoints.down('md')]: {
- width: theme.dimensions.tabletSidebarWidth,
+ width: theme.dimensions.sidebarWidthTablet,
},
[theme.breakpoints.down('xs')]: {
width: '0px',
diff --git a/modern/src/admin/ServerPage.js b/modern/src/admin/ServerPage.js
index 7decac1..ec0034f 100644
--- a/modern/src/admin/ServerPage.js
+++ b/modern/src/admin/ServerPage.js
@@ -8,11 +8,11 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { useHistory } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import t from '../common/localization';
-import MainToolbar from '../MainToolbar';
import { sessionActions } from '../store';
import EditAttributesView from '../attributes/EditAttributesView';
import deviceAttributes from '../attributes/deviceAttributes';
import userAttributes from '../attributes/userAttributes';
+import OptionsLayout from '../settings/OptionsLayout';
const useStyles = makeStyles((theme) => ({
container: {
@@ -51,11 +51,9 @@ const ServerPage = () => {
};
return (
- <>
- <MainToolbar />
+ <OptionsLayout>
<Container maxWidth="xs" className={classes.container}>
- {item
- && (
+ {item && (
<>
<Accordion defaultExpanded>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
@@ -113,7 +111,7 @@ const ServerPage = () => {
</AccordionDetails>
</Accordion>
</>
- )}
+ )}
<FormControl fullWidth margin="normal">
<div className={classes.buttons}>
<Button type="button" color="primary" variant="outlined" onClick={() => history.goBack()}>
@@ -125,7 +123,7 @@ const ServerPage = () => {
</div>
</FormControl>
</Container>
- </>
+ </OptionsLayout>
);
};
diff --git a/modern/src/admin/StatisticsPage.js b/modern/src/admin/StatisticsPage.js
index 131ab7e..1f32c3d 100644
--- a/modern/src/admin/StatisticsPage.js
+++ b/modern/src/admin/StatisticsPage.js
@@ -5,7 +5,7 @@ import {
import moment from 'moment';
import t from '../common/localization';
import { formatDate } from '../common/formatter';
-import ReportLayoutPage from '../reports/ReportLayoutPage';
+import OptionsLayout from '../settings/OptionsLayout';
const Filter = ({ setItems }) => {
const [period, setPeriod] = useState('today');
@@ -98,7 +98,8 @@ const StatisticsPage = () => {
const [items, setItems] = useState([]);
return (
- <ReportLayoutPage filter={<Filter setItems={setItems} />}>
+ <OptionsLayout>
+ <Filter setItems={setItems} />
<TableContainer component={Paper}>
<Table>
<TableHead>
@@ -133,7 +134,7 @@ const StatisticsPage = () => {
</TableBody>
</Table>
</TableContainer>
- </ReportLayoutPage>
+ </OptionsLayout>
);
};
diff --git a/modern/src/admin/UsersPage.js b/modern/src/admin/UsersPage.js
index fc6e673..a8b3c84 100644
--- a/modern/src/admin/UsersPage.js
+++ b/modern/src/admin/UsersPage.js
@@ -3,11 +3,11 @@ import {
TableContainer, Table, TableRow, TableCell, TableHead, TableBody, makeStyles, IconButton,
} from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
-import MainToolbar from '../MainToolbar';
import t from '../common/localization';
import { useEffectAsync } from '../reactHelper';
import EditCollectionView from '../EditCollectionView';
import { formatBoolean } from '../common/formatter';
+import OptionsLayout from '../settings/OptionsLayout';
const useStyles = makeStyles((theme) => ({
columnAction: {
@@ -61,10 +61,9 @@ const UsersView = ({ updateTimestamp, onMenuClick }) => {
};
const UsersPage = () => (
- <>
- <MainToolbar />
+ <OptionsLayout>
<EditCollectionView content={UsersView} editPath="/user" endpoint="users" />
- </>
+ </OptionsLayout>
);
export default UsersPage;
diff --git a/modern/src/attributes/EditAttributesView.js b/modern/src/attributes/EditAttributesView.js
index e38c02a..13ea9bd 100644
--- a/modern/src/attributes/EditAttributesView.js
+++ b/modern/src/attributes/EditAttributesView.js
@@ -52,7 +52,7 @@ const EditAttributesView = ({ attributes, setAttributes, definitions }) => {
const convertToList = (attributes) => {
const booleanList = [];
const otherList = [];
- Object.keys(attributes).forEach((key) => {
+ Object.keys(attributes || []).forEach((key) => {
const value = attributes[key];
const type = getAttributeType(value);
if (type === 'boolean') {
diff --git a/modern/src/common/selectors.js b/modern/src/common/selectors.js
new file mode 100644
index 0000000..44a0c54
--- /dev/null
+++ b/modern/src/common/selectors.js
@@ -0,0 +1,3 @@
+export const getIsAdmin = (state) => state.session.user?.administrator;
+
+export const getUserId = (state) => state.session.user?.id;
diff --git a/modern/src/components/BottomNav.js b/modern/src/components/BottomNav.js
new file mode 100644
index 0000000..6aad1dd
--- /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/components/reports/ReportNavbar.js b/modern/src/components/NavBar.js
index 16807e8..93e79bb 100644
--- a/modern/src/components/reports/ReportNavbar.js
+++ b/modern/src/components/NavBar.js
@@ -3,9 +3,8 @@ import {
AppBar, Toolbar, Typography, IconButton,
} from '@material-ui/core';
import MenuIcon from '@material-ui/icons/Menu';
-import t from '../../common/localization';
-const ReportNavbar = ({ setOpenDrawer, reportTitle }) => (
+const Navbar = ({ setOpenDrawer, title }) => (
<AppBar position="fixed" color="inherit">
<Toolbar>
<IconButton
@@ -17,12 +16,10 @@ const ReportNavbar = ({ setOpenDrawer, reportTitle }) => (
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap>
- {t('reportTitle')}
- {' '}
- {` / ${reportTitle}`}
+ {title}
</Typography>
</Toolbar>
</AppBar>
);
-export default ReportNavbar;
+export default Navbar;
diff --git a/modern/src/components/SideNav.js b/modern/src/components/SideNav.js
new file mode 100644
index 0000000..bcf8ecd
--- /dev/null
+++ b/modern/src/components/SideNav.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import {
+ List, ListItem, ListItemText, ListItemIcon, Divider, ListSubheader,
+} from '@material-ui/core';
+import { Link, useLocation } from 'react-router-dom';
+
+const SideNav = ({ routes }) => {
+ const location = useLocation();
+
+ return (
+ <List disablePadding style={{ paddingTop: '16px' }}>
+ {routes.map((route) => (route.subheader ? (
+ <>
+ <Divider />
+ <ListSubheader>{route.subheader}</ListSubheader>
+ </>
+ ) : (
+ <ListItem
+ disableRipple
+ component={Link}
+ key={route.href || route.subheader}
+ button
+ to={route.href}
+ selected={location.pathname.match(route.match || route.href)}
+ >
+ <ListItemIcon>{route.icon}</ListItemIcon>
+ <ListItemText primary={route.name} />
+ </ListItem>
+ )))}
+ </List>
+ );
+};
+
+export default SideNav;
diff --git a/modern/src/components/registration/LoginForm.js b/modern/src/components/registration/LoginForm.js
index e083541..e6da05e 100644
--- a/modern/src/components/registration/LoginForm.js
+++ b/modern/src/components/registration/LoginForm.js
@@ -41,7 +41,10 @@ const LoginForm = () => {
const handleLogin = async (event) => {
event.preventDefault();
- const response = await fetch('/api/session', { method: 'POST', body: new URLSearchParams(`email=${email}&password=${password}`) });
+ const response = await fetch('/api/session', {
+ method: 'POST',
+ body: new URLSearchParams(`email=${encodeURIComponent(email)}&password=${encodeURIComponent(password)}`),
+ });
if (response.ok) {
const user = await response.json();
dispatch(sessionActions.updateUser(user));
diff --git a/modern/src/components/reports/ReportSidebar.js b/modern/src/components/reports/ReportSidebar.js
deleted file mode 100644
index 686fc04..0000000
--- a/modern/src/components/reports/ReportSidebar.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react';
-import {
- List, ListItem, ListItemText, ListItemIcon,
-} from '@material-ui/core';
-import { Link, useLocation } from 'react-router-dom';
-
-const ReportSidebar = ({ routes }) => {
- const location = useLocation();
-
- return (
- <List disablePadding style={{ paddingTop: '16px' }}>
- {routes.map((route) => (
- <ListItem
- disableRipple
- component={Link}
- key={route}
- button
- to={route.href}
- selected={route.href === location.pathname}
- >
- <ListItemIcon>
- {route.icon}
- </ListItemIcon>
- <ListItemText primary={route.name} />
- </ListItem>
- ))}
- </List>
- );
-};
-
-export default ReportSidebar;
diff --git a/modern/src/map/GeofenceEditMap.js b/modern/src/map/GeofenceEditMap.js
index d639c19..3d1822f 100644
--- a/modern/src/map/GeofenceEditMap.js
+++ b/modern/src/map/GeofenceEditMap.js
@@ -1,3 +1,4 @@
+import 'mapbox-gl/dist/mapbox-gl.css';
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import theme from '@mapbox/mapbox-gl-draw/src/lib/theme';
diff --git a/modern/src/reports/ChartReportPage.js b/modern/src/reports/ChartReportPage.js
index 70ce780..c55ed6d 100644
--- a/modern/src/reports/ChartReportPage.js
+++ b/modern/src/reports/ChartReportPage.js
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import {
Grid, FormControl, InputLabel, Select, MenuItem,
} from '@material-ui/core';
-import ReportLayoutPage from './ReportLayoutPage';
+import ReportLayout from './ReportLayout';
import ReportFilter from './ReportFilter';
import Graph from './Graph';
import { useAttributePreference } from '../common/preferences';
@@ -57,14 +57,14 @@ const ChartReportPage = () => {
const [type, setType] = useState('speed');
return (
- <ReportLayoutPage filter={(
+ <ReportLayout filter={(
<Filter setItems={setItems}>
<ChartType type={type} setType={setType} />
</Filter>
)}
>
<Graph items={items} type={type} />
- </ReportLayoutPage>
+ </ReportLayout>
);
};
diff --git a/modern/src/reports/EventReportPage.js b/modern/src/reports/EventReportPage.js
index 89efcf7..b5c3dae 100644
--- a/modern/src/reports/EventReportPage.js
+++ b/modern/src/reports/EventReportPage.js
@@ -7,7 +7,7 @@ import { useTheme } from '@material-ui/core/styles';
import { useSelector } from 'react-redux';
import { formatDate } from '../common/formatter';
import ReportFilter from './ReportFilter';
-import ReportLayoutPage from './ReportLayoutPage';
+import ReportLayout from './ReportLayout';
import { prefixString } from '../common/stringUtils';
import t from '../common/localization';
@@ -101,14 +101,14 @@ const EventReportPage = () => {
}];
return (
- <ReportLayoutPage filter={<Filter setItems={setItems} />}>
+ <ReportLayout filter={<Filter setItems={setItems} />}>
<DataGrid
rows={items}
columns={columns}
hideFooter
autoHeight
/>
- </ReportLayoutPage>
+ </ReportLayout>
);
};
diff --git a/modern/src/reports/ReportFilter.js b/modern/src/reports/ReportFilter.js
index 1e2eb88..dec8d01 100644
--- a/modern/src/reports/ReportFilter.js
+++ b/modern/src/reports/ReportFilter.js
@@ -58,7 +58,7 @@ const ReportFilter = ({ children, handleSubmit, showOnly }) => {
};
return (
- <Grid container spacing={2}>
+ <Grid container spacing={2} justify="flex-end">
<Grid item xs={12} sm={period === 'custom' ? 3 : 6}>
<FormControl variant="filled" fullWidth>
<InputLabel>{t('reportDevice')}</InputLabel>
diff --git a/modern/src/reports/ReportLayoutPage.js b/modern/src/reports/ReportLayout.js
index d25a887..c231cd8 100644
--- a/modern/src/reports/ReportLayoutPage.js
+++ b/modern/src/reports/ReportLayout.js
@@ -11,8 +11,8 @@ import FormatListBulletedIcon from '@material-ui/icons/FormatListBulleted';
import TrendingUpIcon from '@material-ui/icons/TrendingUp';
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
-import ReportSidebar from '../components/reports/ReportSidebar';
-import ReportNavbar from '../components/reports/ReportNavbar';
+import SideNav from '../components/SideNav';
+import NavBar from '../components/NavBar';
import t from '../common/localization';
const useStyles = makeStyles((theme) => ({
@@ -60,7 +60,7 @@ const routes = [
{ name: t('reportChart'), href: '/reports/chart', icon: <TrendingUpIcon /> },
];
-const ReportLayoutPage = ({ children, filter }) => {
+const ReportLayout = ({ children, filter }) => {
const classes = useStyles();
const history = useHistory();
const location = useLocation();
@@ -79,41 +79,39 @@ const ReportLayoutPage = ({ children, filter }) => {
});
}, [location]);
+ const pageTitle = `${t('reportTitle')} / ${reportTitle}`;
+
return (
<div className={classes.root}>
<Hidden only={['lg', 'xl']}>
- <ReportNavbar setOpenDrawer={setOpenDrawer} reportTitle={reportTitle} />
+ <NavBar setOpenDrawer={setOpenDrawer} title={pageTitle} />
<Drawer
variant="temporary"
open={openDrawer}
onClose={() => setOpenDrawer(!openDrawer)}
classes={{ paper: classes.drawer }}
>
- <ReportSidebar routes={routes} />
+ <SideNav routes={routes} />
</Drawer>
</Hidden>
<Hidden only={['xs', 'sm', 'md']}>
- <div className={classes.drawerContainer}>
- <Drawer
- variant="permanent"
- classes={{ paper: classes.drawer }}
- >
- <div className={classes.drawerHeader}>
- <IconButton
- onClick={() => history.push('/')}
- className={classes.backArrowIconContainer}
- disableRipple
- >
- <ArrowBackIcon />
- </IconButton>
- <Typography variant="h6" color="inherit" noWrap>
- {t('reportTitle')}
- </Typography>
- </div>
- <Divider />
- <ReportSidebar routes={routes} />
- </Drawer>
- </div>
+ <Drawer
+ variant="permanent"
+ classes={{ root: classes.drawerContainer, paper: classes.drawer }}
+ >
+ <div className={classes.drawerHeader}>
+ <IconButton
+ onClick={() => history.push('/')}
+ >
+ <ArrowBackIcon />
+ </IconButton>
+ <Typography variant="h6" color="inherit" noWrap>
+ {t('reportTitle')}
+ </Typography>
+ </div>
+ <Divider />
+ <SideNav routes={routes} />
+ </Drawer>
</Hidden>
<div className={classes.content}>
<div className={classes.toolbar} />
@@ -126,4 +124,4 @@ const ReportLayoutPage = ({ children, filter }) => {
);
};
-export default ReportLayoutPage;
+export default ReportLayout;
diff --git a/modern/src/reports/RouteReportPage.js b/modern/src/reports/RouteReportPage.js
index dce08cb..fffcdcb 100644
--- a/modern/src/reports/RouteReportPage.js
+++ b/modern/src/reports/RouteReportPage.js
@@ -6,7 +6,7 @@ import {
formatDistance, formatSpeed, formatBoolean, formatDate, formatCoordinate,
} from '../common/formatter';
import ReportFilter from './ReportFilter';
-import ReportLayoutPage from './ReportLayoutPage';
+import ReportLayout from './ReportLayout';
import { useAttributePreference, usePreference } from '../common/preferences';
import t from '../common/localization';
@@ -86,7 +86,7 @@ const RouteReportPage = () => {
const [items, setItems] = useState([]);
return (
- <ReportLayoutPage filter={<Filter setItems={setItems} />}>
+ <ReportLayout filter={<Filter setItems={setItems} />}>
<Paper>
<DataGrid
rows={items}
@@ -95,7 +95,7 @@ const RouteReportPage = () => {
autoHeight
/>
</Paper>
- </ReportLayoutPage>
+ </ReportLayout>
);
};
diff --git a/modern/src/reports/StopReportPage.js b/modern/src/reports/StopReportPage.js
index 078a5e5..d2e7e7e 100644
--- a/modern/src/reports/StopReportPage.js
+++ b/modern/src/reports/StopReportPage.js
@@ -5,7 +5,7 @@ import {
formatDistance, formatHours, formatDate, formatVolume,
} from '../common/formatter';
import ReportFilter from './ReportFilter';
-import ReportLayoutPage from './ReportLayoutPage';
+import ReportLayout from './ReportLayout';
import { useAttributePreference } from '../common/preferences';
import t from '../common/localization';
@@ -84,7 +84,7 @@ const StopReportPage = () => {
}];
return (
- <ReportLayoutPage filter={<Filter setItems={setItems} />}>
+ <ReportLayout filter={<Filter setItems={setItems} />}>
<DataGrid
rows={items}
columns={columns}
@@ -92,7 +92,7 @@ const StopReportPage = () => {
autoHeight
getRowId={() => Math.random()}
/>
- </ReportLayoutPage>
+ </ReportLayout>
);
};
diff --git a/modern/src/reports/SummaryReportPage.js b/modern/src/reports/SummaryReportPage.js
index 0e88705..4523e65 100644
--- a/modern/src/reports/SummaryReportPage.js
+++ b/modern/src/reports/SummaryReportPage.js
@@ -6,7 +6,7 @@ import {
formatDistance, formatHours, formatDate, formatSpeed, formatVolume,
} from '../common/formatter';
import ReportFilter from './ReportFilter';
-import ReportLayoutPage from './ReportLayoutPage';
+import ReportLayout from './ReportLayout';
import { useAttributePreference } from '../common/preferences';
import t from '../common/localization';
@@ -103,7 +103,7 @@ const SummaryReportPage = () => {
}];
return (
- <ReportLayoutPage filter={<Filter setItems={setItems} />}>
+ <ReportLayout filter={<Filter setItems={setItems} />}>
<DataGrid
rows={items}
columns={columns}
@@ -111,7 +111,7 @@ const SummaryReportPage = () => {
autoHeight
getRowId={() => Math.random()}
/>
- </ReportLayoutPage>
+ </ReportLayout>
);
};
diff --git a/modern/src/reports/TripReportPage.js b/modern/src/reports/TripReportPage.js
index c10e6b1..f611dde 100644
--- a/modern/src/reports/TripReportPage.js
+++ b/modern/src/reports/TripReportPage.js
@@ -5,7 +5,7 @@ import {
formatDistance, formatSpeed, formatHours, formatDate, formatVolume,
} from '../common/formatter';
import ReportFilter from './ReportFilter';
-import ReportLayoutPage from './ReportLayoutPage';
+import ReportLayout from './ReportLayout';
import { useAttributePreference } from '../common/preferences';
import t from '../common/localization';
@@ -115,7 +115,7 @@ const TripReportPage = () => {
}];
return (
- <ReportLayoutPage filter={<Filter setItems={setItems} />}>
+ <ReportLayout filter={<Filter setItems={setItems} />}>
<DataGrid
rows={items}
columns={columns}
@@ -123,7 +123,7 @@ const TripReportPage = () => {
autoHeight
getRowId={() => Math.random()}
/>
- </ReportLayoutPage>
+ </ReportLayout>
);
};
diff --git a/modern/src/settings/ComputedAttributesPage.js b/modern/src/settings/ComputedAttributesPage.js
index f555259..9f131d3 100644
--- a/modern/src/settings/ComputedAttributesPage.js
+++ b/modern/src/settings/ComputedAttributesPage.js
@@ -4,10 +4,10 @@ import {
} from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import { useSelector } from 'react-redux';
-import MainToolbar from '../MainToolbar';
import t from '../common/localization';
import { useEffectAsync } from '../reactHelper';
import EditCollectionView from '../EditCollectionView';
+import OptionsLayout from './OptionsLayout';
const useStyles = makeStyles((theme) => ({
columnAction: {
@@ -65,10 +65,10 @@ const ComputedAttributeView = ({ updateTimestamp, onMenuClick }) => {
};
const ComputedAttributesPage = () => (
- <>
- <MainToolbar />
+ <OptionsLayout>
+
<EditCollectionView content={ComputedAttributeView} editPath="/settings/attribute" endpoint="attributes/computed" />
- </>
+ </OptionsLayout>
);
export default ComputedAttributesPage;
diff --git a/modern/src/settings/DriversPage.js b/modern/src/settings/DriversPage.js
index 36fc12d..03bf49c 100644
--- a/modern/src/settings/DriversPage.js
+++ b/modern/src/settings/DriversPage.js
@@ -3,10 +3,10 @@ import {
TableContainer, Table, TableRow, TableCell, TableHead, TableBody, makeStyles, IconButton,
} from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
-import MainToolbar from '../MainToolbar';
import t from '../common/localization';
import { useEffectAsync } from '../reactHelper';
import EditCollectionView from '../EditCollectionView';
+import OptionsLayout from './OptionsLayout';
const useStyles = makeStyles((theme) => ({
columnAction: {
@@ -56,10 +56,9 @@ const DriversView = ({ updateTimestamp, onMenuClick }) => {
};
const DriversPage = () => (
- <>
- <MainToolbar />
+ <OptionsLayout>
<EditCollectionView content={DriversView} editPath="/settings/driver" endpoint="drivers" />
- </>
+ </OptionsLayout>
);
export default DriversPage;
diff --git a/modern/src/settings/GroupsPage.js b/modern/src/settings/GroupsPage.js
index d592184..ebebd40 100644
--- a/modern/src/settings/GroupsPage.js
+++ b/modern/src/settings/GroupsPage.js
@@ -3,10 +3,10 @@ import {
TableContainer, Table, TableRow, TableCell, TableHead, TableBody, makeStyles, IconButton,
} from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
-import MainToolbar from '../MainToolbar';
import t from '../common/localization';
import { useEffectAsync } from '../reactHelper';
import EditCollectionView from '../EditCollectionView';
+import OptionsLayout from './OptionsLayout';
const useStyles = makeStyles((theme) => ({
columnAction: {
@@ -54,10 +54,9 @@ const GroupsView = ({ updateTimestamp, onMenuClick }) => {
};
const GroupsPage = () => (
- <>
- <MainToolbar />
+ <OptionsLayout>
<EditCollectionView content={GroupsView} editPath="/settings/group" endpoint="groups" />
- </>
+ </OptionsLayout>
);
export default GroupsPage;
diff --git a/modern/src/settings/MaintenancesPage.js b/modern/src/settings/MaintenancesPage.js
index e598bc9..357d079 100644
--- a/modern/src/settings/MaintenancesPage.js
+++ b/modern/src/settings/MaintenancesPage.js
@@ -3,7 +3,6 @@ import {
TableContainer, Table, TableRow, TableCell, TableHead, TableBody, makeStyles, IconButton,
} from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
-import MainToolbar from '../MainToolbar';
import t from '../common/localization';
import { useEffectAsync } from '../reactHelper';
import EditCollectionView from '../EditCollectionView';
@@ -11,6 +10,7 @@ import EditCollectionView from '../EditCollectionView';
import positionAttributes from '../attributes/positionAttributes';
import { formatDistance, formatSpeed } from '../common/formatter';
import { useAttributePreference } from '../common/preferences';
+import OptionsLayout from './OptionsLayout';
const useStyles = makeStyles((theme) => ({
columnAction: {
@@ -82,10 +82,9 @@ const MaintenancesView = ({ updateTimestamp, onMenuClick }) => {
};
const MaintenacesPage = () => (
- <>
- <MainToolbar />
+ <OptionsLayout>
<EditCollectionView content={MaintenancesView} editPath="/settings/maintenance" endpoint="maintenance" />
- </>
+ </OptionsLayout>
);
export default MaintenacesPage;
diff --git a/modern/src/settings/NotificationsPage.js b/modern/src/settings/NotificationsPage.js
index 4e05292..2f1ee8b 100644
--- a/modern/src/settings/NotificationsPage.js
+++ b/modern/src/settings/NotificationsPage.js
@@ -3,12 +3,12 @@ import {
TableContainer, Table, TableRow, TableCell, TableHead, TableBody, makeStyles, IconButton,
} from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
-import MainToolbar from '../MainToolbar';
import t from '../common/localization';
import { useEffectAsync } from '../reactHelper';
import EditCollectionView from '../EditCollectionView';
import { prefixString } from '../common/stringUtils';
import { formatBoolean } from '../common/formatter';
+import OptionsLayout from './OptionsLayout';
const useStyles = makeStyles((theme) => ({
columnAction: {
@@ -73,10 +73,9 @@ const NotificationsView = ({ updateTimestamp, onMenuClick }) => {
};
const NotificationsPage = () => (
- <>
- <MainToolbar />
+ <OptionsLayout>
<EditCollectionView content={NotificationsView} editPath="/settings/notification" endpoint="notifications" />
- </>
+ </OptionsLayout>
);
export default NotificationsPage;
diff --git a/modern/src/settings/OptionsLayout/index.js b/modern/src/settings/OptionsLayout/index.js
new file mode 100644
index 0000000..4a42e58
--- /dev/null
+++ b/modern/src/settings/OptionsLayout/index.js
@@ -0,0 +1,106 @@
+import React, { useState, useEffect } from 'react';
+import { useLocation } from 'react-router-dom';
+import {
+ Typography,
+ Divider,
+ Drawer,
+ makeStyles,
+ IconButton,
+ Hidden,
+} from '@material-ui/core';
+
+import ArrowBackIcon from '@material-ui/icons/ArrowBack';
+
+import SideNav from '../../components/SideNav';
+import NavBar from '../../components/NavBar';
+import t from '../../common/localization';
+import useRoutes from './useRoutes';
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ display: 'flex',
+ height: '100%',
+ },
+ drawerContainer: {
+ width: theme.dimensions.drawerWidthDesktop,
+ },
+ drawer: {
+ width: theme.dimensions.drawerWidthDesktop,
+ [theme.breakpoints.down('md')]: {
+ width: theme.dimensions.drawerWidthTablet,
+ },
+ },
+ content: {
+ flex: 1,
+ padding: theme.spacing(5, 3, 3, 3),
+ [theme.breakpoints.down('md')]: {
+ paddingTop: theme.spacing(10),
+ },
+ },
+ drawerHeader: {
+ ...theme.mixins.toolbar,
+ display: 'flex',
+ alignItems: 'center',
+ padding: theme.spacing(0, 1),
+ },
+ toolbar: {
+ [theme.breakpoints.down('md')]: {
+ ...theme.mixins.toolbar,
+ },
+ },
+}));
+
+const OptionsLayout = ({ children }) => {
+ const classes = useStyles();
+ const location = useLocation();
+ const [openDrawer, setOpenDrawer] = useState(false);
+ const [optionTitle, setOptionTitle] = useState();
+ const routes = useRoutes();
+
+ useEffect(() => {
+ const activeRoute = routes.find(
+ (route) => route.href && location.pathname.match(route.match || route.href),
+ );
+ setOptionTitle(activeRoute?.name);
+ }, [location, routes]);
+
+ const title = `${t('settingsTitle')} / ${optionTitle}`;
+
+ return (
+ <div className={classes.root}>
+ <Hidden lgUp>
+ <NavBar setOpenDrawer={setOpenDrawer} title={title} />
+ <Drawer
+ variant="temporary"
+ open={openDrawer}
+ onClose={() => setOpenDrawer(!openDrawer)}
+ classes={{ paper: classes.drawer }}
+ >
+ <SideNav routes={routes} />
+ </Drawer>
+ </Hidden>
+
+ <Hidden mdDown>
+ <Drawer
+ variant="permanent"
+ classes={{ root: classes.drawerContainer, paper: classes.drawer }}
+ >
+ <div className={classes.drawerHeader}>
+ <IconButton component="a" href="/">
+ <ArrowBackIcon />
+ </IconButton>
+ <Typography variant="h6" color="inherit" noWrap>
+ {t('settingsTitle')}
+ </Typography>
+ </div>
+ <Divider />
+ <SideNav routes={routes} />
+ </Drawer>
+ </Hidden>
+
+ <div className={classes.content}>{children}</div>
+ </div>
+ );
+};
+
+export default OptionsLayout;
diff --git a/modern/src/settings/OptionsLayout/useRoutes.js b/modern/src/settings/OptionsLayout/useRoutes.js
new file mode 100644
index 0000000..1832081
--- /dev/null
+++ b/modern/src/settings/OptionsLayout/useRoutes.js
@@ -0,0 +1,86 @@
+import React, { useMemo } from 'react';
+import { useSelector } from 'react-redux';
+import CreateIcon from '@material-ui/icons/Create';
+import NotificationsIcon from '@material-ui/icons/Notifications';
+import FolderIcon from '@material-ui/icons/Folder';
+import PersonIcon from '@material-ui/icons/Person';
+import StorageIcon from '@material-ui/icons/Storage';
+import BuildIcon from '@material-ui/icons/Build';
+import PeopleIcon from '@material-ui/icons/People';
+import BarChartIcon from '@material-ui/icons/BarChart';
+import { getIsAdmin, getUserId } from '../../common/selectors';
+import t from '../../common/localization';
+
+const accountRoute = {
+ name: t('settingsUser'),
+ icon: <PersonIcon />,
+};
+
+const adminRoutes = [
+ { subheader: t('userAdmin') },
+ {
+ name: t('settingsServer'),
+ href: '/admin/server',
+ icon: <StorageIcon />,
+ },
+ {
+ name: t('settingsUsers'),
+ href: '/admin/users',
+ icon: <PeopleIcon />,
+ },
+ {
+ name: t('statisticsTitle'),
+ href: '/admin/statistics',
+ icon: <BarChartIcon />,
+ },
+];
+
+const mainRoutes = [
+ accountRoute,
+ {
+ match: 'geofence',
+ name: t('sharedGeofences'),
+ href: '/geofences',
+ icon: <CreateIcon />,
+ },
+ {
+ match: 'notification',
+ name: t('sharedNotifications'),
+ href: '/settings/notifications',
+ icon: <NotificationsIcon />,
+ },
+ {
+ match: 'group',
+ name: t('settingsGroups'),
+ href: '/settings/groups',
+ icon: <FolderIcon />,
+ },
+ {
+ match: 'driver',
+ name: t('sharedDrivers'),
+ href: '/settings/drivers',
+ icon: <PersonIcon />,
+ },
+ {
+ match: 'attribute',
+ name: t('sharedComputedAttributes'),
+ href: '/settings/attributes',
+ icon: <StorageIcon />,
+ },
+ {
+ match: 'maintenance',
+ name: t('sharedMaintenance'),
+ href: '/settings/maintenances',
+ icon: <BuildIcon />,
+ },
+];
+
+export default () => {
+ const isAdmin = useSelector(getIsAdmin);
+ const userId = useSelector(getUserId);
+ accountRoute.href = `/user/${userId}`;
+
+ return useMemo(() => [...mainRoutes, ...(isAdmin ? adminRoutes : [])], [
+ isAdmin,
+ ]);
+};
diff --git a/modern/src/theme/dimensions.js b/modern/src/theme/dimensions.js
index fcdbaee..b6edc5e 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',
};
diff --git a/web/app/store/MapTypes.js b/web/app/store/MapTypes.js
index dd889d4..9cca525 100644
--- a/web/app/store/MapTypes.js
+++ b/web/app/store/MapTypes.js
@@ -38,6 +38,12 @@ Ext.define('Traccar.store.MapTypes', {
key: 'bingHybrid',
name: Strings.mapBingHybrid
}, {
+ key: 'yandexMap',
+ name: Strings.mapYandexMap
+ }, {
+ key: 'yandexSat',
+ name: Strings.mapYandexSat
+ }, {
key: 'custom',
name: Strings.mapCustom
}, {
diff --git a/web/app/view/map/BaseMap.js b/web/app/view/map/BaseMap.js
index 6891598..0107acd 100644
--- a/web/app/view/map/BaseMap.js
+++ b/web/app/view/map/BaseMap.js
@@ -103,9 +103,29 @@ Ext.define('Traccar.view.map.BaseMap', {
})
}),
new ol.layer.Tile({
+ title: Strings.mapYandexMap,
+ type: 'base',
+ visible: type === 'yandexMap',
+ source: new ol.source.XYZ({
+ url: 'https://core-renderer-tiles.maps.yandex.net/tiles?l=map&x={x}&y={y}&z={z}',
+ projection: 'EPSG:3395',
+ attributions: '&copy; <a href="https://yandex.com/maps/">Yandex</a>'
+ })
+ }),
+ new ol.layer.Tile({
+ title: Strings.mapYandexSat,
+ type: 'base',
+ visible: type === 'yandexSat',
+ source: new ol.source.XYZ({
+ url: 'https://core-sat.maps.yandex.net/tiles?l=sat&x={x}&y={y}&z={z}',
+ projection: 'EPSG:3395',
+ attributions: '&copy; <a href="https://yandex.com/maps/">Yandex</a>'
+ })
+ }),
+ new ol.layer.Tile({
title: Strings.mapOsm,
type: 'base',
- visible: type === 'osm' || type === 'yandexMap' || type === 'yandexSat' || type === 'wikimedia' || !type,
+ visible: type === 'osm' || type === 'wikimedia' || !type,
source: new ol.source.OSM({})
})
]
@@ -204,4 +224,12 @@ Ext.define('Traccar.view.map.BaseMap', {
this.map.updateSize();
}
}
+}, function () {
+ var projection;
+ proj4.defs('EPSG:3395', '+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs');
+ ol.proj.proj4.register(proj4);
+ projection = ol.proj.get('EPSG:3395');
+ if (projection) {
+ projection.setExtent([-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244]);
+ }
});