diff options
Diffstat (limited to 'modern/src/common')
-rw-r--r-- | modern/src/common/components/StatusCard.js | 266 |
1 files changed, 266 insertions, 0 deletions
diff --git a/modern/src/common/components/StatusCard.js b/modern/src/common/components/StatusCard.js new file mode 100644 index 00000000..b8d7ffc4 --- /dev/null +++ b/modern/src/common/components/StatusCard.js @@ -0,0 +1,266 @@ +import React, { useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import Draggable from 'react-draggable'; +import { + Card, + CardContent, + Typography, + CardActions, + IconButton, + Table, + TableBody, + TableRow, + TableCell, + Menu, + MenuItem, + CardMedia, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import CloseIcon from '@mui/icons-material/Close'; +import ReplayIcon from '@mui/icons-material/Replay'; +import PublishIcon from '@mui/icons-material/Publish'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import PendingIcon from '@mui/icons-material/Pending'; + +import { useTranslation } from './LocalizationProvider'; +import RemoveDialog from './RemoveDialog'; +import PositionValue from './PositionValue'; +import { useDeviceReadonly, useRestriction } from '../util/permissions'; +import usePersistedState from '../util/usePersistedState'; +import usePositionAttributes from '../attributes/usePositionAttributes'; +import { devicesActions } from '../../store'; +import { useCatch, useCatchCallback } from '../../reactHelper'; + +const useStyles = makeStyles((theme) => ({ + card: { + width: theme.dimensions.popupMaxWidth, + }, + media: { + height: theme.dimensions.popupImageHeight, + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'flex-start', + }, + mediaButton: { + color: theme.palette.colors.white, + mixBlendMode: 'difference', + }, + header: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: theme.spacing(1, 1, 0, 2), + }, + content: { + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + }, + negative: { + color: theme.palette.colors.negative, + }, + icon: { + width: '25px', + height: '25px', + filter: 'brightness(0) invert(1)', + }, + table: { + '& .MuiTableCell-sizeSmall': { + paddingLeft: 0, + paddingRight: 0, + }, + }, + cell: { + borderBottom: 'none', + }, + actions: { + justifyContent: 'space-between', + }, +})); + +const StatusRow = ({ name, content }) => { + const classes = useStyles(); + + return ( + <TableRow> + <TableCell className={classes.cell}> + <Typography variant="body2">{name}</Typography> + </TableCell> + <TableCell className={classes.cell}> + <Typography variant="body2" color="textSecondary">{content}</Typography> + </TableCell> + </TableRow> + ); +}; + +const StatusCard = ({ deviceId, position, onClose }) => { + const classes = useStyles(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const t = useTranslation(); + + const readonly = useRestriction('readonly'); + const deviceReadonly = useDeviceReadonly(); + + const device = useSelector((state) => state.devices.items[deviceId]); + + const deviceImage = device?.attributes?.deviceImage; + + const positionAttributes = usePositionAttributes(t); + const [positionItems] = usePersistedState('positionItems', ['speed', 'address', 'totalDistance', 'course']); + + const [anchorEl, setAnchorEl] = useState(null); + + const [removing, setRemoving] = useState(false); + + const handleRemove = useCatch(async (removed) => { + if (removed) { + const response = await fetch('/api/devices'); + if (response.ok) { + dispatch(devicesActions.refresh(await response.json())); + } else { + throw Error(await response.text()); + } + } + setRemoving(false); + }); + + const handleGeofence = useCatchCallback(async () => { + const newItem = { + name: '', + area: `CIRCLE (${position.latitude} ${position.longitude}, 50)`, + }; + const response = await fetch('/api/geofences', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newItem), + }); + if (response.ok) { + const item = await response.json(); + const permissionResponse = await fetch('/api/permissions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ deviceId: position.deviceId, geofenceId: item.id }), + }); + if (!permissionResponse.ok) { + throw Error(await permissionResponse.text()); + } + navigate(`/settings/geofence/${item.id}`); + } else { + throw Error(await response.text()); + } + }, [navigate]); + + return ( + <> + {device && ( + <Draggable + handle={`.${classes.media}, .${classes.header}`} + > + <Card elevation={3} className={classes.card}> + {deviceImage ? ( + <CardMedia + className={classes.media} + image={`/api/media/${device.uniqueId}/${deviceImage}`} + > + <IconButton + size="small" + onClick={onClose} + onTouchStart={onClose} + > + <CloseIcon fontSize="small" className={classes.mediaButton} /> + </IconButton> + </CardMedia> + ) : ( + <div className={classes.header}> + <Typography variant="body2" color="textSecondary"> + {device.name} + </Typography> + <IconButton + size="small" + onClick={onClose} + onTouchStart={onClose} + > + <CloseIcon fontSize="small" /> + </IconButton> + </div> + )} + {position && ( + <CardContent className={classes.content}> + <Table size="small" classes={{ root: classes.table }}> + <TableBody> + {positionItems.filter((key) => position.hasOwnProperty(key) || position.attributes.hasOwnProperty(key)).map((key) => ( + <StatusRow + key={key} + name={positionAttributes[key].name} + content={( + <PositionValue + position={position} + property={position.hasOwnProperty(key) ? key : null} + attribute={position.hasOwnProperty(key) ? null : key} + /> + )} + /> + ))} + </TableBody> + </Table> + </CardContent> + )} + <CardActions classes={{ root: classes.actions }} disableSpacing> + <IconButton + color="secondary" + onClick={(e) => setAnchorEl(e.currentTarget)} + disabled={!position} + > + <PendingIcon /> + </IconButton> + <IconButton + onClick={() => navigate('/replay')} + disabled={!position} + > + <ReplayIcon /> + </IconButton> + <IconButton + onClick={() => navigate(`/settings/command-send/${deviceId}`)} + disabled={readonly} + > + <PublishIcon /> + </IconButton> + <IconButton + onClick={() => navigate(`/settings/device/${deviceId}`)} + disabled={deviceReadonly} + > + <EditIcon /> + </IconButton> + <IconButton + onClick={() => setRemoving(true)} + disabled={deviceReadonly} + className={classes.negative} + > + <DeleteIcon /> + </IconButton> + </CardActions> + </Card> + </Draggable> + )} + {position && ( + <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={() => setAnchorEl(null)}> + <MenuItem onClick={() => navigate(`/position/${position.id}`)}><Typography color="secondary">{t('sharedShowDetails')}</Typography></MenuItem> + <MenuItem onClick={handleGeofence}>{t('sharedCreateGeofence')}</MenuItem> + <MenuItem component="a" target="_blank" href={`https://www.google.com/maps/search/?api=1&query=${position.latitude}%2C${position.longitude}`}>{t('linkGoogleMaps')}</MenuItem> + <MenuItem component="a" target="_blank" href={`http://maps.apple.com/?ll=${position.latitude},${position.longitude}`}>{t('linkAppleMaps')}</MenuItem> + <MenuItem component="a" target="_blank" href={`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${position.latitude}%2C${position.longitude}&heading=${position.course}`}>{t('linkStreetView')}</MenuItem> + </Menu> + )} + <RemoveDialog + open={removing} + endpoint="devices" + itemId={deviceId} + onResult={(removed) => handleRemove(removed)} + /> + </> + ); +}; + +export default StatusCard; |