From 2cd374bb9fa941d7e2a6fd8aa5079893a158c98f Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 8 May 2022 13:16:57 -0700 Subject: Reorganize remaining files --- modern/src/App.js | 28 +-- modern/src/DevicePage.js | 202 ----------------- modern/src/DevicesList.js | 195 ----------------- modern/src/EditCollectionView.js | 87 -------- modern/src/EditItemView.js | 95 -------- modern/src/EventPage.js | 77 ------- modern/src/GeofencePage.js | 69 ------ modern/src/GeofencesList.js | 56 ----- modern/src/GeofencesPage.js | 87 -------- modern/src/LocalizationProvider.js | 165 -------------- modern/src/MainPage.js | 213 ------------------ modern/src/PositionPage.js | 99 --------- modern/src/RegisterDialog.js | 97 --------- modern/src/SocketController.js | 2 +- modern/src/UserPage.js | 194 ----------------- modern/src/admin/ServerPage.js | 241 --------------------- modern/src/admin/StatisticsPage.js | 145 ------------- modern/src/admin/UsersPage.js | 70 ------ modern/src/attributes/AddAttributeDialog.js | 89 -------- modern/src/attributes/EditAttributesView.js | 203 ----------------- modern/src/attributes/useCommandAttributes.js | 11 - modern/src/attributes/useDeviceAttributes.js | 13 -- modern/src/attributes/useGeofenceAttributes.js | 8 - modern/src/attributes/usePositionAttributes.js | 21 -- modern/src/attributes/useUserAttributes.js | 48 ---- modern/src/common/attributes/AddAttributeDialog.js | 89 ++++++++ modern/src/common/attributes/EditAttributesView.js | 203 +++++++++++++++++ .../src/common/attributes/useCommandAttributes.js | 11 + .../src/common/attributes/useDeviceAttributes.js | 13 ++ .../src/common/attributes/useGeofenceAttributes.js | 8 + .../src/common/attributes/usePositionAttributes.js | 21 ++ modern/src/common/attributes/useUserAttributes.js | 48 ++++ modern/src/common/components/BottomMenu.js | 2 +- modern/src/common/components/LinkField.js | 85 ++++++++ .../src/common/components/LocalizationProvider.js | 165 ++++++++++++++ modern/src/common/components/PositionValue.js | 2 +- modern/src/common/components/RemoveDialog.js | 2 +- modern/src/common/components/SelectField.js | 53 +++++ modern/src/form/LinkField.js | 85 -------- modern/src/form/SelectField.js | 53 ----- modern/src/login/LoginPage.js | 2 +- modern/src/login/RegisterPage.js | 2 +- modern/src/login/ResetPasswordPage.js | 2 +- modern/src/main/DevicesList.js | 195 +++++++++++++++++ modern/src/main/MainPage.js | 213 ++++++++++++++++++ modern/src/main/PositionPage.js | 99 +++++++++ modern/src/main/StatusCard.js | 134 ++++++++++++ modern/src/map/core/Map.js | 2 +- modern/src/map/main/StatusCard.js | 134 ------------ modern/src/other/EventPage.js | 77 +++++++ modern/src/other/GeofencesList.js | 56 +++++ modern/src/other/GeofencesPage.js | 87 ++++++++ modern/src/other/ReplayPage.js | 220 +++++++++++++++++++ modern/src/reports/ChartReportPage.js | 8 +- modern/src/reports/EventReportPage.js | 6 +- modern/src/reports/Graph.js | 33 --- modern/src/reports/ReplayPage.js | 220 ------------------- modern/src/reports/ReportFilter.js | 153 ------------- modern/src/reports/ReportLayout.js | 124 ----------- modern/src/reports/RouteReportPage.js | 6 +- modern/src/reports/StatisticsPage.js | 145 +++++++++++++ modern/src/reports/StopReportPage.js | 6 +- modern/src/reports/SummaryReportPage.js | 6 +- modern/src/reports/TripReportPage.js | 6 +- modern/src/reports/components/Graph.js | 33 +++ modern/src/reports/components/ReportFilter.js | 153 +++++++++++++ modern/src/reports/components/ReportLayout.js | 124 +++++++++++ modern/src/settings/AccumulatorsPage.js | 4 +- modern/src/settings/BaseCommandView.js | 58 ----- modern/src/settings/CalendarPage.js | 6 +- modern/src/settings/CalendarsPage.js | 6 +- modern/src/settings/CommandPage.js | 6 +- modern/src/settings/CommandSendPage.js | 115 ++++++++++ modern/src/settings/CommandsPage.js | 6 +- modern/src/settings/ComputedAttributePage.js | 6 +- modern/src/settings/ComputedAttributesPage.js | 6 +- modern/src/settings/DevicePage.js | 202 +++++++++++++++++ modern/src/settings/DriverPage.js | 6 +- modern/src/settings/DriversPage.js | 6 +- modern/src/settings/GeofencePage.js | 69 ++++++ modern/src/settings/GroupPage.js | 10 +- modern/src/settings/GroupsPage.js | 6 +- modern/src/settings/MaintenancePage.js | 8 +- modern/src/settings/MaintenancesPage.js | 8 +- modern/src/settings/NotificationPage.js | 6 +- modern/src/settings/NotificationsPage.js | 6 +- modern/src/settings/OptionsLayout.js | 148 ------------- modern/src/settings/PreferencesPage.js | 4 +- modern/src/settings/SendCommandPage.js | 115 ---------- modern/src/settings/ServerPage.js | 241 +++++++++++++++++++++ modern/src/settings/UserPage.js | 194 +++++++++++++++++ modern/src/settings/UsersPage.js | 70 ++++++ modern/src/settings/components/BaseCommandView.js | 58 +++++ .../src/settings/components/EditCollectionView.js | 87 ++++++++ modern/src/settings/components/EditItemView.js | 95 ++++++++ modern/src/settings/components/OptionsLayout.js | 148 +++++++++++++ 96 files changed, 3602 insertions(+), 3699 deletions(-) delete mode 100644 modern/src/DevicePage.js delete mode 100644 modern/src/DevicesList.js delete mode 100644 modern/src/EditCollectionView.js delete mode 100644 modern/src/EditItemView.js delete mode 100644 modern/src/EventPage.js delete mode 100644 modern/src/GeofencePage.js delete mode 100644 modern/src/GeofencesList.js delete mode 100644 modern/src/GeofencesPage.js delete mode 100644 modern/src/LocalizationProvider.js delete mode 100644 modern/src/MainPage.js delete mode 100644 modern/src/PositionPage.js delete mode 100644 modern/src/RegisterDialog.js delete mode 100644 modern/src/UserPage.js delete mode 100644 modern/src/admin/ServerPage.js delete mode 100644 modern/src/admin/StatisticsPage.js delete mode 100644 modern/src/admin/UsersPage.js delete mode 100644 modern/src/attributes/AddAttributeDialog.js delete mode 100644 modern/src/attributes/EditAttributesView.js delete mode 100644 modern/src/attributes/useCommandAttributes.js delete mode 100644 modern/src/attributes/useDeviceAttributes.js delete mode 100644 modern/src/attributes/useGeofenceAttributes.js delete mode 100644 modern/src/attributes/usePositionAttributes.js delete mode 100644 modern/src/attributes/useUserAttributes.js create mode 100644 modern/src/common/attributes/AddAttributeDialog.js create mode 100644 modern/src/common/attributes/EditAttributesView.js create mode 100644 modern/src/common/attributes/useCommandAttributes.js create mode 100644 modern/src/common/attributes/useDeviceAttributes.js create mode 100644 modern/src/common/attributes/useGeofenceAttributes.js create mode 100644 modern/src/common/attributes/usePositionAttributes.js create mode 100644 modern/src/common/attributes/useUserAttributes.js create mode 100644 modern/src/common/components/LinkField.js create mode 100644 modern/src/common/components/LocalizationProvider.js create mode 100644 modern/src/common/components/SelectField.js delete mode 100644 modern/src/form/LinkField.js delete mode 100644 modern/src/form/SelectField.js create mode 100644 modern/src/main/DevicesList.js create mode 100644 modern/src/main/MainPage.js create mode 100644 modern/src/main/PositionPage.js create mode 100644 modern/src/main/StatusCard.js delete mode 100644 modern/src/map/main/StatusCard.js create mode 100644 modern/src/other/EventPage.js create mode 100644 modern/src/other/GeofencesList.js create mode 100644 modern/src/other/GeofencesPage.js create mode 100644 modern/src/other/ReplayPage.js delete mode 100644 modern/src/reports/Graph.js delete mode 100644 modern/src/reports/ReplayPage.js delete mode 100644 modern/src/reports/ReportFilter.js delete mode 100644 modern/src/reports/ReportLayout.js create mode 100644 modern/src/reports/StatisticsPage.js create mode 100644 modern/src/reports/components/Graph.js create mode 100644 modern/src/reports/components/ReportFilter.js create mode 100644 modern/src/reports/components/ReportLayout.js delete mode 100644 modern/src/settings/BaseCommandView.js create mode 100644 modern/src/settings/CommandSendPage.js create mode 100644 modern/src/settings/DevicePage.js create mode 100644 modern/src/settings/GeofencePage.js delete mode 100644 modern/src/settings/OptionsLayout.js delete mode 100644 modern/src/settings/SendCommandPage.js create mode 100644 modern/src/settings/ServerPage.js create mode 100644 modern/src/settings/UserPage.js create mode 100644 modern/src/settings/UsersPage.js create mode 100644 modern/src/settings/components/BaseCommandView.js create mode 100644 modern/src/settings/components/EditCollectionView.js create mode 100644 modern/src/settings/components/EditItemView.js create mode 100644 modern/src/settings/components/OptionsLayout.js (limited to 'modern/src') diff --git a/modern/src/App.js b/modern/src/App.js index 4eb73211..5c5c2591 100644 --- a/modern/src/App.js +++ b/modern/src/App.js @@ -4,20 +4,20 @@ import { Switch, Route, useHistory } from 'react-router-dom'; import CssBaseline from '@material-ui/core/CssBaseline'; import { useDispatch, useSelector } from 'react-redux'; import { makeStyles, LinearProgress, useMediaQuery } from '@material-ui/core'; -import MainPage from './MainPage'; +import MainPage from './main/MainPage'; import RouteReportPage from './reports/RouteReportPage'; -import ServerPage from './admin/ServerPage'; -import UsersPage from './admin/UsersPage'; -import DevicePage from './DevicePage'; -import UserPage from './UserPage'; +import ServerPage from './settings/ServerPage'; +import UsersPage from './settings/UsersPage'; +import DevicePage from './settings/DevicePage'; +import UserPage from './settings/UserPage'; import SocketController from './SocketController'; import NotificationsPage from './settings/NotificationsPage'; import NotificationPage from './settings/NotificationPage'; import GroupsPage from './settings/GroupsPage'; import GroupPage from './settings/GroupPage'; -import PositionPage from './PositionPage'; +import PositionPage from './main/PositionPage'; import EventReportPage from './reports/EventReportPage'; -import ReplayPage from './reports/ReplayPage'; +import ReplayPage from './other/ReplayPage'; import TripReportPage from './reports/TripReportPage'; import StopReportPage from './reports/StopReportPage'; import SummaryReportPage from './reports/SummaryReportPage'; @@ -32,7 +32,7 @@ import MaintenancesPage from './settings/MaintenancesPage'; import MaintenancePage from './settings/MaintenancePage'; import CommandsPage from './settings/CommandsPage'; import CommandPage from './settings/CommandPage'; -import StatisticsPage from './admin/StatisticsPage'; +import StatisticsPage from './reports/StatisticsPage'; import CachingController from './CachingController'; import LoginPage from './login/LoginPage'; @@ -40,17 +40,17 @@ import RegisterPage from './login/RegisterPage'; import ResetPasswordPage from './login/ResetPasswordPage'; import theme from './common/theme'; -import GeofencesPage from './GeofencesPage'; -import GeofencePage from './GeofencePage'; -import { LocalizationProvider } from './LocalizationProvider'; +import GeofencesPage from './other/GeofencesPage'; +import GeofencePage from './settings/GeofencePage'; +import { LocalizationProvider } from './common/components/LocalizationProvider'; import useQuery from './common/util/useQuery'; import { useEffectAsync } from './reactHelper'; import { devicesActions } from './store'; -import EventPage from './EventPage'; +import EventPage from './other/EventPage'; import PreferencesPage from './settings/PreferencesPage'; import BottomMenu from './common/components/BottomMenu'; import AccumulatorsPage from './settings/AccumulatorsPage'; -import SendCommandPage from './settings/SendCommandPage'; +import CommandSendPage from './settings/CommandSendPage'; const useStyles = makeStyles(() => ({ root: { @@ -141,7 +141,7 @@ const App = () => { - + diff --git a/modern/src/DevicePage.js b/modern/src/DevicePage.js deleted file mode 100644 index c57faa84..00000000 --- a/modern/src/DevicePage.js +++ /dev/null @@ -1,202 +0,0 @@ -import React, { useState } from 'react'; -import TextField from '@material-ui/core/TextField'; - -import { - Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, FormControlLabel, Checkbox, -} from '@material-ui/core'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import EditItemView from './EditItemView'; -import EditAttributesView from './attributes/EditAttributesView'; -import SelectField from './form/SelectField'; -import deviceCategories from './common/util/deviceCategories'; -import LinkField from './form/LinkField'; -import { prefixString } from './common/util/stringUtils'; -import { useTranslation } from './LocalizationProvider'; -import useDeviceAttributes from './attributes/useDeviceAttributes'; -import { useAdministrator } from './common/util/permissions'; - -const useStyles = makeStyles(() => ({ - details: { - flexDirection: 'column', - }, -})); - -const DevicePage = () => { - const classes = useStyles(); - const t = useTranslation(); - - const admin = useAdministrator(); - - const deviceAttributes = useDeviceAttributes(t); - - const [item, setItem] = useState(); - - const validate = () => item && item.name && item.uniqueId; - - return ( - - {item && ( - <> - - }> - - {t('sharedRequired')} - - - - setItem({ ...item, name: event.target.value })} - label={t('sharedName')} - variant="filled" - /> - setItem({ ...item, uniqueId: event.target.value })} - label={t('deviceIdentifier')} - variant="filled" - /> - - - - }> - - {t('sharedExtra')} - - - - setItem({ ...item, groupId: Number(event.target.value) })} - endpoint="/api/groups" - label={t('groupParent')} - variant="filled" - /> - setItem({ ...item, phone: event.target.value })} - label={t('sharedPhone')} - variant="filled" - /> - setItem({ ...item, model: event.target.value })} - label={t('deviceModel')} - variant="filled" - /> - setItem({ ...item, contact: event.target.value })} - label={t('deviceContact')} - variant="filled" - /> - setItem({ ...item, category: event.target.value })} - data={deviceCategories.map((category) => ({ - id: category, - name: t(`category${category.replace(/^\w/, (c) => c.toUpperCase())}`), - }))} - label={t('deviceCategory')} - variant="filled" - /> - {admin && ( - setItem({ ...item, disabled: event.target.checked })} />} - label={t('sharedDisabled')} - /> - )} - - - - }> - - {t('sharedAttributes')} - - - - setItem({ ...item, attributes })} - definitions={deviceAttributes} - /> - - - {item.id && ( - - }> - - {t('sharedConnections')} - - - - - t(prefixString('event', it.type))} - label={t('sharedNotifications')} - variant="filled" - /> - - it.description} - label={t('sharedComputedAttributes')} - variant="filled" - /> - - - - )} - - )} - - ); -}; - -export default DevicePage; diff --git a/modern/src/DevicesList.js b/modern/src/DevicesList.js deleted file mode 100644 index 6eeaec41..00000000 --- a/modern/src/DevicesList.js +++ /dev/null @@ -1,195 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { makeStyles } from '@material-ui/core/styles'; -import { IconButton, Tooltip } from '@material-ui/core'; -import Avatar from '@material-ui/core/Avatar'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemAvatar from '@material-ui/core/ListItemAvatar'; -import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; -import ListItemText from '@material-ui/core/ListItemText'; -import { FixedSizeList } from 'react-window'; -import AutoSizer from 'react-virtualized-auto-sizer'; -import BatteryFullIcon from '@material-ui/icons/BatteryFull'; -import BatteryChargingFullIcon from '@material-ui/icons/BatteryChargingFull'; -import Battery60Icon from '@material-ui/icons/Battery60'; -import BatteryCharging60Icon from '@material-ui/icons/BatteryCharging60'; -import Battery20Icon from '@material-ui/icons/Battery20'; -import BatteryCharging20Icon from '@material-ui/icons/BatteryCharging20'; -import FlashOnIcon from '@material-ui/icons/FlashOn'; -import FlashOffIcon from '@material-ui/icons/FlashOff'; -import ErrorIcon from '@material-ui/icons/Error'; - -import { devicesActions } from './store'; -import EditCollectionView from './EditCollectionView'; -import { useEffectAsync } from './reactHelper'; -import { - formatAlarm, formatBoolean, formatPercentage, formatStatus, getStatusColor, -} from './common/util/formatter'; -import { useTranslation } from './LocalizationProvider'; - -const useStyles = makeStyles((theme) => ({ - list: { - maxHeight: '100%', - }, - listInner: { - position: 'relative', - margin: theme.spacing(1.5, 0), - }, - icon: { - width: '25px', - height: '25px', - filter: 'brightness(0) invert(1)', - }, - listItem: { - backgroundColor: 'white', - '&:hover': { - backgroundColor: 'white', - }, - }, - batteryText: { - fontSize: '0.75rem', - fontWeight: 'normal', - lineHeight: '0.875rem', - }, - positive: { - color: theme.palette.colors.positive, - }, - medium: { - color: theme.palette.colors.medium, - }, - negative: { - color: theme.palette.colors.negative, - }, - neutral: { - color: theme.palette.colors.neutral, - }, - indicators: { - lineHeight: 1, - }, -})); - -const DeviceRow = ({ data, index, style }) => { - const classes = useStyles(); - const dispatch = useDispatch(); - const t = useTranslation(); - - const { items } = data; - const item = items[index]; - const position = useSelector((state) => state.positions.items[item.id]); - - return ( -
- dispatch(devicesActions.select(item.id))}> - - - - - - - - {position && ( - <> - {position.attributes.hasOwnProperty('alarm') && ( - - - - - - )} - {position.attributes.hasOwnProperty('ignition') && ( - - - {position.attributes.ignition ? ( - - ) : ( - - )} - - - )} - {position.attributes.hasOwnProperty('batteryLevel') && ( - - - {position.attributes.batteryLevel > 70 ? ( - position.attributes.charge - ? () - : () - ) : position.attributes.batteryLevel > 30 ? ( - position.attributes.charge - ? () - : () - ) : ( - position.attributes.charge - ? () - : () - )} - - - )} - - )} - - -
- ); -}; - -const DeviceView = ({ updateTimestamp, onMenuClick, filter }) => { - const classes = useStyles(); - const dispatch = useDispatch(); - const listInnerEl = useRef(null); - - const items = useSelector((state) => state.devices.items); - const [filteredItems, setFilteredItems] = useState(null); - - useEffect(() => { - const array = Object.values(items); - setFilteredItems( - filter.trim().length > 0 - ? array.filter((item) => `${item.name} ${item.uniqueId}`.toLowerCase().includes(filter?.toLowerCase())) - : array, - ); - }, [filter, items]); - - if (listInnerEl.current) { - listInnerEl.current.className = classes.listInner; - } - - useEffectAsync(async () => { - const response = await fetch('/api/devices'); - if (response.ok) { - dispatch(devicesActions.refresh(await response.json())); - } - }, [updateTimestamp]); - - return ( - - {({ height, width }) => ( - - - {DeviceRow} - - - )} - - ); -}; - -const DevicesList = ({ filter }) => ( - -); - -export default DevicesList; diff --git a/modern/src/EditCollectionView.js b/modern/src/EditCollectionView.js deleted file mode 100644 index 9ed0362a..00000000 --- a/modern/src/EditCollectionView.js +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useState } from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import { useHistory } from 'react-router-dom'; -import Menu from '@material-ui/core/Menu'; -import MenuItem from '@material-ui/core/MenuItem'; -import Fab from '@material-ui/core/Fab'; -import AddIcon from '@material-ui/icons/Add'; - -import RemoveDialog from './common/components/RemoveDialog'; -import { useTranslation } from './LocalizationProvider'; -import dimensions from './common/theme/dimensions'; -import { useEditable } from './common/util/permissions'; - -const useStyles = makeStyles((theme) => ({ - fab: { - position: 'fixed', - bottom: theme.spacing(2), - right: theme.spacing(2), - [theme.breakpoints.down('sm')]: { - bottom: dimensions.bottomBarHeight + theme.spacing(2), - }, - }, -})); - -const EditCollectionView = ({ - content, editPath, endpoint, disableAdd, filter, -}) => { - const classes = useStyles(); - const history = useHistory(); - const t = useTranslation(); - - const editable = useEditable(); - - const [selectedId, setSelectedId] = useState(null); - const [selectedAnchorEl, setSelectedAnchorEl] = useState(null); - const [removeDialogShown, setRemoveDialogShown] = useState(false); - const [updateTimestamp, setUpdateTimestamp] = useState(Date.now()); - - const menuShow = (anchorId, itemId) => { - setSelectedAnchorEl(anchorId); - setSelectedId(itemId); - }; - - const menuHide = () => { - setSelectedAnchorEl(null); - }; - - const handleAdd = () => { - history.push(editPath); - menuHide(); - }; - - const handleEdit = () => { - history.push(`${editPath}/${selectedId}`); - menuHide(); - }; - - const handleRemove = () => { - setRemoveDialogShown(true); - menuHide(); - }; - - const handleRemoveResult = () => { - setRemoveDialogShown(false); - setUpdateTimestamp(Date.now()); - }; - - const Content = content; - - return ( - <> - - {editable && !disableAdd && ( - - - - )} - - {t('sharedEdit')} - {t('sharedRemove')} - - - - ); -}; - -export default EditCollectionView; diff --git a/modern/src/EditItemView.js b/modern/src/EditItemView.js deleted file mode 100644 index 550369fc..00000000 --- a/modern/src/EditItemView.js +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react'; -import { useHistory, useParams } from 'react-router-dom'; -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 { useEffectAsync } from './reactHelper'; -import OptionsLayout from './settings/OptionsLayout'; -import { useTranslation } from './LocalizationProvider'; - -const useStyles = makeStyles((theme) => ({ - container: { - marginTop: theme.spacing(2), - }, - buttons: { - display: 'flex', - justifyContent: 'space-evenly', - '& > *': { - flexBasis: '33%', - }, - }, -})); - -const EditItemView = ({ - children, endpoint, item, setItem, validate, onItemSaved, -}) => { - const history = useHistory(); - const classes = useStyles(); - const t = useTranslation(); - - const { id } = useParams(); - - useEffectAsync(async () => { - if (id) { - const response = await fetch(`/api/${endpoint}/${id}`); - if (response.ok) { - setItem(await response.json()); - } - } else { - setItem({}); - } - }, [id]); - - const handleSave = async () => { - let url = `/api/${endpoint}`; - if (id) { - url += `/${id}`; - } - - const response = await fetch(url, { - method: !id ? 'POST' : 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(item), - }); - - if (response.ok) { - if (onItemSaved) { - onItemSaved(await response.json()); - } - history.goBack(); - } - }; - - return ( - - - {children} - -
- - -
-
-
-
- ); -}; - -export default EditItemView; diff --git a/modern/src/EventPage.js b/modern/src/EventPage.js deleted file mode 100644 index 2131baab..00000000 --- a/modern/src/EventPage.js +++ /dev/null @@ -1,77 +0,0 @@ -import React, { useState } from 'react'; - -import { - makeStyles, Typography, AppBar, Toolbar, IconButton, -} from '@material-ui/core'; -import ArrowBackIcon from '@material-ui/icons/ArrowBack'; -import { useHistory, useParams } from 'react-router-dom'; -import ContainerDimensions from 'react-container-dimensions'; -import { useEffectAsync } from './reactHelper'; -import { useTranslation } from './LocalizationProvider'; -import Map from './map/core/Map'; -import PositionsMap from './map/PositionsMap'; - -const useStyles = makeStyles(() => ({ - root: { - height: '100%', - display: 'flex', - flexDirection: 'column', - }, - mapContainer: { - flexGrow: 1, - }, -})); - -const EventPage = () => { - const classes = useStyles(); - const history = useHistory(); - const t = useTranslation(); - - const { id } = useParams(); - - const [event, setEvent] = useState(); - const [position, setPosition] = useState(); - - useEffectAsync(async () => { - if (id) { - const response = await fetch(`/api/events/${id}`); - if (response.ok) { - setEvent(await response.json()); - } - } - }, [id]); - - useEffectAsync(async () => { - if (event && event.positionId) { - const response = await fetch(`/api/positions?id=${event.positionId}`); - if (response.ok) { - const positions = await response.json(); - if (positions.length > 0) { - setPosition(positions[0]); - } - } - } - }, [event]); - - return ( -
- - - history.push('/')}> - - - {t('positionEvent')} - - -
- - - {position && } - - -
-
- ); -}; - -export default EventPage; diff --git a/modern/src/GeofencePage.js b/modern/src/GeofencePage.js deleted file mode 100644 index 89762b60..00000000 --- a/modern/src/GeofencePage.js +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useState } from 'react'; -import TextField from '@material-ui/core/TextField'; - -import { - Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, -} from '@material-ui/core'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import EditItemView from './EditItemView'; -import EditAttributesView from './attributes/EditAttributesView'; -import { useTranslation } from './LocalizationProvider'; -import useGeofenceAttributes from './attributes/useGeofenceAttributes'; - -const useStyles = makeStyles(() => ({ - details: { - flexDirection: 'column', - }, -})); - -const GeofencePage = () => { - const classes = useStyles(); - const t = useTranslation(); - - const geofenceAttributes = useGeofenceAttributes(t); - - const [item, setItem] = useState(); - - const validate = () => item && item.name; - - return ( - - {item && ( - <> - - }> - - {t('sharedRequired')} - - - - setItem({ ...item, name: event.target.value })} - label={t('sharedName')} - variant="filled" - /> - - - - }> - - {t('sharedAttributes')} - - - - setItem({ ...item, attributes })} - definitions={geofenceAttributes} - /> - - - - )} - - ); -}; - -export default GeofencePage; diff --git a/modern/src/GeofencesList.js b/modern/src/GeofencesList.js deleted file mode 100644 index 73bcb520..00000000 --- a/modern/src/GeofencesList.js +++ /dev/null @@ -1,56 +0,0 @@ -import React, { Fragment } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { makeStyles } from '@material-ui/core/styles'; -import Divider from '@material-ui/core/Divider'; -import IconButton from '@material-ui/core/IconButton'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; -import ListItemText from '@material-ui/core/ListItemText'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; - -import { devicesActions } from './store'; -import EditCollectionView from './EditCollectionView'; - -const useStyles = makeStyles(() => ({ - list: { - maxHeight: '100%', - overflow: 'auto', - }, - icon: { - width: '25px', - height: '25px', - filter: 'brightness(0) invert(1)', - }, -})); - -const GeofenceView = ({ onMenuClick }) => { - const classes = useStyles(); - const dispatch = useDispatch(); - - const items = useSelector((state) => state.geofences.items); - - return ( - - {Object.values(items).map((item, index, list) => ( - - dispatch(devicesActions.select(item.id))}> - - - onMenuClick(event.currentTarget, item.id)}> - - - - - {index < list.length - 1 ? : null} - - ))} - - ); -}; - -const GeofencesList = () => ( - -); - -export default GeofencesList; diff --git a/modern/src/GeofencesPage.js b/modern/src/GeofencesPage.js deleted file mode 100644 index a616d8f0..00000000 --- a/modern/src/GeofencesPage.js +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react'; -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 ArrowBackIcon from '@material-ui/icons/ArrowBack'; -import { useHistory } from 'react-router-dom'; -import Map from './map/core/Map'; -import CurrentLocationMap from './map/CurrentLocationMap'; -import GeofenceEditMap from './map/GeofenceEditMap'; -import GeofencesList from './GeofencesList'; -import { useTranslation } from './LocalizationProvider'; - -const useStyles = makeStyles((theme) => ({ - root: { - height: '100%', - display: 'flex', - flexDirection: 'column', - }, - content: { - flexGrow: 1, - overflow: 'hidden', - display: 'flex', - flexDirection: 'row', - [theme.breakpoints.down('xs')]: { - flexDirection: 'column-reverse', - }, - }, - drawerPaper: { - position: 'relative', - [theme.breakpoints.up('sm')]: { - width: 350, - }, - [theme.breakpoints.down('xs')]: { - height: 250, - }, - }, - drawerHeader: { - ...theme.mixins.toolbar, - display: 'flex', - alignItems: 'center', - padding: theme.spacing(0, 1), - }, - mapContainer: { - flexGrow: 1, - }, -})); - -const GeofencesPage = ({ width }) => { - const classes = useStyles(); - const history = useHistory(); - const t = useTranslation(); - - return ( -
-
- -
- history.goBack()}> - - - - {t('sharedGeofences')} - -
- - -
-
- - - - - - -
-
-
- ); -}; - -export default withWidth()(GeofencesPage); diff --git a/modern/src/LocalizationProvider.js b/modern/src/LocalizationProvider.js deleted file mode 100644 index 33f3df8e..00000000 --- a/modern/src/LocalizationProvider.js +++ /dev/null @@ -1,165 +0,0 @@ -import React, { createContext, useContext, useMemo } from 'react'; -import usePersistedState from './common/util/usePersistedState'; - -import af from '../../web/l10n/af.json'; -import ar from '../../web/l10n/ar.json'; -import az from '../../web/l10n/az.json'; -import bg from '../../web/l10n/bg.json'; -import bn from '../../web/l10n/bn.json'; -import cs from '../../web/l10n/cs.json'; -import da from '../../web/l10n/da.json'; -import de from '../../web/l10n/de.json'; -import el from '../../web/l10n/el.json'; -import en from '../../web/l10n/en.json'; -import es from '../../web/l10n/es.json'; -import fa from '../../web/l10n/fa.json'; -import fi from '../../web/l10n/fi.json'; -import fr from '../../web/l10n/fr.json'; -import he from '../../web/l10n/he.json'; -import hi from '../../web/l10n/hi.json'; -import hr from '../../web/l10n/hr.json'; -import hu from '../../web/l10n/hu.json'; -import id from '../../web/l10n/id.json'; -import it from '../../web/l10n/it.json'; -import ja from '../../web/l10n/ja.json'; -import ka from '../../web/l10n/ka.json'; -import kk from '../../web/l10n/kk.json'; -import km from '../../web/l10n/km.json'; -import ko from '../../web/l10n/ko.json'; -import lo from '../../web/l10n/lo.json'; -import lt from '../../web/l10n/lt.json'; -import lv from '../../web/l10n/lv.json'; -import ml from '../../web/l10n/ml.json'; -import mn from '../../web/l10n/mn.json'; -import ms from '../../web/l10n/ms.json'; -import nb from '../../web/l10n/nb.json'; -import ne from '../../web/l10n/ne.json'; -import nl from '../../web/l10n/nl.json'; -import nn from '../../web/l10n/nn.json'; -import pl from '../../web/l10n/pl.json'; -import pt from '../../web/l10n/pt.json'; -import ptBR from '../../web/l10n/pt_BR.json'; -import ro from '../../web/l10n/ro.json'; -import ru from '../../web/l10n/ru.json'; -import si from '../../web/l10n/si.json'; -import sk from '../../web/l10n/sk.json'; -import sl from '../../web/l10n/sl.json'; -import sq from '../../web/l10n/sq.json'; -import sr from '../../web/l10n/sr.json'; -import sv from '../../web/l10n/sv.json'; -import ta from '../../web/l10n/ta.json'; -import th from '../../web/l10n/th.json'; -import tr from '../../web/l10n/tr.json'; -import uk from '../../web/l10n/uk.json'; -import uz from '../../web/l10n/uz.json'; -import vi from '../../web/l10n/vi.json'; -import zh from '../../web/l10n/zh.json'; -import zhTW from '../../web/l10n/zh_TW.json'; - -const languages = { - af: { data: af, name: 'Afrikaans' }, - ar: { data: ar, name: 'العربية' }, - az: { data: az, name: 'Azərbaycanca' }, - bg: { data: bg, name: 'Български' }, - bn: { data: bn, name: 'বাংলা' }, - cs: { data: cs, name: 'Čeština' }, - de: { data: de, name: 'Deutsch' }, - da: { data: da, name: 'Dansk' }, - el: { data: el, name: 'Ελληνικά' }, - en: { data: en, name: 'English' }, - es: { data: es, name: 'Español' }, - fa: { data: fa, name: 'فارسی' }, - fi: { data: fi, name: 'Suomi' }, - fr: { data: fr, name: 'Français' }, - he: { data: he, name: 'עברית' }, - hi: { data: hi, name: 'हिन्दी' }, - hr: { data: hr, name: 'Hrvatski' }, - hu: { data: hu, name: 'Magyar' }, - id: { data: id, name: 'Bahasa Indonesia' }, - it: { data: it, name: 'Italiano' }, - ja: { data: ja, name: '日本語' }, - ka: { data: ka, name: 'ქართული' }, - kk: { data: kk, name: 'Қазақша' }, - ko: { data: ko, name: '한국어' }, - km: { data: km, name: 'ភាសាខ្មែរ' }, - lo: { data: lo, name: 'ລາວ' }, - lt: { data: lt, name: 'Lietuvių' }, - lv: { data: lv, name: 'Latviešu' }, - ml: { data: ml, name: 'മലയാളം' }, - mn: { data: mn, name: 'Монгол хэл' }, - ms: { data: ms, name: 'بهاس ملايو' }, - nb: { data: nb, name: 'Norsk bokmål' }, - ne: { data: ne, name: 'नेपाली' }, - nl: { data: nl, name: 'Nederlands' }, - nn: { data: nn, name: 'Norsk nynorsk' }, - pl: { data: pl, name: 'Polski' }, - pt: { data: pt, name: 'Português' }, - ptBR: { data: ptBR, name: 'Português (Brasil)' }, - ro: { data: ro, name: 'Română' }, - ru: { data: ru, name: 'Русский' }, - si: { data: si, name: 'සිංහල' }, - sk: { data: sk, name: 'Slovenčina' }, - sl: { data: sl, name: 'Slovenščina' }, - sq: { data: sq, name: 'Shqipëria' }, - sr: { data: sr, name: 'Srpski' }, - sv: { data: sv, name: 'Svenska' }, - ta: { data: ta, name: 'தமிழ்' }, - th: { data: th, name: 'ไทย' }, - tr: { data: tr, name: 'Türkçe' }, - uk: { data: uk, name: 'Українська' }, - uz: { data: uz, name: 'Oʻzbekcha' }, - vi: { data: vi, name: 'Tiếng Việt' }, - zh: { data: zh, name: '中文' }, - zhTW: { data: zhTW, name: '中文 (Taiwan)' }, -}; - -const getDefaultLanguage = () => { - const browserLanguages = window.navigator.languages ? window.navigator.languages.slice() : []; - const browserLanguage = window.navigator.userLanguage || window.navigator.language; - browserLanguages.push(browserLanguage); - browserLanguages.push(browserLanguage.substring(0, 2)); - - for (let i = 0; i < browserLanguages.length; i += 1) { - let language = browserLanguages[i].replace('-', ''); - if (language in languages) { - return language; - } - if (language.length > 2) { - language = language.substring(0, 2); - if (language in languages) { - return language; - } - } - } - return 'en'; -}; - -const LocalizationContext = createContext({ - languages, - language: 'en', - setLanguage: () => {}, -}); - -export const LocalizationProvider = ({ children }) => { - const [language, setLanguage] = usePersistedState('language', getDefaultLanguage()); - - return ( - - {children} - - ); -}; - -export const useLocalization = () => useContext(LocalizationContext); - -export const useTranslation = () => { - const context = useContext(LocalizationContext); - const { data } = context.languages[context.language]; - return useMemo(() => (key) => data[key], [data]); -}; - -export const useTranslationKeys = (predicate) => { - const context = useContext(LocalizationContext); - const { data } = context.languages[context.language]; - return Object.keys(data).filter(predicate); -}; diff --git a/modern/src/MainPage.js b/modern/src/MainPage.js deleted file mode 100644 index 70d2a32b..00000000 --- a/modern/src/MainPage.js +++ /dev/null @@ -1,213 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useHistory } from 'react-router-dom'; -import { - makeStyles, Paper, Toolbar, TextField, IconButton, Button, -} from '@material-ui/core'; - -import { useTheme } from '@material-ui/core/styles'; -import useMediaQuery from '@material-ui/core/useMediaQuery'; -import AddIcon from '@material-ui/icons/Add'; -import CloseIcon from '@material-ui/icons/Close'; -import ArrowBackIcon from '@material-ui/icons/ArrowBack'; -import ListIcon from '@material-ui/icons/ViewList'; - -import { useDispatch, useSelector } from 'react-redux'; -import DevicesList from './DevicesList'; -import Map from './map/core/Map'; -import SelectedDeviceMap from './map/main/SelectedDeviceMap'; -import AccuracyMap from './map/main/AccuracyMap'; -import GeofenceMap from './map/main/GeofenceMap'; -import CurrentPositionsMap from './map/main/CurrentPositionsMap'; -import CurrentLocationMap from './map/CurrentLocationMap'; -import BottomMenu from './common/components/BottomMenu'; -import { useTranslation } from './LocalizationProvider'; -import PoiMap from './map/main/PoiMap'; -import MapPadding from './map/MapPadding'; -import StatusCard from './map/main/StatusCard'; -import { devicesActions } from './store'; -import DefaultCameraMap from './map/main/DefaultCameraMap'; -import usePersistedState from './common/util/usePersistedState'; -import LiveRoutesMap from './map/main/LiveRoutesMap'; -import { useDeviceReadonly } from './common/util/permissions'; - -const useStyles = makeStyles((theme) => ({ - root: { - height: '100%', - }, - sidebar: { - display: 'flex', - flexDirection: 'column', - position: 'absolute', - left: 0, - top: 0, - zIndex: 3, - margin: theme.spacing(1.5), - width: theme.dimensions.drawerWidthDesktop, - bottom: theme.dimensions.bottomBarHeight, - transition: 'transform .5s ease', - backgroundColor: 'white', - [theme.breakpoints.down('sm')]: { - width: '100%', - margin: 0, - }, - }, - sidebarCollapsed: { - transform: `translateX(-${theme.dimensions.drawerWidthDesktop})`, - marginLeft: 0, - [theme.breakpoints.down('sm')]: { - transform: 'translateX(-100vw)', - }, - }, - toolbar: { - display: 'flex', - padding: theme.spacing(0, 1), - '& > *': { - margin: theme.spacing(0, 1), - }, - }, - deviceList: { - flex: 1, - }, - statusCard: { - position: 'fixed', - zIndex: 5, - [theme.breakpoints.up('sm')]: { - left: `calc(50% + ${theme.dimensions.drawerWidthDesktop} / 2)`, - bottom: theme.spacing(3), - }, - [theme.breakpoints.down('sm')]: { - left: '50%', - bottom: theme.spacing(3) + theme.dimensions.bottomBarHeight, - }, - transform: 'translateX(-50%)', - }, - sidebarToggle: { - position: 'absolute', - left: theme.spacing(1.5), - top: theme.spacing(3), - borderRadius: '0px', - minWidth: 0, - [theme.breakpoints.down('sm')]: { - left: 0, - }, - }, - sidebarToggleText: { - marginLeft: theme.spacing(1), - [theme.breakpoints.only('xs')]: { - display: 'none', - }, - }, - sidebarToggleBg: { - backgroundColor: 'white', - color: '#777777', - '&:hover': { - backgroundColor: 'white', - }, - }, - bottomMenu: { - position: 'fixed', - left: theme.spacing(1.5), - bottom: theme.spacing(1.5), - zIndex: 4, - width: theme.dimensions.drawerWidthDesktop, - }, -})); - -const MainPage = () => { - const classes = useStyles(); - const history = useHistory(); - const dispatch = useDispatch(); - const theme = useTheme(); - const t = useTranslation(); - - const deviceReadonly = useDeviceReadonly(); - const isTablet = useMediaQuery(theme.breakpoints.down('sm')); - const isPhone = useMediaQuery(theme.breakpoints.down('xs')); - - const [mapLiveRoutes] = usePersistedState('mapLiveRoutes', false); - - const selectedDeviceId = useSelector((state) => state.devices.selectedId); - - const [searchKeyword, setSearchKeyword] = useState(''); - const [collapsed, setCollapsed] = useState(false); - - const handleClose = () => { - setCollapsed(!collapsed); - }; - - useEffect(() => setCollapsed(isTablet), [isTablet]); - - return ( -
- - {!isTablet && } - - - - {mapLiveRoutes && } - - - - - - - - - - {isTablet && ( - - - - )} - setSearchKeyword(event.target.value)} - placeholder={t('sharedSearchDevices')} - variant="filled" - /> - history.push('/device')} disabled={deviceReadonly}> - - - {!isTablet && ( - - - - )} - - -
- -
-
- {!isPhone && !isTablet && ( -
- -
- )} - {selectedDeviceId && ( -
- dispatch(devicesActions.select(null))} - /> -
- )} -
- ); -}; - -export default MainPage; diff --git a/modern/src/PositionPage.js b/modern/src/PositionPage.js deleted file mode 100644 index 946e4a75..00000000 --- a/modern/src/PositionPage.js +++ /dev/null @@ -1,99 +0,0 @@ -import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; - -import { - makeStyles, Typography, Container, Paper, AppBar, Toolbar, IconButton, Table, TableHead, TableRow, TableCell, TableBody, -} from '@material-ui/core'; -import ArrowBackIcon from '@material-ui/icons/ArrowBack'; -import { useHistory, useParams } from 'react-router-dom'; -import { useEffectAsync } from './reactHelper'; -import { prefixString } from './common/util/stringUtils'; -import { useTranslation } from './LocalizationProvider'; -import PositionValue from './common/components/PositionValue'; - -const useStyles = makeStyles((theme) => ({ - root: { - paddingTop: theme.spacing(1), - paddingBottom: theme.spacing(1), - }, -})); - -const PositionPage = () => { - const classes = useStyles(); - const history = useHistory(); - const t = useTranslation(); - - const { id } = useParams(); - - const [item, setItem] = useState(); - - useEffectAsync(async () => { - if (id) { - const response = await fetch(`/api/positions?id=${id}`); - if (response.ok) { - const positions = await response.json(); - if (positions.length > 0) { - setItem(positions[0]); - } - } - } else { - setItem({}); - } - }, [id]); - - const deviceName = useSelector((state) => { - if (item) { - const device = state.devices.items[item.deviceId]; - if (device) { - return device.name; - } - } - return null; - }); - - return ( - <> - - - history.push('/')}> - - - - {deviceName} - - - - - - - - - {t('stateName')} - {t('sharedName')} - {t('stateValue')} - - - - {item && Object.getOwnPropertyNames(item).filter((it) => it !== 'attributes').map((property) => ( - - {property} - {t(prefixString('position', property))} - - - ))} - {item && Object.getOwnPropertyNames(item.attributes).map((attribute) => ( - - {attribute} - {t(prefixString('position', attribute)) || t(prefixString('device', attribute))} - - - ))} - -
-
-
- - ); -}; - -export default PositionPage; diff --git a/modern/src/RegisterDialog.js b/modern/src/RegisterDialog.js deleted file mode 100644 index 6cae602f..00000000 --- a/modern/src/RegisterDialog.js +++ /dev/null @@ -1,97 +0,0 @@ -import React, { useState } from 'react'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogContentText from '@material-ui/core/DialogContentText'; -import TextField from '@material-ui/core/TextField'; -import Snackbar from '@material-ui/core/Snackbar'; -import { useTranslation } from './LocalizationProvider'; -import { snackBarDurationShortMs } from './common/util/duration'; - -const RegisterDialog = ({ showDialog, onResult }) => { - const t = useTranslation(); - - const [name, setName] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [snackbarOpen, setSnackbarOpen] = useState(false); - - const submitDisabled = () => !name || !/(.+)@(.+)\.(.{2,})/.test(email) || !password; - - const handleRegister = async () => { - const response = await fetch('/api/users', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name, email, password }), - }); - - if (response.ok) { - showDialog = false; - setSnackbarOpen(true); - } - }; - - if (snackbarOpen) { - return ( - { onResult(true); }} - message={t('loginCreated')} - /> - ); - } if (showDialog) { - return ( - { onResult(false); }} - > - - {t('loginRegister')} - setName(event.target.value)} - /> - setEmail(event.target.value)} - /> - setPassword(event.target.value)} - /> - - - - - - - ); - } - return null; -}; - -export default RegisterDialog; diff --git a/modern/src/SocketController.js b/modern/src/SocketController.js index adf6ffa5..cf01ff7f 100644 --- a/modern/src/SocketController.js +++ b/modern/src/SocketController.js @@ -5,7 +5,7 @@ import { useHistory } from 'react-router-dom'; import { positionsActions, devicesActions, sessionActions } from './store'; import { useEffectAsync } from './reactHelper'; -import { useTranslation } from './LocalizationProvider'; +import { useTranslation } from './common/components/LocalizationProvider'; import { prefixString } from './common/util/stringUtils'; import { snackBarDurationLongMs } from './common/util/duration'; diff --git a/modern/src/UserPage.js b/modern/src/UserPage.js deleted file mode 100644 index cb643b8a..00000000 --- a/modern/src/UserPage.js +++ /dev/null @@ -1,194 +0,0 @@ -import React, { useState } from 'react'; -import TextField from '@material-ui/core/TextField'; - -import { - Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, FormControl, InputLabel, Select, MenuItem, -} from '@material-ui/core'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import { useDispatch, useSelector } from 'react-redux'; -import EditItemView from './EditItemView'; -import EditAttributesView from './attributes/EditAttributesView'; -import LinkField from './form/LinkField'; -import { useTranslation } from './LocalizationProvider'; -import useUserAttributes from './attributes/useUserAttributes'; -import { sessionActions } from './store'; -import SelectField from './form/SelectField'; - -const useStyles = makeStyles(() => ({ - details: { - flexDirection: 'column', - }, -})); - -const UserPage = () => { - const classes = useStyles(); - const dispatch = useDispatch(); - const t = useTranslation(); - - const currentUserId = useSelector((state) => state.session.user.id); - - const userAttributes = useUserAttributes(t); - - const [item, setItem] = useState(); - - const onItemSaved = (result) => { - if (result.id === currentUserId) { - dispatch(sessionActions.updateUser(result)); - } - }; - - const validate = () => item && item.name && item.email && (item.id || item.password); - - return ( - - {item && ( - <> - - }> - - {t('sharedRequired')} - - - - setItem({ ...item, name: event.target.value })} - label={t('sharedName')} - variant="filled" - /> - setItem({ ...item, email: event.target.value })} - label={t('userEmail')} - variant="filled" - /> - setItem({ ...item, password: event.target.value })} - label={t('userPassword')} - variant="filled" - /> - - - - }> - - {t('sharedPreferences')} - - - - setItem({ ...item, phone: event.target.value })} - label={t('sharedPhone')} - variant="filled" - /> - - {t('settingsSpeedUnit')} - - - - {t('settingsDistanceUnit')} - - - - {t('settingsVolumeUnit')} - - - setItem({ ...item, attributes: { ...item.attributes, timezone: e.target.value } })} - endpoint="/api/server/timezones" - keyGetter={(it) => it} - titleGetter={(it) => it} - label={t('sharedTimezone')} - variant="filled" - /> - setItem({ ...item, poiLayer: event.target.value })} - label={t('mapPoiLayer')} - variant="filled" - /> - - - - }> - - {t('sharedAttributes')} - - - - setItem({ ...item, attributes })} - definitions={userAttributes} - /> - - - {item.id && ( - - }> - - {t('sharedConnections')} - - - - - - - - )} - - )} - - ); -}; - -export default UserPage; diff --git a/modern/src/admin/ServerPage.js b/modern/src/admin/ServerPage.js deleted file mode 100644 index 19a5c993..00000000 --- a/modern/src/admin/ServerPage.js +++ /dev/null @@ -1,241 +0,0 @@ -import React, { useState } from 'react'; -import TextField from '@material-ui/core/TextField'; - -import { - Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, Button, FormControl, Container, Checkbox, FormControlLabel, InputLabel, Select, MenuItem, -} from '@material-ui/core'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import { useHistory } from 'react-router-dom'; -import { useDispatch, useSelector } from 'react-redux'; -import { sessionActions } from '../store'; -import EditAttributesView from '../attributes/EditAttributesView'; -import useDeviceAttributes from '../attributes/useDeviceAttributes'; -import useUserAttributes from '../attributes/useUserAttributes'; -import OptionsLayout from '../settings/OptionsLayout'; -import { useTranslation } from '../LocalizationProvider'; -import SelectField from '../form/SelectField'; - -const useStyles = makeStyles((theme) => ({ - container: { - marginTop: theme.spacing(2), - }, - buttons: { - display: 'flex', - justifyContent: 'space-evenly', - '& > *': { - flexBasis: '33%', - }, - }, - details: { - flexDirection: 'column', - }, -})); - -const ServerPage = () => { - const classes = useStyles(); - const history = useHistory(); - const dispatch = useDispatch(); - const t = useTranslation(); - - const userAttributes = useUserAttributes(t); - const deviceAttributes = useDeviceAttributes(t); - - const original = useSelector((state) => state.session.server); - const [item, setItem] = useState({ ...original }); - - const handleSave = async () => { - const response = await fetch('/api/server', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(item), - }); - - if (response.ok) { - dispatch(sessionActions.updateServer(await response.json())); - history.goBack(); - } - }; - - return ( - - - {item && ( - <> - - }> - - {t('sharedPreferences')} - - - - setItem({ ...item, mapUrl: event.target.value })} - label={t('mapCustomLabel')} - variant="filled" - /> - setItem({ ...item, latitude: Number(event.target.value) })} - label={t('positionLatitude')} - variant="filled" - /> - setItem({ ...item, longitude: Number(event.target.value) })} - label={t('positionLongitude')} - variant="filled" - /> - setItem({ ...item, zoom: Number(event.target.value) })} - label={t('serverZoom')} - variant="filled" - /> - - {t('settingsCoordinateFormat')} - - - - {t('settingsSpeedUnit')} - - - - {t('settingsDistanceUnit')} - - - - {t('settingsVolumeUnit')} - - - setItem({ ...item, attributes: { ...item.attributes, timezone: e.target.value } })} - endpoint="/api/server/timezones" - keyGetter={(it) => it} - titleGetter={(it) => it} - label={t('sharedTimezone')} - variant="filled" - /> - setItem({ ...item, poiLayer: event.target.value })} - label={t('mapPoiLayer')} - variant="filled" - /> - setItem({ ...item, announcement: event.target.value })} - label={t('serverAnnouncement')} - variant="filled" - /> - setItem({ ...item, twelveHourFormat: event.target.checked })} />} - label={t('settingsTwelveHourFormat')} - /> - setItem({ ...item, forceSettings: event.target.checked })} />} - label={t('serverForceSettings')} - /> - - - - }> - - {t('sharedPermissions')} - - - - setItem({ ...item, registration: event.target.checked })} />} - label={t('serverRegistration')} - /> - setItem({ ...item, readonly: event.target.checked })} />} - label={t('serverReadonly')} - /> - setItem({ ...item, deviceReadonly: event.target.checked })} />} - label={t('userDeviceReadonly')} - /> - setItem({ ...item, limitCommands: event.target.checked })} />} - label={t('userLimitCommands')} - /> - setItem({ ...item, disableReports: event.target.checked })} />} - label={t('userDisableReports')} - /> - - - - }> - - {t('sharedAttributes')} - - - - setItem({ ...item, attributes })} - definitions={{ ...userAttributes, ...deviceAttributes }} - /> - - - - )} - -
- - -
-
-
-
- ); -}; - -export default ServerPage; diff --git a/modern/src/admin/StatisticsPage.js b/modern/src/admin/StatisticsPage.js deleted file mode 100644 index b8caa9ec..00000000 --- a/modern/src/admin/StatisticsPage.js +++ /dev/null @@ -1,145 +0,0 @@ -import React, { useState } from 'react'; -import { - FormControl, InputLabel, Select, MenuItem, TextField, Button, TableContainer, Table, TableRow, TableCell, TableHead, TableBody, -} from '@material-ui/core'; -import moment from 'moment'; -import { formatDate } from '../common/util/formatter'; -import OptionsLayout from '../settings/OptionsLayout'; -import { useTranslation } from '../LocalizationProvider'; - -const Filter = ({ setItems }) => { - const t = useTranslation(); - - const [period, setPeriod] = useState('today'); - const [from, setFrom] = useState(moment().subtract(1, 'hour')); - const [to, setTo] = useState(moment()); - - const handleClick = async () => { - let selectedFrom; - let selectedTo; - switch (period) { - case 'today': - selectedFrom = moment().startOf('day'); - selectedTo = moment().endOf('day'); - break; - case 'yesterday': - selectedFrom = moment().subtract(1, 'day').startOf('day'); - selectedTo = moment().subtract(1, 'day').endOf('day'); - break; - case 'thisWeek': - selectedFrom = moment().startOf('week'); - selectedTo = moment().endOf('week'); - break; - case 'previousWeek': - selectedFrom = moment().subtract(1, 'week').startOf('week'); - selectedTo = moment().subtract(1, 'week').endOf('week'); - break; - case 'thisMonth': - selectedFrom = moment().startOf('month'); - selectedTo = moment().endOf('month'); - break; - case 'previousMonth': - selectedFrom = moment().subtract(1, 'month').startOf('month'); - selectedTo = moment().subtract(1, 'month').endOf('month'); - break; - default: - selectedFrom = from; - selectedTo = to; - break; - } - - const query = new URLSearchParams({ from: selectedFrom.toISOString(), to: selectedTo.toISOString() }); - const response = await fetch(`/api/statistics?${query.toString()}`, { Accept: 'application/json' }); - if (response.ok) { - setItems(await response.json()); - } - }; - - return ( - <> - - {t('reportPeriod')} - - - {period === 'custom' && ( - setFrom(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL))} - fullWidth - /> - )} - {period === 'custom' && ( - setTo(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL))} - fullWidth - /> - )} - - - ); -}; - -const StatisticsPage = () => { - const t = useTranslation(); - - const [items, setItems] = useState([]); - - return ( - - - - - - - {t('statisticsCaptureTime')} - {t('statisticsActiveUsers')} - {t('statisticsActiveDevices')} - {t('statisticsRequests')} - {t('statisticsMessagesReceived')} - {t('statisticsMessagesStored')} - {t('notificatorMail')} - {t('notificatorSms')} - {t('statisticsGeocoder')} - {t('statisticsGeolocation')} - - - - {items.map((item) => ( - - {formatDate(item.captureTime)} - {item.activeUsers} - {item.activeDevices} - {item.requests} - {item.messagesReceived} - {item.messagesStored} - {item.mailSent} - {item.smsSent} - {item.geocoderRequests} - {item.geolocationRequests} - - ))} - -
-
-
- ); -}; - -export default StatisticsPage; diff --git a/modern/src/admin/UsersPage.js b/modern/src/admin/UsersPage.js deleted file mode 100644 index d8e8bc3c..00000000 --- a/modern/src/admin/UsersPage.js +++ /dev/null @@ -1,70 +0,0 @@ -import React, { useState } from 'react'; -import { - TableContainer, Table, TableRow, TableCell, TableHead, TableBody, makeStyles, IconButton, -} from '@material-ui/core'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; -import { useEffectAsync } from '../reactHelper'; -import EditCollectionView from '../EditCollectionView'; -import { formatBoolean } from '../common/util/formatter'; -import OptionsLayout from '../settings/OptionsLayout'; -import { useTranslation } from '../LocalizationProvider'; - -const useStyles = makeStyles((theme) => ({ - columnAction: { - width: theme.spacing(1), - padding: theme.spacing(0, 1), - }, -})); - -const UsersView = ({ updateTimestamp, onMenuClick }) => { - const classes = useStyles(); - const t = useTranslation(); - - const [items, setItems] = useState([]); - - useEffectAsync(async () => { - const response = await fetch('/api/users'); - if (response.ok) { - setItems(await response.json()); - } - }, [updateTimestamp]); - - return ( - - - - - - {t('sharedName')} - {t('userEmail')} - {t('userAdmin')} - {t('sharedDisabled')} - - - - {items.map((item) => ( - - - onMenuClick(event.currentTarget, item.id)}> - - - - {item.name} - {item.email} - {formatBoolean(item.administrator, t)} - {formatBoolean(item.disabled, t)} - - ))} - -
-
- ); -}; - -const UsersPage = () => ( - - - -); - -export default UsersPage; diff --git a/modern/src/attributes/AddAttributeDialog.js b/modern/src/attributes/AddAttributeDialog.js deleted file mode 100644 index 4669ba02..00000000 --- a/modern/src/attributes/AddAttributeDialog.js +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useState } from 'react'; -import { - Button, Dialog, DialogActions, DialogContent, FormControl, InputLabel, MenuItem, Select, TextField, -} from '@material-ui/core'; - -import { Autocomplete, createFilterOptions } from '@material-ui/lab'; -import { useTranslation } from '../LocalizationProvider'; - -const AddAttributeDialog = ({ open, onResult, definitions }) => { - const t = useTranslation(); - - const filter = createFilterOptions({ - stringify: (option) => option.name, - }); - - const options = Object.entries(definitions).map(([key, value]) => ({ - key, - name: value.name, - type: value.type, - })); - - const [key, setKey] = useState(); - const [type, setType] = useState('string'); - - return ( - - - { - setKey(option && typeof option === 'object' ? option.key : option); - if (option && option.type) { - setType(option.type); - } - }} - filterOptions={(options, params) => { - const filtered = filter(options, params); - if (params.inputValue) { - filtered.push({ - key: params.inputValue, - name: params.inputValue, - }); - } - return filtered; - }} - options={options} - getOptionLabel={(option) => (option && typeof option === 'object' ? option.name : option)} - renderOption={(option) => option.name} - freeSolo - renderInput={(params) => ( - - )} - /> - - {t('sharedType')} - - - - - - - - - ); -}; - -export default AddAttributeDialog; diff --git a/modern/src/attributes/EditAttributesView.js b/modern/src/attributes/EditAttributesView.js deleted file mode 100644 index 3618aa2f..00000000 --- a/modern/src/attributes/EditAttributesView.js +++ /dev/null @@ -1,203 +0,0 @@ -import React, { useState } from 'react'; - -import { - Button, Checkbox, FilledInput, FormControl, FormControlLabel, Grid, IconButton, InputAdornment, InputLabel, makeStyles, -} from '@material-ui/core'; -import CloseIcon from '@material-ui/icons/Close'; -import AddIcon from '@material-ui/icons/Add'; -import AddAttributeDialog from './AddAttributeDialog'; -import { useTranslation } from '../LocalizationProvider'; -import { useAttributePreference } from '../common/util/preferences'; -import { - distanceFromMeters, distanceToMeters, distanceUnitString, speedFromKnots, speedToKnots, speedUnitString, volumeFromLiters, volumeToLiters, volumeUnitString, -} from '../common/util/converter'; - -const useStyles = makeStyles((theme) => ({ - addButton: { - marginTop: theme.spacing(2), - marginBottom: theme.spacing(1), - }, - removeButton: { - marginRight: theme.spacing(1.5), - }, -})); - -const EditAttributesView = ({ attributes, setAttributes, definitions }) => { - const classes = useStyles(); - const t = useTranslation(); - - const speedUnit = useAttributePreference('speedUnit'); - const distanceUnit = useAttributePreference('distanceUnit'); - const volumeUnit = useAttributePreference('volumeUnit'); - - const [addDialogShown, setAddDialogShown] = useState(false); - - const updateAttribute = (key, value, type, subtype) => { - const updatedAttributes = { ...attributes }; - switch (subtype) { - case 'speed': - updatedAttributes[key] = speedToKnots(Number(value), speedUnit); - break; - case 'distance': - updatedAttributes[key] = distanceToMeters(Number(value), distanceUnit); - break; - case 'volume': - updatedAttributes[key] = volumeToLiters(Number(value), volumeUnit); - break; - default: - updatedAttributes[key] = type === 'number' ? Number(value) : value; - break; - } - setAttributes(updatedAttributes); - }; - - const deleteAttribute = (key) => { - const updatedAttributes = { ...attributes }; - delete updatedAttributes[key]; - setAttributes(updatedAttributes); - }; - - const getAttributeName = (key, subtype) => { - const definition = definitions[key]; - const name = definition ? definition.name : key; - switch (subtype) { - case 'speed': - return `${name} (${speedUnitString(speedUnit, t)})`; - case 'distance': - return `${name} (${distanceUnitString(distanceUnit, t)})`; - case 'volume': - return `${name} (${volumeUnitString(volumeUnit, t)})`; - default: - return name; - } - }; - - const getAttributeType = (value) => { - if (typeof value === 'number') { - return 'number'; - } if (typeof value === 'boolean') { - return 'boolean'; - } - return 'string'; - }; - - const getAttributeSubtype = (key) => { - const definition = definitions[key]; - return definition && definition.subtype; - }; - - const getDisplayValue = (value, subtype) => { - if (value) { - switch (subtype) { - case 'speed': - return speedFromKnots(value, speedUnit); - case 'distance': - return distanceFromMeters(value, distanceUnit); - case 'volume': - return volumeFromLiters(value, volumeUnit); - default: - return value; - } - } - return ''; - }; - - const convertToList = (attributes) => { - const booleanList = []; - const otherList = []; - const excludeAttributes = ['speedUnit', 'distanceUnit', 'volumeUnit', 'timezone']; - Object.keys(attributes || []).filter((key) => !excludeAttributes.includes(key)).forEach((key) => { - const value = attributes[key]; - const type = getAttributeType(value); - const subtype = getAttributeSubtype(key); - if (type === 'boolean') { - booleanList.push({ - key, value, type, subtype, - }); - } else { - otherList.push({ - key, value, type, subtype, - }); - } - }); - return [...otherList, ...booleanList]; - }; - - const handleAddResult = (definition) => { - setAddDialogShown(false); - if (definition) { - switch (definition.type) { - case 'number': - updateAttribute(definition.key, 0); - break; - case 'boolean': - updateAttribute(definition.key, false); - break; - default: - updateAttribute(definition.key, ''); - break; - } - } - }; - - return ( - <> - {convertToList(attributes).map(({ - key, value, type, subtype, - }) => { - if (type === 'boolean') { - return ( - - updateAttribute(key, e.target.checked)} - /> - )} - label={getAttributeName(key, subtype)} - /> - deleteAttribute(key)}> - - - - ); - } - return ( - - {getAttributeName(key, subtype)} - updateAttribute(key, e.target.value, type, subtype)} - endAdornment={( - - deleteAttribute(key)}> - - - - )} - /> - - ); - })} - - - - ); -}; - -export default EditAttributesView; diff --git a/modern/src/attributes/useCommandAttributes.js b/modern/src/attributes/useCommandAttributes.js deleted file mode 100644 index 1212d283..00000000 --- a/modern/src/attributes/useCommandAttributes.js +++ /dev/null @@ -1,11 +0,0 @@ -import { useMemo } from 'react'; - -export default (t) => useMemo(() => ({ - custom: [ - { - key: 'data', - name: t('commandData'), - type: 'string', - }, - ], -}), [t]); diff --git a/modern/src/attributes/useDeviceAttributes.js b/modern/src/attributes/useDeviceAttributes.js deleted file mode 100644 index 8a4d886c..00000000 --- a/modern/src/attributes/useDeviceAttributes.js +++ /dev/null @@ -1,13 +0,0 @@ -import { useMemo } from 'react'; - -export default (t) => useMemo(() => ({ - speedLimit: { - name: t('attributeSpeedLimit'), - type: 'number', - subtype: 'speed', - }, - 'report.ignoreOdometer': { - name: t('attributeReportIgnoreOdometer'), - type: 'boolean', - }, -}), [t]); diff --git a/modern/src/attributes/useGeofenceAttributes.js b/modern/src/attributes/useGeofenceAttributes.js deleted file mode 100644 index 89908aa5..00000000 --- a/modern/src/attributes/useGeofenceAttributes.js +++ /dev/null @@ -1,8 +0,0 @@ -import { useMemo } from 'react'; - -export default (t) => useMemo(() => ({ - speedLimit: { - name: t('attributeSpeedLimit'), - type: 'string', - }, -}), [t]); diff --git a/modern/src/attributes/usePositionAttributes.js b/modern/src/attributes/usePositionAttributes.js deleted file mode 100644 index 7b33720a..00000000 --- a/modern/src/attributes/usePositionAttributes.js +++ /dev/null @@ -1,21 +0,0 @@ -import { useMemo } from 'react'; - -export default (t) => useMemo(() => ({ - raw: { - name: t('positionRaw'), - type: 'string', - }, - index: { - name: t('positionIndex'), - type: 'number', - }, - ignition: { - name: t('positionIgnition'), - type: 'boolean', - }, - odometer: { - name: t('positionOdometer'), - type: 'number', - dataType: 'distance', - }, -}), [t]); diff --git a/modern/src/attributes/useUserAttributes.js b/modern/src/attributes/useUserAttributes.js deleted file mode 100644 index fa6d7d8f..00000000 --- a/modern/src/attributes/useUserAttributes.js +++ /dev/null @@ -1,48 +0,0 @@ -import { useMemo } from 'react'; - -export default (t) => useMemo(() => ({ - notificationTokens: { - name: t('attributeNotificationTokens'), - type: 'string', - }, - /* 'web.liveRouteLength': { - name: t('attributeWebLiveRouteLength'), - type: 'number', - }, - 'web.selectZoom': { - name: t('attributeWebSelectZoom'), - type: 'number', - }, - 'web.maxZoom': { - name: t('attributeWebMaxZoom'), - type: 'number', - }, - 'ui.disableEvents': { - name: t('attributeUiDisableEvents'), - type: 'boolean', - }, - 'ui.disableVehicleFetures': { - name: t('attributeUiDisableVehicleFetures'), - type: 'boolean', - }, - 'ui.disableDrivers': { - name: t('attributeUiDisableDrivers'), - type: 'boolean', - }, - 'ui.disableComputedAttributes': { - name: t('attributeUiDisableComputedAttributes'), - type: 'boolean', - }, - 'ui.disableCalendars': { - name: t('attributeUiDisableCalendars'), - type: 'boolean', - }, - 'ui.disableMaintenance': { - name: t('attributeUiDisableMaintenance'), - type: 'boolean', - }, - 'ui.hidePositionAttributes': { - name: t('attributeUiHidePositionAttributes'), - type: 'string', - }, */ -}), [t]); diff --git a/modern/src/common/attributes/AddAttributeDialog.js b/modern/src/common/attributes/AddAttributeDialog.js new file mode 100644 index 00000000..37b36c76 --- /dev/null +++ b/modern/src/common/attributes/AddAttributeDialog.js @@ -0,0 +1,89 @@ +import React, { useState } from 'react'; +import { + Button, Dialog, DialogActions, DialogContent, FormControl, InputLabel, MenuItem, Select, TextField, +} from '@material-ui/core'; + +import { Autocomplete, createFilterOptions } from '@material-ui/lab'; +import { useTranslation } from '../components/LocalizationProvider'; + +const AddAttributeDialog = ({ open, onResult, definitions }) => { + const t = useTranslation(); + + const filter = createFilterOptions({ + stringify: (option) => option.name, + }); + + const options = Object.entries(definitions).map(([key, value]) => ({ + key, + name: value.name, + type: value.type, + })); + + const [key, setKey] = useState(); + const [type, setType] = useState('string'); + + return ( + + + { + setKey(option && typeof option === 'object' ? option.key : option); + if (option && option.type) { + setType(option.type); + } + }} + filterOptions={(options, params) => { + const filtered = filter(options, params); + if (params.inputValue) { + filtered.push({ + key: params.inputValue, + name: params.inputValue, + }); + } + return filtered; + }} + options={options} + getOptionLabel={(option) => (option && typeof option === 'object' ? option.name : option)} + renderOption={(option) => option.name} + freeSolo + renderInput={(params) => ( + + )} + /> + + {t('sharedType')} + + + + + + + + + ); +}; + +export default AddAttributeDialog; diff --git a/modern/src/common/attributes/EditAttributesView.js b/modern/src/common/attributes/EditAttributesView.js new file mode 100644 index 00000000..4bc34b19 --- /dev/null +++ b/modern/src/common/attributes/EditAttributesView.js @@ -0,0 +1,203 @@ +import React, { useState } from 'react'; + +import { + Button, Checkbox, FilledInput, FormControl, FormControlLabel, Grid, IconButton, InputAdornment, InputLabel, makeStyles, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import AddIcon from '@material-ui/icons/Add'; +import AddAttributeDialog from './AddAttributeDialog'; +import { useTranslation } from '../components/LocalizationProvider'; +import { useAttributePreference } from '../util/preferences'; +import { + distanceFromMeters, distanceToMeters, distanceUnitString, speedFromKnots, speedToKnots, speedUnitString, volumeFromLiters, volumeToLiters, volumeUnitString, +} from '../util/converter'; + +const useStyles = makeStyles((theme) => ({ + addButton: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(1), + }, + removeButton: { + marginRight: theme.spacing(1.5), + }, +})); + +const EditAttributesView = ({ attributes, setAttributes, definitions }) => { + const classes = useStyles(); + const t = useTranslation(); + + const speedUnit = useAttributePreference('speedUnit'); + const distanceUnit = useAttributePreference('distanceUnit'); + const volumeUnit = useAttributePreference('volumeUnit'); + + const [addDialogShown, setAddDialogShown] = useState(false); + + const updateAttribute = (key, value, type, subtype) => { + const updatedAttributes = { ...attributes }; + switch (subtype) { + case 'speed': + updatedAttributes[key] = speedToKnots(Number(value), speedUnit); + break; + case 'distance': + updatedAttributes[key] = distanceToMeters(Number(value), distanceUnit); + break; + case 'volume': + updatedAttributes[key] = volumeToLiters(Number(value), volumeUnit); + break; + default: + updatedAttributes[key] = type === 'number' ? Number(value) : value; + break; + } + setAttributes(updatedAttributes); + }; + + const deleteAttribute = (key) => { + const updatedAttributes = { ...attributes }; + delete updatedAttributes[key]; + setAttributes(updatedAttributes); + }; + + const getAttributeName = (key, subtype) => { + const definition = definitions[key]; + const name = definition ? definition.name : key; + switch (subtype) { + case 'speed': + return `${name} (${speedUnitString(speedUnit, t)})`; + case 'distance': + return `${name} (${distanceUnitString(distanceUnit, t)})`; + case 'volume': + return `${name} (${volumeUnitString(volumeUnit, t)})`; + default: + return name; + } + }; + + const getAttributeType = (value) => { + if (typeof value === 'number') { + return 'number'; + } if (typeof value === 'boolean') { + return 'boolean'; + } + return 'string'; + }; + + const getAttributeSubtype = (key) => { + const definition = definitions[key]; + return definition && definition.subtype; + }; + + const getDisplayValue = (value, subtype) => { + if (value) { + switch (subtype) { + case 'speed': + return speedFromKnots(value, speedUnit); + case 'distance': + return distanceFromMeters(value, distanceUnit); + case 'volume': + return volumeFromLiters(value, volumeUnit); + default: + return value; + } + } + return ''; + }; + + const convertToList = (attributes) => { + const booleanList = []; + const otherList = []; + const excludeAttributes = ['speedUnit', 'distanceUnit', 'volumeUnit', 'timezone']; + Object.keys(attributes || []).filter((key) => !excludeAttributes.includes(key)).forEach((key) => { + const value = attributes[key]; + const type = getAttributeType(value); + const subtype = getAttributeSubtype(key); + if (type === 'boolean') { + booleanList.push({ + key, value, type, subtype, + }); + } else { + otherList.push({ + key, value, type, subtype, + }); + } + }); + return [...otherList, ...booleanList]; + }; + + const handleAddResult = (definition) => { + setAddDialogShown(false); + if (definition) { + switch (definition.type) { + case 'number': + updateAttribute(definition.key, 0); + break; + case 'boolean': + updateAttribute(definition.key, false); + break; + default: + updateAttribute(definition.key, ''); + break; + } + } + }; + + return ( + <> + {convertToList(attributes).map(({ + key, value, type, subtype, + }) => { + if (type === 'boolean') { + return ( + + updateAttribute(key, e.target.checked)} + /> + )} + label={getAttributeName(key, subtype)} + /> + deleteAttribute(key)}> + + + + ); + } + return ( + + {getAttributeName(key, subtype)} + updateAttribute(key, e.target.value, type, subtype)} + endAdornment={( + + deleteAttribute(key)}> + + + + )} + /> + + ); + })} + + + + ); +}; + +export default EditAttributesView; diff --git a/modern/src/common/attributes/useCommandAttributes.js b/modern/src/common/attributes/useCommandAttributes.js new file mode 100644 index 00000000..1212d283 --- /dev/null +++ b/modern/src/common/attributes/useCommandAttributes.js @@ -0,0 +1,11 @@ +import { useMemo } from 'react'; + +export default (t) => useMemo(() => ({ + custom: [ + { + key: 'data', + name: t('commandData'), + type: 'string', + }, + ], +}), [t]); diff --git a/modern/src/common/attributes/useDeviceAttributes.js b/modern/src/common/attributes/useDeviceAttributes.js new file mode 100644 index 00000000..8a4d886c --- /dev/null +++ b/modern/src/common/attributes/useDeviceAttributes.js @@ -0,0 +1,13 @@ +import { useMemo } from 'react'; + +export default (t) => useMemo(() => ({ + speedLimit: { + name: t('attributeSpeedLimit'), + type: 'number', + subtype: 'speed', + }, + 'report.ignoreOdometer': { + name: t('attributeReportIgnoreOdometer'), + type: 'boolean', + }, +}), [t]); diff --git a/modern/src/common/attributes/useGeofenceAttributes.js b/modern/src/common/attributes/useGeofenceAttributes.js new file mode 100644 index 00000000..89908aa5 --- /dev/null +++ b/modern/src/common/attributes/useGeofenceAttributes.js @@ -0,0 +1,8 @@ +import { useMemo } from 'react'; + +export default (t) => useMemo(() => ({ + speedLimit: { + name: t('attributeSpeedLimit'), + type: 'string', + }, +}), [t]); diff --git a/modern/src/common/attributes/usePositionAttributes.js b/modern/src/common/attributes/usePositionAttributes.js new file mode 100644 index 00000000..7b33720a --- /dev/null +++ b/modern/src/common/attributes/usePositionAttributes.js @@ -0,0 +1,21 @@ +import { useMemo } from 'react'; + +export default (t) => useMemo(() => ({ + raw: { + name: t('positionRaw'), + type: 'string', + }, + index: { + name: t('positionIndex'), + type: 'number', + }, + ignition: { + name: t('positionIgnition'), + type: 'boolean', + }, + odometer: { + name: t('positionOdometer'), + type: 'number', + dataType: 'distance', + }, +}), [t]); diff --git a/modern/src/common/attributes/useUserAttributes.js b/modern/src/common/attributes/useUserAttributes.js new file mode 100644 index 00000000..fa6d7d8f --- /dev/null +++ b/modern/src/common/attributes/useUserAttributes.js @@ -0,0 +1,48 @@ +import { useMemo } from 'react'; + +export default (t) => useMemo(() => ({ + notificationTokens: { + name: t('attributeNotificationTokens'), + type: 'string', + }, + /* 'web.liveRouteLength': { + name: t('attributeWebLiveRouteLength'), + type: 'number', + }, + 'web.selectZoom': { + name: t('attributeWebSelectZoom'), + type: 'number', + }, + 'web.maxZoom': { + name: t('attributeWebMaxZoom'), + type: 'number', + }, + 'ui.disableEvents': { + name: t('attributeUiDisableEvents'), + type: 'boolean', + }, + 'ui.disableVehicleFetures': { + name: t('attributeUiDisableVehicleFetures'), + type: 'boolean', + }, + 'ui.disableDrivers': { + name: t('attributeUiDisableDrivers'), + type: 'boolean', + }, + 'ui.disableComputedAttributes': { + name: t('attributeUiDisableComputedAttributes'), + type: 'boolean', + }, + 'ui.disableCalendars': { + name: t('attributeUiDisableCalendars'), + type: 'boolean', + }, + 'ui.disableMaintenance': { + name: t('attributeUiDisableMaintenance'), + type: 'boolean', + }, + 'ui.hidePositionAttributes': { + name: t('attributeUiHidePositionAttributes'), + type: 'string', + }, */ +}), [t]); diff --git a/modern/src/common/components/BottomMenu.js b/modern/src/common/components/BottomMenu.js index d26b4ae2..3865de29 100644 --- a/modern/src/common/components/BottomMenu.js +++ b/modern/src/common/components/BottomMenu.js @@ -12,7 +12,7 @@ import PersonIcon from '@material-ui/icons/Person'; import ExitToAppIcon from '@material-ui/icons/ExitToApp'; import { sessionActions } from '../../store'; -import { useTranslation } from '../../LocalizationProvider'; +import { useTranslation } from './LocalizationProvider'; import { useReadonly } from '../util/permissions'; const BottomMenu = () => { diff --git a/modern/src/common/components/LinkField.js b/modern/src/common/components/LinkField.js new file mode 100644 index 00000000..e11438df --- /dev/null +++ b/modern/src/common/components/LinkField.js @@ -0,0 +1,85 @@ +import { + FormControl, InputLabel, MenuItem, Select, +} from '@material-ui/core'; +import React, { useState } from 'react'; +import { useEffectAsync } from '../../reactHelper'; + +const LinkField = ({ + margin, + variant, + label, + endpointAll, + endpointLinked, + baseId, + keyBase, + keyLink, + keyGetter = (item) => item.id, + titleGetter = (item) => item.name, +}) => { + const [items, setItems] = useState(); + const [linked, setLinked] = useState(); + + useEffectAsync(async () => { + const response = await fetch(endpointAll); + if (response.ok) { + setItems(await response.json()); + } + }, []); + + useEffectAsync(async () => { + const response = await fetch(endpointLinked); + if (response.ok) { + const data = await response.json(); + setLinked(data.map((it) => it.id)); + } + }, []); + + const createBody = (linkId) => { + const body = {}; + body[keyBase] = baseId; + body[keyLink] = linkId; + return body; + }; + + const onChange = async (event) => { + const oldValue = linked; + const newValue = event.target.value; + const results = []; + newValue.filter((it) => !oldValue.includes(it)).forEach((added) => { + results.push(fetch('/api/permissions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(createBody(added)), + })); + }); + oldValue.filter((it) => !newValue.includes(it)).forEach((removed) => { + results.push(fetch('/api/permissions', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(createBody(removed)), + })); + }); + await Promise.all(results); + setLinked(newValue); + }; + + if (items && linked) { + return ( + + {label} + + + ); + } + return null; +}; + +export default LinkField; diff --git a/modern/src/common/components/LocalizationProvider.js b/modern/src/common/components/LocalizationProvider.js new file mode 100644 index 00000000..0df242dc --- /dev/null +++ b/modern/src/common/components/LocalizationProvider.js @@ -0,0 +1,165 @@ +import React, { createContext, useContext, useMemo } from 'react'; +import usePersistedState from '../util/usePersistedState'; + +import af from '../../../../web/l10n/af.json'; +import ar from '../../../../web/l10n/ar.json'; +import az from '../../../../web/l10n/az.json'; +import bg from '../../../../web/l10n/bg.json'; +import bn from '../../../../web/l10n/bn.json'; +import cs from '../../../../web/l10n/cs.json'; +import da from '../../../../web/l10n/da.json'; +import de from '../../../../web/l10n/de.json'; +import el from '../../../../web/l10n/el.json'; +import en from '../../../../web/l10n/en.json'; +import es from '../../../../web/l10n/es.json'; +import fa from '../../../../web/l10n/fa.json'; +import fi from '../../../../web/l10n/fi.json'; +import fr from '../../../../web/l10n/fr.json'; +import he from '../../../../web/l10n/he.json'; +import hi from '../../../../web/l10n/hi.json'; +import hr from '../../../../web/l10n/hr.json'; +import hu from '../../../../web/l10n/hu.json'; +import id from '../../../../web/l10n/id.json'; +import it from '../../../../web/l10n/it.json'; +import ja from '../../../../web/l10n/ja.json'; +import ka from '../../../../web/l10n/ka.json'; +import kk from '../../../../web/l10n/kk.json'; +import km from '../../../../web/l10n/km.json'; +import ko from '../../../../web/l10n/ko.json'; +import lo from '../../../../web/l10n/lo.json'; +import lt from '../../../../web/l10n/lt.json'; +import lv from '../../../../web/l10n/lv.json'; +import ml from '../../../../web/l10n/ml.json'; +import mn from '../../../../web/l10n/mn.json'; +import ms from '../../../../web/l10n/ms.json'; +import nb from '../../../../web/l10n/nb.json'; +import ne from '../../../../web/l10n/ne.json'; +import nl from '../../../../web/l10n/nl.json'; +import nn from '../../../../web/l10n/nn.json'; +import pl from '../../../../web/l10n/pl.json'; +import pt from '../../../../web/l10n/pt.json'; +import ptBR from '../../../../web/l10n/pt_BR.json'; +import ro from '../../../../web/l10n/ro.json'; +import ru from '../../../../web/l10n/ru.json'; +import si from '../../../../web/l10n/si.json'; +import sk from '../../../../web/l10n/sk.json'; +import sl from '../../../../web/l10n/sl.json'; +import sq from '../../../../web/l10n/sq.json'; +import sr from '../../../../web/l10n/sr.json'; +import sv from '../../../../web/l10n/sv.json'; +import ta from '../../../../web/l10n/ta.json'; +import th from '../../../../web/l10n/th.json'; +import tr from '../../../../web/l10n/tr.json'; +import uk from '../../../../web/l10n/uk.json'; +import uz from '../../../../web/l10n/uz.json'; +import vi from '../../../../web/l10n/vi.json'; +import zh from '../../../../web/l10n/zh.json'; +import zhTW from '../../../../web/l10n/zh_TW.json'; + +const languages = { + af: { data: af, name: 'Afrikaans' }, + ar: { data: ar, name: 'العربية' }, + az: { data: az, name: 'Azərbaycanca' }, + bg: { data: bg, name: 'Български' }, + bn: { data: bn, name: 'বাংলা' }, + cs: { data: cs, name: 'Čeština' }, + de: { data: de, name: 'Deutsch' }, + da: { data: da, name: 'Dansk' }, + el: { data: el, name: 'Ελληνικά' }, + en: { data: en, name: 'English' }, + es: { data: es, name: 'Español' }, + fa: { data: fa, name: 'فارسی' }, + fi: { data: fi, name: 'Suomi' }, + fr: { data: fr, name: 'Français' }, + he: { data: he, name: 'עברית' }, + hi: { data: hi, name: 'हिन्दी' }, + hr: { data: hr, name: 'Hrvatski' }, + hu: { data: hu, name: 'Magyar' }, + id: { data: id, name: 'Bahasa Indonesia' }, + it: { data: it, name: 'Italiano' }, + ja: { data: ja, name: '日本語' }, + ka: { data: ka, name: 'ქართული' }, + kk: { data: kk, name: 'Қазақша' }, + ko: { data: ko, name: '한국어' }, + km: { data: km, name: 'ភាសាខ្មែរ' }, + lo: { data: lo, name: 'ລາວ' }, + lt: { data: lt, name: 'Lietuvių' }, + lv: { data: lv, name: 'Latviešu' }, + ml: { data: ml, name: 'മലയാളം' }, + mn: { data: mn, name: 'Монгол хэл' }, + ms: { data: ms, name: 'بهاس ملايو' }, + nb: { data: nb, name: 'Norsk bokmål' }, + ne: { data: ne, name: 'नेपाली' }, + nl: { data: nl, name: 'Nederlands' }, + nn: { data: nn, name: 'Norsk nynorsk' }, + pl: { data: pl, name: 'Polski' }, + pt: { data: pt, name: 'Português' }, + ptBR: { data: ptBR, name: 'Português (Brasil)' }, + ro: { data: ro, name: 'Română' }, + ru: { data: ru, name: 'Русский' }, + si: { data: si, name: 'සිංහල' }, + sk: { data: sk, name: 'Slovenčina' }, + sl: { data: sl, name: 'Slovenščina' }, + sq: { data: sq, name: 'Shqipëria' }, + sr: { data: sr, name: 'Srpski' }, + sv: { data: sv, name: 'Svenska' }, + ta: { data: ta, name: 'தமிழ்' }, + th: { data: th, name: 'ไทย' }, + tr: { data: tr, name: 'Türkçe' }, + uk: { data: uk, name: 'Українська' }, + uz: { data: uz, name: 'Oʻzbekcha' }, + vi: { data: vi, name: 'Tiếng Việt' }, + zh: { data: zh, name: '中文' }, + zhTW: { data: zhTW, name: '中文 (Taiwan)' }, +}; + +const getDefaultLanguage = () => { + const browserLanguages = window.navigator.languages ? window.navigator.languages.slice() : []; + const browserLanguage = window.navigator.userLanguage || window.navigator.language; + browserLanguages.push(browserLanguage); + browserLanguages.push(browserLanguage.substring(0, 2)); + + for (let i = 0; i < browserLanguages.length; i += 1) { + let language = browserLanguages[i].replace('-', ''); + if (language in languages) { + return language; + } + if (language.length > 2) { + language = language.substring(0, 2); + if (language in languages) { + return language; + } + } + } + return 'en'; +}; + +const LocalizationContext = createContext({ + languages, + language: 'en', + setLanguage: () => {}, +}); + +export const LocalizationProvider = ({ children }) => { + const [language, setLanguage] = usePersistedState('language', getDefaultLanguage()); + + return ( + + {children} + + ); +}; + +export const useLocalization = () => useContext(LocalizationContext); + +export const useTranslation = () => { + const context = useContext(LocalizationContext); + const { data } = context.languages[context.language]; + return useMemo(() => (key) => data[key], [data]); +}; + +export const useTranslationKeys = (predicate) => { + const context = useContext(LocalizationContext); + const { data } = context.languages[context.language]; + return Object.keys(data).filter(predicate); +}; diff --git a/modern/src/common/components/PositionValue.js b/modern/src/common/components/PositionValue.js index b160be34..17e7b1d2 100644 --- a/modern/src/common/components/PositionValue.js +++ b/modern/src/common/components/PositionValue.js @@ -5,7 +5,7 @@ import { formatAlarm, formatBoolean, formatCoordinate, formatCourse, formatDistance, formatNumber, formatPercentage, formatSpeed, formatTime, } from '../util/formatter'; import { useAttributePreference, usePreference } from '../util/preferences'; -import { useTranslation } from '../../LocalizationProvider'; +import { useTranslation } from './LocalizationProvider'; import { useAdministrator } from '../util/permissions'; const PositionValue = ({ position, property, attribute }) => { diff --git a/modern/src/common/components/RemoveDialog.js b/modern/src/common/components/RemoveDialog.js index 1b75e926..6d191d6a 100644 --- a/modern/src/common/components/RemoveDialog.js +++ b/modern/src/common/components/RemoveDialog.js @@ -4,7 +4,7 @@ import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import DialogContentText from '@material-ui/core/DialogContentText'; -import { useTranslation } from '../../LocalizationProvider'; +import { useTranslation } from './LocalizationProvider'; const RemoveDialog = ({ open, endpoint, itemId, onResult, diff --git a/modern/src/common/components/SelectField.js b/modern/src/common/components/SelectField.js new file mode 100644 index 00000000..98473b16 --- /dev/null +++ b/modern/src/common/components/SelectField.js @@ -0,0 +1,53 @@ +import { + FormControl, InputLabel, MenuItem, Select, +} from '@material-ui/core'; +import React, { useState } from 'react'; +import { useEffectAsync } from '../../reactHelper'; + +const SelectField = ({ + margin, + variant, + label, + multiple, + value, + emptyValue = 0, + emptyTitle = '\u00a0', + onChange, + endpoint, + data, + keyGetter = (item) => item.id, + titleGetter = (item) => item.name, +}) => { + const [items, setItems] = useState(data); + + useEffectAsync(async () => { + if (endpoint) { + const response = await fetch(endpoint); + if (response.ok) { + setItems(await response.json()); + } + } + }, []); + + if (items) { + return ( + + {label} + + + ); + } + return null; +}; + +export default SelectField; diff --git a/modern/src/form/LinkField.js b/modern/src/form/LinkField.js deleted file mode 100644 index 81467a1b..00000000 --- a/modern/src/form/LinkField.js +++ /dev/null @@ -1,85 +0,0 @@ -import { - FormControl, InputLabel, MenuItem, Select, -} from '@material-ui/core'; -import React, { useState } from 'react'; -import { useEffectAsync } from '../reactHelper'; - -const LinkField = ({ - margin, - variant, - label, - endpointAll, - endpointLinked, - baseId, - keyBase, - keyLink, - keyGetter = (item) => item.id, - titleGetter = (item) => item.name, -}) => { - const [items, setItems] = useState(); - const [linked, setLinked] = useState(); - - useEffectAsync(async () => { - const response = await fetch(endpointAll); - if (response.ok) { - setItems(await response.json()); - } - }, []); - - useEffectAsync(async () => { - const response = await fetch(endpointLinked); - if (response.ok) { - const data = await response.json(); - setLinked(data.map((it) => it.id)); - } - }, []); - - const createBody = (linkId) => { - const body = {}; - body[keyBase] = baseId; - body[keyLink] = linkId; - return body; - }; - - const onChange = async (event) => { - const oldValue = linked; - const newValue = event.target.value; - const results = []; - newValue.filter((it) => !oldValue.includes(it)).forEach((added) => { - results.push(fetch('/api/permissions', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(createBody(added)), - })); - }); - oldValue.filter((it) => !newValue.includes(it)).forEach((removed) => { - results.push(fetch('/api/permissions', { - method: 'DELETE', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(createBody(removed)), - })); - }); - await Promise.all(results); - setLinked(newValue); - }; - - if (items && linked) { - return ( - - {label} - - - ); - } - return null; -}; - -export default LinkField; diff --git a/modern/src/form/SelectField.js b/modern/src/form/SelectField.js deleted file mode 100644 index 420ad19e..00000000 --- a/modern/src/form/SelectField.js +++ /dev/null @@ -1,53 +0,0 @@ -import { - FormControl, InputLabel, MenuItem, Select, -} from '@material-ui/core'; -import React, { useState } from 'react'; -import { useEffectAsync } from '../reactHelper'; - -const SelectField = ({ - margin, - variant, - label, - multiple, - value, - emptyValue = 0, - emptyTitle = '\u00a0', - onChange, - endpoint, - data, - keyGetter = (item) => item.id, - titleGetter = (item) => item.name, -}) => { - const [items, setItems] = useState(data); - - useEffectAsync(async () => { - if (endpoint) { - const response = await fetch(endpoint); - if (response.ok) { - setItems(await response.json()); - } - } - }, []); - - if (items) { - return ( - - {label} - - - ); - } - return null; -}; - -export default SelectField; diff --git a/modern/src/login/LoginPage.js b/modern/src/login/LoginPage.js index d89ccd73..ae7b982d 100644 --- a/modern/src/login/LoginPage.js +++ b/modern/src/login/LoginPage.js @@ -8,7 +8,7 @@ import { useTheme } from '@material-ui/core/styles'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { sessionActions } from '../store'; -import { useLocalization, useTranslation } from '../LocalizationProvider'; +import { useLocalization, useTranslation } from '../common/components/LocalizationProvider'; import LoginLayout from './LoginLayout'; import usePersistedState from '../common/util/usePersistedState'; diff --git a/modern/src/login/RegisterPage.js b/modern/src/login/RegisterPage.js index 6f252b1a..cd6fc381 100644 --- a/modern/src/login/RegisterPage.js +++ b/modern/src/login/RegisterPage.js @@ -5,7 +5,7 @@ import { import { useHistory } from 'react-router-dom'; import ArrowBackIcon from '@material-ui/icons/ArrowBack'; import LoginLayout from './LoginLayout'; -import { useTranslation } from '../LocalizationProvider'; +import { useTranslation } from '../common/components/LocalizationProvider'; import { snackBarDurationShortMs } from '../common/util/duration'; const useStyles = makeStyles((theme) => ({ diff --git a/modern/src/login/ResetPasswordPage.js b/modern/src/login/ResetPasswordPage.js index 86a289ee..6f7e784f 100644 --- a/modern/src/login/ResetPasswordPage.js +++ b/modern/src/login/ResetPasswordPage.js @@ -5,7 +5,7 @@ import { import { useHistory } from 'react-router-dom'; import ArrowBackIcon from '@material-ui/icons/ArrowBack'; import LoginLayout from './LoginLayout'; -import { useTranslation } from '../LocalizationProvider'; +import { useTranslation } from '../common/components/LocalizationProvider'; import useQuery from '../common/util/useQuery'; import { snackBarDurationShortMs } from '../common/util/duration'; diff --git a/modern/src/main/DevicesList.js b/modern/src/main/DevicesList.js new file mode 100644 index 00000000..57778667 --- /dev/null +++ b/modern/src/main/DevicesList.js @@ -0,0 +1,195 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { makeStyles } from '@material-ui/core/styles'; +import { IconButton, Tooltip } from '@material-ui/core'; +import Avatar from '@material-ui/core/Avatar'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemAvatar from '@material-ui/core/ListItemAvatar'; +import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; +import ListItemText from '@material-ui/core/ListItemText'; +import { FixedSizeList } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import BatteryFullIcon from '@material-ui/icons/BatteryFull'; +import BatteryChargingFullIcon from '@material-ui/icons/BatteryChargingFull'; +import Battery60Icon from '@material-ui/icons/Battery60'; +import BatteryCharging60Icon from '@material-ui/icons/BatteryCharging60'; +import Battery20Icon from '@material-ui/icons/Battery20'; +import BatteryCharging20Icon from '@material-ui/icons/BatteryCharging20'; +import FlashOnIcon from '@material-ui/icons/FlashOn'; +import FlashOffIcon from '@material-ui/icons/FlashOff'; +import ErrorIcon from '@material-ui/icons/Error'; + +import { devicesActions } from '../store'; +import EditCollectionView from '../settings/components/EditCollectionView'; +import { useEffectAsync } from '../reactHelper'; +import { + formatAlarm, formatBoolean, formatPercentage, formatStatus, getStatusColor, +} from '../common/util/formatter'; +import { useTranslation } from '../common/components/LocalizationProvider'; + +const useStyles = makeStyles((theme) => ({ + list: { + maxHeight: '100%', + }, + listInner: { + position: 'relative', + margin: theme.spacing(1.5, 0), + }, + icon: { + width: '25px', + height: '25px', + filter: 'brightness(0) invert(1)', + }, + listItem: { + backgroundColor: 'white', + '&:hover': { + backgroundColor: 'white', + }, + }, + batteryText: { + fontSize: '0.75rem', + fontWeight: 'normal', + lineHeight: '0.875rem', + }, + positive: { + color: theme.palette.colors.positive, + }, + medium: { + color: theme.palette.colors.medium, + }, + negative: { + color: theme.palette.colors.negative, + }, + neutral: { + color: theme.palette.colors.neutral, + }, + indicators: { + lineHeight: 1, + }, +})); + +const DeviceRow = ({ data, index, style }) => { + const classes = useStyles(); + const dispatch = useDispatch(); + const t = useTranslation(); + + const { items } = data; + const item = items[index]; + const position = useSelector((state) => state.positions.items[item.id]); + + return ( +
+ dispatch(devicesActions.select(item.id))}> + + + + + + + + {position && ( + <> + {position.attributes.hasOwnProperty('alarm') && ( + + + + + + )} + {position.attributes.hasOwnProperty('ignition') && ( + + + {position.attributes.ignition ? ( + + ) : ( + + )} + + + )} + {position.attributes.hasOwnProperty('batteryLevel') && ( + + + {position.attributes.batteryLevel > 70 ? ( + position.attributes.charge + ? () + : () + ) : position.attributes.batteryLevel > 30 ? ( + position.attributes.charge + ? () + : () + ) : ( + position.attributes.charge + ? () + : () + )} + + + )} + + )} + + +
+ ); +}; + +const DeviceView = ({ updateTimestamp, onMenuClick, filter }) => { + const classes = useStyles(); + const dispatch = useDispatch(); + const listInnerEl = useRef(null); + + const items = useSelector((state) => state.devices.items); + const [filteredItems, setFilteredItems] = useState(null); + + useEffect(() => { + const array = Object.values(items); + setFilteredItems( + filter.trim().length > 0 + ? array.filter((item) => `${item.name} ${item.uniqueId}`.toLowerCase().includes(filter?.toLowerCase())) + : array, + ); + }, [filter, items]); + + if (listInnerEl.current) { + listInnerEl.current.className = classes.listInner; + } + + useEffectAsync(async () => { + const response = await fetch('/api/devices'); + if (response.ok) { + dispatch(devicesActions.refresh(await response.json())); + } + }, [updateTimestamp]); + + return ( + + {({ height, width }) => ( + + + {DeviceRow} + + + )} + + ); +}; + +const DevicesList = ({ filter }) => ( + +); + +export default DevicesList; diff --git a/modern/src/main/MainPage.js b/modern/src/main/MainPage.js new file mode 100644 index 00000000..569978af --- /dev/null +++ b/modern/src/main/MainPage.js @@ -0,0 +1,213 @@ +import React, { useState, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { + makeStyles, Paper, Toolbar, TextField, IconButton, Button, +} from '@material-ui/core'; + +import { useTheme } from '@material-ui/core/styles'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; +import AddIcon from '@material-ui/icons/Add'; +import CloseIcon from '@material-ui/icons/Close'; +import ArrowBackIcon from '@material-ui/icons/ArrowBack'; +import ListIcon from '@material-ui/icons/ViewList'; + +import { useDispatch, useSelector } from 'react-redux'; +import DevicesList from './DevicesList'; +import Map from '../map/core/Map'; +import SelectedDeviceMap from '../map/main/SelectedDeviceMap'; +import AccuracyMap from '../map/main/AccuracyMap'; +import GeofenceMap from '../map/main/GeofenceMap'; +import CurrentPositionsMap from '../map/main/CurrentPositionsMap'; +import CurrentLocationMap from '../map/CurrentLocationMap'; +import BottomMenu from '../common/components/BottomMenu'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PoiMap from '../map/main/PoiMap'; +import MapPadding from '../map/MapPadding'; +import StatusCard from './StatusCard'; +import { devicesActions } from '../store'; +import DefaultCameraMap from '../map/main/DefaultCameraMap'; +import usePersistedState from '../common/util/usePersistedState'; +import LiveRoutesMap from '../map/main/LiveRoutesMap'; +import { useDeviceReadonly } from '../common/util/permissions'; + +const useStyles = makeStyles((theme) => ({ + root: { + height: '100%', + }, + sidebar: { + display: 'flex', + flexDirection: 'column', + position: 'absolute', + left: 0, + top: 0, + zIndex: 3, + margin: theme.spacing(1.5), + width: theme.dimensions.drawerWidthDesktop, + bottom: theme.dimensions.bottomBarHeight, + transition: 'transform .5s ease', + backgroundColor: 'white', + [theme.breakpoints.down('sm')]: { + width: '100%', + margin: 0, + }, + }, + sidebarCollapsed: { + transform: `translateX(-${theme.dimensions.drawerWidthDesktop})`, + marginLeft: 0, + [theme.breakpoints.down('sm')]: { + transform: 'translateX(-100vw)', + }, + }, + toolbar: { + display: 'flex', + padding: theme.spacing(0, 1), + '& > *': { + margin: theme.spacing(0, 1), + }, + }, + deviceList: { + flex: 1, + }, + statusCard: { + position: 'fixed', + zIndex: 5, + [theme.breakpoints.up('sm')]: { + left: `calc(50% + ${theme.dimensions.drawerWidthDesktop} / 2)`, + bottom: theme.spacing(3), + }, + [theme.breakpoints.down('sm')]: { + left: '50%', + bottom: theme.spacing(3) + theme.dimensions.bottomBarHeight, + }, + transform: 'translateX(-50%)', + }, + sidebarToggle: { + position: 'absolute', + left: theme.spacing(1.5), + top: theme.spacing(3), + borderRadius: '0px', + minWidth: 0, + [theme.breakpoints.down('sm')]: { + left: 0, + }, + }, + sidebarToggleText: { + marginLeft: theme.spacing(1), + [theme.breakpoints.only('xs')]: { + display: 'none', + }, + }, + sidebarToggleBg: { + backgroundColor: 'white', + color: '#777777', + '&:hover': { + backgroundColor: 'white', + }, + }, + bottomMenu: { + position: 'fixed', + left: theme.spacing(1.5), + bottom: theme.spacing(1.5), + zIndex: 4, + width: theme.dimensions.drawerWidthDesktop, + }, +})); + +const MainPage = () => { + const classes = useStyles(); + const history = useHistory(); + const dispatch = useDispatch(); + const theme = useTheme(); + const t = useTranslation(); + + const deviceReadonly = useDeviceReadonly(); + const isTablet = useMediaQuery(theme.breakpoints.down('sm')); + const isPhone = useMediaQuery(theme.breakpoints.down('xs')); + + const [mapLiveRoutes] = usePersistedState('mapLiveRoutes', false); + + const selectedDeviceId = useSelector((state) => state.devices.selectedId); + + const [searchKeyword, setSearchKeyword] = useState(''); + const [collapsed, setCollapsed] = useState(false); + + const handleClose = () => { + setCollapsed(!collapsed); + }; + + useEffect(() => setCollapsed(isTablet), [isTablet]); + + return ( +
+ + {!isTablet && } + + + + {mapLiveRoutes && } + + + + + + + + + + {isTablet && ( + + + + )} + setSearchKeyword(event.target.value)} + placeholder={t('sharedSearchDevices')} + variant="filled" + /> + history.push('/device')} disabled={deviceReadonly}> + + + {!isTablet && ( + + + + )} + + +
+ +
+
+ {!isPhone && !isTablet && ( +
+ +
+ )} + {selectedDeviceId && ( +
+ dispatch(devicesActions.select(null))} + /> +
+ )} +
+ ); +}; + +export default MainPage; diff --git a/modern/src/main/PositionPage.js b/modern/src/main/PositionPage.js new file mode 100644 index 00000000..ecb4095d --- /dev/null +++ b/modern/src/main/PositionPage.js @@ -0,0 +1,99 @@ +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { + makeStyles, Typography, Container, Paper, AppBar, Toolbar, IconButton, Table, TableHead, TableRow, TableCell, TableBody, +} from '@material-ui/core'; +import ArrowBackIcon from '@material-ui/icons/ArrowBack'; +import { useHistory, useParams } from 'react-router-dom'; +import { useEffectAsync } from '../reactHelper'; +import { prefixString } from '../common/util/stringUtils'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PositionValue from '../common/components/PositionValue'; + +const useStyles = makeStyles((theme) => ({ + root: { + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + }, +})); + +const PositionPage = () => { + const classes = useStyles(); + const history = useHistory(); + const t = useTranslation(); + + const { id } = useParams(); + + const [item, setItem] = useState(); + + useEffectAsync(async () => { + if (id) { + const response = await fetch(`/api/positions?id=${id}`); + if (response.ok) { + const positions = await response.json(); + if (positions.length > 0) { + setItem(positions[0]); + } + } + } else { + setItem({}); + } + }, [id]); + + const deviceName = useSelector((state) => { + if (item) { + const device = state.devices.items[item.deviceId]; + if (device) { + return device.name; + } + } + return null; + }); + + return ( + <> + + + history.push('/')}> + + + + {deviceName} + + + + + + + + + {t('stateName')} + {t('sharedName')} + {t('stateValue')} + + + + {item && Object.getOwnPropertyNames(item).filter((it) => it !== 'attributes').map((property) => ( + + {property} + {t(prefixString('position', property))} + + + ))} + {item && Object.getOwnPropertyNames(item.attributes).map((attribute) => ( + + {attribute} + {t(prefixString('position', attribute)) || t(prefixString('device', attribute))} + + + ))} + +
+
+
+ + ); +}; + +export default PositionPage; diff --git a/modern/src/main/StatusCard.js b/modern/src/main/StatusCard.js new file mode 100644 index 00000000..46d288c6 --- /dev/null +++ b/modern/src/main/StatusCard.js @@ -0,0 +1,134 @@ +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { + makeStyles, Button, Card, CardContent, Typography, CardActions, CardHeader, IconButton, Avatar, Table, TableBody, TableRow, TableCell, TableContainer, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import ReplayIcon from '@material-ui/icons/Replay'; +import PublishIcon from '@material-ui/icons/Publish'; +import EditIcon from '@material-ui/icons/Edit'; +import DeleteIcon from '@material-ui/icons/Delete'; + +import { useTranslation } from '../common/components/LocalizationProvider'; +import { formatStatus } from '../common/util/formatter'; +import RemoveDialog from '../common/components/RemoveDialog'; +import PositionValue from '../common/components/PositionValue'; +import dimensions from '../common/theme/dimensions'; +import { useDeviceReadonly, useReadonly } from '../common/util/permissions'; + +const useStyles = makeStyles((theme) => ({ + card: { + width: dimensions.popupMaxWidth, + }, + negative: { + color: theme.palette.colors.negative, + }, + icon: { + width: '25px', + height: '25px', + filter: 'brightness(0) invert(1)', + }, + table: { + '& .MuiTableCell-sizeSmall': { + paddingLeft: theme.spacing(0.5), + paddingRight: theme.spacing(0.5), + }, + }, + cell: { + borderBottom: 'none', + }, +})); + +const StatusRow = ({ name, content }) => { + const classes = useStyles(); + + return ( + + + {name} + + + {content} + + + ); +}; + +const StatusCard = ({ deviceId, onClose }) => { + const classes = useStyles(); + const history = useHistory(); + const t = useTranslation(); + + const readonly = useReadonly(); + const deviceReadonly = useDeviceReadonly(); + + const device = useSelector((state) => state.devices.items[deviceId]); + const position = useSelector((state) => state.positions.items[deviceId]); + + const [removeDialogShown, setRemoveDialogShown] = useState(false); + + return ( + <> + {device && ( + + + + + )} + action={( + + + + )} + title={device.name} + subheader={formatStatus(device.status, t)} + /> + {position && ( + + + + + } /> + } /> + {position.attributes.odometer + ? } /> + : } />} + } /> + +
+
+
+ )} + + + history.push('/replay')} disabled={!position}> + + + history.push(`/command/${deviceId}`)} disabled={readonly}> + + + history.push(`/device/${deviceId}`)} disabled={deviceReadonly}> + + + setRemoveDialogShown(true)} disabled={deviceReadonly} className={classes.negative}> + + + +
+ )} + setRemoveDialogShown(false)} + /> + + ); +}; + +export default StatusCard; diff --git a/modern/src/map/core/Map.js b/modern/src/map/core/Map.js index e1254af6..c328ba56 100644 --- a/modern/src/map/core/Map.js +++ b/modern/src/map/core/Map.js @@ -12,7 +12,7 @@ import { } from './mapStyles'; import { useAttributePreference } from '../../common/util/preferences'; import palette from '../../common/theme/palette'; -import { useTranslation } from '../../LocalizationProvider'; +import { useTranslation } from '../../common/components/LocalizationProvider'; import usePersistedState, { savePersistedState } from '../../common/util/usePersistedState'; const element = document.createElement('div'); diff --git a/modern/src/map/main/StatusCard.js b/modern/src/map/main/StatusCard.js deleted file mode 100644 index 33f924b3..00000000 --- a/modern/src/map/main/StatusCard.js +++ /dev/null @@ -1,134 +0,0 @@ -import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import { - makeStyles, Button, Card, CardContent, Typography, CardActions, CardHeader, IconButton, Avatar, Table, TableBody, TableRow, TableCell, TableContainer, -} from '@material-ui/core'; -import CloseIcon from '@material-ui/icons/Close'; -import ReplayIcon from '@material-ui/icons/Replay'; -import PublishIcon from '@material-ui/icons/Publish'; -import EditIcon from '@material-ui/icons/Edit'; -import DeleteIcon from '@material-ui/icons/Delete'; - -import { useTranslation } from '../../LocalizationProvider'; -import { formatStatus } from '../../common/util/formatter'; -import RemoveDialog from '../../common/components/RemoveDialog'; -import PositionValue from '../../common/components/PositionValue'; -import dimensions from '../../common/theme/dimensions'; -import { useDeviceReadonly, useReadonly } from '../../common/util/permissions'; - -const useStyles = makeStyles((theme) => ({ - card: { - width: dimensions.popupMaxWidth, - }, - negative: { - color: theme.palette.colors.negative, - }, - icon: { - width: '25px', - height: '25px', - filter: 'brightness(0) invert(1)', - }, - table: { - '& .MuiTableCell-sizeSmall': { - paddingLeft: theme.spacing(0.5), - paddingRight: theme.spacing(0.5), - }, - }, - cell: { - borderBottom: 'none', - }, -})); - -const StatusRow = ({ name, content }) => { - const classes = useStyles(); - - return ( - - - {name} - - - {content} - - - ); -}; - -const StatusCard = ({ deviceId, onClose }) => { - const classes = useStyles(); - const history = useHistory(); - const t = useTranslation(); - - const readonly = useReadonly(); - const deviceReadonly = useDeviceReadonly(); - - const device = useSelector((state) => state.devices.items[deviceId]); - const position = useSelector((state) => state.positions.items[deviceId]); - - const [removeDialogShown, setRemoveDialogShown] = useState(false); - - return ( - <> - {device && ( - - - - - )} - action={( - - - - )} - title={device.name} - subheader={formatStatus(device.status, t)} - /> - {position && ( - - - - - } /> - } /> - {position.attributes.odometer - ? } /> - : } />} - } /> - -
-
-
- )} - - - history.push('/replay')} disabled={!position}> - - - history.push(`/command/${deviceId}`)} disabled={readonly}> - - - history.push(`/device/${deviceId}`)} disabled={deviceReadonly}> - - - setRemoveDialogShown(true)} disabled={deviceReadonly} className={classes.negative}> - - - -
- )} - setRemoveDialogShown(false)} - /> - - ); -}; - -export default StatusCard; diff --git a/modern/src/other/EventPage.js b/modern/src/other/EventPage.js new file mode 100644 index 00000000..46f5e67c --- /dev/null +++ b/modern/src/other/EventPage.js @@ -0,0 +1,77 @@ +import React, { useState } from 'react'; + +import { + makeStyles, Typography, AppBar, Toolbar, IconButton, +} from '@material-ui/core'; +import ArrowBackIcon from '@material-ui/icons/ArrowBack'; +import { useHistory, useParams } from 'react-router-dom'; +import ContainerDimensions from 'react-container-dimensions'; +import { useEffectAsync } from '../reactHelper'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import Map from '../map/core/Map'; +import PositionsMap from '../map/PositionsMap'; + +const useStyles = makeStyles(() => ({ + root: { + height: '100%', + display: 'flex', + flexDirection: 'column', + }, + mapContainer: { + flexGrow: 1, + }, +})); + +const EventPage = () => { + const classes = useStyles(); + const history = useHistory(); + const t = useTranslation(); + + const { id } = useParams(); + + const [event, setEvent] = useState(); + const [position, setPosition] = useState(); + + useEffectAsync(async () => { + if (id) { + const response = await fetch(`/api/events/${id}`); + if (response.ok) { + setEvent(await response.json()); + } + } + }, [id]); + + useEffectAsync(async () => { + if (event && event.positionId) { + const response = await fetch(`/api/positions?id=${event.positionId}`); + if (response.ok) { + const positions = await response.json(); + if (positions.length > 0) { + setPosition(positions[0]); + } + } + } + }, [event]); + + return ( +
+ + + history.push('/')}> + + + {t('positionEvent')} + + +
+ + + {position && } + + +
+
+ ); +}; + +export default EventPage; diff --git a/modern/src/other/GeofencesList.js b/modern/src/other/GeofencesList.js new file mode 100644 index 00000000..b4fde749 --- /dev/null +++ b/modern/src/other/GeofencesList.js @@ -0,0 +1,56 @@ +import React, { Fragment } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { makeStyles } from '@material-ui/core/styles'; +import Divider from '@material-ui/core/Divider'; +import IconButton from '@material-ui/core/IconButton'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; +import ListItemText from '@material-ui/core/ListItemText'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; + +import { devicesActions } from '../store'; +import EditCollectionView from '../settings/components/EditCollectionView'; + +const useStyles = makeStyles(() => ({ + list: { + maxHeight: '100%', + overflow: 'auto', + }, + icon: { + width: '25px', + height: '25px', + filter: 'brightness(0) invert(1)', + }, +})); + +const GeofenceView = ({ onMenuClick }) => { + const classes = useStyles(); + const dispatch = useDispatch(); + + const items = useSelector((state) => state.geofences.items); + + return ( + + {Object.values(items).map((item, index, list) => ( + + dispatch(devicesActions.select(item.id))}> + + + onMenuClick(event.currentTarget, item.id)}> + + + + + {index < list.length - 1 ? : null} + + ))} + + ); +}; + +const GeofencesList = () => ( + +); + +export default GeofencesList; diff --git a/modern/src/other/GeofencesPage.js b/modern/src/other/GeofencesPage.js new file mode 100644 index 00000000..79eed22a --- /dev/null +++ b/modern/src/other/GeofencesPage.js @@ -0,0 +1,87 @@ +import React from 'react'; +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 ArrowBackIcon from '@material-ui/icons/ArrowBack'; +import { useHistory } from 'react-router-dom'; +import Map from '../map/core/Map'; +import CurrentLocationMap from '../map/CurrentLocationMap'; +import GeofenceEditMap from '../map/GeofenceEditMap'; +import GeofencesList from './GeofencesList'; +import { useTranslation } from '../common/components/LocalizationProvider'; + +const useStyles = makeStyles((theme) => ({ + root: { + height: '100%', + display: 'flex', + flexDirection: 'column', + }, + content: { + flexGrow: 1, + overflow: 'hidden', + display: 'flex', + flexDirection: 'row', + [theme.breakpoints.down('xs')]: { + flexDirection: 'column-reverse', + }, + }, + drawerPaper: { + position: 'relative', + [theme.breakpoints.up('sm')]: { + width: 350, + }, + [theme.breakpoints.down('xs')]: { + height: 250, + }, + }, + drawerHeader: { + ...theme.mixins.toolbar, + display: 'flex', + alignItems: 'center', + padding: theme.spacing(0, 1), + }, + mapContainer: { + flexGrow: 1, + }, +})); + +const GeofencesPage = ({ width }) => { + const classes = useStyles(); + const history = useHistory(); + const t = useTranslation(); + + return ( +
+
+ +
+ history.goBack()}> + + + + {t('sharedGeofences')} + +
+ + +
+
+ + + + + + +
+
+
+ ); +}; + +export default withWidth()(GeofencesPage); diff --git a/modern/src/other/ReplayPage.js b/modern/src/other/ReplayPage.js new file mode 100644 index 00000000..9b66853d --- /dev/null +++ b/modern/src/other/ReplayPage.js @@ -0,0 +1,220 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { + Grid, FormControlLabel, Switch, IconButton, TextField, makeStyles, Paper, Slider, Toolbar, Tooltip, Typography, +} from '@material-ui/core'; +import ArrowBackIcon from '@material-ui/icons/ArrowBack'; +import SettingsIcon from '@material-ui/icons/Settings'; +import PlayArrowIcon from '@material-ui/icons/PlayArrow'; +import PauseIcon from '@material-ui/icons/Pause'; +import FastForwardIcon from '@material-ui/icons/FastForward'; +import FastRewindIcon from '@material-ui/icons/FastRewind'; +import { useHistory } from 'react-router-dom'; +import { useSelector } from 'react-redux'; +import Map from '../map/core/Map'; +import ReplayPathMap from '../map/ReplayPathMap'; +import PositionsMap from '../map/PositionsMap'; +import { formatTime } from '../common/util/formatter'; +import ReportFilter from '../reports/components/ReportFilter'; +import { useTranslation } from '../common/components/LocalizationProvider'; + +const useStyles = makeStyles((theme) => ({ + root: { + height: '100%', + }, + sidebar: { + position: 'absolute', + left: 0, + top: 0, + margin: theme.spacing(1.5), + width: theme.dimensions.drawerWidthDesktop, + [theme.breakpoints.down('sm')]: { + width: '100%', + margin: 0, + }, + }, + formControlLabel: { + height: '100%', + width: '100%', + paddingRight: theme.spacing(1), + justifyContent: 'space-between', + alignItems: 'center', + }, + reportFilterContainer: { + flex: 1, + padding: theme.spacing(2), + [theme.breakpoints.down('sm')]: { + margin: theme.spacing(1), + }, + }, + sliderContainer: { + padding: theme.spacing(2), + }, +})); + +const TimeLabel = ({ children, open, value }) => ( + + {children} + +); + +const ReplayPage = () => { + const t = useTranslation(); + const classes = useStyles(); + const history = useHistory(); + const timerRef = useRef(); + + const [positions, setPositions] = useState([]); + const [index, setIndex] = useState(0); + const [selectedDeviceId, setSelectedDeviceId] = useState(); + const [playbackSpeed, setPlaybackSpeed] = useState(''); + const [expanded, setExpanded] = useState(true); + const [isPlaying, setIsPlaying] = useState(false); + + const deviceName = useSelector((state) => { + if (selectedDeviceId) { + const device = state.devices.items[selectedDeviceId]; + if (device) { + return device.name; + } + } + return null; + }); + + useEffect(() => { + if (isPlaying && positions.length > 0) { + timerRef.current = setInterval(() => { + setIndex((index) => index + 1); + }, 500); + } else { + clearInterval(timerRef.current); + } + + return () => clearInterval(timerRef.current); + }, [isPlaying, positions]); + + useEffect(() => { + if (index >= positions.length) { + clearInterval(timerRef.current); + } + }, [index, positions]); + + const handleSubmit = async (deviceId, from, to, _, headers) => { + setSelectedDeviceId(deviceId); + const query = new URLSearchParams({ deviceId, from, to }); + const response = await fetch(`/api/positions?${query.toString()}`, { headers }); + if (response.ok) { + setIndex(0); + setPositions(await response.json()); + setExpanded(false); + } + }; + + return ( +
+ + + {index < positions.length && } + +
+ + + + + + + history.push('/')}> + + + + + {t('reportReplay')} + + {!expanded && ( + + setExpanded(true)}> + + + + )} + + + + + + {!expanded ? ( + + + + {deviceName} + + + ({ value: index }))} + value={index} + onChange={(_, index) => setIndex(index)} + valueLabelDisplay="auto" + valueLabelFormat={(i) => (i < positions.length ? formatTime(positions[i]) : '')} + ValueLabelComponent={TimeLabel} + /> + + + {`${index}/${positions.length}`} + + setIndex((index) => index - 1)} disabled={isPlaying}> + + + + + setIsPlaying(!isPlaying)}> + {isPlaying ? : } + + + + setIndex((index) => index + 1)} disabled={isPlaying}> + + + + {formatTime(positions[index])} + + + + ) : ( + + + + setPlaybackSpeed(e.target.value)} + variant="filled" + /> + + + setIsPlaying(e.target.checked)} + color="primary" + edge="start" + /> + )} + label={t('reportAutoPlay')} + labelPlacement="start" + /> + + + + )} + + +
+
+ ); +}; + +export default ReplayPage; diff --git a/modern/src/reports/ChartReportPage.js b/modern/src/reports/ChartReportPage.js index 8803f2be..70429ec5 100644 --- a/modern/src/reports/ChartReportPage.js +++ b/modern/src/reports/ChartReportPage.js @@ -2,13 +2,13 @@ import React, { useState } from 'react'; import { Grid, FormControl, InputLabel, Select, MenuItem, } from '@material-ui/core'; -import ReportLayout from './ReportLayout'; -import ReportFilter from './ReportFilter'; -import Graph from './Graph'; +import ReportLayout from './components/ReportLayout'; +import ReportFilter from './components/ReportFilter'; +import Graph from './components/Graph'; import { useAttributePreference } from '../common/util/preferences'; import { formatDate } from '../common/util/formatter'; import { speedFromKnots } from '../common/util/converter'; -import { useTranslation } from '../LocalizationProvider'; +import { useTranslation } from '../common/components/LocalizationProvider'; const Filter = ({ children, setItems }) => { const speedUnit = useAttributePreference('speedUnit'); diff --git a/modern/src/reports/EventReportPage.js b/modern/src/reports/EventReportPage.js index 45bb4c25..bbd92d59 100644 --- a/modern/src/reports/EventReportPage.js +++ b/modern/src/reports/EventReportPage.js @@ -6,10 +6,10 @@ import { import { useTheme } from '@material-ui/core/styles'; import { useSelector } from 'react-redux'; import { formatDate } from '../common/util/formatter'; -import ReportFilter from './ReportFilter'; -import ReportLayout from './ReportLayout'; +import ReportFilter from './components/ReportFilter'; +import ReportLayout from './components/ReportLayout'; import { prefixString } from '../common/util/stringUtils'; -import { useTranslation } from '../LocalizationProvider'; +import { useTranslation } from '../common/components/LocalizationProvider'; const Filter = ({ setItems }) => { const t = useTranslation(); diff --git a/modern/src/reports/Graph.js b/modern/src/reports/Graph.js deleted file mode 100644 index 63d24eee..00000000 --- a/modern/src/reports/Graph.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { withWidth } from '@material-ui/core'; -import { - LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, -} from 'recharts'; - -const CustomizedAxisTick = ({ x, y, payload }) => { - if (!payload.value) { - return payload.value; - } - const parts = payload.value.split(' '); - return ( - - {parts[0]} - {parts[1]} - - ); -}; - -const Graph = ({ type, items }) => ( - - - } height={60} /> - - - - - - - -); - -export default withWidth()(Graph); diff --git a/modern/src/reports/ReplayPage.js b/modern/src/reports/ReplayPage.js deleted file mode 100644 index a8d4844f..00000000 --- a/modern/src/reports/ReplayPage.js +++ /dev/null @@ -1,220 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { - Grid, FormControlLabel, Switch, IconButton, TextField, makeStyles, Paper, Slider, Toolbar, Tooltip, Typography, -} from '@material-ui/core'; -import ArrowBackIcon from '@material-ui/icons/ArrowBack'; -import SettingsIcon from '@material-ui/icons/Settings'; -import PlayArrowIcon from '@material-ui/icons/PlayArrow'; -import PauseIcon from '@material-ui/icons/Pause'; -import FastForwardIcon from '@material-ui/icons/FastForward'; -import FastRewindIcon from '@material-ui/icons/FastRewind'; -import { useHistory } from 'react-router-dom'; -import { useSelector } from 'react-redux'; -import Map from '../map/core/Map'; -import ReplayPathMap from '../map/ReplayPathMap'; -import PositionsMap from '../map/PositionsMap'; -import { formatTime } from '../common/util/formatter'; -import ReportFilter from './ReportFilter'; -import { useTranslation } from '../LocalizationProvider'; - -const useStyles = makeStyles((theme) => ({ - root: { - height: '100%', - }, - sidebar: { - position: 'absolute', - left: 0, - top: 0, - margin: theme.spacing(1.5), - width: theme.dimensions.drawerWidthDesktop, - [theme.breakpoints.down('sm')]: { - width: '100%', - margin: 0, - }, - }, - formControlLabel: { - height: '100%', - width: '100%', - paddingRight: theme.spacing(1), - justifyContent: 'space-between', - alignItems: 'center', - }, - reportFilterContainer: { - flex: 1, - padding: theme.spacing(2), - [theme.breakpoints.down('sm')]: { - margin: theme.spacing(1), - }, - }, - sliderContainer: { - padding: theme.spacing(2), - }, -})); - -const TimeLabel = ({ children, open, value }) => ( - - {children} - -); - -const ReplayPage = () => { - const t = useTranslation(); - const classes = useStyles(); - const history = useHistory(); - const timerRef = useRef(); - - const [positions, setPositions] = useState([]); - const [index, setIndex] = useState(0); - const [selectedDeviceId, setSelectedDeviceId] = useState(); - const [playbackSpeed, setPlaybackSpeed] = useState(''); - const [expanded, setExpanded] = useState(true); - const [isPlaying, setIsPlaying] = useState(false); - - const deviceName = useSelector((state) => { - if (selectedDeviceId) { - const device = state.devices.items[selectedDeviceId]; - if (device) { - return device.name; - } - } - return null; - }); - - useEffect(() => { - if (isPlaying && positions.length > 0) { - timerRef.current = setInterval(() => { - setIndex((index) => index + 1); - }, 500); - } else { - clearInterval(timerRef.current); - } - - return () => clearInterval(timerRef.current); - }, [isPlaying, positions]); - - useEffect(() => { - if (index >= positions.length) { - clearInterval(timerRef.current); - } - }, [index, positions]); - - const handleSubmit = async (deviceId, from, to, _, headers) => { - setSelectedDeviceId(deviceId); - const query = new URLSearchParams({ deviceId, from, to }); - const response = await fetch(`/api/positions?${query.toString()}`, { headers }); - if (response.ok) { - setIndex(0); - setPositions(await response.json()); - setExpanded(false); - } - }; - - return ( -
- - - {index < positions.length && } - -
- - - - - - - history.push('/')}> - - - - - {t('reportReplay')} - - {!expanded && ( - - setExpanded(true)}> - - - - )} - - - - - - {!expanded ? ( - - - - {deviceName} - - - ({ value: index }))} - value={index} - onChange={(_, index) => setIndex(index)} - valueLabelDisplay="auto" - valueLabelFormat={(i) => (i < positions.length ? formatTime(positions[i]) : '')} - ValueLabelComponent={TimeLabel} - /> - - - {`${index}/${positions.length}`} - - setIndex((index) => index - 1)} disabled={isPlaying}> - - - - - setIsPlaying(!isPlaying)}> - {isPlaying ? : } - - - - setIndex((index) => index + 1)} disabled={isPlaying}> - - - - {formatTime(positions[index])} - - - - ) : ( - - - - setPlaybackSpeed(e.target.value)} - variant="filled" - /> - - - setIsPlaying(e.target.checked)} - color="primary" - edge="start" - /> - )} - label={t('reportAutoPlay')} - labelPlacement="start" - /> - - - - )} - - -
-
- ); -}; - -export default ReplayPage; diff --git a/modern/src/reports/ReportFilter.js b/modern/src/reports/ReportFilter.js deleted file mode 100644 index 827b36fb..00000000 --- a/modern/src/reports/ReportFilter.js +++ /dev/null @@ -1,153 +0,0 @@ -import React, { useState } from 'react'; -import { - FormControl, InputLabel, Select, MenuItem, Button, TextField, Grid, Typography, -} from '@material-ui/core'; -import { useSelector } from 'react-redux'; -import moment from 'moment'; -import { useTranslation } from '../LocalizationProvider'; - -const ReportFilter = ({ children, handleSubmit, showOnly }) => { - const t = useTranslation(); - - const devices = useSelector((state) => state.devices.items); - const [deviceId, setDeviceId] = useState(); - const [period, setPeriod] = useState('today'); - const [from, setFrom] = useState(moment().subtract(1, 'hour')); - const [to, setTo] = useState(moment()); - - const handleClick = (mail, json) => { - let selectedFrom; - let selectedTo; - switch (period) { - case 'today': - selectedFrom = moment().startOf('day'); - selectedTo = moment().endOf('day'); - break; - case 'yesterday': - selectedFrom = moment().subtract(1, 'day').startOf('day'); - selectedTo = moment().subtract(1, 'day').endOf('day'); - break; - case 'thisWeek': - selectedFrom = moment().startOf('week'); - selectedTo = moment().endOf('week'); - break; - case 'previousWeek': - selectedFrom = moment().subtract(1, 'week').startOf('week'); - selectedTo = moment().subtract(1, 'week').endOf('week'); - break; - case 'thisMonth': - selectedFrom = moment().startOf('month'); - selectedTo = moment().endOf('month'); - break; - case 'previousMonth': - selectedFrom = moment().subtract(1, 'month').startOf('month'); - selectedTo = moment().subtract(1, 'month').endOf('month'); - break; - default: - selectedFrom = from; - selectedTo = to; - break; - } - - const accept = json ? 'application/json' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; - handleSubmit( - deviceId, - selectedFrom.toISOString(), - selectedTo.toISOString(), - mail, - { Accept: accept }, - ); - }; - - return ( - - - - {t('reportDevice')} - - - - - - {t('reportPeriod')} - - - - {period === 'custom' && ( - - setFrom(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL))} - fullWidth - /> - - )} - {period === 'custom' && ( - - setTo(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL))} - fullWidth - /> - - )} - {children} - - - - {!showOnly - && ( - - - - )} - {!showOnly - && ( - - - - )} - - ); -}; - -export default ReportFilter; diff --git a/modern/src/reports/ReportLayout.js b/modern/src/reports/ReportLayout.js deleted file mode 100644 index b2be2ac1..00000000 --- a/modern/src/reports/ReportLayout.js +++ /dev/null @@ -1,124 +0,0 @@ -import React, { useState, useEffect, useMemo } from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; -import { - Grid, Typography, Divider, Drawer, makeStyles, IconButton, Hidden, -} from '@material-ui/core'; -import TimelineIcon from '@material-ui/icons/Timeline'; -import PauseCircleFilledIcon from '@material-ui/icons/PauseCircleFilled'; -import PlayCircleFilledIcon from '@material-ui/icons/PlayCircleFilled'; -import NotificationsActiveIcon from '@material-ui/icons/NotificationsActive'; -import FormatListBulletedIcon from '@material-ui/icons/FormatListBulleted'; -import TrendingUpIcon from '@material-ui/icons/TrendingUp'; -import ArrowBackIcon from '@material-ui/icons/ArrowBack'; - -import SideNav from '../common/components/SideNav'; -import NavBar from '../common/components/NavBar'; -import { useTranslation } from '../LocalizationProvider'; - -const useStyles = makeStyles((theme) => ({ - root: { - display: 'flex', - [theme.breakpoints.down('sm')]: { - flexDirection: 'column', - }, - height: '100%', - }, - drawerContainer: { - width: theme.dimensions.drawerWidthDesktop, - }, - drawer: { - width: theme.dimensions.drawerWidthDesktop, - [theme.breakpoints.down('sm')]: { - width: theme.dimensions.drawerWidthTablet, - }, - }, - content: { - flex: 1, - padding: theme.spacing(3, 3, 3, 3), - }, - drawerHeader: { - ...theme.mixins.toolbar, - display: 'flex', - alignItems: 'center', - padding: theme.spacing(0, 1), - }, - backArrowIconContainer: { - '&:hover': { - backgroundColor: 'transparent', - }, - }, -})); - -const ReportLayout = ({ children, filter }) => { - const classes = useStyles(); - const history = useHistory(); - const location = useLocation(); - const t = useTranslation(); - - const [openDrawer, setOpenDrawer] = useState(false); - const [reportTitle, setReportTitle] = useState(); - - const routes = useMemo(() => [ - { name: t('reportRoute'), href: '/reports/route', icon: }, - { name: t('reportEvents'), href: '/reports/event', icon: }, - { name: t('reportTrips'), href: '/reports/trip', icon: }, - { name: t('reportStops'), href: '/reports/stop', icon: }, - { name: t('reportSummary'), href: '/reports/summary', icon: }, - { name: t('reportChart'), href: '/reports/chart', icon: }, - ], [t]); - - useEffect(() => { - routes.forEach((route) => { - switch (location.pathname) { - case `${route.href}`: - setReportTitle(route.name); - break; - default: - break; - } - }); - }, [routes, location]); - - const pageTitle = `${t('reportTitle')} / ${reportTitle}`; - - return ( -
- - - setOpenDrawer(!openDrawer)} - classes={{ paper: classes.drawer }} - > - - - - - -
- history.push('/')}> - - - - {t('reportTitle')} - -
- - -
-
-
- - {filter} - {children} - -
-
- ); -}; - -export default ReportLayout; diff --git a/modern/src/reports/RouteReportPage.js b/modern/src/reports/RouteReportPage.js index a3549924..035f6acd 100644 --- a/modern/src/reports/RouteReportPage.js +++ b/modern/src/reports/RouteReportPage.js @@ -5,10 +5,10 @@ import { useTheme } from '@material-ui/core/styles'; import { formatDistance, formatSpeed, formatBoolean, formatDate, formatCoordinate, } from '../common/util/formatter'; -import ReportFilter from './ReportFilter'; -import ReportLayout from './ReportLayout'; +import ReportFilter from './components/ReportFilter'; +import ReportLayout from './components/ReportLayout'; import { useAttributePreference, usePreference } from '../common/util/preferences'; -import { useTranslation } from '../LocalizationProvider'; +import { useTranslation } from '../common/components/LocalizationProvider'; const Filter = ({ setItems }) => { const handleSubmit = async (deviceId, from, to, mail, headers) => { diff --git a/modern/src/reports/StatisticsPage.js b/modern/src/reports/StatisticsPage.js new file mode 100644 index 00000000..4b0d9bfe --- /dev/null +++ b/modern/src/reports/StatisticsPage.js @@ -0,0 +1,145 @@ +import React, { useState } from 'react'; +import { + FormControl, InputLabel, Select, MenuItem, TextField, Button, TableContainer, Table, TableRow, TableCell, TableHead, TableBody, +} from '@material-ui/core'; +import moment from 'moment'; +import { formatDate } from '../common/util/formatter'; +import OptionsLayout from '../settings/components/OptionsLayout'; +import { useTranslation } from '../common/components/LocalizationProvider'; + +const Filter = ({ setItems }) => { + const t = useTranslation(); + + const [period, setPeriod] = useState('today'); + const [from, setFrom] = useState(moment().subtract(1, 'hour')); + const [to, setTo] = useState(moment()); + + const handleClick = async () => { + let selectedFrom; + let selectedTo; + switch (period) { + case 'today': + selectedFrom = moment().startOf('day'); + selectedTo = moment().endOf('day'); + break; + case 'yesterday': + selectedFrom = moment().subtract(1, 'day').startOf('day'); + selectedTo = moment().subtract(1, 'day').endOf('day'); + break; + case 'thisWeek': + selectedFrom = moment().startOf('week'); + selectedTo = moment().endOf('week'); + break; + case 'previousWeek': + selectedFrom = moment().subtract(1, 'week').startOf('week'); + selectedTo = moment().subtract(1, 'week').endOf('week'); + break; + case 'thisMonth': + selectedFrom = moment().startOf('month'); + selectedTo = moment().endOf('month'); + break; + case 'previousMonth': + selectedFrom = moment().subtract(1, 'month').startOf('month'); + selectedTo = moment().subtract(1, 'month').endOf('month'); + break; + default: + selectedFrom = from; + selectedTo = to; + break; + } + + const query = new URLSearchParams({ from: selectedFrom.toISOString(), to: selectedTo.toISOString() }); + const response = await fetch(`/api/statistics?${query.toString()}`, { Accept: 'application/json' }); + if (response.ok) { + setItems(await response.json()); + } + }; + + return ( + <> + + {t('reportPeriod')} + + + {period === 'custom' && ( + setFrom(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL))} + fullWidth + /> + )} + {period === 'custom' && ( + setTo(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL))} + fullWidth + /> + )} + + + ); +}; + +const StatisticsPage = () => { + const t = useTranslation(); + + const [items, setItems] = useState([]); + + return ( + + + + + + + {t('statisticsCaptureTime')} + {t('statisticsActiveUsers')} + {t('statisticsActiveDevices')} + {t('statisticsRequests')} + {t('statisticsMessagesReceived')} + {t('statisticsMessagesStored')} + {t('notificatorMail')} + {t('notificatorSms')} + {t('statisticsGeocoder')} + {t('statisticsGeolocation')} + + + + {items.map((item) => ( + + {formatDate(item.captureTime)} + {item.activeUsers} + {item.activeDevices} + {item.requests} + {item.messagesReceived} + {item.messagesStored} + {item.mailSent} + {item.smsSent} + {item.geocoderRequests} + {item.geolocationRequests} + + ))} + +
+
+
+ ); +}; + +export default StatisticsPage; diff --git a/modern/src/reports/StopReportPage.js b/modern/src/reports/StopReportPage.js index b0c5169f..0dca99b3 100644 --- a/modern/src/reports/StopReportPage.js +++ b/modern/src/reports/StopReportPage.js @@ -4,10 +4,10 @@ import { useTheme } from '@material-ui/core/styles'; import { formatDistance, formatHours, formatDate, formatVolume, } from '../common/util/formatter'; -import ReportFilter from './ReportFilter'; -import ReportLayout from './ReportLayout'; +import ReportFilter from './components/ReportFilter'; +import ReportLayout from './components/ReportLayout'; import { useAttributePreference } from '../common/util/preferences'; -import { useTranslation } from '../LocalizationProvider'; +import { useTranslation } from '../common/components/LocalizationProvider'; const Filter = ({ setItems }) => { const handleSubmit = async (deviceId, from, to, mail, headers) => { diff --git a/modern/src/reports/SummaryReportPage.js b/modern/src/reports/SummaryReportPage.js index f86a55fe..fe6f4aa1 100644 --- a/modern/src/reports/SummaryReportPage.js +++ b/modern/src/reports/SummaryReportPage.js @@ -5,10 +5,10 @@ import { useTheme } from '@material-ui/core/styles'; import { formatDistance, formatHours, formatDate, formatSpeed, formatVolume, } from '../common/util/formatter'; -import ReportFilter from './ReportFilter'; -import ReportLayout from './ReportLayout'; +import ReportFilter from './components/ReportFilter'; +import ReportLayout from './components/ReportLayout'; import { useAttributePreference } from '../common/util/preferences'; -import { useTranslation } from '../LocalizationProvider'; +import { useTranslation } from '../common/components/LocalizationProvider'; const Filter = ({ setItems }) => { const t = useTranslation(); diff --git a/modern/src/reports/TripReportPage.js b/modern/src/reports/TripReportPage.js index 7a74e673..a87031d3 100644 --- a/modern/src/reports/TripReportPage.js +++ b/modern/src/reports/TripReportPage.js @@ -4,10 +4,10 @@ import { useTheme } from '@material-ui/core/styles'; import { formatDistance, formatSpeed, formatHours, formatDate, formatVolume, } from '../common/util/formatter'; -import ReportFilter from './ReportFilter'; -import ReportLayout from './ReportLayout'; +import ReportFilter from './components/ReportFilter'; +import ReportLayout from './components/ReportLayout'; import { useAttributePreference } from '../common/util/preferences'; -import { useTranslation } from '../LocalizationProvider'; +import { useTranslation } from '../common/components/LocalizationProvider'; const Filter = ({ setItems }) => { const handleSubmit = async (deviceId, from, to, mail, headers) => { diff --git a/modern/src/reports/components/Graph.js b/modern/src/reports/components/Graph.js new file mode 100644 index 00000000..63d24eee --- /dev/null +++ b/modern/src/reports/components/Graph.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { withWidth } from '@material-ui/core'; +import { + LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, +} from 'recharts'; + +const CustomizedAxisTick = ({ x, y, payload }) => { + if (!payload.value) { + return payload.value; + } + const parts = payload.value.split(' '); + return ( + + {parts[0]} + {parts[1]} + + ); +}; + +const Graph = ({ type, items }) => ( + + + } height={60} /> + + + + + + + +); + +export default withWidth()(Graph); diff --git a/modern/src/reports/components/ReportFilter.js b/modern/src/reports/components/ReportFilter.js new file mode 100644 index 00000000..bc9c5af6 --- /dev/null +++ b/modern/src/reports/components/ReportFilter.js @@ -0,0 +1,153 @@ +import React, { useState } from 'react'; +import { + FormControl, InputLabel, Select, MenuItem, Button, TextField, Grid, Typography, +} from '@material-ui/core'; +import { useSelector } from 'react-redux'; +import moment from 'moment'; +import { useTranslation } from '../../common/components/LocalizationProvider'; + +const ReportFilter = ({ children, handleSubmit, showOnly }) => { + const t = useTranslation(); + + const devices = useSelector((state) => state.devices.items); + const [deviceId, setDeviceId] = useState(); + const [period, setPeriod] = useState('today'); + const [from, setFrom] = useState(moment().subtract(1, 'hour')); + const [to, setTo] = useState(moment()); + + const handleClick = (mail, json) => { + let selectedFrom; + let selectedTo; + switch (period) { + case 'today': + selectedFrom = moment().startOf('day'); + selectedTo = moment().endOf('day'); + break; + case 'yesterday': + selectedFrom = moment().subtract(1, 'day').startOf('day'); + selectedTo = moment().subtract(1, 'day').endOf('day'); + break; + case 'thisWeek': + selectedFrom = moment().startOf('week'); + selectedTo = moment().endOf('week'); + break; + case 'previousWeek': + selectedFrom = moment().subtract(1, 'week').startOf('week'); + selectedTo = moment().subtract(1, 'week').endOf('week'); + break; + case 'thisMonth': + selectedFrom = moment().startOf('month'); + selectedTo = moment().endOf('month'); + break; + case 'previousMonth': + selectedFrom = moment().subtract(1, 'month').startOf('month'); + selectedTo = moment().subtract(1, 'month').endOf('month'); + break; + default: + selectedFrom = from; + selectedTo = to; + break; + } + + const accept = json ? 'application/json' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + handleSubmit( + deviceId, + selectedFrom.toISOString(), + selectedTo.toISOString(), + mail, + { Accept: accept }, + ); + }; + + return ( + + + + {t('reportDevice')} + + + + + + {t('reportPeriod')} + + + + {period === 'custom' && ( + + setFrom(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL))} + fullWidth + /> + + )} + {period === 'custom' && ( + + setTo(moment(e.target.value, moment.HTML5_FMT.DATETIME_LOCAL))} + fullWidth + /> + + )} + {children} + + + + {!showOnly + && ( + + + + )} + {!showOnly + && ( + + + + )} + + ); +}; + +export default ReportFilter; diff --git a/modern/src/reports/components/ReportLayout.js b/modern/src/reports/components/ReportLayout.js new file mode 100644 index 00000000..c028530b --- /dev/null +++ b/modern/src/reports/components/ReportLayout.js @@ -0,0 +1,124 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { + Grid, Typography, Divider, Drawer, makeStyles, IconButton, Hidden, +} from '@material-ui/core'; +import TimelineIcon from '@material-ui/icons/Timeline'; +import PauseCircleFilledIcon from '@material-ui/icons/PauseCircleFilled'; +import PlayCircleFilledIcon from '@material-ui/icons/PlayCircleFilled'; +import NotificationsActiveIcon from '@material-ui/icons/NotificationsActive'; +import FormatListBulletedIcon from '@material-ui/icons/FormatListBulleted'; +import TrendingUpIcon from '@material-ui/icons/TrendingUp'; +import ArrowBackIcon from '@material-ui/icons/ArrowBack'; + +import SideNav from '../../common/components/SideNav'; +import NavBar from '../../common/components/NavBar'; +import { useTranslation } from '../../common/components/LocalizationProvider'; + +const useStyles = makeStyles((theme) => ({ + root: { + display: 'flex', + [theme.breakpoints.down('sm')]: { + flexDirection: 'column', + }, + height: '100%', + }, + drawerContainer: { + width: theme.dimensions.drawerWidthDesktop, + }, + drawer: { + width: theme.dimensions.drawerWidthDesktop, + [theme.breakpoints.down('sm')]: { + width: theme.dimensions.drawerWidthTablet, + }, + }, + content: { + flex: 1, + padding: theme.spacing(3, 3, 3, 3), + }, + drawerHeader: { + ...theme.mixins.toolbar, + display: 'flex', + alignItems: 'center', + padding: theme.spacing(0, 1), + }, + backArrowIconContainer: { + '&:hover': { + backgroundColor: 'transparent', + }, + }, +})); + +const ReportLayout = ({ children, filter }) => { + const classes = useStyles(); + const history = useHistory(); + const location = useLocation(); + const t = useTranslation(); + + const [openDrawer, setOpenDrawer] = useState(false); + const [reportTitle, setReportTitle] = useState(); + + const routes = useMemo(() => [ + { name: t('reportRoute'), href: '/reports/route', icon: }, + { name: t('reportEvents'), href: '/reports/event', icon: }, + { name: t('reportTrips'), href: '/reports/trip', icon: }, + { name: t('reportStops'), href: '/reports/stop', icon: }, + { name: t('reportSummary'), href: '/reports/summary', icon: }, + { name: t('reportChart'), href: '/reports/chart', icon: }, + ], [t]); + + useEffect(() => { + routes.forEach((route) => { + switch (location.pathname) { + case `${route.href}`: + setReportTitle(route.name); + break; + default: + break; + } + }); + }, [routes, location]); + + const pageTitle = `${t('reportTitle')} / ${reportTitle}`; + + return ( +
+ + + setOpenDrawer(!openDrawer)} + classes={{ paper: classes.drawer }} + > + + + + + +
+ history.push('/')}> + + + + {t('reportTitle')} + +
+ + +
+
+
+ + {filter} + {children} + +
+
+ ); +}; + +export default ReportLayout; diff --git a/modern/src/settings/AccumulatorsPage.js b/modern/src/settings/AccumulatorsPage.js index 3e98612a..f8895bbe 100644 --- a/modern/src/settings/AccumulatorsPage.js +++ b/modern/src/settings/AccumulatorsPage.js @@ -5,8 +5,8 @@ import { Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, Container, TextField, FormControl, Button, } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import { useTranslation } from '../LocalizationProvider'; -import OptionsLayout from './OptionsLayout'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import OptionsLayout from './components/OptionsLayout'; const useStyles = makeStyles((theme) => ({ container: { diff --git a/modern/src/settings/BaseCommandView.js b/modern/src/settings/BaseCommandView.js deleted file mode 100644 index 03605d0f..00000000 --- a/modern/src/settings/BaseCommandView.js +++ /dev/null @@ -1,58 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { - TextField, FormControlLabel, Checkbox, -} from '@material-ui/core'; -import { useTranslation } from '../LocalizationProvider'; -import SelectField from '../form/SelectField'; -import { prefixString } from '../common/util/stringUtils'; -import useCommandAttributes from '../attributes/useCommandAttributes'; - -const BaseCommandView = ({ item, setItem }) => { - const t = useTranslation(); - - const availableAttributes = useCommandAttributes(t); - - const [attributes, setAttributes] = useState([]); - - useEffect(() => { - if (item && item.type) { - setAttributes(availableAttributes[item.type] || []); - } else { - setAttributes([]); - } - }, [availableAttributes, item]); - - return ( - <> - setItem({ ...item, type: e.target.value, attributes: {} })} - endpoint="/api/commands/types" - keyGetter={(it) => it.type} - titleGetter={(it) => t(prefixString('command', it.type))} - label={t('sharedType')} - variant="filled" - /> - {attributes.map((attribute) => ( - { - const updateItem = { ...item, attributes: { ...item.attributes } }; - updateItem.attributes[attribute.key] = e.target.value; - setItem(updateItem); - }} - label={attribute.name} - variant="filled" - /> - ))} - setItem({ ...item, textChannel: event.target.checked })} />} - label={t('commandSendSms')} - /> - - ); -}; - -export default BaseCommandView; diff --git a/modern/src/settings/CalendarPage.js b/modern/src/settings/CalendarPage.js index 0d35d2f3..bd3aa700 100644 --- a/modern/src/settings/CalendarPage.js +++ b/modern/src/settings/CalendarPage.js @@ -5,9 +5,9 @@ import { } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { DropzoneArea } from 'material-ui-dropzone'; -import EditItemView from '../EditItemView'; -import EditAttributesView from '../attributes/EditAttributesView'; -import { useTranslation } from '../LocalizationProvider'; +import EditItemView from './components/EditItemView'; +import EditAttributesView from '../common/attributes/EditAttributesView'; +import { useTranslation } from '../common/components/LocalizationProvider'; const useStyles = makeStyles(() => ({ details: { diff --git a/modern/src/settings/CalendarsPage.js b/modern/src/settings/CalendarsPage.js index 8b7a2c1b..db4dc770 100644 --- a/modern/src/settings/CalendarsPage.js +++ b/modern/src/settings/CalendarsPage.js @@ -4,9 +4,9 @@ import { } from '@material-ui/core'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import { useEffectAsync } from '../reactHelper'; -import EditCollectionView from '../EditCollectionView'; -import OptionsLayout from './OptionsLayout'; -import { useTranslation } from '../LocalizationProvider'; +import EditCollectionView from './components/EditCollectionView'; +import OptionsLayout from './components/OptionsLayout'; +import { useTranslation } from '../common/components/LocalizationProvider'; const useStyles = makeStyles((theme) => ({ columnAction: { diff --git a/modern/src/settings/CommandPage.js b/modern/src/settings/CommandPage.js index 0cd440e1..99bb21f2 100644 --- a/modern/src/settings/CommandPage.js +++ b/modern/src/settings/CommandPage.js @@ -3,9 +3,9 @@ import { Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, TextField, } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import EditItemView from '../EditItemView'; -import { useTranslation } from '../LocalizationProvider'; -import BaseCommandView from './BaseCommandView'; +import EditItemView from './components/EditItemView'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import BaseCommandView from './components/BaseCommandView'; const useStyles = makeStyles(() => ({ details: { diff --git a/modern/src/settings/CommandSendPage.js b/modern/src/settings/CommandSendPage.js new file mode 100644 index 00000000..79de1b84 --- /dev/null +++ b/modern/src/settings/CommandSendPage.js @@ -0,0 +1,115 @@ +import React, { useState } from 'react'; +import { useHistory, useParams } from 'react-router-dom'; +import { + Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, Container, Button, FormControl, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import OptionsLayout from './components/OptionsLayout'; +import BaseCommandView from './components/BaseCommandView'; +import SelectField from '../common/components/SelectField'; + +const useStyles = makeStyles((theme) => ({ + container: { + marginTop: theme.spacing(2), + }, + buttons: { + display: 'flex', + justifyContent: 'space-evenly', + '& > *': { + flexBasis: '33%', + }, + }, + details: { + flexDirection: 'column', + }, +})); + +const CommandSendPage = () => { + const history = useHistory(); + const classes = useStyles(); + const t = useTranslation(); + + const { deviceId } = useParams(); + + const [savedId, setSavedId] = useState(0); + const [item, setItem] = useState({}); + + const handleSend = async () => { + let command; + if (savedId) { + const response = await fetch(`/api/commands/${savedId}`); + if (response.ok) { + command = await response.json(); + } + } else { + command = item; + } + + command.deviceId = parseInt(deviceId, 10); + + const response = await fetch('/api/commands/send', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(command), + }); + + if (response.ok) { + history.goBack(); + } + }; + + const validate = () => savedId || (item && item.type); + + return ( + + + + }> + + {t('sharedRequired')} + + + + setSavedId(e.target.value)} + endpoint={`/api/commands/send?deviceId=${deviceId}`} + titleGetter={(it) => it.description} + label={t('sharedSavedCommand')} + variant="filled" + /> + {!savedId && ( + + )} + + + +
+ + +
+
+
+
+ ); +}; + +export default CommandSendPage; diff --git a/modern/src/settings/CommandsPage.js b/modern/src/settings/CommandsPage.js index c9240f11..6081b48c 100644 --- a/modern/src/settings/CommandsPage.js +++ b/modern/src/settings/CommandsPage.js @@ -4,9 +4,9 @@ import { } from '@material-ui/core'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import { useEffectAsync } from '../reactHelper'; -import EditCollectionView from '../EditCollectionView'; -import OptionsLayout from './OptionsLayout'; -import { useTranslation } from '../LocalizationProvider'; +import EditCollectionView from './components/EditCollectionView'; +import OptionsLayout from './components/OptionsLayout'; +import { useTranslation } from '../common/components/LocalizationProvider'; import { formatBoolean } from '../common/util/formatter'; import { prefixString } from '../common/util/stringUtils'; diff --git a/modern/src/settings/ComputedAttributePage.js b/modern/src/settings/ComputedAttributePage.js index 2aa40ef0..52a583dc 100644 --- a/modern/src/settings/ComputedAttributePage.js +++ b/modern/src/settings/ComputedAttributePage.js @@ -3,9 +3,9 @@ import { Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, FormControl, InputLabel, MenuItem, Select, TextField, } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import EditItemView from '../EditItemView'; -import { useTranslation } from '../LocalizationProvider'; -import usePositionAttributes from '../attributes/usePositionAttributes'; +import EditItemView from './components/EditItemView'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import usePositionAttributes from '../common/attributes/usePositionAttributes'; const useStyles = makeStyles(() => ({ details: { diff --git a/modern/src/settings/ComputedAttributesPage.js b/modern/src/settings/ComputedAttributesPage.js index 40c7e339..3b60419d 100644 --- a/modern/src/settings/ComputedAttributesPage.js +++ b/modern/src/settings/ComputedAttributesPage.js @@ -4,9 +4,9 @@ import { } from '@material-ui/core'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import { useEffectAsync } from '../reactHelper'; -import EditCollectionView from '../EditCollectionView'; -import OptionsLayout from './OptionsLayout'; -import { useTranslation } from '../LocalizationProvider'; +import EditCollectionView from './components/EditCollectionView'; +import OptionsLayout from './components/OptionsLayout'; +import { useTranslation } from '../common/components/LocalizationProvider'; import { useAdministrator } from '../common/util/permissions'; const useStyles = makeStyles((theme) => ({ diff --git a/modern/src/settings/DevicePage.js b/modern/src/settings/DevicePage.js new file mode 100644 index 00000000..93b7f638 --- /dev/null +++ b/modern/src/settings/DevicePage.js @@ -0,0 +1,202 @@ +import React, { useState } from 'react'; +import TextField from '@material-ui/core/TextField'; + +import { + Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, FormControlLabel, Checkbox, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import EditItemView from './components/EditItemView'; +import EditAttributesView from '../common/attributes/EditAttributesView'; +import SelectField from '../common/components/SelectField'; +import deviceCategories from '../common/util/deviceCategories'; +import LinkField from '../common/components/LinkField'; +import { prefixString } from '../common/util/stringUtils'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import useDeviceAttributes from '../common/attributes/useDeviceAttributes'; +import { useAdministrator } from '../common/util/permissions'; + +const useStyles = makeStyles(() => ({ + details: { + flexDirection: 'column', + }, +})); + +const DevicePage = () => { + const classes = useStyles(); + const t = useTranslation(); + + const admin = useAdministrator(); + + const deviceAttributes = useDeviceAttributes(t); + + const [item, setItem] = useState(); + + const validate = () => item && item.name && item.uniqueId; + + return ( + + {item && ( + <> + + }> + + {t('sharedRequired')} + + + + setItem({ ...item, name: event.target.value })} + label={t('sharedName')} + variant="filled" + /> + setItem({ ...item, uniqueId: event.target.value })} + label={t('deviceIdentifier')} + variant="filled" + /> + + + + }> + + {t('sharedExtra')} + + + + setItem({ ...item, groupId: Number(event.target.value) })} + endpoint="/api/groups" + label={t('groupParent')} + variant="filled" + /> + setItem({ ...item, phone: event.target.value })} + label={t('sharedPhone')} + variant="filled" + /> + setItem({ ...item, model: event.target.value })} + label={t('deviceModel')} + variant="filled" + /> + setItem({ ...item, contact: event.target.value })} + label={t('deviceContact')} + variant="filled" + /> + setItem({ ...item, category: event.target.value })} + data={deviceCategories.map((category) => ({ + id: category, + name: t(`category${category.replace(/^\w/, (c) => c.toUpperCase())}`), + }))} + label={t('deviceCategory')} + variant="filled" + /> + {admin && ( + setItem({ ...item, disabled: event.target.checked })} />} + label={t('sharedDisabled')} + /> + )} + + + + }> + + {t('sharedAttributes')} + + + + setItem({ ...item, attributes })} + definitions={deviceAttributes} + /> + + + {item.id && ( + + }> + + {t('sharedConnections')} + + + + + t(prefixString('event', it.type))} + label={t('sharedNotifications')} + variant="filled" + /> + + it.description} + label={t('sharedComputedAttributes')} + variant="filled" + /> + + + + )} + + )} + + ); +}; + +export default DevicePage; diff --git a/modern/src/settings/DriverPage.js b/modern/src/settings/DriverPage.js index ff703916..06b290fc 100644 --- a/modern/src/settings/DriverPage.js +++ b/modern/src/settings/DriverPage.js @@ -4,9 +4,9 @@ import { Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import EditItemView from '../EditItemView'; -import EditAttributesView from '../attributes/EditAttributesView'; -import { useTranslation } from '../LocalizationProvider'; +import EditItemView from './components/EditItemView'; +import EditAttributesView from '../common/attributes/EditAttributesView'; +import { useTranslation } from '../common/components/LocalizationProvider'; const useStyles = makeStyles(() => ({ details: { diff --git a/modern/src/settings/DriversPage.js b/modern/src/settings/DriversPage.js index 5fb93a99..5b01bdfa 100644 --- a/modern/src/settings/DriversPage.js +++ b/modern/src/settings/DriversPage.js @@ -4,9 +4,9 @@ import { } from '@material-ui/core'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import { useEffectAsync } from '../reactHelper'; -import EditCollectionView from '../EditCollectionView'; -import OptionsLayout from './OptionsLayout'; -import { useTranslation } from '../LocalizationProvider'; +import EditCollectionView from './components/EditCollectionView'; +import OptionsLayout from './components/OptionsLayout'; +import { useTranslation } from '../common/components/LocalizationProvider'; const useStyles = makeStyles((theme) => ({ columnAction: { diff --git a/modern/src/settings/GeofencePage.js b/modern/src/settings/GeofencePage.js new file mode 100644 index 00000000..27259ed2 --- /dev/null +++ b/modern/src/settings/GeofencePage.js @@ -0,0 +1,69 @@ +import React, { useState } from 'react'; +import TextField from '@material-ui/core/TextField'; + +import { + Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import EditItemView from './components/EditItemView'; +import EditAttributesView from '../common/attributes/EditAttributesView'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import useGeofenceAttributes from '../common/attributes/useGeofenceAttributes'; + +const useStyles = makeStyles(() => ({ + details: { + flexDirection: 'column', + }, +})); + +const GeofencePage = () => { + const classes = useStyles(); + const t = useTranslation(); + + const geofenceAttributes = useGeofenceAttributes(t); + + const [item, setItem] = useState(); + + const validate = () => item && item.name; + + return ( + + {item && ( + <> + + }> + + {t('sharedRequired')} + + + + setItem({ ...item, name: event.target.value })} + label={t('sharedName')} + variant="filled" + /> + + + + }> + + {t('sharedAttributes')} + + + + setItem({ ...item, attributes })} + definitions={geofenceAttributes} + /> + + + + )} + + ); +}; + +export default GeofencePage; diff --git a/modern/src/settings/GroupPage.js b/modern/src/settings/GroupPage.js index 3750fc0e..2864a961 100644 --- a/modern/src/settings/GroupPage.js +++ b/modern/src/settings/GroupPage.js @@ -5,11 +5,11 @@ import { Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import EditItemView from '../EditItemView'; -import EditAttributesView from '../attributes/EditAttributesView'; -import useDeviceAttributes from '../attributes/useDeviceAttributes'; -import SelectField from '../form/SelectField'; -import { useTranslation } from '../LocalizationProvider'; +import EditItemView from './components/EditItemView'; +import EditAttributesView from '../common/attributes/EditAttributesView'; +import useDeviceAttributes from '../common/attributes/useDeviceAttributes'; +import SelectField from '../common/components/SelectField'; +import { useTranslation } from '../common/components/LocalizationProvider'; const useStyles = makeStyles(() => ({ details: { diff --git a/modern/src/settings/GroupsPage.js b/modern/src/settings/GroupsPage.js index 8ae5a33f..3642c02f 100644 --- a/modern/src/settings/GroupsPage.js +++ b/modern/src/settings/GroupsPage.js @@ -4,9 +4,9 @@ import { } from '@material-ui/core'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import { useEffectAsync } from '../reactHelper'; -import EditCollectionView from '../EditCollectionView'; -import OptionsLayout from './OptionsLayout'; -import { useTranslation } from '../LocalizationProvider'; +import EditCollectionView from './components/EditCollectionView'; +import OptionsLayout from './components/OptionsLayout'; +import { useTranslation } from '../common/components/LocalizationProvider'; const useStyles = makeStyles((theme) => ({ columnAction: { diff --git a/modern/src/settings/MaintenancePage.js b/modern/src/settings/MaintenancePage.js index 7f539ddf..3d32a0b4 100644 --- a/modern/src/settings/MaintenancePage.js +++ b/modern/src/settings/MaintenancePage.js @@ -5,14 +5,14 @@ import { import InputAdornment from '@material-ui/core/InputAdornment'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { prefixString } from '../common/util/stringUtils'; -import EditItemView from '../EditItemView'; -import EditAttributesView from '../attributes/EditAttributesView'; +import EditItemView from './components/EditItemView'; +import EditAttributesView from '../common/attributes/EditAttributesView'; import { useAttributePreference } from '../common/util/preferences'; import { speedFromKnots, speedToKnots, distanceFromMeters, distanceToMeters, } from '../common/util/converter'; -import { useTranslation } from '../LocalizationProvider'; -import usePositionAttributes from '../attributes/usePositionAttributes'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import usePositionAttributes from '../common/attributes/usePositionAttributes'; const useStyles = makeStyles(() => ({ details: { diff --git a/modern/src/settings/MaintenancesPage.js b/modern/src/settings/MaintenancesPage.js index a5ef530e..038e5dff 100644 --- a/modern/src/settings/MaintenancesPage.js +++ b/modern/src/settings/MaintenancesPage.js @@ -4,13 +4,13 @@ import { } from '@material-ui/core'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import { useEffectAsync } from '../reactHelper'; -import EditCollectionView from '../EditCollectionView'; +import EditCollectionView from './components/EditCollectionView'; -import usePositionAttributes from '../attributes/usePositionAttributes'; +import usePositionAttributes from '../common/attributes/usePositionAttributes'; import { formatDistance, formatSpeed } from '../common/util/formatter'; import { useAttributePreference } from '../common/util/preferences'; -import OptionsLayout from './OptionsLayout'; -import { useTranslation } from '../LocalizationProvider'; +import OptionsLayout from './components/OptionsLayout'; +import { useTranslation } from '../common/components/LocalizationProvider'; const useStyles = makeStyles((theme) => ({ columnAction: { diff --git a/modern/src/settings/NotificationPage.js b/modern/src/settings/NotificationPage.js index 3daa4e51..ed0b3e5e 100644 --- a/modern/src/settings/NotificationPage.js +++ b/modern/src/settings/NotificationPage.js @@ -4,10 +4,10 @@ import { Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, FormControlLabel, Checkbox, } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import { useTranslation, useTranslationKeys } from '../LocalizationProvider'; -import EditItemView from '../EditItemView'; +import { useTranslation, useTranslationKeys } from '../common/components/LocalizationProvider'; +import EditItemView from './components/EditItemView'; import { prefixString, unprefixString } from '../common/util/stringUtils'; -import SelectField from '../form/SelectField'; +import SelectField from '../common/components/SelectField'; const useStyles = makeStyles(() => ({ details: { diff --git a/modern/src/settings/NotificationsPage.js b/modern/src/settings/NotificationsPage.js index 9bc553d6..d96bc3da 100644 --- a/modern/src/settings/NotificationsPage.js +++ b/modern/src/settings/NotificationsPage.js @@ -4,11 +4,11 @@ import { } from '@material-ui/core'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import { useEffectAsync } from '../reactHelper'; -import EditCollectionView from '../EditCollectionView'; +import EditCollectionView from './components/EditCollectionView'; import { prefixString } from '../common/util/stringUtils'; import { formatBoolean } from '../common/util/formatter'; -import OptionsLayout from './OptionsLayout'; -import { useTranslation } from '../LocalizationProvider'; +import OptionsLayout from './components/OptionsLayout'; +import { useTranslation } from '../common/components/LocalizationProvider'; const useStyles = makeStyles((theme) => ({ columnAction: { diff --git a/modern/src/settings/OptionsLayout.js b/modern/src/settings/OptionsLayout.js deleted file mode 100644 index b427d0c0..00000000 --- a/modern/src/settings/OptionsLayout.js +++ /dev/null @@ -1,148 +0,0 @@ -import React, { useState, useEffect, useMemo } from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; -import { - Typography, - Divider, - Drawer, - makeStyles, - IconButton, - Hidden, -} from '@material-ui/core'; - -import { useSelector } from 'react-redux'; -import SettingsIcon from '@material-ui/icons/Settings'; -import CreateIcon from '@material-ui/icons/Create'; -import ArrowBackIcon from '@material-ui/icons/ArrowBack'; -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 TodayIcon from '@material-ui/icons/Today'; -import ExitToAppIcon from '@material-ui/icons/ExitToApp'; - -import SideNav from '../common/components/SideNav'; -import NavBar from '../common/components/NavBar'; -import { useTranslation } from '../LocalizationProvider'; -import { useAdministrator, useReadonly } from '../common/util/permissions'; - -const useStyles = makeStyles((theme) => ({ - root: { - display: 'flex', - [theme.breakpoints.down('sm')]: { - flexDirection: 'column', - }, - height: '100%', - }, - drawerContainer: { - width: theme.dimensions.drawerWidthDesktop, - }, - drawer: { - width: theme.dimensions.drawerWidthDesktop, - [theme.breakpoints.down('sm')]: { - width: theme.dimensions.drawerWidthTablet, - }, - }, - content: { - flex: 1, - }, - drawerHeader: { - ...theme.mixins.toolbar, - display: 'flex', - alignItems: 'center', - padding: theme.spacing(0, 1), - }, - toolbar: { - [theme.breakpoints.down('sm')]: { - ...theme.mixins.toolbar, - }, - }, -})); - -const OptionsLayout = ({ children }) => { - const t = useTranslation(); - const classes = useStyles(); - const location = useLocation(); - const history = useHistory(); - - const [openDrawer, setOpenDrawer] = useState(false); - const [optionTitle, setOptionTitle] = useState(); - - const readonly = useReadonly(); - const admin = useAdministrator(); - const userId = useSelector((state) => state.session.user?.id); - - const readonlyRoutes = useMemo(() => [ - { name: t('sharedPreferences'), href: '/settings/preferences', icon: }, - ], [t]); - - const mainRoutes = useMemo(() => [ - { name: t('sharedNotifications'), href: '/settings/notifications', icon: }, - { name: t('settingsUser'), href: `/user/${userId}`, icon: }, - { name: t('sharedGeofences'), href: '/geofences', icon: }, - { name: t('settingsGroups'), href: '/settings/groups', icon: }, - { name: t('sharedDrivers'), href: '/settings/drivers', icon: }, - { name: t('sharedCalendars'), href: '/settings/calendars', icon: }, - { name: t('sharedComputedAttributes'), href: '/settings/attributes', icon: }, - { name: t('sharedMaintenance'), href: '/settings/maintenances', icon: }, - { name: t('sharedSavedCommands'), href: '/settings/commands', icon: }, - ], [t, userId]); - - const adminRoutes = useMemo(() => [ - { subheader: t('userAdmin') }, - { name: t('settingsServer'), href: '/admin/server', icon: }, - { name: t('settingsUsers'), href: '/admin/users', icon: }, - { name: t('statisticsTitle'), href: '/admin/statistics', icon: }, - ], [t]); - - const routes = useMemo(() => ( - [...readonlyRoutes, ...(!readonly ? mainRoutes : []), ...(admin ? adminRoutes : [])] - ), [readonlyRoutes, readonly, mainRoutes, admin, adminRoutes]); - - useEffect(() => { - const activeRoute = routes.find((route) => route.href && location.pathname.includes(route.href)); - setOptionTitle(activeRoute?.name); - }, [location, routes]); - - const title = `${t('settingsTitle')} / ${optionTitle}`; - - return ( -
- - - setOpenDrawer(!openDrawer)} - classes={{ paper: classes.drawer }} - > - - - - - - -
- history.push('/')}> - - - - {t('settingsTitle')} - -
- - -
-
- -
{children}
-
- ); -}; - -export default OptionsLayout; diff --git a/modern/src/settings/PreferencesPage.js b/modern/src/settings/PreferencesPage.js index bd03f8d2..8b259bf2 100644 --- a/modern/src/settings/PreferencesPage.js +++ b/modern/src/settings/PreferencesPage.js @@ -3,8 +3,8 @@ import { Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, Container, FormControl, InputLabel, Select, MenuItem, Checkbox, FormControlLabel, } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import { useLocalization, useTranslation } from '../LocalizationProvider'; -import OptionsLayout from './OptionsLayout'; +import { useLocalization, useTranslation } from '../common/components/LocalizationProvider'; +import OptionsLayout from './components/OptionsLayout'; import usePersistedState from '../common/util/usePersistedState'; const useStyles = makeStyles((theme) => ({ diff --git a/modern/src/settings/SendCommandPage.js b/modern/src/settings/SendCommandPage.js deleted file mode 100644 index 91130fa1..00000000 --- a/modern/src/settings/SendCommandPage.js +++ /dev/null @@ -1,115 +0,0 @@ -import React, { useState } from 'react'; -import { useHistory, useParams } from 'react-router-dom'; -import { - Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, Container, Button, FormControl, -} from '@material-ui/core'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import { useTranslation } from '../LocalizationProvider'; -import OptionsLayout from './OptionsLayout'; -import BaseCommandView from './BaseCommandView'; -import SelectField from '../form/SelectField'; - -const useStyles = makeStyles((theme) => ({ - container: { - marginTop: theme.spacing(2), - }, - buttons: { - display: 'flex', - justifyContent: 'space-evenly', - '& > *': { - flexBasis: '33%', - }, - }, - details: { - flexDirection: 'column', - }, -})); - -const SendCommandPage = () => { - const history = useHistory(); - const classes = useStyles(); - const t = useTranslation(); - - const { deviceId } = useParams(); - - const [savedId, setSavedId] = useState(0); - const [item, setItem] = useState({}); - - const handleSend = async () => { - let command; - if (savedId) { - const response = await fetch(`/api/commands/${savedId}`); - if (response.ok) { - command = await response.json(); - } - } else { - command = item; - } - - command.deviceId = parseInt(deviceId, 10); - - const response = await fetch('/api/commands/send', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(command), - }); - - if (response.ok) { - history.goBack(); - } - }; - - const validate = () => savedId || (item && item.type); - - return ( - - - - }> - - {t('sharedRequired')} - - - - setSavedId(e.target.value)} - endpoint={`/api/commands/send?deviceId=${deviceId}`} - titleGetter={(it) => it.description} - label={t('sharedSavedCommand')} - variant="filled" - /> - {!savedId && ( - - )} - - - -
- - -
-
-
-
- ); -}; - -export default SendCommandPage; diff --git a/modern/src/settings/ServerPage.js b/modern/src/settings/ServerPage.js new file mode 100644 index 00000000..e7026bb4 --- /dev/null +++ b/modern/src/settings/ServerPage.js @@ -0,0 +1,241 @@ +import React, { useState } from 'react'; +import TextField from '@material-ui/core/TextField'; + +import { + Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, Button, FormControl, Container, Checkbox, FormControlLabel, InputLabel, Select, MenuItem, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { useHistory } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import { sessionActions } from '../store'; +import EditAttributesView from '../common/attributes/EditAttributesView'; +import useDeviceAttributes from '../common/attributes/useDeviceAttributes'; +import useUserAttributes from '../common/attributes/useUserAttributes'; +import OptionsLayout from './components/OptionsLayout'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import SelectField from '../common/components/SelectField'; + +const useStyles = makeStyles((theme) => ({ + container: { + marginTop: theme.spacing(2), + }, + buttons: { + display: 'flex', + justifyContent: 'space-evenly', + '& > *': { + flexBasis: '33%', + }, + }, + details: { + flexDirection: 'column', + }, +})); + +const ServerPage = () => { + const classes = useStyles(); + const history = useHistory(); + const dispatch = useDispatch(); + const t = useTranslation(); + + const userAttributes = useUserAttributes(t); + const deviceAttributes = useDeviceAttributes(t); + + const original = useSelector((state) => state.session.server); + const [item, setItem] = useState({ ...original }); + + const handleSave = async () => { + const response = await fetch('/api/server', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(item), + }); + + if (response.ok) { + dispatch(sessionActions.updateServer(await response.json())); + history.goBack(); + } + }; + + return ( + + + {item && ( + <> + + }> + + {t('sharedPreferences')} + + + + setItem({ ...item, mapUrl: event.target.value })} + label={t('mapCustomLabel')} + variant="filled" + /> + setItem({ ...item, latitude: Number(event.target.value) })} + label={t('positionLatitude')} + variant="filled" + /> + setItem({ ...item, longitude: Number(event.target.value) })} + label={t('positionLongitude')} + variant="filled" + /> + setItem({ ...item, zoom: Number(event.target.value) })} + label={t('serverZoom')} + variant="filled" + /> + + {t('settingsCoordinateFormat')} + + + + {t('settingsSpeedUnit')} + + + + {t('settingsDistanceUnit')} + + + + {t('settingsVolumeUnit')} + + + setItem({ ...item, attributes: { ...item.attributes, timezone: e.target.value } })} + endpoint="/api/server/timezones" + keyGetter={(it) => it} + titleGetter={(it) => it} + label={t('sharedTimezone')} + variant="filled" + /> + setItem({ ...item, poiLayer: event.target.value })} + label={t('mapPoiLayer')} + variant="filled" + /> + setItem({ ...item, announcement: event.target.value })} + label={t('serverAnnouncement')} + variant="filled" + /> + setItem({ ...item, twelveHourFormat: event.target.checked })} />} + label={t('settingsTwelveHourFormat')} + /> + setItem({ ...item, forceSettings: event.target.checked })} />} + label={t('serverForceSettings')} + /> + + + + }> + + {t('sharedPermissions')} + + + + setItem({ ...item, registration: event.target.checked })} />} + label={t('serverRegistration')} + /> + setItem({ ...item, readonly: event.target.checked })} />} + label={t('serverReadonly')} + /> + setItem({ ...item, deviceReadonly: event.target.checked })} />} + label={t('userDeviceReadonly')} + /> + setItem({ ...item, limitCommands: event.target.checked })} />} + label={t('userLimitCommands')} + /> + setItem({ ...item, disableReports: event.target.checked })} />} + label={t('userDisableReports')} + /> + + + + }> + + {t('sharedAttributes')} + + + + setItem({ ...item, attributes })} + definitions={{ ...userAttributes, ...deviceAttributes }} + /> + + + + )} + +
+ + +
+
+
+
+ ); +}; + +export default ServerPage; diff --git a/modern/src/settings/UserPage.js b/modern/src/settings/UserPage.js new file mode 100644 index 00000000..abb12edf --- /dev/null +++ b/modern/src/settings/UserPage.js @@ -0,0 +1,194 @@ +import React, { useState } from 'react'; +import TextField from '@material-ui/core/TextField'; + +import { + Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, FormControl, InputLabel, Select, MenuItem, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { useDispatch, useSelector } from 'react-redux'; +import EditItemView from './components/EditItemView'; +import EditAttributesView from '../common/attributes/EditAttributesView'; +import LinkField from '../common/components/LinkField'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import useUserAttributes from '../common/attributes/useUserAttributes'; +import { sessionActions } from '../store'; +import SelectField from '../common/components/SelectField'; + +const useStyles = makeStyles(() => ({ + details: { + flexDirection: 'column', + }, +})); + +const UserPage = () => { + const classes = useStyles(); + const dispatch = useDispatch(); + const t = useTranslation(); + + const currentUserId = useSelector((state) => state.session.user.id); + + const userAttributes = useUserAttributes(t); + + const [item, setItem] = useState(); + + const onItemSaved = (result) => { + if (result.id === currentUserId) { + dispatch(sessionActions.updateUser(result)); + } + }; + + const validate = () => item && item.name && item.email && (item.id || item.password); + + return ( + + {item && ( + <> + + }> + + {t('sharedRequired')} + + + + setItem({ ...item, name: event.target.value })} + label={t('sharedName')} + variant="filled" + /> + setItem({ ...item, email: event.target.value })} + label={t('userEmail')} + variant="filled" + /> + setItem({ ...item, password: event.target.value })} + label={t('userPassword')} + variant="filled" + /> + + + + }> + + {t('sharedPreferences')} + + + + setItem({ ...item, phone: event.target.value })} + label={t('sharedPhone')} + variant="filled" + /> + + {t('settingsSpeedUnit')} + + + + {t('settingsDistanceUnit')} + + + + {t('settingsVolumeUnit')} + + + setItem({ ...item, attributes: { ...item.attributes, timezone: e.target.value } })} + endpoint="/api/server/timezones" + keyGetter={(it) => it} + titleGetter={(it) => it} + label={t('sharedTimezone')} + variant="filled" + /> + setItem({ ...item, poiLayer: event.target.value })} + label={t('mapPoiLayer')} + variant="filled" + /> + + + + }> + + {t('sharedAttributes')} + + + + setItem({ ...item, attributes })} + definitions={userAttributes} + /> + + + {item.id && ( + + }> + + {t('sharedConnections')} + + + + + + + + )} + + )} + + ); +}; + +export default UserPage; diff --git a/modern/src/settings/UsersPage.js b/modern/src/settings/UsersPage.js new file mode 100644 index 00000000..07c859d5 --- /dev/null +++ b/modern/src/settings/UsersPage.js @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import { + TableContainer, Table, TableRow, TableCell, TableHead, TableBody, makeStyles, IconButton, +} from '@material-ui/core'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import { useEffectAsync } from '../reactHelper'; +import EditCollectionView from './components/EditCollectionView'; +import { formatBoolean } from '../common/util/formatter'; +import OptionsLayout from './components/OptionsLayout'; +import { useTranslation } from '../common/components/LocalizationProvider'; + +const useStyles = makeStyles((theme) => ({ + columnAction: { + width: theme.spacing(1), + padding: theme.spacing(0, 1), + }, +})); + +const UsersView = ({ updateTimestamp, onMenuClick }) => { + const classes = useStyles(); + const t = useTranslation(); + + const [items, setItems] = useState([]); + + useEffectAsync(async () => { + const response = await fetch('/api/users'); + if (response.ok) { + setItems(await response.json()); + } + }, [updateTimestamp]); + + return ( + + + + + + {t('sharedName')} + {t('userEmail')} + {t('userAdmin')} + {t('sharedDisabled')} + + + + {items.map((item) => ( + + + onMenuClick(event.currentTarget, item.id)}> + + + + {item.name} + {item.email} + {formatBoolean(item.administrator, t)} + {formatBoolean(item.disabled, t)} + + ))} + +
+
+ ); +}; + +const UsersPage = () => ( + + + +); + +export default UsersPage; diff --git a/modern/src/settings/components/BaseCommandView.js b/modern/src/settings/components/BaseCommandView.js new file mode 100644 index 00000000..b422e153 --- /dev/null +++ b/modern/src/settings/components/BaseCommandView.js @@ -0,0 +1,58 @@ +import React, { useEffect, useState } from 'react'; +import { + TextField, FormControlLabel, Checkbox, +} from '@material-ui/core'; +import { useTranslation } from '../../common/components/LocalizationProvider'; +import SelectField from '../../common/components/SelectField'; +import { prefixString } from '../../common/util/stringUtils'; +import useCommandAttributes from '../../common/attributes/useCommandAttributes'; + +const BaseCommandView = ({ item, setItem }) => { + const t = useTranslation(); + + const availableAttributes = useCommandAttributes(t); + + const [attributes, setAttributes] = useState([]); + + useEffect(() => { + if (item && item.type) { + setAttributes(availableAttributes[item.type] || []); + } else { + setAttributes([]); + } + }, [availableAttributes, item]); + + return ( + <> + setItem({ ...item, type: e.target.value, attributes: {} })} + endpoint="/api/commands/types" + keyGetter={(it) => it.type} + titleGetter={(it) => t(prefixString('command', it.type))} + label={t('sharedType')} + variant="filled" + /> + {attributes.map((attribute) => ( + { + const updateItem = { ...item, attributes: { ...item.attributes } }; + updateItem.attributes[attribute.key] = e.target.value; + setItem(updateItem); + }} + label={attribute.name} + variant="filled" + /> + ))} + setItem({ ...item, textChannel: event.target.checked })} />} + label={t('commandSendSms')} + /> + + ); +}; + +export default BaseCommandView; diff --git a/modern/src/settings/components/EditCollectionView.js b/modern/src/settings/components/EditCollectionView.js new file mode 100644 index 00000000..9107b68e --- /dev/null +++ b/modern/src/settings/components/EditCollectionView.js @@ -0,0 +1,87 @@ +import React, { useState } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { useHistory } from 'react-router-dom'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import Fab from '@material-ui/core/Fab'; +import AddIcon from '@material-ui/icons/Add'; + +import RemoveDialog from '../../common/components/RemoveDialog'; +import { useTranslation } from '../../common/components/LocalizationProvider'; +import dimensions from '../../common/theme/dimensions'; +import { useEditable } from '../../common/util/permissions'; + +const useStyles = makeStyles((theme) => ({ + fab: { + position: 'fixed', + bottom: theme.spacing(2), + right: theme.spacing(2), + [theme.breakpoints.down('sm')]: { + bottom: dimensions.bottomBarHeight + theme.spacing(2), + }, + }, +})); + +const EditCollectionView = ({ + content, editPath, endpoint, disableAdd, filter, +}) => { + const classes = useStyles(); + const history = useHistory(); + const t = useTranslation(); + + const editable = useEditable(); + + const [selectedId, setSelectedId] = useState(null); + const [selectedAnchorEl, setSelectedAnchorEl] = useState(null); + const [removeDialogShown, setRemoveDialogShown] = useState(false); + const [updateTimestamp, setUpdateTimestamp] = useState(Date.now()); + + const menuShow = (anchorId, itemId) => { + setSelectedAnchorEl(anchorId); + setSelectedId(itemId); + }; + + const menuHide = () => { + setSelectedAnchorEl(null); + }; + + const handleAdd = () => { + history.push(editPath); + menuHide(); + }; + + const handleEdit = () => { + history.push(`${editPath}/${selectedId}`); + menuHide(); + }; + + const handleRemove = () => { + setRemoveDialogShown(true); + menuHide(); + }; + + const handleRemoveResult = () => { + setRemoveDialogShown(false); + setUpdateTimestamp(Date.now()); + }; + + const Content = content; + + return ( + <> + + {editable && !disableAdd && ( + + + + )} + + {t('sharedEdit')} + {t('sharedRemove')} + + + + ); +}; + +export default EditCollectionView; diff --git a/modern/src/settings/components/EditItemView.js b/modern/src/settings/components/EditItemView.js new file mode 100644 index 00000000..90e5294a --- /dev/null +++ b/modern/src/settings/components/EditItemView.js @@ -0,0 +1,95 @@ +import React from 'react'; +import { useHistory, useParams } from 'react-router-dom'; +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 { useEffectAsync } from '../../reactHelper'; +import OptionsLayout from './OptionsLayout'; +import { useTranslation } from '../../common/components/LocalizationProvider'; + +const useStyles = makeStyles((theme) => ({ + container: { + marginTop: theme.spacing(2), + }, + buttons: { + display: 'flex', + justifyContent: 'space-evenly', + '& > *': { + flexBasis: '33%', + }, + }, +})); + +const EditItemView = ({ + children, endpoint, item, setItem, validate, onItemSaved, +}) => { + const history = useHistory(); + const classes = useStyles(); + const t = useTranslation(); + + const { id } = useParams(); + + useEffectAsync(async () => { + if (id) { + const response = await fetch(`/api/${endpoint}/${id}`); + if (response.ok) { + setItem(await response.json()); + } + } else { + setItem({}); + } + }, [id]); + + const handleSave = async () => { + let url = `/api/${endpoint}`; + if (id) { + url += `/${id}`; + } + + const response = await fetch(url, { + method: !id ? 'POST' : 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(item), + }); + + if (response.ok) { + if (onItemSaved) { + onItemSaved(await response.json()); + } + history.goBack(); + } + }; + + return ( + + + {children} + +
+ + +
+
+
+
+ ); +}; + +export default EditItemView; diff --git a/modern/src/settings/components/OptionsLayout.js b/modern/src/settings/components/OptionsLayout.js new file mode 100644 index 00000000..9f2a1aac --- /dev/null +++ b/modern/src/settings/components/OptionsLayout.js @@ -0,0 +1,148 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { + Typography, + Divider, + Drawer, + makeStyles, + IconButton, + Hidden, +} from '@material-ui/core'; + +import { useSelector } from 'react-redux'; +import SettingsIcon from '@material-ui/icons/Settings'; +import CreateIcon from '@material-ui/icons/Create'; +import ArrowBackIcon from '@material-ui/icons/ArrowBack'; +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 TodayIcon from '@material-ui/icons/Today'; +import ExitToAppIcon from '@material-ui/icons/ExitToApp'; + +import SideNav from '../../common/components/SideNav'; +import NavBar from '../../common/components/NavBar'; +import { useTranslation } from '../../common/components/LocalizationProvider'; +import { useAdministrator, useReadonly } from '../../common/util/permissions'; + +const useStyles = makeStyles((theme) => ({ + root: { + display: 'flex', + [theme.breakpoints.down('sm')]: { + flexDirection: 'column', + }, + height: '100%', + }, + drawerContainer: { + width: theme.dimensions.drawerWidthDesktop, + }, + drawer: { + width: theme.dimensions.drawerWidthDesktop, + [theme.breakpoints.down('sm')]: { + width: theme.dimensions.drawerWidthTablet, + }, + }, + content: { + flex: 1, + }, + drawerHeader: { + ...theme.mixins.toolbar, + display: 'flex', + alignItems: 'center', + padding: theme.spacing(0, 1), + }, + toolbar: { + [theme.breakpoints.down('sm')]: { + ...theme.mixins.toolbar, + }, + }, +})); + +const OptionsLayout = ({ children }) => { + const t = useTranslation(); + const classes = useStyles(); + const location = useLocation(); + const history = useHistory(); + + const [openDrawer, setOpenDrawer] = useState(false); + const [optionTitle, setOptionTitle] = useState(); + + const readonly = useReadonly(); + const admin = useAdministrator(); + const userId = useSelector((state) => state.session.user?.id); + + const readonlyRoutes = useMemo(() => [ + { name: t('sharedPreferences'), href: '/settings/preferences', icon: }, + ], [t]); + + const mainRoutes = useMemo(() => [ + { name: t('sharedNotifications'), href: '/settings/notifications', icon: }, + { name: t('settingsUser'), href: `/user/${userId}`, icon: }, + { name: t('sharedGeofences'), href: '/geofences', icon: }, + { name: t('settingsGroups'), href: '/settings/groups', icon: }, + { name: t('sharedDrivers'), href: '/settings/drivers', icon: }, + { name: t('sharedCalendars'), href: '/settings/calendars', icon: }, + { name: t('sharedComputedAttributes'), href: '/settings/attributes', icon: }, + { name: t('sharedMaintenance'), href: '/settings/maintenances', icon: }, + { name: t('sharedSavedCommands'), href: '/settings/commands', icon: }, + ], [t, userId]); + + const adminRoutes = useMemo(() => [ + { subheader: t('userAdmin') }, + { name: t('settingsServer'), href: '/admin/server', icon: }, + { name: t('settingsUsers'), href: '/admin/users', icon: }, + { name: t('statisticsTitle'), href: '/admin/statistics', icon: }, + ], [t]); + + const routes = useMemo(() => ( + [...readonlyRoutes, ...(!readonly ? mainRoutes : []), ...(admin ? adminRoutes : [])] + ), [readonlyRoutes, readonly, mainRoutes, admin, adminRoutes]); + + useEffect(() => { + const activeRoute = routes.find((route) => route.href && location.pathname.includes(route.href)); + setOptionTitle(activeRoute?.name); + }, [location, routes]); + + const title = `${t('settingsTitle')} / ${optionTitle}`; + + return ( +
+ + + setOpenDrawer(!openDrawer)} + classes={{ paper: classes.drawer }} + > + + + + + + +
+ history.push('/')}> + + + + {t('settingsTitle')} + +
+ + +
+
+ +
{children}
+
+ ); +}; + +export default OptionsLayout; -- cgit v1.2.3