diff options
34 files changed, 403 insertions, 234 deletions
diff --git a/modern/.eslintrc.js b/modern/.eslintrc.js index a1089804..a4cf3053 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 365d9865..66066499 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/styles.css b/modern/public/styles.css index 7e9de656..e7aee866 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/EditItemView.js b/modern/src/EditItemView.js index 9bf91a6a..e067a39f 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 95c7151e..71219c16 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/MainToolbar.js b/modern/src/MainToolbar.js index da7c42c5..59c2eca8 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 3edc5b31..1472f501 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 7decac1a..ec0034f5 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 131ab7e4..1f32c3d9 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 fc6e6736..a8b3c849 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 e38c02ae..13ea9bd4 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 00000000..44a0c547 --- /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/reports/ReportNavbar.js b/modern/src/components/NavBar.js index 16807e89..93e79bbb 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 00000000..bcf8ecd5 --- /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 e083541c..e6da05e3 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 686fc040..00000000 --- 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 d639c192..3d1822fe 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 70ce780f..c55ed6d5 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 89efcf76..b5c3daeb 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 1e2eb887..dec8d018 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 d25a8876..c231cd80 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 dce08cb1..fffcdcb5 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 078a5e5e..d2e7e7ed 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 0e88705f..4523e652 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 c10e6b1a..f611dde1 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 f555259b..9f131d33 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 36fc12d6..03bf49c5 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 d5921844..ebebd40a 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 e598bc90..357d0791 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 4e052927..2f1ee8b7 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 00000000..4a42e583 --- /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 00000000..18320813 --- /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/web/app/store/MapTypes.js b/web/app/store/MapTypes.js index dd889d46..9cca525e 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 6891598d..0107acdc 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: '© <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: '© <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]); + } }); |