From 236b10503612706ed914678d942ac604f2973f47 Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sun, 27 Sep 2020 21:54:39 -0700 Subject: Implement notification settings --- modern/src/App.js | 4 + modern/src/DevicePage.js | 2 +- modern/src/EditItemView.js | 4 +- modern/src/MainToolbar.js | 2 +- modern/src/StatusView.js | 14 ++-- modern/src/UserPage.js | 2 +- modern/src/admin/UsersPage.js | 19 ++--- modern/src/common/formatter.js | 23 +++--- modern/src/common/localization.js | 128 ++++++++++++++++--------------- modern/src/common/stringUtils.js | 7 ++ modern/src/reports/RouteReportPage.js | 12 +-- modern/src/settings/NotificationPage.js | 109 ++++++++++++++++++++++++++ modern/src/settings/NotificationsPage.js | 82 ++++++++++++++++++++ 13 files changed, 306 insertions(+), 102 deletions(-) create mode 100644 modern/src/common/stringUtils.js create mode 100644 modern/src/settings/NotificationPage.js create mode 100644 modern/src/settings/NotificationsPage.js (limited to 'modern') 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 = () => { + + 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 ( - item}> + {item && <> 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 = () => { - + history.push('/settings/notifications')}> 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 ( <> - {t('deviceStatus')}: {formatter(device.status, 'status')}
- {t('sharedLocation')}: {formatter(position, 'latitude')} {formatter(position, 'longitude')}
- {t('positionSpeed')}: {formatter(position.speed, 'speed')}
- {t('positionCourse')}: {formatter(position.course, 'course')}
- {t('positionDistance')}: {formatter(position.attributes.totalDistance, 'distance')}
+ {t('deviceStatus')}: {formatPosition(device.status, 'status')}
+ {t('sharedLocation')}: {formatPosition(position, 'latitude')} {formatPosition(position, 'longitude')}
+ {t('positionSpeed')}: {formatPosition(position.speed, 'speed')}
+ {t('positionCourse')}: {formatPosition(position.course, 'course')}
+ {t('positionDistance')}: {formatPosition(position.attributes.totalDistance, 'distance')}
{position.attributes.batteryLevel && - <>{t('positionBattery')}: {formatter(position.attributes.batteryLevel, 'batteryLevel')}
+ <>{t('positionBattery')}: {formatPosition(position.attributes.batteryLevel, 'batteryLevel')}
} ); 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 ( - item}> + {item && <> 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 }) => { - {formatter(item, 'name')} - {formatter(item, 'email')} - {formatter(item, 'administrator')} - {formatter(item, 'disabled')} + {item.name} + {item.email} + {formatBoolean(item, 'administrator')} + {formatBoolean(item, 'disabled')} ))} @@ -62,13 +59,11 @@ const UsersView = ({ updateTimestamp, onMenuClick }) => { } const UsersPage = () => { - const classes = useStyles(); - return ( -
+ <> -
+ ); } 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 = () => { {data.map((item) => ( - {formatter(item, 'fixTime')} - {formatter(item, 'latitude')} - {formatter(item, 'longitude')} - {formatter(item, 'speed')} - {formatter(item, 'address')} + {formatPosition(item, 'fixTime')} + {formatPosition(item, 'latitude')} + {formatPosition(item, 'longitude')} + {formatPosition(item, 'speed')} + {formatPosition(item, 'address')} ))} 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 ( + + {item && + <> + + }> + + {t('sharedRequired')} + + + + {types && + + {t('sharedType')} + + + } + {notificators && + + {t('notificationNotificators')} + + + } + {item.type === 'alarm' && + + {t('sharedAlarms')} + + + } + setItem({...item, always: event.target.checked})} + /> + } + label={t('notificationAlways')} /> + + + + } + + ); +} + +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 ( + + + + + + {t('notificationType')} + {t('notificationAlways')} + {t('sharedAlarms')} + {t('notificationNotificators')} + + + + {items.map(item => ( + + + onMenuClick(event.currentTarget, item.id)}> + + + + {t(prefixString('event', item.type))} + {formatBoolean(item.always)} + {formatList('alarm', item.attributes.alarms)} + {formatList('notificator', item.notificators)} + + ))} + +
+
+ ); +} + +const NotificationsPage = () => { + return ( + <> + + + + ); +} + +export default NotificationsPage; -- cgit v1.2.3