diff options
author | Anton Tananaev <anton@traccar.org> | 2022-06-04 09:13:49 -0700 |
---|---|---|
committer | Anton Tananaev <anton@traccar.org> | 2022-06-04 09:13:49 -0700 |
commit | 71c5dfe153705bc6ee1931920f7713e73284115f (patch) | |
tree | 1ac2e3d9e4c383f210cc62d9d92f83b7e0b363df | |
parent | 022718d7d0831c246bbe01cc2d8fb66709be3189 (diff) | |
download | trackermap-web-71c5dfe153705bc6ee1931920f7713e73284115f.tar.gz trackermap-web-71c5dfe153705bc6ee1931920f7713e73284115f.tar.bz2 trackermap-web-71c5dfe153705bc6ee1931920f7713e73284115f.zip |
Filtering and sorting (fix #952)
-rw-r--r-- | modern/src/common/theme/dimensions.js | 6 | ||||
-rw-r--r-- | modern/src/common/util/usePersistedState.js | 6 | ||||
-rw-r--r-- | modern/src/main/DevicesList.js | 20 | ||||
-rw-r--r-- | modern/src/main/MainPage.js | 121 | ||||
-rw-r--r-- | web/l10n/en.json | 2 |
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", |