diff options
-rw-r--r-- | modern/src/App.js | 4 | ||||
-rw-r--r-- | modern/src/DevicePage.js | 2 | ||||
-rw-r--r-- | modern/src/EditItemView.js | 4 | ||||
-rw-r--r-- | modern/src/MainToolbar.js | 2 | ||||
-rw-r--r-- | modern/src/StatusView.js | 14 | ||||
-rw-r--r-- | modern/src/UserPage.js | 2 | ||||
-rw-r--r-- | modern/src/admin/UsersPage.js | 19 | ||||
-rw-r--r-- | modern/src/common/formatter.js | 23 | ||||
-rw-r--r-- | modern/src/common/localization.js | 128 | ||||
-rw-r--r-- | modern/src/common/stringUtils.js | 7 | ||||
-rw-r--r-- | modern/src/reports/RouteReportPage.js | 12 | ||||
-rw-r--r-- | modern/src/settings/NotificationPage.js | 109 | ||||
-rw-r--r-- | modern/src/settings/NotificationsPage.js | 82 |
13 files changed, 306 insertions, 102 deletions
diff --git a/modern/src/App.js b/modern/src/App.js index 4d786390..b25d1aba 100644 --- a/modern/src/App.js +++ b/modern/src/App.js @@ -9,6 +9,8 @@ import UsersPage from './admin/UsersPage'; import DevicePage from './DevicePage'; import UserPage from './UserPage'; import SocketController from './SocketController'; +import NotificationsPage from './settings/NotificationsPage'; +import NotificationPage from './settings/NotificationPage'; const App = () => { return ( @@ -21,6 +23,8 @@ const App = () => { <Route exact path='/user/:id?' component={UserPage} /> <Route exact path='/device/:id?' component={DevicePage} /> <Route exact path='/reports/route' component={RouteReportPage} /> + <Route exact path='/settings/notifications' component={NotificationsPage} /> + <Route exact path='/settings/notification/:id?' component={NotificationPage} /> <Route exact path='/admin/server' component={ServerPage} /> <Route exact path='/admin/users' component={UsersPage} /> </Switch> diff --git a/modern/src/DevicePage.js b/modern/src/DevicePage.js index b531f46b..45eba3fa 100644 --- a/modern/src/DevicePage.js +++ b/modern/src/DevicePage.js @@ -53,7 +53,7 @@ const DevicePage = () => { }, []); return ( - <EditItemView endpoint="devices" setItem={setItem} getItem={() => item}> + <EditItemView endpoint="devices" item={item} setItem={setItem}> {item && <> <Accordion defaultExpanded> diff --git a/modern/src/EditItemView.js b/modern/src/EditItemView.js index a6f1d229..16fbbaee 100644 --- a/modern/src/EditItemView.js +++ b/modern/src/EditItemView.js @@ -22,7 +22,7 @@ const useStyles = makeStyles(theme => ({ }, })); -const EditItemView = ({ children, endpoint, setItem, getItem }) => { +const EditItemView = ({ children, endpoint, item, setItem }) => { const history = useHistory(); const classes = useStyles(); const { id } = useParams(); @@ -47,7 +47,7 @@ const EditItemView = ({ children, endpoint, setItem, getItem }) => { const response = await fetch(url, { method: !id ? 'POST' : 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(getItem()), + body: JSON.stringify(item), }); if (response.ok) { diff --git a/modern/src/MainToolbar.js b/modern/src/MainToolbar.js index 9b6005d9..1098d980 100644 --- a/modern/src/MainToolbar.js +++ b/modern/src/MainToolbar.js @@ -146,7 +146,7 @@ const MainToolbar = () => { </ListItemIcon> <ListItemText primary={t('settingsUser')} /> </ListItem> - <ListItem button disabled> + <ListItem button onClick={() => history.push('/settings/notifications')}> <ListItemIcon> <SettingsIcon /> </ListItemIcon> diff --git a/modern/src/StatusView.js b/modern/src/StatusView.js index 756a95a5..61fd1504 100644 --- a/modern/src/StatusView.js +++ b/modern/src/StatusView.js @@ -1,7 +1,7 @@ import t from './common/localization' import React from 'react'; import { useSelector } from 'react-redux'; -import formatter from './common/formatter'; +import { formatPosition } from './common/formatter'; const StatusView = (props) => { const device = useSelector(state => state.devices.items[props.deviceId]); @@ -9,13 +9,13 @@ const StatusView = (props) => { return ( <> - <b>{t('deviceStatus')}:</b> {formatter(device.status, 'status')}<br /> - <b>{t('sharedLocation')}:</b> {formatter(position, 'latitude')} {formatter(position, 'longitude')}<br /> - <b>{t('positionSpeed')}:</b> {formatter(position.speed, 'speed')}<br /> - <b>{t('positionCourse')}:</b> {formatter(position.course, 'course')}<br /> - <b>{t('positionDistance')}:</b> {formatter(position.attributes.totalDistance, 'distance')}<br /> + <b>{t('deviceStatus')}:</b> {formatPosition(device.status, 'status')}<br /> + <b>{t('sharedLocation')}:</b> {formatPosition(position, 'latitude')} {formatPosition(position, 'longitude')}<br /> + <b>{t('positionSpeed')}:</b> {formatPosition(position.speed, 'speed')}<br /> + <b>{t('positionCourse')}:</b> {formatPosition(position.course, 'course')}<br /> + <b>{t('positionDistance')}:</b> {formatPosition(position.attributes.totalDistance, 'distance')}<br /> {position.attributes.batteryLevel && - <><b>{t('positionBattery')}:</b> {formatter(position.attributes.batteryLevel, 'batteryLevel')}<br /></> + <><b>{t('positionBattery')}:</b> {formatPosition(position.attributes.batteryLevel, 'batteryLevel')}<br /></> } </> ); diff --git a/modern/src/UserPage.js b/modern/src/UserPage.js index 81e33893..98b9b414 100644 --- a/modern/src/UserPage.js +++ b/modern/src/UserPage.js @@ -20,7 +20,7 @@ const UserPage = () => { const [item, setItem] = useState(); return ( - <EditItemView endpoint="users" setItem={setItem} getItem={() => item}> + <EditItemView endpoint="users" item={item} setItem={setItem}> {item && <> <Accordion defaultExpanded> diff --git a/modern/src/admin/UsersPage.js b/modern/src/admin/UsersPage.js index d0861f1c..630bea43 100644 --- a/modern/src/admin/UsersPage.js +++ b/modern/src/admin/UsersPage.js @@ -3,14 +3,11 @@ import MainToolbar from '../MainToolbar'; import { TableContainer, Table, TableRow, TableCell, TableHead, TableBody, makeStyles, IconButton } from '@material-ui/core'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import t from '../common/localization'; -import formatter from '../common/formatter'; import { useEffectAsync } from '../reactHelper'; import EditCollectionView from '../EditCollectionView'; +import { formatBoolean } from '../common/formatter'; const useStyles = makeStyles(theme => ({ - root: { - height: '100%', - }, columnAction: { width: theme.spacing(1), padding: theme.spacing(0, 1), @@ -49,10 +46,10 @@ const UsersView = ({ updateTimestamp, onMenuClick }) => { <MoreVertIcon /> </IconButton> </TableCell> - <TableCell>{formatter(item, 'name')}</TableCell> - <TableCell>{formatter(item, 'email')}</TableCell> - <TableCell>{formatter(item, 'administrator')}</TableCell> - <TableCell>{formatter(item, 'disabled')}</TableCell> + <TableCell>{item.name}</TableCell> + <TableCell>{item.email}</TableCell> + <TableCell>{formatBoolean(item, 'administrator')}</TableCell> + <TableCell>{formatBoolean(item, 'disabled')}</TableCell> </TableRow> ))} </TableBody> @@ -62,13 +59,11 @@ const UsersView = ({ updateTimestamp, onMenuClick }) => { } const UsersPage = () => { - const classes = useStyles(); - return ( - <div className={classes.root}> + <> <MainToolbar /> <EditCollectionView content={UsersView} editPath="/user" endpoint="users" /> - </div> + </> ); } diff --git a/modern/src/common/formatter.js b/modern/src/common/formatter.js index 8ad87e70..29108cec 100644 --- a/modern/src/common/formatter.js +++ b/modern/src/common/formatter.js @@ -1,7 +1,10 @@ import moment from 'moment'; import t from '../common/localization'; -const formatValue = (key, value) => { +export const formatPosition = (key, value) => { + if (value != null && typeof value == 'object') { + value = value[key]; + } switch (key) { case 'fixTime': return moment(value).format('LLL'); @@ -15,19 +18,19 @@ const formatValue = (key, value) => { return value + '%'; default: if (typeof value === 'number') { - return Number(value.toFixed(1)); + return formatNumber(value); } else if (typeof value === 'boolean') { - return value ? t('sharedYes') : t('sharedNo'); + return formatBoolean(value); } else { return value; } } } -export default (object, key) => { - if (object != null && typeof object == 'object') { - return formatValue(key, object[key]); - } else { - return formatValue(key, object); - } -}; +export const formatBoolean = (value) => { + return value ? t('sharedYes') : t('sharedNo'); +} + +export const formatNumber = (value, precision = 1) => { + return Number(value.toFixed(precision)); +} diff --git a/modern/src/common/localization.js b/modern/src/common/localization.js index 4c25697d..0a21a896 100644 --- a/modern/src/common/localization.js +++ b/modern/src/common/localization.js @@ -53,59 +53,59 @@ import zh from '../../../web/l10n/zh.json'; import zh_TW from '../../../web/l10n/zh_TW.json'; const supportedLanguages = { - '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: 'മലയാളം' }, - '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' }, - 'pt_BR': { data: pt_BR, 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: '中文' }, - 'zh_TW': { data: zh_TW, name: '中文 (Taiwan)' } + '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: 'മലയാളം' }, + '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' }, + 'pt_BR': { data: pt_BR, 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: '中文' }, + 'zh_TW': { data: zh_TW, name: '中文 (Taiwan)' } }; const languages = window.navigator.languages !== undefined ? window.navigator.languages.slice() : []; @@ -114,20 +114,24 @@ languages.push(language); languages.push(language.substring(0, 2)); languages.push('en'); for (let i = 0; i < languages.length; i++) { - language = languages[i].replace('-', '_'); + language = languages[i].replace('-', '_'); + if (language in supportedLanguages) { + break; + } + if (language.length > 2) { + language = languages[i].substring(0, 2); if (language in supportedLanguages) { - break; - } - if (language.length > 2) { - language = languages[i].substring(0, 2); - if (language in supportedLanguages) { - break; - } + break; } + } } const selectedLanguage = supportedLanguages[language]; +export const findStringKeys = (predicate) => { + return Object.keys(selectedLanguage.data).filter(predicate); +} + export default key => { - return selectedLanguage.data[key] + return selectedLanguage.data[key]; }; diff --git a/modern/src/common/stringUtils.js b/modern/src/common/stringUtils.js new file mode 100644 index 00000000..7bd68c85 --- /dev/null +++ b/modern/src/common/stringUtils.js @@ -0,0 +1,7 @@ +export const prefixString = (prefix, value) => { + return prefix + value.charAt(0).toUpperCase() + value.slice(1); +} + +export const unprefixString = (prefix, value) => { + return value.charAt(prefix.length).toLowerCase() + value.slice(prefix.length + 1); +} diff --git a/modern/src/reports/RouteReportPage.js b/modern/src/reports/RouteReportPage.js index 37e74f46..5e577838 100644 --- a/modern/src/reports/RouteReportPage.js +++ b/modern/src/reports/RouteReportPage.js @@ -4,7 +4,7 @@ import { Grid, TableContainer, Table, TableRow, TableCell, TableHead, TableBody, import t from '../common/localization'; import { useSelector } from 'react-redux'; import moment from 'moment'; -import formatter from '../common/formatter'; +import { formatPosition } from '../common/formatter'; const useStyles = makeStyles(theme => ({ root: { @@ -146,11 +146,11 @@ const RouteReportPage = () => { <TableBody> {data.map((item) => ( <TableRow key={item.id}> - <TableCell>{formatter(item, 'fixTime')}</TableCell> - <TableCell>{formatter(item, 'latitude')}</TableCell> - <TableCell>{formatter(item, 'longitude')}</TableCell> - <TableCell>{formatter(item, 'speed')}</TableCell> - <TableCell>{formatter(item, 'address')}</TableCell> + <TableCell>{formatPosition(item, 'fixTime')}</TableCell> + <TableCell>{formatPosition(item, 'latitude')}</TableCell> + <TableCell>{formatPosition(item, 'longitude')}</TableCell> + <TableCell>{formatPosition(item, 'speed')}</TableCell> + <TableCell>{formatPosition(item, 'address')}</TableCell> </TableRow> ))} </TableBody> diff --git a/modern/src/settings/NotificationPage.js b/modern/src/settings/NotificationPage.js new file mode 100644 index 00000000..f09f3fe2 --- /dev/null +++ b/modern/src/settings/NotificationPage.js @@ -0,0 +1,109 @@ +import React, { useState } from 'react'; +import TextField from '@material-ui/core/TextField'; + +import t, { findStringKeys } from '../common/localization'; +import EditItemView from '../EditItemView'; +import { Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, FormControlLabel, Checkbox, FormControl, InputLabel, Select, MenuItem } from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { useEffectAsync } from '../reactHelper'; +import { prefixString, unprefixString } from '../common/stringUtils'; + +const useStyles = makeStyles(() => ({ + details: { + flexDirection: 'column', + }, +})); + +const NotificationPage = () => { + const classes = useStyles(); + + const [item, setItem] = useState(); + const [types, setTypes] = useState(); + const [notificators, setNotificators] = useState(); + + const alarms = findStringKeys(it => it.startsWith('alarm')).map(it => ({ + key: unprefixString('alarm', it), + name: t(it), + })); + + useEffectAsync(async () => { + const response = await fetch('/api/notifications/types'); + if (response.ok) { + setTypes(await response.json()); + } + }, []); + + useEffectAsync(async () => { + const response = await fetch('/api/notifications/notificators'); + if (response.ok) { + setNotificators(await response.json()); + } + }, []); + + return ( + <EditItemView endpoint="notifications" item={item} setItem={setItem}> + {item && + <> + <Accordion defaultExpanded> + <AccordionSummary expandIcon={<ExpandMoreIcon />}> + <Typography variant="subtitle1"> + {t('sharedRequired')} + </Typography> + </AccordionSummary> + <AccordionDetails className={classes.details}> + {types && + <FormControl margin="normal" variant="filled"> + <InputLabel>{t('sharedType')}</InputLabel> + <Select + native + defaultValue={item.type} + onChange={e => setItem({...item, type: e.target.value})}> + {types.map(it => ( + <option key={it.type} value={it.type}>{t(prefixString('event', it.type))}</option> + ))} + </Select> + </FormControl> + } + {notificators && + <FormControl margin="normal" variant="filled"> + <InputLabel>{t('notificationNotificators')}</InputLabel> + <Select + multiple + defaultValue={item.notificators ? item.notificators.split(/[, ]+/) : []} + onChange={e => setItem({...item, notificators: e.target.value.join()})}> + {notificators.map(it => ( + <MenuItem key={it.type} value={it.type}>{t(prefixString('notificator', it.type))}</MenuItem> + ))} + </Select> + </FormControl> + } + {item.type === 'alarm' && + <FormControl margin="normal" variant="filled"> + <InputLabel>{t('sharedAlarms')}</InputLabel> + <Select + multiple + defaultValue={item.attributes.alarms ? item.attributes.alarms.split(/[, ]+/) : []} + onChange={e => setItem({...item, attributes: {...item.attributes, alarms: e.target.value.join()}})}> + {alarms.map(it => ( + <MenuItem key={it.key} value={it.key}>{it.name}</MenuItem> + ))} + </Select> + </FormControl> + } + <FormControlLabel + control={ + <Checkbox + checked={item.always} + onChange={event => setItem({...item, always: event.target.checked})} + /> + } + label={t('notificationAlways')} /> + </AccordionDetails> + </Accordion> + </> + } + </EditItemView> + ); +} + +export default NotificationPage; diff --git a/modern/src/settings/NotificationsPage.js b/modern/src/settings/NotificationsPage.js new file mode 100644 index 00000000..15da0de7 --- /dev/null +++ b/modern/src/settings/NotificationsPage.js @@ -0,0 +1,82 @@ +import React, { useState } from 'react'; +import MainToolbar from '../MainToolbar'; +import { TableContainer, Table, TableRow, TableCell, TableHead, TableBody, makeStyles, IconButton } from '@material-ui/core'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; +import t from '../common/localization'; +import { useEffectAsync } from '../reactHelper'; +import EditCollectionView from '../EditCollectionView'; +import { prefixString } from '../common/stringUtils'; +import { formatBoolean } from '../common/formatter'; + +const useStyles = makeStyles(theme => ({ + columnAction: { + width: theme.spacing(1), + padding: theme.spacing(0, 1), + }, +})); + +const NotificationsView = ({ updateTimestamp, onMenuClick }) => { + const classes = useStyles(); + + const [items, setItems] = useState([]); + + useEffectAsync(async () => { + const response = await fetch('/api/notifications'); + if (response.ok) { + setItems(await response.json()); + } + }, [updateTimestamp]); + + const formatList = (prefix, value) => { + if (value) { + return value + .split(/[, ]+/) + .filter(Boolean) + .map(it => t(prefixString(prefix, it))) + .join(', '); + } + return ''; + }; + + return ( + <TableContainer> + <Table> + <TableHead> + <TableRow> + <TableCell className={classes.columnAction} /> + <TableCell>{t('notificationType')}</TableCell> + <TableCell>{t('notificationAlways')}</TableCell> + <TableCell>{t('sharedAlarms')}</TableCell> + <TableCell>{t('notificationNotificators')}</TableCell> + </TableRow> + </TableHead> + <TableBody> + {items.map(item => ( + <TableRow key={item.id}> + <TableCell className={classes.columnAction} padding="none"> + <IconButton onClick={(event) => onMenuClick(event.currentTarget, item.id)}> + <MoreVertIcon /> + </IconButton> + </TableCell> + <TableCell>{t(prefixString('event', item.type))}</TableCell> + <TableCell>{formatBoolean(item.always)}</TableCell> + <TableCell>{formatList('alarm', item.attributes.alarms)}</TableCell> + <TableCell>{formatList('notificator', item.notificators)}</TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </TableContainer> + ); +} + +const NotificationsPage = () => { + return ( + <> + <MainToolbar /> + <EditCollectionView content={NotificationsView} editPath="/settings/notification" endpoint="notifications" /> + </> + ); +} + +export default NotificationsPage; |