aboutsummaryrefslogtreecommitdiff
path: root/modern
diff options
context:
space:
mode:
authorAnton Tananaev <anton@traccar.org>2022-07-03 11:00:47 -0700
committerAnton Tananaev <anton@traccar.org>2022-07-03 11:00:47 -0700
commit0fb931d6b2193891eb8cefd002ef941eb6daed75 (patch)
tree18f966fb2cc04b75f10305565de968f68d6036bb /modern
parent0ab655eb5c031e1a2aac18ef475aff6c7724b227 (diff)
downloadtrackermap-web-0fb931d6b2193891eb8cefd002ef941eb6daed75.tar.gz
trackermap-web-0fb931d6b2193891eb8cefd002ef941eb6daed75.tar.bz2
trackermap-web-0fb931d6b2193891eb8cefd002ef941eb6daed75.zip
Add simple events drawer
Diffstat (limited to 'modern')
-rw-r--r--modern/src/SocketController.js2
-rw-r--r--modern/src/common/theme/dimensions.js1
-rw-r--r--modern/src/main/EventsDrawer.js55
-rw-r--r--modern/src/main/MainPage.js18
-rw-r--r--modern/src/map/notification/MapNotification.js29
-rw-r--r--modern/src/map/notification/notification.css7
-rw-r--r--modern/src/store/events.js22
-rw-r--r--modern/src/store/index.js3
8 files changed, 122 insertions, 15 deletions
diff --git a/modern/src/SocketController.js b/modern/src/SocketController.js
index 79661353..bfc0378d 100644
--- a/modern/src/SocketController.js
+++ b/modern/src/SocketController.js
@@ -10,6 +10,7 @@ import { snackBarDurationLongMs } from './common/util/duration';
import usePersistedState from './common/util/usePersistedState';
import alarm from './resources/alarm.mp3';
+import { eventsActions } from './store/events';
const SocketController = () => {
const dispatch = useDispatch();
@@ -60,6 +61,7 @@ const SocketController = () => {
dispatch(positionsActions.update(data.positions));
}
if (data.events) {
+ dispatch(eventsActions.add(data.events));
setEvents(data.events);
}
};
diff --git a/modern/src/common/theme/dimensions.js b/modern/src/common/theme/dimensions.js
index c5974ec3..2e8d7e5d 100644
--- a/modern/src/common/theme/dimensions.js
+++ b/modern/src/common/theme/dimensions.js
@@ -5,6 +5,7 @@ export default {
drawerWidthTablet: '320px',
drawerHeightPhone: '250px',
filterFormWidth: '160px',
+ eventsDrawerWidth: '320px',
bottomBarHeight: 56,
popupMapOffset: 300,
popupMaxWidth: 272,
diff --git a/modern/src/main/EventsDrawer.js b/modern/src/main/EventsDrawer.js
new file mode 100644
index 00000000..91d6e3db
--- /dev/null
+++ b/modern/src/main/EventsDrawer.js
@@ -0,0 +1,55 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import {
+ Drawer, IconButton, List, ListItem, ListItemText, Toolbar, Typography,
+} from '@mui/material';
+import { makeStyles } from '@mui/styles';
+import DeleteIcon from '@mui/icons-material/Delete';
+import { useTranslation } from '../common/components/LocalizationProvider';
+import { eventsActions } from '../store';
+
+const useStyles = makeStyles((theme) => ({
+ drawer: {
+ width: theme.dimensions.eventsDrawerWidth,
+ },
+ header: {
+ flexGrow: 1,
+ },
+}));
+
+const EventsDrawer = ({ open, onClose }) => {
+ const classes = useStyles();
+ const dispatch = useDispatch();
+ const t = useTranslation();
+
+ const events = useSelector((state) => state.events.items);
+
+ return (
+ <Drawer
+ anchor="right"
+ open={open}
+ onClose={onClose}
+ >
+ <Toolbar>
+ <Typography variant="h6" className={classes.header}>
+ {t('reportEvents')}
+ </Typography>
+ <IconButton color="inherit" edge="end" onClick={() => dispatch(eventsActions.deleteAll())}>
+ <DeleteIcon />
+ </IconButton>
+ </Toolbar>
+ <List className={classes.drawer} dense>
+ {events.map((event) => (
+ <ListItem key={event.id}>
+ <ListItemText primary={event.attributes.message} />
+ <IconButton size="small" onClick={() => dispatch(eventsActions.delete(event))}>
+ <DeleteIcon fontSize="small" className={classes.negative} />
+ </IconButton>
+ </ListItem>
+ ))}
+ </List>
+ </Drawer>
+ );
+};
+
+export default EventsDrawer;
diff --git a/modern/src/main/MainPage.js b/modern/src/main/MainPage.js
index e322dca0..31248271 100644
--- a/modern/src/main/MainPage.js
+++ b/modern/src/main/MainPage.js
@@ -37,6 +37,7 @@ import MapOverlay from '../map/overlay/MapOverlay';
import MapGeocoder from '../map/geocoder/MapGeocoder';
import MapScale from '../map/MapScale';
import MapNotification from '../map/notification/MapNotification';
+import EventsDrawer from './EventsDrawer';
const useStyles = makeStyles((theme) => ({
root: {
@@ -164,17 +165,21 @@ const MainPage = () => {
const filterRef = useRef();
const [filterAnchorEl, setFilterAnchorEl] = useState(null);
- const [collapsed, setCollapsed] = useState(false);
+ const [devicesOpen, setDevicesOpen] = useState(false);
+ const [eventsOpen, setEventsOpen] = useState(false);
+
+ const eventHandler = useCallback(() => setEventsOpen(true), [setEventsOpen]);
+ const eventsAvailable = useSelector((state) => !!state.events.items.length);
const handleClose = () => {
- setCollapsed(!collapsed);
+ setDevicesOpen(!devicesOpen);
};
- useEffect(() => setCollapsed(!desktop), [desktop]);
+ useEffect(() => setDevicesOpen(desktop), [desktop]);
useEffect(() => {
if (!desktop && mapMapOnSelect && selectedDeviceId) {
- setCollapsed(true);
+ setDevicesOpen(false);
}
}, [desktop, mapMapOnSelect, selectedDeviceId]);
@@ -218,7 +223,7 @@ const MainPage = () => {
<MapScale />
<MapCurrentLocation />
<MapGeocoder />
- <MapNotification />
+ <MapNotification enabled={eventsAvailable} onClick={eventHandler} />
{desktop && <MapPadding left={parseInt(theme.dimensions.drawerWidthDesktop, 10)} />}
<Button
variant="contained"
@@ -231,7 +236,7 @@ const MainPage = () => {
<ListIcon />
<div className={classes.sidebarToggleText}>{t('deviceTitle')}</div>
</Button>
- <Paper square elevation={3} className={`${classes.sidebar} ${collapsed && classes.sidebarCollapsed}`}>
+ <Paper square elevation={3} className={`${classes.sidebar} ${!devicesOpen && classes.sidebarCollapsed}`}>
<Paper square elevation={3} className={classes.toolbarContainer}>
<Toolbar className={classes.toolbar} disableGutters>
{!desktop && (
@@ -329,6 +334,7 @@ const MainPage = () => {
<BottomMenu />
</div>
)}
+ <EventsDrawer open={eventsOpen} onClose={() => setEventsOpen(false)} />
{selectedDeviceId && (
<div className={classes.statusCard}>
<StatusCard
diff --git a/modern/src/map/notification/MapNotification.js b/modern/src/map/notification/MapNotification.js
index 5be80fac..81038f95 100644
--- a/modern/src/map/notification/MapNotification.js
+++ b/modern/src/map/notification/MapNotification.js
@@ -1,21 +1,23 @@
-import { useEffect } from 'react';
+import { useEffect, useMemo } from 'react';
import { map } from '../core/MapView';
import './notification.css';
+const statusClass = (status) => `maplibregl-ctrl-icon maplibre-ctrl-notification maplibre-ctrl-notification-${status}`;
+
class NotificationControl {
constructor(eventHandler) {
this.eventHandler = eventHandler;
}
onAdd() {
- const button = document.createElement('button');
- button.className = 'maplibregl-ctrl-icon maplibre-ctrl-notification';
- button.type = 'button';
- button.onclick = this.eventHandler;
+ this.button = document.createElement('button');
+ this.button.className = statusClass('off');
+ this.button.type = 'button';
+ this.button.onclick = () => this.eventHandler(this);
this.container = document.createElement('div');
this.container.className = 'maplibregl-ctrl-group maplibregl-ctrl';
- this.container.appendChild(button);
+ this.container.appendChild(this.button);
return this.container;
}
@@ -23,14 +25,23 @@ class NotificationControl {
onRemove() {
this.container.parentNode.removeChild(this.container);
}
+
+ setEnabled(enabled) {
+ this.button.className = statusClass(enabled ? 'on' : 'off');
+ }
}
-const MapNotification = () => {
+const MapNotification = ({ enabled, onClick }) => {
+ const control = useMemo(() => new NotificationControl(onClick), [onClick]);
+
useEffect(() => {
- const control = new NotificationControl(() => {});
map.addControl(control);
return () => map.removeControl(control);
- }, []);
+ }, [onClick]);
+
+ useEffect(() => {
+ control.setEnabled(enabled);
+ }, [enabled]);
return null;
};
diff --git a/modern/src/map/notification/notification.css b/modern/src/map/notification/notification.css
index b0c82357..73db13bb 100644
--- a/modern/src/map/notification/notification.css
+++ b/modern/src/map/notification/notification.css
@@ -2,5 +2,12 @@
background-repeat: no-repeat;
background-position: center;
pointer-events: auto;
+}
+
+.maplibre-ctrl-notification-on {
+ background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' version='1.1' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23f44336' d='m6.4728 3.4802-1.1412-1.1412c-1.9152 1.4604-3.1761 3.7108-3.2878 6.2645h1.596c0.1197-2.1148 1.205-3.9662 2.833-5.1233zm9.8875 5.1233h1.596c-0.1197-2.5537-1.3806-4.8041-3.2878-6.2645l-1.1332 1.1412c1.612 1.1571 2.7053 3.0085 2.825 5.1233zm-1.5721 0.39901c0-2.4499-1.3088-4.5008-3.5911-5.0435v-0.54265c0-0.66236-0.53467-1.197-1.197-1.197-0.66236 0-1.197 0.53467-1.197 1.197v0.54265c-2.2903 0.54265-3.5911 2.5856-3.5911 5.0435v3.9901l-1.596 1.596v0.79802h12.768v-0.79802l-1.596-1.596zm-4.7881 8.7782c0.11172 0 0.21546-8e-3 0.31921-0.03192 0.51871-0.11172 0.94166-0.46285 1.1491-0.94166 0.0798-0.19152 0.1197-0.39901 0.1197-0.62246h-3.1921c0.00798 0.87782 0.71822 1.596 1.604 1.596z' stroke-width='.79802'/%3E%3C/svg%3E");
+}
+
+.maplibre-ctrl-notification-off {
background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' version='1.1' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m6.4728 3.4802-1.1412-1.1412c-1.9152 1.4604-3.1761 3.7108-3.2878 6.2645h1.596c0.1197-2.1148 1.205-3.9662 2.833-5.1233zm9.8875 5.1233h1.596c-0.1197-2.5537-1.3806-4.8041-3.2878-6.2645l-1.1332 1.1412c1.612 1.1571 2.7053 3.0085 2.825 5.1233zm-1.5721 0.39901c0-2.4499-1.3088-4.5008-3.5911-5.0435v-0.54265c0-0.66236-0.53467-1.197-1.197-1.197-0.66236 0-1.197 0.53467-1.197 1.197v0.54265c-2.2903 0.54265-3.5911 2.5856-3.5911 5.0435v3.9901l-1.596 1.596v0.79802h12.768v-0.79802l-1.596-1.596zm-4.7881 8.7782c0.11172 0 0.21546-8e-3 0.31921-0.03192 0.51871-0.11172 0.94166-0.46285 1.1491-0.94166 0.0798-0.19152 0.1197-0.39901 0.1197-0.62246h-3.1921c0.00798 0.87782 0.71822 1.596 1.604 1.596z' stroke-width='.79802'/%3E%3C/svg%3E");
}
diff --git a/modern/src/store/events.js b/modern/src/store/events.js
new file mode 100644
index 00000000..d4f313c0
--- /dev/null
+++ b/modern/src/store/events.js
@@ -0,0 +1,22 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+const { reducer, actions } = createSlice({
+ name: 'events',
+ initialState: {
+ items: [],
+ },
+ reducers: {
+ add(state, action) {
+ state.items.unshift(...action.payload);
+ },
+ delete(state, action) {
+ state.items = state.items.filter((item) => item.id !== action.payload.id);
+ },
+ deleteAll(state) {
+ state.items = [];
+ },
+ },
+});
+
+export { actions as eventsActions };
+export { reducer as eventsReducer };
diff --git a/modern/src/store/index.js b/modern/src/store/index.js
index 2ab0c982..9fce43cf 100644
--- a/modern/src/store/index.js
+++ b/modern/src/store/index.js
@@ -4,6 +4,7 @@ import { errorsReducer as errors } from './errors';
import { sessionReducer as session } from './session';
import { devicesReducer as devices } from './devices';
import { positionsReducer as positions } from './positions';
+import { eventsReducer as events } from './events';
import { geofencesReducer as geofences } from './geofences';
import { groupsReducer as groups } from './groups';
import { driversReducer as drivers } from './drivers';
@@ -15,6 +16,7 @@ const reducer = combineReducers({
session,
devices,
positions,
+ events,
geofences,
groups,
drivers,
@@ -25,6 +27,7 @@ export { errorsActions } from './errors';
export { sessionActions } from './session';
export { devicesActions } from './devices';
export { positionsActions } from './positions';
+export { eventsActions } from './events';
export { geofencesActions } from './geofences';
export { groupsActions } from './groups';
export { driversActions } from './drivers';