aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modern/src/common/theme/dimensions.js6
-rw-r--r--modern/src/common/util/usePersistedState.js6
-rw-r--r--modern/src/main/DevicesList.js20
-rw-r--r--modern/src/main/MainPage.js121
-rw-r--r--web/l10n/en.json2
5 files changed, 118 insertions, 37 deletions
diff --git a/modern/src/common/theme/dimensions.js b/modern/src/common/theme/dimensions.js
index d68a94f5..c5974ec3 100644
--- a/modern/src/common/theme/dimensions.js
+++ b/modern/src/common/theme/dimensions.js
@@ -1,6 +1,4 @@
export default {
- inputHeight: '42px',
- borderRadius: '4px',
sidebarWidth: '28%',
sidebarWidthTablet: '52px',
drawerWidthDesktop: '360px',
@@ -8,10 +6,6 @@ export default {
drawerHeightPhone: '250px',
filterFormWidth: '160px',
bottomBarHeight: 56,
- columnWidthDate: 160,
- columnWidthNumber: 130,
- columnWidthString: 160,
- columnWidthBoolean: 130,
popupMapOffset: 300,
popupMaxWidth: 272,
};
diff --git a/modern/src/common/util/usePersistedState.js b/modern/src/common/util/usePersistedState.js
index 8bc4401f..70a652ad 100644
--- a/modern/src/common/util/usePersistedState.js
+++ b/modern/src/common/util/usePersistedState.js
@@ -11,7 +11,11 @@ export default (key, defaultValue) => {
});
useEffect(() => {
- savePersistedState(key, value);
+ if (value !== defaultValue) {
+ savePersistedState(key, value);
+ } else {
+ window.localStorage.removeItem(key);
+ }
}, [key, value]);
return [value, setValue];
diff --git a/modern/src/main/DevicesList.js b/modern/src/main/DevicesList.js
index 108c3397..baf18dd8 100644
--- a/modern/src/main/DevicesList.js
+++ b/modern/src/main/DevicesList.js
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useState } from 'react';
+import React, { useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import makeStyles from '@mui/styles/makeStyles';
import { IconButton, Tooltip } from '@mui/material';
@@ -142,23 +142,11 @@ const DeviceRow = ({ data, index, style }) => {
);
};
-const DevicesList = ({ filter }) => {
+const DevicesList = ({ devices }) => {
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;
}
@@ -179,8 +167,8 @@ const DevicesList = ({ filter }) => {
<FixedSizeList
width={width}
height={height}
- itemCount={filteredItems.length}
- itemData={{ items: filteredItems }}
+ itemCount={devices.length}
+ itemData={{ items: devices }}
itemSize={72}
overscanCount={10}
innerRef={listInnerEl}
diff --git a/modern/src/main/MainPage.js b/modern/src/main/MainPage.js
index 50209568..7bc2eb88 100644
--- a/modern/src/main/MainPage.js
+++ b/modern/src/main/MainPage.js
@@ -1,11 +1,11 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React, {
+ useState, useEffect, useCallback, useRef,
+} from 'react';
import { useNavigate } from 'react-router-dom';
import {
- Paper, Toolbar, IconButton, Button, OutlinedInput, InputAdornment,
+ Paper, Toolbar, IconButton, Button, OutlinedInput, InputAdornment, Popover, FormControl, InputLabel, Select, MenuItem, FormGroup, FormControlLabel, Checkbox,
} from '@mui/material';
-
-import makeStyles from '@mui/styles/makeStyles';
-
+import { makeStyles } from '@mui/styles';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import AddIcon from '@mui/icons-material/Add';
@@ -13,8 +13,8 @@ import CloseIcon from '@mui/icons-material/Close';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ListIcon from '@mui/icons-material/ViewList';
import TuneIcon from '@mui/icons-material/Tune';
-
import { useDispatch, useSelector } from 'react-redux';
+import moment from 'moment';
import DevicesList from './DevicesList';
import MapView from '../map/core/MapView';
import MapSelectedDevice from '../map/main/MapSelectedDevice';
@@ -118,6 +118,13 @@ const useStyles = makeStyles((theme) => ({
zIndex: 4,
width: theme.dimensions.drawerWidthDesktop,
},
+ filterPanel: {
+ display: 'flex',
+ flexDirection: 'column',
+ padding: theme.spacing(2),
+ gap: theme.spacing(2),
+ width: theme.dimensions.drawerWidthTablet,
+ },
}));
const MainPage = () => {
@@ -136,10 +143,23 @@ const MainPage = () => {
const [mapLiveRoutes] = usePersistedState('mapLiveRoutes', false);
const selectedDeviceId = useSelector((state) => state.devices.selectedId);
- const positions = useSelector((state) => Object.values(state.positions.items));
- const selectedPosition = positions.find((position) => selectedDeviceId && position.deviceId === selectedDeviceId);
+ const positions = useSelector((state) => state.positions.items);
+ const [filteredPositions, setFilteredPositions] = useState([]);
+ const selectedPosition = filteredPositions.find((position) => selectedDeviceId && position.deviceId === selectedDeviceId);
+
+ const groups = useSelector((state) => state.groups.items);
+ const devices = useSelector((state) => state.devices.items);
+ const [filteredDevices, setFilteredDevices] = useState([]);
+
+ const [filterKeyword, setFilterKeyword] = useState('');
+ const [filterStatuses, setFilterStatuses] = useState([]);
+ const [filterGroups, setFilterGroups] = useState([]);
+ const [filterSort, setFilterSort] = usePersistedState('filterSort', '');
+ const [filterMap, setFilterMap] = usePersistedState('filterMap', false);
+
+ const filterRef = useRef();
+ const [filterAnchorEl, setFilterAnchorEl] = useState(null);
- const [searchKeyword, setSearchKeyword] = useState('');
const [collapsed, setCollapsed] = useState(false);
const handleClose = () => {
@@ -158,13 +178,31 @@ const MainPage = () => {
dispatch(devicesActions.select(deviceId));
}, [dispatch]);
+ useEffect(() => {
+ const filtered = Object.values(devices)
+ .filter((device) => !filterStatuses.length || filterStatuses.includes(device.status))
+ .filter((device) => !filterGroups.length || filterGroups.includes(device.groupId))
+ .filter((device) => `${device.name} ${device.uniqueId}`.toLowerCase().includes(filterKeyword.toLowerCase()));
+ if (filterSort === 'lastUpdate') {
+ filtered.sort((device1, device2) => {
+ const time1 = device1.lastUpdate ? moment(device1.lastUpdate).valueOf() : 0;
+ const time2 = device2.lastUpdate ? moment(device2.lastUpdate).valueOf() : 0;
+ return time2 - time1;
+ });
+ }
+ setFilteredDevices(filtered);
+ setFilteredPositions(filterMap
+ ? filtered.map((device) => positions[device.id]).filter(Boolean)
+ : Object.values(positions));
+ }, [devices, positions, filterKeyword, filterStatuses, filterGroups, filterSort, filterMap]);
+
return (
<div className={classes.root}>
<MapView>
<MapGeofence />
<MapAccuracy />
{mapLiveRoutes && <MapLiveRoutes />}
- <MapPositions positions={positions} onClick={onClick} showStatus />
+ <MapPositions positions={filteredPositions} onClick={onClick} showStatus />
{selectedPosition && selectedPosition.course && (
<MapDirection position={selectedPosition} />
)}
@@ -194,12 +232,13 @@ const MainPage = () => {
</IconButton>
)}
<OutlinedInput
+ ref={filterRef}
placeholder={t('sharedSearchDevices')}
- value={searchKeyword}
- onChange={(event) => setSearchKeyword(event.target.value)}
+ value={filterKeyword}
+ onChange={(event) => setFilterKeyword(event.target.value)}
endAdornment={(
<InputAdornment position="end">
- <IconButton size="small" onClick={() => {}}>
+ <IconButton size="small" onClick={() => setFilterAnchorEl(filterRef.current)}>
<TuneIcon fontSize="small" />
</IconButton>
</InputAdornment>
@@ -207,6 +246,60 @@ const MainPage = () => {
size="small"
fullWidth
/>
+ <Popover
+ open={!!filterAnchorEl}
+ anchorEl={filterAnchorEl}
+ onClose={() => setFilterAnchorEl(null)}
+ anchorOrigin={{
+ vertical: 'bottom',
+ horizontal: 'left',
+ }}
+ >
+ <div className={classes.filterPanel}>
+ <FormControl>
+ <InputLabel>{t('deviceStatus')}</InputLabel>
+ <Select
+ label={t('deviceStatus')}
+ value={filterStatuses}
+ onChange={(e) => setFilterStatuses(e.target.value)}
+ multiple
+ >
+ <MenuItem value="online">{t('deviceStatusOnline')}</MenuItem>
+ <MenuItem value="offline">{t('deviceStatusOffline')}</MenuItem>
+ <MenuItem value="unknown">{t('deviceStatusUnknown')}</MenuItem>
+ </Select>
+ </FormControl>
+ <FormControl>
+ <InputLabel>{t('settingsGroups')}</InputLabel>
+ <Select
+ label={t('settingsGroups')}
+ value={filterGroups}
+ onChange={(e) => setFilterGroups(e.target.value)}
+ multiple
+ >
+ {Object.values(groups).map((group) => (<MenuItem key={group.id} value={group.id}>{group.name}</MenuItem>))}
+ </Select>
+ </FormControl>
+ <FormControl>
+ <InputLabel>{t('sharedSortBy')}</InputLabel>
+ <Select
+ label={t('sharedSortBy')}
+ value={filterSort}
+ onChange={(e) => setFilterSort(e.target.value)}
+ displayEmpty
+ >
+ <MenuItem value="">{'\u00a0'}</MenuItem>
+ <MenuItem value="lastUpdate">{t('deviceLastUpdate')}</MenuItem>
+ </Select>
+ </FormControl>
+ <FormGroup>
+ <FormControlLabel
+ control={<Checkbox checked={filterMap} onChange={(e) => setFilterMap(e.target.checked)} />}
+ label={t('sharedFilterMap')}
+ />
+ </FormGroup>
+ </div>
+ </Popover>
<IconButton onClick={() => navigate('/settings/device')} disabled={deviceReadonly}>
<AddIcon />
</IconButton>
@@ -218,7 +311,7 @@ const MainPage = () => {
</Toolbar>
</Paper>
<div className={classes.deviceList}>
- <DevicesList filter={searchKeyword} />
+ <DevicesList devices={filteredDevices} />
</div>
</Paper>
{desktop && (
diff --git a/web/l10n/en.json b/web/l10n/en.json
index 17e00108..8d851911 100644
--- a/web/l10n/en.json
+++ b/web/l10n/en.json
@@ -62,6 +62,8 @@
"sharedCalendars": "Calendars",
"sharedFile": "File",
"sharedSearchDevices": "Search Devices",
+ "sharedSortBy": "Sort By",
+ "sharedFilterMap": "Filter on Map",
"sharedSelectFile": "Select File",
"sharedPhone": "Phone",
"sharedRequired": "Required",