aboutsummaryrefslogtreecommitdiff
path: root/modern/src/settings
diff options
context:
space:
mode:
Diffstat (limited to 'modern/src/settings')
-rw-r--r--modern/src/settings/AccumulatorsPage.jsx107
-rw-r--r--modern/src/settings/AnnouncementPage.jsx106
-rw-r--r--modern/src/settings/CalendarPage.jsx208
-rw-r--r--modern/src/settings/CalendarsPage.jsx64
-rw-r--r--modern/src/settings/CommandDevicePage.jsx111
-rw-r--r--modern/src/settings/CommandGroupPage.jsx105
-rw-r--r--modern/src/settings/CommandPage.jsx50
-rw-r--r--modern/src/settings/CommandsPage.jsx74
-rw-r--r--modern/src/settings/ComputedAttributePage.jsx177
-rw-r--r--modern/src/settings/ComputedAttributesPage.jsx74
-rw-r--r--modern/src/settings/DeviceConnectionsPage.jsx107
-rw-r--r--modern/src/settings/DevicePage.jsx176
-rw-r--r--modern/src/settings/DevicesPage.jsx114
-rw-r--r--modern/src/settings/DriverPage.jsx62
-rw-r--r--modern/src/settings/DriversPage.jsx66
-rw-r--r--modern/src/settings/GeofencePage.jsx88
-rw-r--r--modern/src/settings/GroupConnectionsPage.jsx107
-rw-r--r--modern/src/settings/GroupPage.jsx93
-rw-r--r--modern/src/settings/GroupsPage.jsx91
-rw-r--r--modern/src/settings/MaintenancePage.jsx174
-rw-r--r--modern/src/settings/MaintenancesPage.jsx100
-rw-r--r--modern/src/settings/NotificationPage.jsx144
-rw-r--r--modern/src/settings/NotificationsPage.jsx83
-rw-r--r--modern/src/settings/PreferencesPage.jsx375
-rw-r--r--modern/src/settings/ServerPage.jsx316
-rw-r--r--modern/src/settings/SharePage.jsx109
-rw-r--r--modern/src/settings/UserConnectionsPage.jsx129
-rw-r--r--modern/src/settings/UserPage.jsx428
-rw-r--r--modern/src/settings/UsersPage.jsx130
-rw-r--r--modern/src/settings/common/useSettingsStyles.js33
-rw-r--r--modern/src/settings/components/AddAttributeDialog.jsx104
-rw-r--r--modern/src/settings/components/BaseCommandView.jsx79
-rw-r--r--modern/src/settings/components/CollectionActions.jsx104
-rw-r--r--modern/src/settings/components/CollectionFab.jsx35
-rw-r--r--modern/src/settings/components/EditAttributesAccordion.jsx217
-rw-r--r--modern/src/settings/components/EditItemView.jsx101
-rw-r--r--modern/src/settings/components/SearchHeader.jsx38
-rw-r--r--modern/src/settings/components/SettingsMenu.jsx173
38 files changed, 0 insertions, 4852 deletions
diff --git a/modern/src/settings/AccumulatorsPage.jsx b/modern/src/settings/AccumulatorsPage.jsx
deleted file mode 100644
index 1c9b6e65..00000000
--- a/modern/src/settings/AccumulatorsPage.jsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useSelector } from 'react-redux';
-import { useNavigate, useParams } from 'react-router-dom';
-import {
- Accordion,
- AccordionSummary,
- AccordionDetails,
- Typography,
- Container,
- TextField,
- Button,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import { useCatch } from '../reactHelper';
-import { useAttributePreference } from '../common/util/preferences';
-import { distanceFromMeters, distanceToMeters, distanceUnitString } from '../common/util/converter';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const AccumulatorsPage = () => {
- const navigate = useNavigate();
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const distanceUnit = useAttributePreference('distanceUnit');
-
- const { deviceId } = useParams();
- const position = useSelector((state) => state.session.positions[deviceId]);
-
- const [item, setItem] = useState();
-
- useEffect(() => {
- if (position) {
- setItem({
- deviceId: parseInt(deviceId, 10),
- hours: position.attributes.hours || 0,
- totalDistance: position.attributes.totalDistance || 0,
- });
- }
- }, [deviceId, position]);
-
- const handleSave = useCatch(async () => {
- const response = await fetch(`/api/devices/${deviceId}/accumulators`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(item),
- });
-
- if (response.ok) {
- navigate(-1);
- } else {
- throw Error(await response.text());
- }
- });
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['sharedDeviceAccumulators']}>
- {item && (
- <Container maxWidth="xs" className={classes.container}>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedRequired')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- type="number"
- value={item.hours / 3600000}
- onChange={(event) => setItem({ ...item, hours: Number(event.target.value) * 3600000 })}
- label={t('positionHours')}
- />
- <TextField
- type="number"
- value={distanceFromMeters(item.totalDistance, distanceUnit)}
- onChange={(event) => setItem({ ...item, totalDistance: distanceToMeters(Number(event.target.value), distanceUnit) })}
- label={`${t('deviceTotalDistance')} (${distanceUnitString(distanceUnit, t)})`}
- />
- </AccordionDetails>
- </Accordion>
- <div className={classes.buttons}>
- <Button
- type="button"
- color="primary"
- variant="outlined"
- onClick={() => navigate(-1)}
- >
- {t('sharedCancel')}
- </Button>
- <Button
- type="button"
- color="primary"
- variant="contained"
- onClick={handleSave}
- >
- {t('sharedSave')}
- </Button>
- </div>
- </Container>
- )}
- </PageLayout>
- );
-};
-
-export default AccumulatorsPage;
diff --git a/modern/src/settings/AnnouncementPage.jsx b/modern/src/settings/AnnouncementPage.jsx
deleted file mode 100644
index 39488f02..00000000
--- a/modern/src/settings/AnnouncementPage.jsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import React, { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-import {
- Accordion,
- AccordionSummary,
- AccordionDetails,
- Typography,
- Container,
- TextField,
- Button,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import { useCatchCallback } from '../reactHelper';
-import useSettingsStyles from './common/useSettingsStyles';
-import SelectField from '../common/components/SelectField';
-import { prefixString } from '../common/util/stringUtils';
-
-const AnnouncementPage = () => {
- const navigate = useNavigate();
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const [users, setUsers] = useState([]);
- const [notificator, setNotificator] = useState();
- const [message, setMessage] = useState({});
-
- const handleSend = useCatchCallback(async () => {
- const query = new URLSearchParams();
- users.forEach((userId) => query.append('userId', userId));
- const response = await fetch(`/api/notifications/send/${notificator}?${query.toString()}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(message),
- });
- if (response.ok) {
- navigate(-1);
- } else {
- throw Error(await response.text());
- }
- }, [users, notificator, message, navigate]);
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['serverAnnouncement']}>
- <Container maxWidth="xs" className={classes.container}>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedRequired')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <SelectField
- multiple
- value={users}
- onChange={(e) => setUsers(e.target.value)}
- endpoint="/api/users"
- label={t('settingsUsers')}
- />
- <SelectField
- value={notificator}
- onChange={(e) => setNotificator(e.target.value)}
- endpoint="/api/notifications/notificators"
- keyGetter={(it) => it.type}
- titleGetter={(it) => t(prefixString('notificator', it.type))}
- label={t('notificationNotificators')}
- />
- <TextField
- value={message.subject}
- onChange={(e) => setMessage({ ...message, subject: e.target.value })}
- label={t('sharedSubject')}
- />
- <TextField
- value={message.body}
- onChange={(e) => setMessage({ ...message, body: e.target.value })}
- label={t('commandMessage')}
- />
- </AccordionDetails>
- </Accordion>
- <div className={classes.buttons}>
- <Button
- type="button"
- color="primary"
- variant="outlined"
- onClick={() => navigate(-1)}
- >
- {t('sharedCancel')}
- </Button>
- <Button
- type="button"
- color="primary"
- variant="contained"
- onClick={handleSend}
- disabled={!notificator || !message.subject || !message.body}
- >
- {t('commandSend')}
- </Button>
- </div>
- </Container>
- </PageLayout>
- );
-};
-
-export default AnnouncementPage;
diff --git a/modern/src/settings/CalendarPage.jsx b/modern/src/settings/CalendarPage.jsx
deleted file mode 100644
index 8a3dc986..00000000
--- a/modern/src/settings/CalendarPage.jsx
+++ /dev/null
@@ -1,208 +0,0 @@
-import dayjs from 'dayjs';
-import React, { useState } from 'react';
-import { useDispatch } from 'react-redux';
-import TextField from '@mui/material/TextField';
-import {
- Accordion, AccordionSummary, AccordionDetails, Typography, FormControl, InputLabel, Select, MenuItem,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import { DropzoneArea } from 'react-mui-dropzone';
-import EditItemView from './components/EditItemView';
-import EditAttributesAccordion from './components/EditAttributesAccordion';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import SettingsMenu from './components/SettingsMenu';
-import { prefixString } from '../common/util/stringUtils';
-import { calendarsActions } from '../store';
-import { useCatch } from '../reactHelper';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const formatCalendarTime = (time) => {
- const tzid = Intl.DateTimeFormat().resolvedOptions().timeZone;
- return `TZID=${tzid}:${time.locale('en').format('YYYYMMDDTHHmmss')}`;
-};
-
-const parseRule = (rule) => {
- if (rule.endsWith('COUNT=1')) {
- return { frequency: 'ONCE' };
- }
- const fragments = rule.split(';');
- const frequency = fragments[0].substring(11);
- const by = fragments.length > 1 ? fragments[1].split('=')[1].split(',') : null;
- return { frequency, by };
-};
-
-const formatRule = (rule) => {
- const by = rule.by && rule.by.join(',');
- switch (rule.frequency) {
- case 'DAILY':
- return `RRULE:FREQ=${rule.frequency}`;
- case 'WEEKLY':
- return `RRULE:FREQ=${rule.frequency};BYDAY=${by || 'SU'}`;
- case 'MONTHLY':
- return `RRULE:FREQ=${rule.frequency};BYMONTHDAY=${by || 1}`;
- default:
- return 'RRULE:FREQ=DAILY;COUNT=1';
- }
-};
-
-const updateCalendar = (lines, index, element) => window.btoa(lines.map((e, i) => (i !== index ? e : element)).join('\n'));
-
-const simpleCalendar = () => window.btoa([
- 'BEGIN:VCALENDAR',
- 'VERSION:2.0',
- 'PRODID:-//Traccar//NONSGML Traccar//EN',
- 'BEGIN:VEVENT',
- 'UID:00000000-0000-0000-0000-000000000000',
- `DTSTART;${formatCalendarTime(dayjs())}`,
- `DTEND;${formatCalendarTime(dayjs().add(1, 'hours'))}`,
- 'RRULE:FREQ=DAILY',
- 'SUMMARY:Event',
- 'END:VEVENT',
- 'END:VCALENDAR',
-].join('\n'));
-
-const CalendarPage = () => {
- const classes = useSettingsStyles();
- const dispatch = useDispatch();
- const t = useTranslation();
-
- const [item, setItem] = useState();
-
- const decoded = item && item.data && window.atob(item.data);
-
- const simple = decoded && decoded.indexOf('//Traccar//') > 0;
-
- const lines = decoded && decoded.split('\n');
-
- const rule = simple && parseRule(lines[7]);
-
- const handleFiles = (files) => {
- if (files.length > 0) {
- const reader = new FileReader();
- reader.onload = (event) => {
- const { result } = event.target;
- setItem({ ...item, data: result.substr(result.indexOf(',') + 1) });
- };
- reader.readAsDataURL(files[0]);
- }
- };
-
- const onItemSaved = useCatch(async () => {
- const response = await fetch('/api/calendars');
- if (response.ok) {
- dispatch(calendarsActions.refresh(await response.json()));
- } else {
- throw Error(await response.text());
- }
- });
-
- const validate = () => item && item.name && item.data;
-
- return (
- <EditItemView
- endpoint="calendars"
- item={item}
- setItem={setItem}
- defaultItem={{ data: simpleCalendar() }}
- validate={validate}
- onItemSaved={onItemSaved}
- menu={<SettingsMenu />}
- breadcrumbs={['settingsTitle', 'sharedCalendar']}
- >
- {item && (
- <>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedRequired')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- value={item.name || ''}
- onChange={(event) => setItem({ ...item, name: event.target.value })}
- label={t('sharedName')}
- />
- <FormControl>
- <InputLabel>{t('sharedType')}</InputLabel>
- <Select
- label={t('sharedType')}
- value={simple ? 'simple' : 'custom'}
- onChange={(e) => setItem({ ...item, data: (e.target.value === 'simple' ? simpleCalendar() : null) })}
- >
- <MenuItem value="simple">{t('calendarSimple')}</MenuItem>
- <MenuItem value="custom">{t('reportCustom')}</MenuItem>
- </Select>
- </FormControl>
- {simple ? (
- <>
- <TextField
- label={t('reportFrom')}
- type="datetime-local"
- value={dayjs(lines[5].slice(-15)).locale('en').format('YYYY-MM-DDTHH:mm')}
- onChange={(e) => {
- const time = formatCalendarTime(dayjs(e.target.value, 'YYYY-MM-DDTHH:mm'));
- setItem({ ...item, data: updateCalendar(lines, 5, `DTSTART;${time}`) });
- }}
- />
- <TextField
- label={t('reportTo')}
- type="datetime-local"
- value={dayjs(lines[6].slice(-15)).locale('en').format('YYYY-MM-DDTHH:mm')}
- onChange={(e) => {
- const time = formatCalendarTime(dayjs(e.target.value, 'YYYY-MM-DDTHH:mm'));
- setItem({ ...item, data: updateCalendar(lines, 6, `DTEND;${time}`) });
- }}
- />
- <FormControl>
- <InputLabel>{t('calendarRecurrence')}</InputLabel>
- <Select
- label={t('calendarRecurrence')}
- value={rule.frequency}
- onChange={(e) => setItem({ ...item, data: updateCalendar(lines, 7, formatRule({ frequency: e.target.value })) })}
- >
- {['ONCE', 'DAILY', 'WEEKLY', 'MONTHLY'].map((it) => (
- <MenuItem key={it} value={it}>{t(prefixString('calendar', it.toLowerCase()))}</MenuItem>
- ))}
- </Select>
- </FormControl>
- {['WEEKLY', 'MONTHLY'].includes(rule.frequency) && (
- <FormControl>
- <InputLabel>{t('calendarDays')}</InputLabel>
- <Select
- multiple
- label={t('calendarDays')}
- value={rule.by}
- onChange={(e) => setItem({ ...item, data: updateCalendar(lines, 7, formatRule({ ...rule, by: e.target.value })) })}
- >
- {rule.frequency === 'WEEKLY' ? ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'].map((it) => (
- <MenuItem key={it} value={it.substring(0, 2).toUpperCase()}>{t(prefixString('calendar', it))}</MenuItem>
- )) : Array.from({ length: 31 }, (_, i) => i + 1).map((it) => (
- <MenuItem key={it} value={it}>{it}</MenuItem>
- ))}
- </Select>
- </FormControl>
- )}
- </>
- ) : (
- <DropzoneArea
- dropzoneText={t('sharedDropzoneText')}
- filesLimit={1}
- onChange={handleFiles}
- showAlerts={false}
- />
- )}
- </AccordionDetails>
- </Accordion>
- <EditAttributesAccordion
- attributes={item.attributes}
- setAttributes={(attributes) => setItem({ ...item, attributes })}
- definitions={{}}
- />
- </>
- )}
- </EditItemView>
- );
-};
-
-export default CalendarPage;
diff --git a/modern/src/settings/CalendarsPage.jsx b/modern/src/settings/CalendarsPage.jsx
deleted file mode 100644
index de27a451..00000000
--- a/modern/src/settings/CalendarsPage.jsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import React, { useState } from 'react';
-import {
- Table, TableRow, TableCell, TableHead, TableBody,
-} from '@mui/material';
-import { useEffectAsync } from '../reactHelper';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import CollectionFab from './components/CollectionFab';
-import CollectionActions from './components/CollectionActions';
-import TableShimmer from '../common/components/TableShimmer';
-import SearchHeader, { filterByKeyword } from './components/SearchHeader';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const CalendarsPage = () => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const [timestamp, setTimestamp] = useState(Date.now());
- const [items, setItems] = useState([]);
- const [searchKeyword, setSearchKeyword] = useState('');
- const [loading, setLoading] = useState(false);
-
- useEffectAsync(async () => {
- setLoading(true);
- try {
- const response = await fetch('/api/calendars');
- if (response.ok) {
- setItems(await response.json());
- } else {
- throw Error(await response.text());
- }
- } finally {
- setLoading(false);
- }
- }, [timestamp]);
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'sharedCalendars']}>
- <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} />
- <Table className={classes.table}>
- <TableHead>
- <TableRow>
- <TableCell>{t('sharedName')}</TableCell>
- <TableCell className={classes.columnAction} />
- </TableRow>
- </TableHead>
- <TableBody>
- {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => (
- <TableRow key={item.id}>
- <TableCell>{item.name}</TableCell>
- <TableCell className={classes.columnAction} padding="none">
- <CollectionActions itemId={item.id} editPath="/settings/calendar" endpoint="calendars" setTimestamp={setTimestamp} />
- </TableCell>
- </TableRow>
- )) : (<TableShimmer columns={2} endAction />)}
- </TableBody>
- </Table>
- <CollectionFab editPath="/settings/calendar" />
- </PageLayout>
- );
-};
-
-export default CalendarsPage;
diff --git a/modern/src/settings/CommandDevicePage.jsx b/modern/src/settings/CommandDevicePage.jsx
deleted file mode 100644
index b3144cd0..00000000
--- a/modern/src/settings/CommandDevicePage.jsx
+++ /dev/null
@@ -1,111 +0,0 @@
-import React, { useState } from 'react';
-import { useNavigate, useParams } from 'react-router-dom';
-import {
- Accordion,
- AccordionSummary,
- AccordionDetails,
- Typography,
- Container,
- Button,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import BaseCommandView from './components/BaseCommandView';
-import SelectField from '../common/components/SelectField';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import { useCatch } from '../reactHelper';
-import { useRestriction } from '../common/util/permissions';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const CommandDevicePage = () => {
- const navigate = useNavigate();
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const { id } = useParams();
-
- const [savedId, setSavedId] = useState(0);
- const [item, setItem] = useState({});
-
- const limitCommands = useRestriction('limitCommands');
-
- const handleSend = useCatch(async () => {
- let command;
- if (savedId) {
- const response = await fetch(`/api/commands/${savedId}`);
- if (response.ok) {
- command = await response.json();
- } else {
- throw Error(await response.text());
- }
- } else {
- command = item;
- }
-
- command.deviceId = parseInt(id, 10);
-
- const response = await fetch('/api/commands/send', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(command),
- });
-
- if (response.ok) {
- navigate(-1);
- } else {
- throw Error(await response.text());
- }
- });
-
- const validate = () => savedId || (item && item.type);
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'deviceCommand']}>
- <Container maxWidth="xs" className={classes.container}>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedRequired')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <SelectField
- value={savedId}
- emptyValue={limitCommands ? null : 0}
- emptyTitle={t('sharedNew')}
- onChange={(e) => setSavedId(e.target.value)}
- endpoint={`/api/commands/send?deviceId=${id}`}
- titleGetter={(it) => it.description}
- label={t('sharedSavedCommand')}
- />
- {!limitCommands && !savedId && (
- <BaseCommandView deviceId={id} item={item} setItem={setItem} />
- )}
- </AccordionDetails>
- </Accordion>
- <div className={classes.buttons}>
- <Button
- type="button"
- color="primary"
- variant="outlined"
- onClick={() => navigate(-1)}
- >
- {t('sharedCancel')}
- </Button>
- <Button
- type="button"
- color="primary"
- variant="contained"
- onClick={handleSend}
- disabled={!validate()}
- >
- {t('commandSend')}
- </Button>
- </div>
- </Container>
- </PageLayout>
- );
-};
-
-export default CommandDevicePage;
diff --git a/modern/src/settings/CommandGroupPage.jsx b/modern/src/settings/CommandGroupPage.jsx
deleted file mode 100644
index e55a235d..00000000
--- a/modern/src/settings/CommandGroupPage.jsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import React, { useState } from 'react';
-import { useSelector } from 'react-redux';
-import { useNavigate, useParams } from 'react-router-dom';
-import {
- Accordion,
- AccordionSummary,
- AccordionDetails,
- Typography,
- Container,
- Button,
- FormControl,
- InputLabel,
- Select,
- MenuItem,
- FormControlLabel,
- Checkbox,
- TextField,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import { useCatch } from '../reactHelper';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const CommandDevicePage = () => {
- const navigate = useNavigate();
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const { id } = useParams();
-
- const textEnabled = useSelector((state) => state.session.server.textEnabled);
-
- const [item, setItem] = useState({ type: 'custom', attributes: {} });
-
- const handleSend = useCatch(async () => {
- const query = new URLSearchParams({ groupId: id });
- const response = await fetch(`/api/commands/send?${query.toString()}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(item),
- });
-
- if (response.ok) {
- navigate(-1);
- } else {
- throw Error(await response.text());
- }
- });
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'deviceCommand']}>
- <Container maxWidth="xs" className={classes.container}>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedRequired')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <FormControl fullWidth>
- <InputLabel>{t('sharedType')}</InputLabel>
- <Select label={t('sharedType')} value="custom" disabled>
- <MenuItem value="custom">{t('commandCustom')}</MenuItem>
- </Select>
- </FormControl>
- <TextField
- value={item.attributes.data}
- onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, data: e.target.value } })}
- label={t('commandData')}
- />
- {textEnabled && (
- <FormControlLabel
- control={<Checkbox checked={item.textChannel} onChange={(event) => setItem({ ...item, textChannel: event.target.checked })} />}
- label={t('commandSendSms')}
- />
- )}
- </AccordionDetails>
- </Accordion>
- <div className={classes.buttons}>
- <Button
- type="button"
- color="primary"
- variant="outlined"
- onClick={() => navigate(-1)}
- >
- {t('sharedCancel')}
- </Button>
- <Button
- type="button"
- color="primary"
- variant="contained"
- onClick={handleSend}
- disabled={!item.attributes.data}
- >
- {t('commandSend')}
- </Button>
- </div>
- </Container>
- </PageLayout>
- );
-};
-
-export default CommandDevicePage;
diff --git a/modern/src/settings/CommandPage.jsx b/modern/src/settings/CommandPage.jsx
deleted file mode 100644
index e65ecd76..00000000
--- a/modern/src/settings/CommandPage.jsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React, { useState } from 'react';
-import {
- Accordion, AccordionSummary, AccordionDetails, Typography, TextField,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import EditItemView from './components/EditItemView';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import BaseCommandView from './components/BaseCommandView';
-import SettingsMenu from './components/SettingsMenu';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const CommandPage = () => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const [item, setItem] = useState();
-
- const validate = () => item && item.type;
-
- return (
- <EditItemView
- endpoint="commands"
- item={item}
- setItem={setItem}
- validate={validate}
- menu={<SettingsMenu />}
- breadcrumbs={['settingsTitle', 'sharedSavedCommand']}
- >
- {item && (
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedRequired')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- value={item.description || ''}
- onChange={(event) => setItem({ ...item, description: event.target.value })}
- label={t('sharedDescription')}
- />
- <BaseCommandView item={item} setItem={setItem} />
- </AccordionDetails>
- </Accordion>
- )}
- </EditItemView>
- );
-};
-
-export default CommandPage;
diff --git a/modern/src/settings/CommandsPage.jsx b/modern/src/settings/CommandsPage.jsx
deleted file mode 100644
index 1b893831..00000000
--- a/modern/src/settings/CommandsPage.jsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import React, { useState } from 'react';
-import {
- Table, TableRow, TableCell, TableHead, TableBody,
-} from '@mui/material';
-import { useEffectAsync } from '../reactHelper';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import { formatBoolean } from '../common/util/formatter';
-import { prefixString } from '../common/util/stringUtils';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import CollectionFab from './components/CollectionFab';
-import CollectionActions from './components/CollectionActions';
-import TableShimmer from '../common/components/TableShimmer';
-import SearchHeader, { filterByKeyword } from './components/SearchHeader';
-import { useRestriction } from '../common/util/permissions';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const CommandsPage = () => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const [timestamp, setTimestamp] = useState(Date.now());
- const [items, setItems] = useState([]);
- const [searchKeyword, setSearchKeyword] = useState('');
- const [loading, setLoading] = useState(false);
- const limitCommands = useRestriction('limitCommands');
-
- useEffectAsync(async () => {
- setLoading(true);
- try {
- const response = await fetch('/api/commands');
- if (response.ok) {
- setItems(await response.json());
- } else {
- throw Error(await response.text());
- }
- } finally {
- setLoading(false);
- }
- }, [timestamp]);
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'sharedSavedCommands']}>
- <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} />
- <Table className={classes.table}>
- <TableHead>
- <TableRow>
- <TableCell>{t('sharedDescription')}</TableCell>
- <TableCell>{t('sharedType')}</TableCell>
- <TableCell>{t('commandSendSms')}</TableCell>
- {!limitCommands && <TableCell className={classes.columnAction} />}
- </TableRow>
- </TableHead>
- <TableBody>
- {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => (
- <TableRow key={item.id}>
- <TableCell>{item.description}</TableCell>
- <TableCell>{t(prefixString('command', item.type))}</TableCell>
- <TableCell>{formatBoolean(item.textChannel, t)}</TableCell>
- {!limitCommands && (
- <TableCell className={classes.columnAction} padding="none">
- <CollectionActions itemId={item.id} editPath="/settings/command" endpoint="commands" setTimestamp={setTimestamp} />
- </TableCell>
- )}
- </TableRow>
- )) : (<TableShimmer columns={limitCommands ? 3 : 4} endAction />)}
- </TableBody>
- </Table>
- <CollectionFab editPath="/settings/command" disabled={limitCommands} />
- </PageLayout>
- );
-};
-
-export default CommandsPage;
diff --git a/modern/src/settings/ComputedAttributePage.jsx b/modern/src/settings/ComputedAttributePage.jsx
deleted file mode 100644
index 1b19033c..00000000
--- a/modern/src/settings/ComputedAttributePage.jsx
+++ /dev/null
@@ -1,177 +0,0 @@
-import React, { useState } from 'react';
-import {
- Accordion,
- AccordionSummary,
- AccordionDetails,
- Typography,
- FormControl,
- InputLabel,
- MenuItem,
- Select,
- TextField,
- createFilterOptions,
- Autocomplete,
- Button,
- Snackbar,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import EditItemView from './components/EditItemView';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import usePositionAttributes from '../common/attributes/usePositionAttributes';
-import SettingsMenu from './components/SettingsMenu';
-import SelectField from '../common/components/SelectField';
-import { useCatch } from '../reactHelper';
-import { snackBarDurationLongMs } from '../common/util/duration';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const allowedProperties = ['valid', 'latitude', 'longitude', 'altitude', 'speed', 'course', 'address', 'accuracy'];
-
-const ComputedAttributePage = () => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const positionAttributes = usePositionAttributes(t);
-
- const [item, setItem] = useState();
- const [deviceId, setDeviceId] = useState();
- const [result, setResult] = useState();
-
- const options = Object.entries(positionAttributes).filter(([key, value]) => !value.property || allowedProperties.includes(key)).map(([key, value]) => ({
- key,
- name: value.name,
- type: value.type,
- }));
-
- const filter = createFilterOptions({
- stringify: (option) => option.name,
- });
-
- const testAttribute = useCatch(async () => {
- const query = new URLSearchParams({ deviceId });
- const url = `/api/attributes/computed/test?${query.toString()}`;
- const response = await fetch(url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(item),
- });
- if (response.ok) {
- setResult(await response.text());
- } else {
- throw Error(await response.text());
- }
- });
-
- const validate = () => item && item.description && item.expression;
-
- return (
- <EditItemView
- endpoint="attributes/computed"
- item={item}
- setItem={setItem}
- validate={validate}
- menu={<SettingsMenu />}
- breadcrumbs={['settingsTitle', 'sharedComputedAttribute']}
- >
- {item && (
- <>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedRequired')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- value={item.description || ''}
- onChange={(e) => setItem({ ...item, description: e.target.value })}
- label={t('sharedDescription')}
- />
- <Autocomplete
- value={options.find((option) => option.key === item.attribute) || item.attribute}
- onChange={(_, option) => {
- const attribute = option ? option.key || option : null;
- if (option && option.type) {
- setItem({ ...item, attribute, type: option.type });
- } else {
- setItem({ ...item, attribute });
- }
- }}
- filterOptions={(options, params) => {
- const filtered = filter(options, params);
- if (params.inputValue) {
- filtered.push({
- key: params.inputValue,
- name: params.inputValue,
- });
- }
- return filtered;
- }}
- options={options}
- getOptionLabel={(option) => option.name || option}
- renderOption={(props, option) => (
- <li {...props}>
- {option.name}
- </li>
- )}
- renderInput={(params) => (
- <TextField {...params} label={t('sharedAttribute')} />
- )}
- freeSolo
- />
- <TextField
- value={item.expression || ''}
- onChange={(e) => setItem({ ...item, expression: e.target.value })}
- label={t('sharedExpression')}
- multiline
- rows={4}
- />
- <FormControl disabled={item.attribute in positionAttributes}>
- <InputLabel>{t('sharedType')}</InputLabel>
- <Select
- label={t('sharedType')}
- value={item.type || ''}
- onChange={(e) => setItem({ ...item, type: e.target.value })}
- >
- <MenuItem value="string">{t('sharedTypeString')}</MenuItem>
- <MenuItem value="number">{t('sharedTypeNumber')}</MenuItem>
- <MenuItem value="boolean">{t('sharedTypeBoolean')}</MenuItem>
- </Select>
- </FormControl>
- </AccordionDetails>
- </Accordion>
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedTest')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <SelectField
- value={deviceId}
- onChange={(e) => setDeviceId(Number(e.target.value))}
- endpoint="/api/devices"
- label={t('sharedDevice')}
- />
- <Button
- variant="outlined"
- color="primary"
- onClick={testAttribute}
- disabled={!deviceId}
- >
- {t('sharedTestExpression')}
- </Button>
- <Snackbar
- open={!!result}
- onClose={() => setResult(null)}
- autoHideDuration={snackBarDurationLongMs}
- message={result}
- />
- </AccordionDetails>
- </Accordion>
- </>
- )}
- </EditItemView>
- );
-};
-
-export default ComputedAttributePage;
diff --git a/modern/src/settings/ComputedAttributesPage.jsx b/modern/src/settings/ComputedAttributesPage.jsx
deleted file mode 100644
index 6d098547..00000000
--- a/modern/src/settings/ComputedAttributesPage.jsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import React, { useState } from 'react';
-import {
- Table, TableRow, TableCell, TableHead, TableBody,
-} from '@mui/material';
-import { useEffectAsync } from '../reactHelper';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import { useAdministrator } from '../common/util/permissions';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import CollectionFab from './components/CollectionFab';
-import CollectionActions from './components/CollectionActions';
-import TableShimmer from '../common/components/TableShimmer';
-import SearchHeader, { filterByKeyword } from './components/SearchHeader';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const ComputedAttributesPage = () => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const [timestamp, setTimestamp] = useState(Date.now());
- const [items, setItems] = useState([]);
- const [searchKeyword, setSearchKeyword] = useState('');
- const [loading, setLoading] = useState(false);
- const administrator = useAdministrator();
-
- useEffectAsync(async () => {
- setLoading(true);
- try {
- const response = await fetch('/api/attributes/computed');
- if (response.ok) {
- setItems(await response.json());
- } else {
- throw Error(await response.text());
- }
- } finally {
- setLoading(false);
- }
- }, [timestamp]);
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'sharedComputedAttributes']}>
- <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} />
- <Table className={classes.table}>
- <TableHead>
- <TableRow>
- <TableCell>{t('sharedDescription')}</TableCell>
- <TableCell>{t('sharedAttribute')}</TableCell>
- <TableCell>{t('sharedExpression')}</TableCell>
- <TableCell>{t('sharedType')}</TableCell>
- {administrator && <TableCell className={classes.columnAction} />}
- </TableRow>
- </TableHead>
- <TableBody>
- {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => (
- <TableRow key={item.id}>
- <TableCell>{item.description}</TableCell>
- <TableCell>{item.attribute}</TableCell>
- <TableCell>{item.expression}</TableCell>
- <TableCell>{item.type}</TableCell>
- {administrator && (
- <TableCell className={classes.columnAction} padding="none">
- <CollectionActions itemId={item.id} editPath="/settings/attribute" endpoint="attributes/computed" setTimestamp={setTimestamp} />
- </TableCell>
- )}
- </TableRow>
- )) : (<TableShimmer columns={administrator ? 5 : 4} endAction={administrator} />)}
- </TableBody>
- </Table>
- <CollectionFab editPath="/settings/attribute" disabled={!administrator} />
- </PageLayout>
- );
-};
-
-export default ComputedAttributesPage;
diff --git a/modern/src/settings/DeviceConnectionsPage.jsx b/modern/src/settings/DeviceConnectionsPage.jsx
deleted file mode 100644
index c711d719..00000000
--- a/modern/src/settings/DeviceConnectionsPage.jsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import React from 'react';
-import { useParams } from 'react-router-dom';
-import {
- Accordion,
- AccordionSummary,
- AccordionDetails,
- Typography,
- Container,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import LinkField from '../common/components/LinkField';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import SettingsMenu from './components/SettingsMenu';
-import { formatNotificationTitle } from '../common/util/formatter';
-import PageLayout from '../common/components/PageLayout';
-import useFeatures from '../common/util/useFeatures';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const DeviceConnectionsPage = () => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const { id } = useParams();
-
- const features = useFeatures();
-
- return (
- <PageLayout
- menu={<SettingsMenu />}
- breadcrumbs={['settingsTitle', 'sharedDevice', 'sharedConnections']}
- >
- <Container maxWidth="xs" className={classes.container}>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedConnections')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <LinkField
- endpointAll="/api/geofences"
- endpointLinked={`/api/geofences?deviceId=${id}`}
- baseId={id}
- keyBase="deviceId"
- keyLink="geofenceId"
- label={t('sharedGeofences')}
- />
- <LinkField
- endpointAll="/api/notifications"
- endpointLinked={`/api/notifications?deviceId=${id}`}
- baseId={id}
- keyBase="deviceId"
- keyLink="notificationId"
- titleGetter={(it) => formatNotificationTitle(t, it)}
- label={t('sharedNotifications')}
- />
- {!features.disableDrivers && (
- <LinkField
- endpointAll="/api/drivers"
- endpointLinked={`/api/drivers?deviceId=${id}`}
- baseId={id}
- keyBase="deviceId"
- keyLink="driverId"
- titleGetter={(it) => `${it.name} (${it.uniqueId})`}
- label={t('sharedDrivers')}
- />
- )}
- {!features.disableComputedAttributes && (
- <LinkField
- endpointAll="/api/attributes/computed"
- endpointLinked={`/api/attributes/computed?deviceId=${id}`}
- baseId={id}
- keyBase="deviceId"
- keyLink="attributeId"
- titleGetter={(it) => it.description}
- label={t('sharedComputedAttributes')}
- />
- )}
- {!features.disableSavedCommands && (
- <LinkField
- endpointAll="/api/commands"
- endpointLinked={`/api/commands?deviceId=${id}`}
- baseId={id}
- keyBase="deviceId"
- keyLink="commandId"
- titleGetter={(it) => it.description}
- label={t('sharedSavedCommands')}
- />
- )}
- {!features.disableMaintenance && (
- <LinkField
- endpointAll="/api/maintenance"
- endpointLinked={`/api/maintenance?deviceId=${id}`}
- baseId={id}
- keyBase="deviceId"
- keyLink="maintenanceId"
- label={t('sharedMaintenance')}
- />
- )}
- </AccordionDetails>
- </Accordion>
- </Container>
- </PageLayout>
- );
-};
-
-export default DeviceConnectionsPage;
diff --git a/modern/src/settings/DevicePage.jsx b/modern/src/settings/DevicePage.jsx
deleted file mode 100644
index 8933a210..00000000
--- a/modern/src/settings/DevicePage.jsx
+++ /dev/null
@@ -1,176 +0,0 @@
-import React, { useState } from 'react';
-import dayjs from 'dayjs';
-import {
- Accordion,
- AccordionSummary,
- AccordionDetails,
- Typography,
- FormControlLabel,
- Checkbox,
- TextField,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import { DropzoneArea } from 'react-mui-dropzone';
-import EditItemView from './components/EditItemView';
-import EditAttributesAccordion from './components/EditAttributesAccordion';
-import SelectField from '../common/components/SelectField';
-import deviceCategories from '../common/util/deviceCategories';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import useDeviceAttributes from '../common/attributes/useDeviceAttributes';
-import { useAdministrator } from '../common/util/permissions';
-import SettingsMenu from './components/SettingsMenu';
-import useCommonDeviceAttributes from '../common/attributes/useCommonDeviceAttributes';
-import { useCatch } from '../reactHelper';
-import useQuery from '../common/util/useQuery';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const DevicePage = () => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const admin = useAdministrator();
-
- const commonDeviceAttributes = useCommonDeviceAttributes(t);
- const deviceAttributes = useDeviceAttributes(t);
-
- const query = useQuery();
- const uniqueId = query.get('uniqueId');
-
- const [item, setItem] = useState(uniqueId ? { uniqueId } : null);
-
- const handleFiles = useCatch(async (files) => {
- if (files.length > 0) {
- const response = await fetch(`/api/devices/${item.id}/image`, {
- method: 'POST',
- body: files[0],
- });
- if (response.ok) {
- setItem({ ...item, attributes: { ...item.attributes, deviceImage: await response.text() } });
- } else {
- throw Error(await response.text());
- }
- }
- });
-
- const validate = () => item && item.name && item.uniqueId;
-
- return (
- <EditItemView
- endpoint="devices"
- item={item}
- setItem={setItem}
- validate={validate}
- menu={<SettingsMenu />}
- breadcrumbs={['settingsTitle', 'sharedDevice']}
- >
- {item && (
- <>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedRequired')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- value={item.name || ''}
- onChange={(event) => setItem({ ...item, name: event.target.value })}
- label={t('sharedName')}
- />
- <TextField
- value={item.uniqueId || ''}
- onChange={(event) => setItem({ ...item, uniqueId: event.target.value })}
- label={t('deviceIdentifier')}
- helperText={t('deviceIdentifierHelp')}
- disabled={Boolean(uniqueId)}
- />
- </AccordionDetails>
- </Accordion>
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedExtra')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <SelectField
- value={item.groupId}
- onChange={(event) => setItem({ ...item, groupId: Number(event.target.value) })}
- endpoint="/api/groups"
- label={t('groupParent')}
- />
- <TextField
- value={item.phone || ''}
- onChange={(event) => setItem({ ...item, phone: event.target.value })}
- label={t('sharedPhone')}
- />
- <TextField
- value={item.model || ''}
- onChange={(event) => setItem({ ...item, model: event.target.value })}
- label={t('deviceModel')}
- />
- <TextField
- value={item.contact || ''}
- onChange={(event) => setItem({ ...item, contact: event.target.value })}
- label={t('deviceContact')}
- />
- <SelectField
- value={item.category || 'default'}
- onChange={(event) => setItem({ ...item, category: event.target.value })}
- data={deviceCategories.map((category) => ({
- id: category,
- name: t(`category${category.replace(/^\w/, (c) => c.toUpperCase())}`),
- }))}
- label={t('deviceCategory')}
- />
- <SelectField
- value={item.calendarId}
- onChange={(event) => setItem({ ...item, calendarId: Number(event.target.value) })}
- endpoint="/api/calendars"
- label={t('sharedCalendar')}
- />
- <TextField
- label={t('userExpirationTime')}
- type="date"
- value={(item.expirationTime && dayjs(item.expirationTime).locale('en').format('YYYY-MM-DD')) || '2099-01-01'}
- onChange={(e) => setItem({ ...item, expirationTime: dayjs(e.target.value, 'YYYY-MM-DD').locale('en').format() })}
- disabled={!admin}
- />
- <FormControlLabel
- control={<Checkbox checked={item.disabled} onChange={(event) => setItem({ ...item, disabled: event.target.checked })} />}
- label={t('sharedDisabled')}
- disabled={!admin}
- />
- </AccordionDetails>
- </Accordion>
- {item.id && (
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('attributeDeviceImage')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <DropzoneArea
- dropzoneText={t('sharedDropzoneText')}
- acceptedFiles={['image/*']}
- filesLimit={1}
- onChange={handleFiles}
- showAlerts={false}
- maxFileSize={500000}
- />
- </AccordionDetails>
- </Accordion>
- )}
- <EditAttributesAccordion
- attributes={item.attributes}
- setAttributes={(attributes) => setItem({ ...item, attributes })}
- definitions={{ ...commonDeviceAttributes, ...deviceAttributes }}
- />
- </>
- )}
- </EditItemView>
- );
-};
-
-export default DevicePage;
diff --git a/modern/src/settings/DevicesPage.jsx b/modern/src/settings/DevicesPage.jsx
deleted file mode 100644
index c0da0ba7..00000000
--- a/modern/src/settings/DevicesPage.jsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import React, { useState } from 'react';
-import { useSelector } from 'react-redux';
-import { useNavigate } from 'react-router-dom';
-import {
- Table, TableRow, TableCell, TableHead, TableBody, Button, TableFooter,
-} from '@mui/material';
-import LinkIcon from '@mui/icons-material/Link';
-import { useEffectAsync } from '../reactHelper';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import CollectionFab from './components/CollectionFab';
-import CollectionActions from './components/CollectionActions';
-import TableShimmer from '../common/components/TableShimmer';
-import SearchHeader, { filterByKeyword } from './components/SearchHeader';
-import { usePreference } from '../common/util/preferences';
-import { formatTime } from '../common/util/formatter';
-import { useDeviceReadonly } from '../common/util/permissions';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const DevicesPage = () => {
- const classes = useSettingsStyles();
- const navigate = useNavigate();
- const t = useTranslation();
-
- const groups = useSelector((state) => state.groups.items);
-
- const hours12 = usePreference('twelveHourFormat');
-
- const deviceReadonly = useDeviceReadonly();
-
- const [timestamp, setTimestamp] = useState(Date.now());
- const [items, setItems] = useState([]);
- const [searchKeyword, setSearchKeyword] = useState('');
- const [loading, setLoading] = useState(false);
-
- useEffectAsync(async () => {
- setLoading(true);
- try {
- const response = await fetch('/api/devices');
- if (response.ok) {
- setItems(await response.json());
- } else {
- throw Error(await response.text());
- }
- } finally {
- setLoading(false);
- }
- }, [timestamp]);
-
- const handleExport = () => {
- window.location.assign('/api/reports/devices/xlsx');
- };
-
- const actionConnections = {
- key: 'connections',
- title: t('sharedConnections'),
- icon: <LinkIcon fontSize="small" />,
- handler: (deviceId) => navigate(`/settings/device/${deviceId}/connections`),
- };
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'deviceTitle']}>
- <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} />
- <Table className={classes.table}>
- <TableHead>
- <TableRow>
- <TableCell>{t('sharedName')}</TableCell>
- <TableCell>{t('deviceIdentifier')}</TableCell>
- <TableCell>{t('groupParent')}</TableCell>
- <TableCell>{t('sharedPhone')}</TableCell>
- <TableCell>{t('deviceModel')}</TableCell>
- <TableCell>{t('deviceContact')}</TableCell>
- <TableCell>{t('userExpirationTime')}</TableCell>
- <TableCell className={classes.columnAction} />
- </TableRow>
- </TableHead>
- <TableBody>
- {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => (
- <TableRow key={item.id}>
- <TableCell>{item.name}</TableCell>
- <TableCell>{item.uniqueId}</TableCell>
- <TableCell>{item.groupId ? groups[item.groupId]?.name : null}</TableCell>
- <TableCell>{item.phone}</TableCell>
- <TableCell>{item.model}</TableCell>
- <TableCell>{item.contact}</TableCell>
- <TableCell>{formatTime(item.expirationTime, 'date', hours12)}</TableCell>
- <TableCell className={classes.columnAction} padding="none">
- <CollectionActions
- itemId={item.id}
- editPath="/settings/device"
- endpoint="devices"
- setTimestamp={setTimestamp}
- customActions={[actionConnections]}
- readonly={deviceReadonly}
- />
- </TableCell>
- </TableRow>
- )) : (<TableShimmer columns={7} endAction />)}
- </TableBody>
- <TableFooter>
- <TableRow>
- <TableCell colSpan={8} align="right">
- <Button onClick={handleExport} variant="text">{t('reportExport')}</Button>
- </TableCell>
- </TableRow>
- </TableFooter>
- </Table>
- <CollectionFab editPath="/settings/device" />
- </PageLayout>
- );
-};
-
-export default DevicesPage;
diff --git a/modern/src/settings/DriverPage.jsx b/modern/src/settings/DriverPage.jsx
deleted file mode 100644
index 5f70a44a..00000000
--- a/modern/src/settings/DriverPage.jsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React, { useState } from 'react';
-import TextField from '@mui/material/TextField';
-import {
- Accordion, AccordionSummary, AccordionDetails, Typography,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import EditItemView from './components/EditItemView';
-import EditAttributesAccordion from './components/EditAttributesAccordion';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import SettingsMenu from './components/SettingsMenu';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const DriverPage = () => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const [item, setItem] = useState();
-
- const validate = () => item && item.name && item.uniqueId;
-
- return (
- <EditItemView
- endpoint="drivers"
- item={item}
- setItem={setItem}
- validate={validate}
- menu={<SettingsMenu />}
- breadcrumbs={['settingsTitle', 'sharedDriver']}
- >
- {item && (
- <>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedRequired')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- value={item.name || ''}
- onChange={(event) => setItem({ ...item, name: event.target.value })}
- label={t('sharedName')}
- />
- <TextField
- value={item.uniqueId || ''}
- onChange={(event) => setItem({ ...item, uniqueId: event.target.value })}
- label={t('deviceIdentifier')}
- />
- </AccordionDetails>
- </Accordion>
- <EditAttributesAccordion
- attributes={item.attributes}
- setAttributes={(attributes) => setItem({ ...item, attributes })}
- definitions={{}}
- />
- </>
- )}
- </EditItemView>
- );
-};
-
-export default DriverPage;
diff --git a/modern/src/settings/DriversPage.jsx b/modern/src/settings/DriversPage.jsx
deleted file mode 100644
index 72834860..00000000
--- a/modern/src/settings/DriversPage.jsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import React, { useState } from 'react';
-import {
- Table, TableRow, TableCell, TableHead, TableBody,
-} from '@mui/material';
-import { useEffectAsync } from '../reactHelper';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import CollectionFab from './components/CollectionFab';
-import CollectionActions from './components/CollectionActions';
-import TableShimmer from '../common/components/TableShimmer';
-import SearchHeader, { filterByKeyword } from './components/SearchHeader';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const DriversPage = () => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const [timestamp, setTimestamp] = useState(Date.now());
- const [items, setItems] = useState([]);
- const [searchKeyword, setSearchKeyword] = useState('');
- const [loading, setLoading] = useState(false);
-
- useEffectAsync(async () => {
- setLoading(true);
- try {
- const response = await fetch('/api/drivers');
- if (response.ok) {
- setItems(await response.json());
- } else {
- throw Error(await response.text());
- }
- } finally {
- setLoading(false);
- }
- }, [timestamp]);
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'sharedDrivers']}>
- <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} />
- <Table className={classes.table}>
- <TableHead>
- <TableRow>
- <TableCell>{t('sharedName')}</TableCell>
- <TableCell>{t('deviceIdentifier')}</TableCell>
- <TableCell className={classes.columnAction} />
- </TableRow>
- </TableHead>
- <TableBody>
- {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => (
- <TableRow key={item.id}>
- <TableCell>{item.name}</TableCell>
- <TableCell>{item.uniqueId}</TableCell>
- <TableCell className={classes.columnAction} padding="none">
- <CollectionActions itemId={item.id} editPath="/settings/driver" endpoint="drivers" setTimestamp={setTimestamp} />
- </TableCell>
- </TableRow>
- )) : (<TableShimmer columns={3} endAction />)}
- </TableBody>
- </Table>
- <CollectionFab editPath="/settings/driver" />
- </PageLayout>
- );
-};
-
-export default DriversPage;
diff --git a/modern/src/settings/GeofencePage.jsx b/modern/src/settings/GeofencePage.jsx
deleted file mode 100644
index c3c96ef8..00000000
--- a/modern/src/settings/GeofencePage.jsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import React, { useState } from 'react';
-import { useDispatch } from 'react-redux';
-import {
- Accordion, AccordionSummary, AccordionDetails, Typography, TextField,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import EditItemView from './components/EditItemView';
-import EditAttributesAccordion from './components/EditAttributesAccordion';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import useGeofenceAttributes from '../common/attributes/useGeofenceAttributes';
-import SettingsMenu from './components/SettingsMenu';
-import SelectField from '../common/components/SelectField';
-import { geofencesActions } from '../store';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const GeofencePage = () => {
- const classes = useSettingsStyles();
- const dispatch = useDispatch();
- const t = useTranslation();
-
- const geofenceAttributes = useGeofenceAttributes(t);
-
- const [item, setItem] = useState();
-
- const onItemSaved = (result) => {
- dispatch(geofencesActions.update([result]));
- };
-
- const validate = () => item && item.name;
-
- return (
- <EditItemView
- endpoint="geofences"
- item={item}
- setItem={setItem}
- validate={validate}
- onItemSaved={onItemSaved}
- menu={<SettingsMenu />}
- breadcrumbs={['settingsTitle', 'sharedGeofence']}
- >
- {item && (
- <>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedRequired')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- value={item.name || ''}
- onChange={(event) => setItem({ ...item, name: event.target.value })}
- label={t('sharedName')}
- />
- </AccordionDetails>
- </Accordion>
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedExtra')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- value={item.description || ''}
- onChange={(event) => setItem({ ...item, description: event.target.value })}
- label={t('sharedDescription')}
- />
- <SelectField
- value={item.calendarId}
- onChange={(event) => setItem({ ...item, calendarId: Number(event.target.value) })}
- endpoint="/api/calendars"
- label={t('sharedCalendar')}
- />
- </AccordionDetails>
- </Accordion>
- <EditAttributesAccordion
- attributes={item.attributes}
- setAttributes={(attributes) => setItem({ ...item, attributes })}
- definitions={geofenceAttributes}
- />
- </>
- )}
- </EditItemView>
- );
-};
-
-export default GeofencePage;
diff --git a/modern/src/settings/GroupConnectionsPage.jsx b/modern/src/settings/GroupConnectionsPage.jsx
deleted file mode 100644
index 980bd9da..00000000
--- a/modern/src/settings/GroupConnectionsPage.jsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import React from 'react';
-import { useParams } from 'react-router-dom';
-import {
- Accordion,
- AccordionSummary,
- AccordionDetails,
- Typography,
- Container,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import LinkField from '../common/components/LinkField';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import SettingsMenu from './components/SettingsMenu';
-import { formatNotificationTitle } from '../common/util/formatter';
-import PageLayout from '../common/components/PageLayout';
-import useFeatures from '../common/util/useFeatures';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const GroupConnectionsPage = () => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const { id } = useParams();
-
- const features = useFeatures();
-
- return (
- <PageLayout
- menu={<SettingsMenu />}
- breadcrumbs={['settingsTitle', 'groupDialog', 'sharedConnections']}
- >
- <Container maxWidth="xs" className={classes.container}>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedConnections')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <LinkField
- endpointAll="/api/geofences"
- endpointLinked={`/api/geofences?groupId=${id}`}
- baseId={id}
- keyBase="groupId"
- keyLink="geofenceId"
- label={t('sharedGeofences')}
- />
- <LinkField
- endpointAll="/api/notifications"
- endpointLinked={`/api/notifications?groupId=${id}`}
- baseId={id}
- keyBase="groupId"
- keyLink="notificationId"
- titleGetter={(it) => formatNotificationTitle(t, it)}
- label={t('sharedNotifications')}
- />
- {!features.disableDrivers && (
- <LinkField
- endpointAll="/api/drivers"
- endpointLinked={`/api/drivers?groupId=${id}`}
- baseId={id}
- keyBase="groupId"
- keyLink="driverId"
- titleGetter={(it) => `${it.name} (${it.uniqueId})`}
- label={t('sharedDrivers')}
- />
- )}
- {!features.disableComputedAttributes && (
- <LinkField
- endpointAll="/api/attributes/computed"
- endpointLinked={`/api/attributes/computed?groupId=${id}`}
- baseId={id}
- keyBase="groupId"
- keyLink="attributeId"
- titleGetter={(it) => it.description}
- label={t('sharedComputedAttributes')}
- />
- )}
- {!features.disableSavedCommands && (
- <LinkField
- endpointAll="/api/commands"
- endpointLinked={`/api/commands?groupId=${id}`}
- baseId={id}
- keyBase="groupId"
- keyLink="commandId"
- titleGetter={(it) => it.description}
- label={t('sharedSavedCommands')}
- />
- )}
- {!features.disableMaintenance && (
- <LinkField
- endpointAll="/api/maintenance"
- endpointLinked={`/api/maintenance?groupId=${id}`}
- baseId={id}
- keyBase="groupId"
- keyLink="maintenanceId"
- label={t('sharedMaintenance')}
- />
- )}
- </AccordionDetails>
- </Accordion>
- </Container>
- </PageLayout>
- );
-};
-
-export default GroupConnectionsPage;
diff --git a/modern/src/settings/GroupPage.jsx b/modern/src/settings/GroupPage.jsx
deleted file mode 100644
index ba1cbc76..00000000
--- a/modern/src/settings/GroupPage.jsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import React, { useState } from 'react';
-import { useDispatch } from 'react-redux';
-import TextField from '@mui/material/TextField';
-
-import {
- Accordion, AccordionSummary, AccordionDetails, Typography,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import EditItemView from './components/EditItemView';
-import EditAttributesAccordion from './components/EditAttributesAccordion';
-import SelectField from '../common/components/SelectField';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import SettingsMenu from './components/SettingsMenu';
-import useCommonDeviceAttributes from '../common/attributes/useCommonDeviceAttributes';
-import useGroupAttributes from '../common/attributes/useGroupAttributes';
-import { useCatch } from '../reactHelper';
-import { groupsActions } from '../store';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const GroupPage = () => {
- const classes = useSettingsStyles();
- const dispatch = useDispatch();
- const t = useTranslation();
-
- const commonDeviceAttributes = useCommonDeviceAttributes(t);
- const groupAttributes = useGroupAttributes(t);
-
- const [item, setItem] = useState();
-
- const onItemSaved = useCatch(async () => {
- const response = await fetch('/api/groups');
- if (response.ok) {
- dispatch(groupsActions.refresh(await response.json()));
- } else {
- throw Error(await response.text());
- }
- });
-
- const validate = () => item && item.name;
-
- return (
- <EditItemView
- endpoint="groups"
- item={item}
- setItem={setItem}
- validate={validate}
- onItemSaved={onItemSaved}
- menu={<SettingsMenu />}
- breadcrumbs={['settingsTitle', 'groupDialog']}
- >
- {item && (
- <>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedRequired')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- value={item.name || ''}
- onChange={(event) => setItem({ ...item, name: event.target.value })}
- label={t('sharedName')}
- />
- </AccordionDetails>
- </Accordion>
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedExtra')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <SelectField
- value={item.groupId}
- onChange={(event) => setItem({ ...item, groupId: Number(event.target.value) })}
- endpoint="/api/groups"
- label={t('groupParent')}
- />
- </AccordionDetails>
- </Accordion>
- <EditAttributesAccordion
- attributes={item.attributes}
- setAttributes={(attributes) => setItem({ ...item, attributes })}
- definitions={{ ...commonDeviceAttributes, ...groupAttributes }}
- />
- </>
- )}
- </EditItemView>
- );
-};
-
-export default GroupPage;
diff --git a/modern/src/settings/GroupsPage.jsx b/modern/src/settings/GroupsPage.jsx
deleted file mode 100644
index baba7f72..00000000
--- a/modern/src/settings/GroupsPage.jsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import React, { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-import {
- Table, TableRow, TableCell, TableHead, TableBody,
-} from '@mui/material';
-import LinkIcon from '@mui/icons-material/Link';
-import PublishIcon from '@mui/icons-material/Publish';
-import { useEffectAsync } from '../reactHelper';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import CollectionFab from './components/CollectionFab';
-import CollectionActions from './components/CollectionActions';
-import TableShimmer from '../common/components/TableShimmer';
-import SearchHeader, { filterByKeyword } from './components/SearchHeader';
-import { useRestriction } from '../common/util/permissions';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const GroupsPage = () => {
- const classes = useSettingsStyles();
- const navigate = useNavigate();
- const t = useTranslation();
-
- const limitCommands = useRestriction('limitCommands');
-
- const [timestamp, setTimestamp] = useState(Date.now());
- const [items, setItems] = useState([]);
- const [searchKeyword, setSearchKeyword] = useState('');
- const [loading, setLoading] = useState(false);
-
- useEffectAsync(async () => {
- setLoading(true);
- try {
- const response = await fetch('/api/groups');
- if (response.ok) {
- setItems(await response.json());
- } else {
- throw Error(await response.text());
- }
- } finally {
- setLoading(false);
- }
- }, [timestamp]);
-
- const actionCommand = {
- key: 'command',
- title: t('deviceCommand'),
- icon: <PublishIcon fontSize="small" />,
- handler: (groupId) => navigate(`/settings/group/${groupId}/command`),
- };
-
- const actionConnections = {
- key: 'connections',
- title: t('sharedConnections'),
- icon: <LinkIcon fontSize="small" />,
- handler: (groupId) => navigate(`/settings/group/${groupId}/connections`),
- };
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'settingsGroups']}>
- <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} />
- <Table className={classes.table}>
- <TableHead>
- <TableRow>
- <TableCell>{t('sharedName')}</TableCell>
- <TableCell className={classes.columnAction} />
- </TableRow>
- </TableHead>
- <TableBody>
- {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => (
- <TableRow key={item.id}>
- <TableCell>{item.name}</TableCell>
- <TableCell className={classes.columnAction} padding="none">
- <CollectionActions
- itemId={item.id}
- editPath="/settings/group"
- endpoint="groups"
- setTimestamp={setTimestamp}
- customActions={limitCommands ? [actionConnections] : [actionConnections, actionCommand]}
- />
- </TableCell>
- </TableRow>
- )) : (<TableShimmer columns={2} endAction />)}
- </TableBody>
- </Table>
- <CollectionFab editPath="/settings/group" />
- </PageLayout>
- );
-};
-
-export default GroupsPage;
diff --git a/modern/src/settings/MaintenancePage.jsx b/modern/src/settings/MaintenancePage.jsx
deleted file mode 100644
index 491a0d3b..00000000
--- a/modern/src/settings/MaintenancePage.jsx
+++ /dev/null
@@ -1,174 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import dayjs from 'dayjs';
-import {
- Accordion,
- AccordionSummary,
- AccordionDetails,
- Typography,
- TextField,
- FormControl,
- InputLabel,
- MenuItem,
- Select,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import { prefixString } from '../common/util/stringUtils';
-import EditItemView from './components/EditItemView';
-import EditAttributesAccordion from './components/EditAttributesAccordion';
-import { useAttributePreference } from '../common/util/preferences';
-import {
- speedFromKnots, speedToKnots, distanceFromMeters, distanceToMeters,
-} from '../common/util/converter';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import usePositionAttributes from '../common/attributes/usePositionAttributes';
-import SettingsMenu from './components/SettingsMenu';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const MaintenancePage = () => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const positionAttributes = usePositionAttributes(t);
-
- const [item, setItem] = useState();
- const [labels, setLabels] = useState({ start: '', period: '' });
-
- const speedUnit = useAttributePreference('speedUnit', 'kn');
- const distanceUnit = useAttributePreference('distanceUnit', 'km');
-
- const convertToList = (attributes) => {
- const otherList = [];
- Object.keys(attributes).forEach((key) => {
- const value = attributes[key];
- if (value.type === 'number' || key.endsWith('Time')) {
- otherList.push({ key, name: value.name, type: value.type });
- }
- });
- return otherList;
- };
-
- useEffect(() => {
- const attribute = positionAttributes[item?.type];
- if (item?.type?.endsWith('Time')) {
- setLabels({ ...labels, start: null, period: t('sharedDays') });
- } else if (attribute && attribute.dataType) {
- switch (attribute.dataType) {
- case 'distance':
- setLabels({ ...labels, start: t(prefixString('shared', distanceUnit)), period: t(prefixString('shared', distanceUnit)) });
- break;
- case 'speed':
- setLabels({ ...labels, start: t(prefixString('shared', speedUnit)), period: t(prefixString('shared', speedUnit)) });
- break;
- default:
- setLabels({ ...labels, start: null, period: null });
- break;
- }
- } else {
- setLabels({ ...labels, start: null, period: null });
- }
- }, [item?.type]);
-
- const rawToValue = (start, value) => {
- const attribute = positionAttributes[item.type];
- if (item.type?.endsWith('Time')) {
- if (start) {
- return dayjs(value).locale('en').format('YYYY-MM-DD');
- }
- return value / 86400000;
- }
- if (attribute && attribute.dataType) {
- switch (attribute.dataType) {
- case 'speed':
- return speedFromKnots(value, speedUnit);
- case 'distance':
- return distanceFromMeters(value, distanceUnit);
- default:
- return value;
- }
- }
- return value;
- };
-
- const valueToRaw = (start, value) => {
- const attribute = positionAttributes[item.type];
- if (item.type?.endsWith('Time')) {
- if (start) {
- return dayjs(value, 'YYYY-MM-DD').valueOf();
- }
- return value * 86400000;
- } if (attribute && attribute.dataType) {
- switch (attribute.dataType) {
- case 'speed':
- return speedToKnots(value, speedUnit);
- case 'distance':
- return distanceToMeters(value, distanceUnit);
- default:
- return value;
- }
- }
- return value;
- };
-
- const validate = () => item && item.name && item.type && item.start && item.period;
-
- return (
- <EditItemView
- endpoint="maintenance"
- item={item}
- setItem={setItem}
- validate={validate}
- menu={<SettingsMenu />}
- breadcrumbs={['settingsTitle', 'sharedMaintenance']}
- >
- {item && (
- <>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedRequired')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- value={item.name || ''}
- onChange={(e) => setItem({ ...item, name: e.target.value })}
- label={t('sharedName')}
- />
- <FormControl>
- <InputLabel>{t('sharedType')}</InputLabel>
- <Select
- label={t('sharedType')}
- value={item.type || ''}
- onChange={(e) => setItem({ ...item, type: e.target.value, start: 0, period: 0 })}
- >
- {convertToList(positionAttributes).map(({ key, name }) => (
- <MenuItem key={key} value={key}>{name}</MenuItem>
- ))}
- </Select>
- </FormControl>
- <TextField
- type={item.type?.endsWith('Time') ? 'date' : 'number'}
- value={rawToValue(true, item.start) || ''}
- onChange={(e) => setItem({ ...item, start: valueToRaw(true, e.target.value) })}
- label={labels.start ? `${t('maintenanceStart')} (${labels.start})` : t('maintenanceStart')}
- />
- <TextField
- type="number"
- value={rawToValue(false, item.period) || ''}
- onChange={(e) => setItem({ ...item, period: valueToRaw(false, e.target.value) })}
- label={labels.period ? `${t('maintenancePeriod')} (${labels.period})` : t('maintenancePeriod')}
- />
- </AccordionDetails>
- </Accordion>
- <EditAttributesAccordion
- attributes={item.attributes}
- setAttributes={(attributes) => setItem({ ...item, attributes })}
- definitions={{}}
- />
- </>
- )}
- </EditItemView>
- );
-};
-
-export default MaintenancePage;
diff --git a/modern/src/settings/MaintenancesPage.jsx b/modern/src/settings/MaintenancesPage.jsx
deleted file mode 100644
index 9241eb3e..00000000
--- a/modern/src/settings/MaintenancesPage.jsx
+++ /dev/null
@@ -1,100 +0,0 @@
-import React, { useState } from 'react';
-import dayjs from 'dayjs';
-import {
- Table, TableRow, TableCell, TableHead, TableBody,
-} from '@mui/material';
-import { useEffectAsync } from '../reactHelper';
-import usePositionAttributes from '../common/attributes/usePositionAttributes';
-import { formatDistance, formatSpeed } from '../common/util/formatter';
-import { useAttributePreference } from '../common/util/preferences';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import CollectionFab from './components/CollectionFab';
-import CollectionActions from './components/CollectionActions';
-import TableShimmer from '../common/components/TableShimmer';
-import SearchHeader, { filterByKeyword } from './components/SearchHeader';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const MaintenacesPage = () => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const positionAttributes = usePositionAttributes(t);
-
- const [timestamp, setTimestamp] = useState(Date.now());
- const [items, setItems] = useState([]);
- const [searchKeyword, setSearchKeyword] = useState('');
- const [loading, setLoading] = useState(false);
- const speedUnit = useAttributePreference('speedUnit');
- const distanceUnit = useAttributePreference('distanceUnit');
-
- useEffectAsync(async () => {
- setLoading(true);
- try {
- const response = await fetch('/api/maintenance');
- if (response.ok) {
- setItems(await response.json());
- } else {
- throw Error(await response.text());
- }
- } finally {
- setLoading(false);
- }
- }, [timestamp]);
-
- const convertAttribute = (key, start, value) => {
- const attribute = positionAttributes[key];
- if (key.endsWith('Time')) {
- if (start) {
- return dayjs(value).locale('en').format('YYYY-MM-DD');
- }
- return `${value / 86400000} ${t('sharedDays')}`;
- }
- if (attribute && attribute.dataType) {
- switch (attribute.dataType) {
- case 'speed':
- return formatSpeed(value, speedUnit, t);
- case 'distance':
- return formatDistance(value, distanceUnit, t);
- default:
- return value;
- }
- }
-
- return value;
- };
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'sharedMaintenance']}>
- <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} />
- <Table className={classes.table}>
- <TableHead>
- <TableRow>
- <TableCell>{t('sharedName')}</TableCell>
- <TableCell>{t('sharedType')}</TableCell>
- <TableCell>{t('maintenanceStart')}</TableCell>
- <TableCell>{t('maintenancePeriod')}</TableCell>
- <TableCell className={classes.columnAction} />
- </TableRow>
- </TableHead>
- <TableBody>
- {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => (
- <TableRow key={item.id}>
- <TableCell>{item.name}</TableCell>
- <TableCell>{item.type}</TableCell>
- <TableCell>{convertAttribute(item.type, true, item.start)}</TableCell>
- <TableCell>{convertAttribute(item.type, false, item.period)}</TableCell>
- <TableCell className={classes.columnAction} padding="none">
- <CollectionActions itemId={item.id} editPath="/settings/maintenance" endpoint="maintenance" setTimestamp={setTimestamp} />
- </TableCell>
- </TableRow>
- )) : (<TableShimmer columns={5} endAction />)}
- </TableBody>
- </Table>
- <CollectionFab editPath="/settings/maintenance" />
- </PageLayout>
- );
-};
-
-export default MaintenacesPage;
diff --git a/modern/src/settings/NotificationPage.jsx b/modern/src/settings/NotificationPage.jsx
deleted file mode 100644
index 63aa9b95..00000000
--- a/modern/src/settings/NotificationPage.jsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import React, { useState } from 'react';
-
-import {
- Accordion,
- AccordionSummary,
- AccordionDetails,
- Typography,
- FormControlLabel,
- Checkbox,
- FormGroup,
- Button,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import { useTranslation, useTranslationKeys } from '../common/components/LocalizationProvider';
-import EditItemView from './components/EditItemView';
-import { prefixString, unprefixString } from '../common/util/stringUtils';
-import SelectField from '../common/components/SelectField';
-import SettingsMenu from './components/SettingsMenu';
-import { useCatch } from '../reactHelper';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const NotificationPage = () => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const [item, setItem] = useState();
-
- const alarms = useTranslationKeys((it) => it.startsWith('alarm')).map((it) => ({
- key: unprefixString('alarm', it),
- name: t(it),
- }));
-
- const testNotificators = useCatch(async () => {
- await Promise.all(item.notificators.split(/[, ]+/).map(async (notificator) => {
- const response = await fetch(`/api/notifications/test/${notificator}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(item),
- });
- if (!response.ok) {
- throw Error(await response.text());
- }
- }));
- });
-
- const validate = () => item && item.type && item.notificators && (!item.notificators?.includes('command') || item.commandId);
-
- return (
- <EditItemView
- endpoint="notifications"
- item={item}
- setItem={setItem}
- validate={validate}
- menu={<SettingsMenu />}
- breadcrumbs={['settingsTitle', 'sharedNotification']}
- >
- {item && (
- <>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedRequired')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <SelectField
- value={item.type}
- onChange={(e) => setItem({ ...item, type: e.target.value })}
- endpoint="/api/notifications/types"
- keyGetter={(it) => it.type}
- titleGetter={(it) => t(prefixString('event', it.type))}
- label={t('sharedType')}
- />
- {item.type === 'alarm' && (
- <SelectField
- multiple
- value={item.attributes && item.attributes.alarms ? item.attributes.alarms.split(/[, ]+/) : []}
- onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, alarms: e.target.value.join() } })}
- data={alarms}
- keyGetter={(it) => it.key}
- label={t('sharedAlarms')}
- />
- )}
- <SelectField
- multiple
- value={item.notificators ? item.notificators.split(/[, ]+/) : []}
- onChange={(e) => setItem({ ...item, notificators: e.target.value.join() })}
- endpoint="/api/notifications/notificators"
- keyGetter={(it) => it.type}
- titleGetter={(it) => t(prefixString('notificator', it.type))}
- label={t('notificationNotificators')}
- />
- {item.notificators?.includes('command') && (
- <SelectField
- value={item.commandId}
- onChange={(event) => setItem({ ...item, commandId: Number(event.target.value) })}
- endpoint="/api/commands"
- titleGetter={(it) => it.description}
- label={t('sharedSavedCommand')}
- />
- )}
- <Button
- variant="outlined"
- color="primary"
- onClick={testNotificators}
- disabled={!item.notificators}
- >
- {t('sharedTestNotificators')}
- </Button>
- <FormGroup>
- <FormControlLabel
- control={(
- <Checkbox
- checked={item.always}
- onChange={(event) => setItem({ ...item, always: event.target.checked })}
- />
- )}
- label={t('notificationAlways')}
- />
- </FormGroup>
- </AccordionDetails>
- </Accordion>
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedExtra')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <SelectField
- value={item.calendarId}
- onChange={(event) => setItem({ ...item, calendarId: Number(event.target.value) })}
- endpoint="/api/calendars"
- label={t('sharedCalendar')}
- />
- </AccordionDetails>
- </Accordion>
- </>
- )}
- </EditItemView>
- );
-};
-
-export default NotificationPage;
diff --git a/modern/src/settings/NotificationsPage.jsx b/modern/src/settings/NotificationsPage.jsx
deleted file mode 100644
index f1e70a85..00000000
--- a/modern/src/settings/NotificationsPage.jsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import React, { useState } from 'react';
-import {
- Table, TableRow, TableCell, TableHead, TableBody,
-} from '@mui/material';
-import { useEffectAsync } from '../reactHelper';
-import { prefixString } from '../common/util/stringUtils';
-import { formatBoolean } from '../common/util/formatter';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import CollectionFab from './components/CollectionFab';
-import CollectionActions from './components/CollectionActions';
-import TableShimmer from '../common/components/TableShimmer';
-import SearchHeader, { filterByKeyword } from './components/SearchHeader';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const NotificationsPage = () => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const [timestamp, setTimestamp] = useState(Date.now());
- const [items, setItems] = useState([]);
- const [searchKeyword, setSearchKeyword] = useState('');
- const [loading, setLoading] = useState(false);
-
- useEffectAsync(async () => {
- setLoading(true);
- try {
- const response = await fetch('/api/notifications');
- if (response.ok) {
- setItems(await response.json());
- } else {
- throw Error(await response.text());
- }
- } finally {
- setLoading(false);
- }
- }, [timestamp]);
-
- const formatList = (prefix, value) => {
- if (value) {
- return value
- .split(/[, ]+/)
- .filter(Boolean)
- .map((it) => t(prefixString(prefix, it)))
- .join(', ');
- }
- return '';
- };
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'sharedNotifications']}>
- <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} />
- <Table className={classes.table}>
- <TableHead>
- <TableRow>
- <TableCell>{t('notificationType')}</TableCell>
- <TableCell>{t('notificationAlways')}</TableCell>
- <TableCell>{t('sharedAlarms')}</TableCell>
- <TableCell>{t('notificationNotificators')}</TableCell>
- <TableCell className={classes.columnAction} />
- </TableRow>
- </TableHead>
- <TableBody>
- {!loading ? items.filter(filterByKeyword(searchKeyword)).map((item) => (
- <TableRow key={item.id}>
- <TableCell>{t(prefixString('event', item.type))}</TableCell>
- <TableCell>{formatBoolean(item.always, t)}</TableCell>
- <TableCell>{formatList('alarm', item.attributes.alarms)}</TableCell>
- <TableCell>{formatList('notificator', item.notificators)}</TableCell>
- <TableCell className={classes.columnAction} padding="none">
- <CollectionActions itemId={item.id} editPath="/settings/notification" endpoint="notifications" setTimestamp={setTimestamp} />
- </TableCell>
- </TableRow>
- )) : (<TableShimmer columns={5} endAction />)}
- </TableBody>
- </Table>
- <CollectionFab editPath="/settings/notification" />
- </PageLayout>
- );
-};
-
-export default NotificationsPage;
diff --git a/modern/src/settings/PreferencesPage.jsx b/modern/src/settings/PreferencesPage.jsx
deleted file mode 100644
index 2d6df62f..00000000
--- a/modern/src/settings/PreferencesPage.jsx
+++ /dev/null
@@ -1,375 +0,0 @@
-import React, { useState } from 'react';
-import dayjs from 'dayjs';
-import { useDispatch, useSelector } from 'react-redux';
-import { useNavigate } from 'react-router-dom';
-import {
- Accordion, AccordionSummary, AccordionDetails, Typography, Container, FormControl, InputLabel, Select, MenuItem, Checkbox, FormControlLabel, FormGroup, InputAdornment, IconButton, OutlinedInput, Autocomplete, TextField, createFilterOptions, Button,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import CachedIcon from '@mui/icons-material/Cached';
-import ContentCopyIcon from '@mui/icons-material/ContentCopy';
-import { useTranslation, useTranslationKeys } from '../common/components/LocalizationProvider';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import usePositionAttributes from '../common/attributes/usePositionAttributes';
-import { prefixString, unprefixString } from '../common/util/stringUtils';
-import SelectField from '../common/components/SelectField';
-import useMapStyles from '../map/core/useMapStyles';
-import useMapOverlays from '../map/overlay/useMapOverlays';
-import { useCatch } from '../reactHelper';
-import { sessionActions } from '../store';
-import { useRestriction } from '../common/util/permissions';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const deviceFields = [
- { id: 'name', name: 'sharedName' },
- { id: 'uniqueId', name: 'deviceIdentifier' },
- { id: 'phone', name: 'sharedPhone' },
- { id: 'model', name: 'deviceModel' },
- { id: 'contact', name: 'deviceContact' },
-];
-
-const PreferencesPage = () => {
- const classes = useSettingsStyles();
- const dispatch = useDispatch();
- const navigate = useNavigate();
- const t = useTranslation();
-
- const readonly = useRestriction('readonly');
-
- const user = useSelector((state) => state.session.user);
- const [attributes, setAttributes] = useState(user.attributes);
-
- const versionApp = import.meta.env.VITE_APP_VERSION.slice(0, -2);
- const versionServer = useSelector((state) => state.session.server.version);
- const socket = useSelector((state) => state.session.socket);
-
- const [token, setToken] = useState(null);
- const [tokenExpiration, setTokenExpiration] = useState(dayjs().add(1, 'week').locale('en').format('YYYY-MM-DD'));
-
- const mapStyles = useMapStyles();
- const mapOverlays = useMapOverlays();
-
- const positionAttributes = usePositionAttributes(t);
-
- const filter = createFilterOptions();
-
- const generateToken = useCatch(async () => {
- const expiration = dayjs(tokenExpiration, 'YYYY-MM-DD').toISOString();
- const response = await fetch('/api/session/token', {
- method: 'POST',
- body: new URLSearchParams(`expiration=${expiration}`),
- });
- if (response.ok) {
- setToken(await response.text());
- } else {
- throw Error(await response.text());
- }
- });
-
- const alarms = useTranslationKeys((it) => it.startsWith('alarm')).map((it) => ({
- key: unprefixString('alarm', it),
- name: t(it),
- }));
-
- const handleSave = useCatch(async () => {
- const response = await fetch(`/api/users/${user.id}`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ ...user, attributes }),
- });
- if (response.ok) {
- dispatch(sessionActions.updateUser(await response.json()));
- navigate(-1);
- } else {
- throw Error(await response.text());
- }
- });
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'sharedPreferences']}>
- <Container maxWidth="xs" className={classes.container}>
- {!readonly && (
- <>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('mapTitle')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <FormControl>
- <InputLabel>{t('mapActive')}</InputLabel>
- <Select
- label={t('mapActive')}
- value={attributes.activeMapStyles?.split(',') || ['locationIqStreets', 'osm', 'carto']}
- onChange={(e, child) => {
- const clicked = mapStyles.find((s) => s.id === child.props.value);
- if (clicked.available) {
- setAttributes({ ...attributes, activeMapStyles: e.target.value.join(',') });
- } else if (clicked.id !== 'custom') {
- const query = new URLSearchParams({ attribute: clicked.attribute });
- navigate(`/settings/user/${user.id}?${query.toString()}`);
- }
- }}
- multiple
- >
- {mapStyles.map((style) => (
- <MenuItem key={style.id} value={style.id}>
- <Typography component="span" color={style.available ? 'textPrimary' : 'error'}>{style.title}</Typography>
- </MenuItem>
- ))}
- </Select>
- </FormControl>
- <FormControl>
- <InputLabel>{t('mapOverlay')}</InputLabel>
- <Select
- label={t('mapOverlay')}
- value={attributes.selectedMapOverlay || ''}
- onChange={(e) => {
- const clicked = mapOverlays.find((o) => o.id === e.target.value);
- if (!clicked || clicked.available) {
- setAttributes({ ...attributes, selectedMapOverlay: e.target.value });
- } else if (clicked.id !== 'custom') {
- const query = new URLSearchParams({ attribute: clicked.attribute });
- navigate(`/settings/user/${user.id}?${query.toString()}`);
- }
- }}
- >
- <MenuItem value="">{'\u00a0'}</MenuItem>
- {mapOverlays.map((overlay) => (
- <MenuItem key={overlay.id} value={overlay.id}>
- <Typography component="span" color={overlay.available ? 'textPrimary' : 'error'}>{overlay.title}</Typography>
- </MenuItem>
- ))}
- </Select>
- </FormControl>
- <Autocomplete
- multiple
- freeSolo
- options={Object.keys(positionAttributes)}
- getOptionLabel={(option) => (positionAttributes[option]?.name || option)}
- value={attributes.positionItems?.split(',') || ['speed', 'address', 'totalDistance', 'course']}
- onChange={(_, option) => {
- setAttributes({ ...attributes, positionItems: option.join(',') });
- }}
- filterOptions={(options, params) => {
- const filtered = filter(options, params);
- if (params.inputValue && !filtered.includes(params.inputValue)) {
- filtered.push(params.inputValue);
- }
- return filtered;
- }}
- renderInput={(params) => (
- <TextField
- {...params}
- label={t('attributePopupInfo')}
- />
- )}
- />
- <FormControl>
- <InputLabel>{t('mapLiveRoutes')}</InputLabel>
- <Select
- label={t('mapLiveRoutes')}
- value={attributes.mapLiveRoutes || 'none'}
- onChange={(e) => setAttributes({ ...attributes, mapLiveRoutes: e.target.value })}
- >
- <MenuItem value="none">{t('sharedDisabled')}</MenuItem>
- <MenuItem value="selected">{t('deviceSelected')}</MenuItem>
- <MenuItem value="all">{t('notificationAlways')}</MenuItem>
- </Select>
- </FormControl>
- <FormControl>
- <InputLabel>{t('mapDirection')}</InputLabel>
- <Select
- label={t('mapDirection')}
- value={attributes.mapDirection || 'selected'}
- onChange={(e) => setAttributes({ ...attributes, mapDirection: e.target.value })}
- >
- <MenuItem value="none">{t('sharedDisabled')}</MenuItem>
- <MenuItem value="selected">{t('deviceSelected')}</MenuItem>
- <MenuItem value="all">{t('notificationAlways')}</MenuItem>
- </Select>
- </FormControl>
- <FormGroup>
- <FormControlLabel
- control={(
- <Checkbox
- checked={attributes.hasOwnProperty('mapGeofences') ? attributes.mapGeofences : true}
- onChange={(e) => setAttributes({ ...attributes, mapGeofences: e.target.checked })}
- />
- )}
- label={t('attributeShowGeofences')}
- />
- <FormControlLabel
- control={(
- <Checkbox
- checked={attributes.hasOwnProperty('mapFollow') ? attributes.mapFollow : false}
- onChange={(e) => setAttributes({ ...attributes, mapFollow: e.target.checked })}
- />
- )}
- label={t('deviceFollow')}
- />
- <FormControlLabel
- control={(
- <Checkbox
- checked={attributes.hasOwnProperty('mapCluster') ? attributes.mapCluster : true}
- onChange={(e) => setAttributes({ ...attributes, mapCluster: e.target.checked })}
- />
- )}
- label={t('mapClustering')}
- />
- <FormControlLabel
- control={(
- <Checkbox
- checked={attributes.hasOwnProperty('mapOnSelect') ? attributes.mapOnSelect : true}
- onChange={(e) => setAttributes({ ...attributes, mapOnSelect: e.target.checked })}
- />
- )}
- label={t('mapOnSelect')}
- />
- </FormGroup>
- </AccordionDetails>
- </Accordion>
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('deviceTitle')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <SelectField
- value={attributes.devicePrimary || 'name'}
- onChange={(e) => setAttributes({ ...attributes, devicePrimary: e.target.value })}
- data={deviceFields}
- titleGetter={(it) => t(it.name)}
- label={t('devicePrimaryInfo')}
- />
- <SelectField
- value={attributes.deviceSecondary}
- onChange={(e) => setAttributes({ ...attributes, deviceSecondary: e.target.value })}
- data={deviceFields}
- titleGetter={(it) => t(it.name)}
- label={t('deviceSecondaryInfo')}
- />
- </AccordionDetails>
- </Accordion>
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedSound')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <SelectField
- multiple
- value={attributes.soundEvents?.split(',') || []}
- onChange={(e) => setAttributes({ ...attributes, soundEvents: e.target.value.join(',') })}
- endpoint="/api/notifications/types"
- keyGetter={(it) => it.type}
- titleGetter={(it) => t(prefixString('event', it.type))}
- label={t('eventsSoundEvents')}
- />
- <SelectField
- multiple
- value={attributes.soundAlarms?.split(',') || ['sos']}
- onChange={(e) => setAttributes({ ...attributes, soundAlarms: e.target.value.join(',') })}
- data={alarms}
- keyGetter={(it) => it.key}
- label={t('eventsSoundAlarms')}
- />
- </AccordionDetails>
- </Accordion>
- </>
- )}
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('userToken')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- label={t('userExpirationTime')}
- type="date"
- value={tokenExpiration}
- onChange={(e) => {
- setTokenExpiration(e.target.value);
- setToken(null);
- }}
- />
- <FormControl>
- <OutlinedInput
- multiline
- rows={6}
- readOnly
- type="text"
- value={token || ''}
- endAdornment={(
- <InputAdornment position="end">
- <div className={classes.verticalActions}>
- <IconButton size="small" edge="end" onClick={generateToken} disabled={!!token}>
- <CachedIcon fontSize="small" />
- </IconButton>
- <IconButton size="small" edge="end" onClick={() => navigator.clipboard.writeText(token)} disabled={!token}>
- <ContentCopyIcon fontSize="small" />
- </IconButton>
- </div>
- </InputAdornment>
- )}
- />
- </FormControl>
- </AccordionDetails>
- </Accordion>
- {!readonly && (
- <>
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedInfoTitle')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- value={versionApp}
- label={t('settingsAppVersion')}
- disabled
- />
- <TextField
- value={versionServer || '-'}
- label={t('settingsServerVersion')}
- disabled
- />
- <TextField
- value={socket ? t('deviceStatusOnline') : t('deviceStatusOffline')}
- label={t('settingsConnection')}
- disabled
- />
- </AccordionDetails>
- </Accordion>
- <div className={classes.buttons}>
- <Button
- type="button"
- color="primary"
- variant="outlined"
- onClick={() => navigate(-1)}
- >
- {t('sharedCancel')}
- </Button>
- <Button
- type="button"
- color="primary"
- variant="contained"
- onClick={handleSave}
- >
- {t('sharedSave')}
- </Button>
- </div>
- </>
- )}
- </Container>
- </PageLayout>
- );
-};
-
-export default PreferencesPage;
diff --git a/modern/src/settings/ServerPage.jsx b/modern/src/settings/ServerPage.jsx
deleted file mode 100644
index 0ac76334..00000000
--- a/modern/src/settings/ServerPage.jsx
+++ /dev/null
@@ -1,316 +0,0 @@
-import React, { useState } from 'react';
-import TextField from '@mui/material/TextField';
-
-import {
- Accordion,
- AccordionSummary,
- AccordionDetails,
- Typography,
- Button,
- FormControl,
- Container,
- Checkbox,
- FormControlLabel,
- InputLabel,
- Select,
- MenuItem,
- FormGroup,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import { useNavigate } from 'react-router-dom';
-import { useDispatch, useSelector } from 'react-redux';
-import { DropzoneArea } from 'react-mui-dropzone';
-import { sessionActions } from '../store';
-import EditAttributesAccordion from './components/EditAttributesAccordion';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import SelectField from '../common/components/SelectField';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import useCommonDeviceAttributes from '../common/attributes/useCommonDeviceAttributes';
-import useCommonUserAttributes from '../common/attributes/useCommonUserAttributes';
-import { useCatch } from '../reactHelper';
-import useServerAttributes from '../common/attributes/useServerAttributes';
-import useMapStyles from '../map/core/useMapStyles';
-import { map } from '../map/core/MapView';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const ServerPage = () => {
- const classes = useSettingsStyles();
- const navigate = useNavigate();
- const dispatch = useDispatch();
- const t = useTranslation();
-
- const mapStyles = useMapStyles();
- const commonUserAttributes = useCommonUserAttributes(t);
- const commonDeviceAttributes = useCommonDeviceAttributes(t);
- const serverAttributes = useServerAttributes(t);
-
- const original = useSelector((state) => state.session.server);
- const [item, setItem] = useState({ ...original });
-
- const handleFiles = useCatch(async (files) => {
- if (files.length > 0) {
- const file = files[0];
- const response = await fetch(`/api/server/file/${file.path}`, {
- method: 'POST',
- body: file,
- });
- if (!response.ok) {
- throw Error(await response.text());
- }
- }
- });
-
- const handleSave = useCatch(async () => {
- const response = await fetch('/api/server', {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(item),
- });
-
- if (response.ok) {
- dispatch(sessionActions.updateServer(await response.json()));
- navigate(-1);
- } else {
- throw Error(await response.text());
- }
- });
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'settingsServer']}>
- <Container maxWidth="xs" className={classes.container}>
- {item && (
- <>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedPreferences')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- value={item.mapUrl || ''}
- onChange={(event) => setItem({ ...item, mapUrl: event.target.value })}
- label={t('mapCustomLabel')}
- />
- <TextField
- value={item.overlayUrl || ''}
- onChange={(event) => setItem({ ...item, overlayUrl: event.target.value })}
- label={t('mapOverlayCustom')}
- />
- <FormControl>
- <InputLabel>{t('mapDefault')}</InputLabel>
- <Select
- label={t('mapDefault')}
- value={item.map || 'locationIqStreets'}
- onChange={(e) => setItem({ ...item, map: e.target.value })}
- >
- {mapStyles.filter((style) => style.available).map((style) => (
- <MenuItem key={style.id} value={style.id}>
- <Typography component="span">{style.title}</Typography>
- </MenuItem>
- ))}
- </Select>
- </FormControl>
- <FormControl>
- <InputLabel>{t('settingsCoordinateFormat')}</InputLabel>
- <Select
- label={t('settingsCoordinateFormat')}
- value={item.coordinateFormat || 'dd'}
- onChange={(event) => setItem({ ...item, coordinateFormat: event.target.value })}
- >
- <MenuItem value="dd">{t('sharedDecimalDegrees')}</MenuItem>
- <MenuItem value="ddm">{t('sharedDegreesDecimalMinutes')}</MenuItem>
- <MenuItem value="dms">{t('sharedDegreesMinutesSeconds')}</MenuItem>
- </Select>
- </FormControl>
- <FormControl>
- <InputLabel>{t('settingsSpeedUnit')}</InputLabel>
- <Select
- label={t('settingsSpeedUnit')}
- value={item.attributes.speedUnit || 'kn'}
- onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, speedUnit: e.target.value } })}
- >
- <MenuItem value="kn">{t('sharedKn')}</MenuItem>
- <MenuItem value="kmh">{t('sharedKmh')}</MenuItem>
- <MenuItem value="mph">{t('sharedMph')}</MenuItem>
- </Select>
- </FormControl>
- <FormControl>
- <InputLabel>{t('settingsDistanceUnit')}</InputLabel>
- <Select
- label={t('settingsDistanceUnit')}
- value={item.attributes.distanceUnit || 'km'}
- onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, distanceUnit: e.target.value } })}
- >
- <MenuItem value="km">{t('sharedKm')}</MenuItem>
- <MenuItem value="mi">{t('sharedMi')}</MenuItem>
- <MenuItem value="nmi">{t('sharedNmi')}</MenuItem>
- </Select>
- </FormControl>
- <FormControl>
- <InputLabel>{t('settingsAltitudeUnit')}</InputLabel>
- <Select
- label={t('settingsAltitudeUnit')}
- value={item.attributes.altitudeUnit || 'm'}
- onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, altitudeUnit: e.target.value } })}
- >
- <MenuItem value="m">{t('sharedMeters')}</MenuItem>
- <MenuItem value="ft">{t('sharedFeet')}</MenuItem>
- </Select>
- </FormControl>
- <FormControl>
- <InputLabel>{t('settingsVolumeUnit')}</InputLabel>
- <Select
- label={t('settingsVolumeUnit')}
- value={item.attributes.volumeUnit || 'ltr'}
- onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, volumeUnit: e.target.value } })}
- >
- <MenuItem value="ltr">{t('sharedLiter')}</MenuItem>
- <MenuItem value="usGal">{t('sharedUsGallon')}</MenuItem>
- <MenuItem value="impGal">{t('sharedImpGallon')}</MenuItem>
- </Select>
- </FormControl>
- <SelectField
- value={item.attributes.timezone}
- onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, timezone: e.target.value } })}
- endpoint="/api/server/timezones"
- keyGetter={(it) => it}
- titleGetter={(it) => it}
- label={t('sharedTimezone')}
- />
- <TextField
- value={item.poiLayer || ''}
- onChange={(event) => setItem({ ...item, poiLayer: event.target.value })}
- label={t('mapPoiLayer')}
- />
- <TextField
- value={item.announcement || ''}
- onChange={(event) => setItem({ ...item, announcement: event.target.value })}
- label={t('serverAnnouncement')}
- />
- <FormGroup>
- <FormControlLabel
- control={<Checkbox checked={item.twelveHourFormat} onChange={(event) => setItem({ ...item, twelveHourFormat: event.target.checked })} />}
- label={t('settingsTwelveHourFormat')}
- />
- <FormControlLabel
- control={<Checkbox checked={item.forceSettings} onChange={(event) => setItem({ ...item, forceSettings: event.target.checked })} />}
- label={t('serverForceSettings')}
- />
- </FormGroup>
- </AccordionDetails>
- </Accordion>
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedLocation')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- type="number"
- value={item.latitude || 0}
- onChange={(event) => setItem({ ...item, latitude: Number(event.target.value) })}
- label={t('positionLatitude')}
- />
- <TextField
- type="number"
- value={item.longitude || 0}
- onChange={(event) => setItem({ ...item, longitude: Number(event.target.value) })}
- label={t('positionLongitude')}
- />
- <TextField
- type="number"
- value={item.zoom || 0}
- onChange={(event) => setItem({ ...item, zoom: Number(event.target.value) })}
- label={t('serverZoom')}
- />
- <Button
- variant="outlined"
- color="primary"
- onClick={() => {
- const { lng, lat } = map.getCenter();
- setItem({
- ...item,
- latitude: Number(lat.toFixed(6)),
- longitude: Number(lng.toFixed(6)),
- zoom: Number(map.getZoom().toFixed(1)),
- });
- }}
- >
- {t('mapCurrentLocation')}
- </Button>
- </AccordionDetails>
- </Accordion>
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedPermissions')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <FormGroup>
- <FormControlLabel
- control={<Checkbox checked={item.registration} onChange={(event) => setItem({ ...item, registration: event.target.checked })} />}
- label={t('serverRegistration')}
- />
- <FormControlLabel
- control={<Checkbox checked={item.readonly} onChange={(event) => setItem({ ...item, readonly: event.target.checked })} />}
- label={t('serverReadonly')}
- />
- <FormControlLabel
- control={<Checkbox checked={item.deviceReadonly} onChange={(event) => setItem({ ...item, deviceReadonly: event.target.checked })} />}
- label={t('userDeviceReadonly')}
- />
- <FormControlLabel
- control={<Checkbox checked={item.limitCommands} onChange={(event) => setItem({ ...item, limitCommands: event.target.checked })} />}
- label={t('userLimitCommands')}
- />
- <FormControlLabel
- control={<Checkbox checked={item.disableReports} onChange={(event) => setItem({ ...item, disableReports: event.target.checked })} />}
- label={t('userDisableReports')}
- />
- <FormControlLabel
- control={<Checkbox checked={item.fixedEmail} onChange={(e) => setItem({ ...item, fixedEmail: e.target.checked })} />}
- label={t('userFixedEmail')}
- />
- </FormGroup>
- </AccordionDetails>
- </Accordion>
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedFile')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <DropzoneArea
- dropzoneText={t('sharedDropzoneText')}
- filesLimit={1}
- onChange={handleFiles}
- showAlerts={false}
- />
- </AccordionDetails>
- </Accordion>
- <EditAttributesAccordion
- attributes={item.attributes}
- setAttributes={(attributes) => setItem({ ...item, attributes })}
- definitions={{ ...commonUserAttributes, ...commonDeviceAttributes, ...serverAttributes }}
- />
- </>
- )}
- <div className={classes.buttons}>
- <Button type="button" color="primary" variant="outlined" onClick={() => navigate(-1)}>
- {t('sharedCancel')}
- </Button>
- <Button type="button" color="primary" variant="contained" onClick={handleSave}>
- {t('sharedSave')}
- </Button>
- </div>
- </Container>
- </PageLayout>
- );
-};
-
-export default ServerPage;
diff --git a/modern/src/settings/SharePage.jsx b/modern/src/settings/SharePage.jsx
deleted file mode 100644
index d16fe44d..00000000
--- a/modern/src/settings/SharePage.jsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import React, { useState } from 'react';
-import { useSelector } from 'react-redux';
-import { useNavigate, useParams } from 'react-router-dom';
-import dayjs from 'dayjs';
-import {
- Accordion,
- AccordionSummary,
- AccordionDetails,
- Typography,
- Container,
- TextField,
- Button,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import { useCatchCallback } from '../reactHelper';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const SharePage = () => {
- const navigate = useNavigate();
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const { id } = useParams();
-
- const device = useSelector((state) => state.devices.items[id]);
-
- const [expiration, setExpiration] = useState(dayjs().add(1, 'week').locale('en').format('YYYY-MM-DD'));
- const [link, setLink] = useState();
-
- const handleShare = useCatchCallback(async () => {
- const expirationTime = dayjs(expiration).toISOString();
- const response = await fetch('/api/devices/share', {
- method: 'POST',
- body: new URLSearchParams(`deviceId=${id}&expiration=${expirationTime}`),
- });
- if (response.ok) {
- const token = await response.text();
- setLink(`${window.location.origin}?token=${token}`);
- } else {
- throw Error(await response.text());
- }
- }, [id, expiration, setLink]);
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['deviceShare']}>
- <Container maxWidth="xs" className={classes.container}>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedRequired')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- value={device.name}
- label={t('sharedDevice')}
- disabled
- />
- <TextField
- label={t('userExpirationTime')}
- type="date"
- value={(expiration && dayjs(expiration).locale('en').format('YYYY-MM-DD')) || '2099-01-01'}
- onChange={(e) => setExpiration(dayjs(e.target.value, 'YYYY-MM-DD').locale('en').format())}
- />
- <Button
- variant="outlined"
- color="primary"
- onClick={handleShare}
- >
- {t('reportShow')}
- </Button>
- <TextField
- value={link || ''}
- onChange={(e) => setLink(e.target.value)}
- label={t('sharedLink')}
- InputProps={{
- readOnly: true,
- }}
- />
- </AccordionDetails>
- </Accordion>
- <div className={classes.buttons}>
- <Button
- type="button"
- color="primary"
- variant="outlined"
- onClick={() => navigate(-1)}
- >
- {t('sharedCancel')}
- </Button>
- <Button
- type="button"
- color="primary"
- variant="contained"
- onClick={() => navigator.clipboard?.writeText(link)}
- disabled={!link}
- >
- {t('sharedCopy')}
- </Button>
- </div>
- </Container>
- </PageLayout>
- );
-};
-
-export default SharePage;
diff --git a/modern/src/settings/UserConnectionsPage.jsx b/modern/src/settings/UserConnectionsPage.jsx
deleted file mode 100644
index 3ca0bdc1..00000000
--- a/modern/src/settings/UserConnectionsPage.jsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import React from 'react';
-import { useParams } from 'react-router-dom';
-import {
- Accordion,
- AccordionSummary,
- AccordionDetails,
- Typography,
- Container,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import LinkField from '../common/components/LinkField';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import SettingsMenu from './components/SettingsMenu';
-import { formatNotificationTitle } from '../common/util/formatter';
-import PageLayout from '../common/components/PageLayout';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const UserConnectionsPage = () => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const { id } = useParams();
-
- return (
- <PageLayout
- menu={<SettingsMenu />}
- breadcrumbs={['settingsTitle', 'settingsUser', 'sharedConnections']}
- >
- <Container maxWidth="xs" className={classes.container}>
- <Accordion defaultExpanded>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedConnections')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <LinkField
- endpointAll="/api/devices?all=true"
- endpointLinked={`/api/devices?userId=${id}`}
- baseId={id}
- keyBase="userId"
- keyLink="deviceId"
- titleGetter={(it) => `${it.name} (${it.uniqueId})`}
- label={t('deviceTitle')}
- />
- <LinkField
- endpointAll="/api/groups?all=true"
- endpointLinked={`/api/groups?userId=${id}`}
- baseId={id}
- keyBase="userId"
- keyLink="groupId"
- label={t('settingsGroups')}
- />
- <LinkField
- endpointAll="/api/geofences?all=true"
- endpointLinked={`/api/geofences?userId=${id}`}
- baseId={id}
- keyBase="userId"
- keyLink="geofenceId"
- label={t('sharedGeofences')}
- />
- <LinkField
- endpointAll="/api/notifications?all=true"
- endpointLinked={`/api/notifications?userId=${id}`}
- baseId={id}
- keyBase="userId"
- keyLink="notificationId"
- titleGetter={(it) => formatNotificationTitle(t, it, true)}
- label={t('sharedNotifications')}
- />
- <LinkField
- endpointAll="/api/calendars?all=true"
- endpointLinked={`/api/calendars?userId=${id}`}
- baseId={id}
- keyBase="userId"
- keyLink="calendarId"
- label={t('sharedCalendars')}
- />
- <LinkField
- endpointAll="/api/users?all=true"
- endpointLinked={`/api/users?userId=${id}`}
- baseId={id}
- keyBase="userId"
- keyLink="managedUserId"
- label={t('settingsUsers')}
- />
- <LinkField
- endpointAll="/api/attributes/computed?all=true"
- endpointLinked={`/api/attributes/computed?userId=${id}`}
- baseId={id}
- keyBase="userId"
- keyLink="attributeId"
- titleGetter={(it) => it.description}
- label={t('sharedComputedAttributes')}
- />
- <LinkField
- endpointAll="/api/drivers?all=true"
- endpointLinked={`/api/drivers?userId=${id}`}
- baseId={id}
- keyBase="userId"
- keyLink="driverId"
- titleGetter={(it) => `${it.name} (${it.uniqueId})`}
- label={t('sharedDrivers')}
- />
- <LinkField
- endpointAll="/api/commands?all=true"
- endpointLinked={`/api/commands?userId=${id}`}
- baseId={id}
- keyBase="userId"
- keyLink="commandId"
- titleGetter={(it) => it.description}
- label={t('sharedSavedCommands')}
- />
- <LinkField
- endpointAll="/api/maintenance?all=true"
- endpointLinked={`/api/maintenance?userId=${id}`}
- baseId={id}
- keyBase="userId"
- keyLink="maintenanceId"
- label={t('sharedMaintenance')}
- />
- </AccordionDetails>
- </Accordion>
- </Container>
- </PageLayout>
- );
-};
-
-export default UserConnectionsPage;
diff --git a/modern/src/settings/UserPage.jsx b/modern/src/settings/UserPage.jsx
deleted file mode 100644
index 6748dd31..00000000
--- a/modern/src/settings/UserPage.jsx
+++ /dev/null
@@ -1,428 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useNavigate, useParams } from 'react-router-dom';
-import {
- Accordion,
- AccordionSummary,
- AccordionDetails,
- Typography,
- FormControl,
- InputLabel,
- Select,
- MenuItem,
- FormControlLabel,
- Checkbox,
- FormGroup,
- TextField,
- Button,
- InputAdornment,
- IconButton,
- OutlinedInput,
-} from '@mui/material';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
-import CachedIcon from '@mui/icons-material/Cached';
-import CloseIcon from '@mui/icons-material/Close';
-import { useDispatch, useSelector } from 'react-redux';
-import dayjs from 'dayjs';
-import EditItemView from './components/EditItemView';
-import EditAttributesAccordion from './components/EditAttributesAccordion';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import useUserAttributes from '../common/attributes/useUserAttributes';
-import { sessionActions } from '../store';
-import SelectField from '../common/components/SelectField';
-import SettingsMenu from './components/SettingsMenu';
-import useCommonUserAttributes from '../common/attributes/useCommonUserAttributes';
-import { useAdministrator, useRestriction, useManager } from '../common/util/permissions';
-import useQuery from '../common/util/useQuery';
-import { useCatch } from '../reactHelper';
-import useMapStyles from '../map/core/useMapStyles';
-import { map } from '../map/core/MapView';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const UserPage = () => {
- const classes = useSettingsStyles();
- const navigate = useNavigate();
- const dispatch = useDispatch();
- const t = useTranslation();
-
- const admin = useAdministrator();
- const manager = useManager();
- const fixedEmail = useRestriction('fixedEmail');
-
- const currentUser = useSelector((state) => state.session.user);
- const registrationEnabled = useSelector((state) => state.session.server.registration);
- const openIdForced = useSelector((state) => state.session.server.openIdForce);
- const totpEnable = useSelector((state) => state.session.server.attributes.totpEnable);
- const totpForce = useSelector((state) => state.session.server.attributes.totpForce);
-
- const mapStyles = useMapStyles();
- const commonUserAttributes = useCommonUserAttributes(t);
- const userAttributes = useUserAttributes(t);
-
- const { id } = useParams();
- const [item, setItem] = useState(id === currentUser.id.toString() ? currentUser : null);
-
- const [deleteEmail, setDeleteEmail] = useState();
- const [deleteFailed, setDeleteFailed] = useState(false);
-
- const handleDelete = useCatch(async () => {
- if (deleteEmail === currentUser.email) {
- setDeleteFailed(false);
- const response = await fetch(`/api/users/${currentUser.id}`, { method: 'DELETE' });
- if (response.ok) {
- navigate('/login');
- dispatch(sessionActions.updateUser(null));
- } else {
- throw Error(await response.text());
- }
- } else {
- setDeleteFailed(true);
- }
- });
-
- const handleGenerateTotp = useCatch(async () => {
- const response = await fetch('/api/users/totp', { method: 'POST' });
- if (response.ok) {
- setItem({ ...item, totpKey: await response.text() });
- } else {
- throw Error(await response.text());
- }
- });
-
- const query = useQuery();
- const [queryHandled, setQueryHandled] = useState(false);
- const attribute = query.get('attribute');
-
- useEffect(() => {
- if (!queryHandled && item && attribute) {
- if (!item.attributes.hasOwnProperty('attribute')) {
- const updatedAttributes = { ...item.attributes };
- updatedAttributes[attribute] = '';
- setItem({ ...item, attributes: updatedAttributes });
- }
- setQueryHandled(true);
- }
- }, [item, queryHandled, setQueryHandled, attribute]);
-
- const onItemSaved = (result) => {
- if (result.id === currentUser.id) {
- dispatch(sessionActions.updateUser(result));
- }
- };
-
- const validate = () => item && item.name && item.email && (item.id || item.password) && (admin || !totpForce || item.totpKey);
-
- return (
- <EditItemView
- endpoint="users"
- item={item}
- setItem={setItem}
- defaultItem={admin ? { deviceLimit: -1 } : {}}
- validate={validate}
- onItemSaved={onItemSaved}
- menu={<SettingsMenu />}
- breadcrumbs={['settingsTitle', 'settingsUser']}
- >
- {item && (
- <>
- <Accordion defaultExpanded={!attribute}>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedRequired')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- value={item.name || ''}
- onChange={(e) => setItem({ ...item, name: e.target.value })}
- label={t('sharedName')}
- />
- <TextField
- value={item.email || ''}
- onChange={(e) => setItem({ ...item, email: e.target.value })}
- label={t('userEmail')}
- disabled={fixedEmail}
- />
- {!openIdForced && (
- <TextField
- type="password"
- onChange={(e) => setItem({ ...item, password: e.target.value })}
- label={t('userPassword')}
- />
- )}
- {totpEnable && (
- <FormControl>
- <InputLabel>{t('loginTotpKey')}</InputLabel>
- <OutlinedInput
- readOnly
- label={t('loginTotpKey')}
- value={item.totpKey || ''}
- endAdornment={(
- <InputAdornment position="end">
- <IconButton size="small" edge="end" onClick={handleGenerateTotp}>
- <CachedIcon fontSize="small" />
- </IconButton>
- <IconButton size="small" edge="end" onClick={() => setItem({ ...item, totpKey: null })}>
- <CloseIcon fontSize="small" />
- </IconButton>
- </InputAdornment>
- )}
- />
- </FormControl>
- )}
- </AccordionDetails>
- </Accordion>
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedPreferences')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- value={item.phone || ''}
- onChange={(e) => setItem({ ...item, phone: e.target.value })}
- label={t('sharedPhone')}
- />
- <FormControl>
- <InputLabel>{t('mapDefault')}</InputLabel>
- <Select
- label={t('mapDefault')}
- value={item.map || 'locationIqStreets'}
- onChange={(e) => setItem({ ...item, map: e.target.value })}
- >
- {mapStyles.filter((style) => style.available).map((style) => (
- <MenuItem key={style.id} value={style.id}>
- <Typography component="span">{style.title}</Typography>
- </MenuItem>
- ))}
- </Select>
- </FormControl>
- <FormControl>
- <InputLabel>{t('settingsCoordinateFormat')}</InputLabel>
- <Select
- label={t('settingsCoordinateFormat')}
- value={item.coordinateFormat || 'dd'}
- onChange={(e) => setItem({ ...item, coordinateFormat: e.target.value })}
- >
- <MenuItem value="dd">{t('sharedDecimalDegrees')}</MenuItem>
- <MenuItem value="ddm">{t('sharedDegreesDecimalMinutes')}</MenuItem>
- <MenuItem value="dms">{t('sharedDegreesMinutesSeconds')}</MenuItem>
- </Select>
- </FormControl>
- <FormControl>
- <InputLabel>{t('settingsSpeedUnit')}</InputLabel>
- <Select
- label={t('settingsSpeedUnit')}
- value={(item.attributes && item.attributes.speedUnit) || 'kn'}
- onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, speedUnit: e.target.value } })}
- >
- <MenuItem value="kn">{t('sharedKn')}</MenuItem>
- <MenuItem value="kmh">{t('sharedKmh')}</MenuItem>
- <MenuItem value="mph">{t('sharedMph')}</MenuItem>
- </Select>
- </FormControl>
- <FormControl>
- <InputLabel>{t('settingsDistanceUnit')}</InputLabel>
- <Select
- label={t('settingsDistanceUnit')}
- value={(item.attributes && item.attributes.distanceUnit) || 'km'}
- onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, distanceUnit: e.target.value } })}
- >
- <MenuItem value="km">{t('sharedKm')}</MenuItem>
- <MenuItem value="mi">{t('sharedMi')}</MenuItem>
- <MenuItem value="nmi">{t('sharedNmi')}</MenuItem>
- </Select>
- </FormControl>
- <FormControl>
- <InputLabel>{t('settingsAltitudeUnit')}</InputLabel>
- <Select
- label={t('settingsAltitudeUnit')}
- value={(item.attributes && item.attributes.altitudeUnit) || 'm'}
- onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, altitudeUnit: e.target.value } })}
- >
- <MenuItem value="m">{t('sharedMeters')}</MenuItem>
- <MenuItem value="ft">{t('sharedFeet')}</MenuItem>
- </Select>
- </FormControl>
- <FormControl>
- <InputLabel>{t('settingsVolumeUnit')}</InputLabel>
- <Select
- label={t('settingsVolumeUnit')}
- value={(item.attributes && item.attributes.volumeUnit) || 'ltr'}
- onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, volumeUnit: e.target.value } })}
- >
- <MenuItem value="ltr">{t('sharedLiter')}</MenuItem>
- <MenuItem value="usGal">{t('sharedUsGallon')}</MenuItem>
- <MenuItem value="impGal">{t('sharedImpGallon')}</MenuItem>
- </Select>
- </FormControl>
- <SelectField
- value={item.attributes && item.attributes.timezone}
- onChange={(e) => setItem({ ...item, attributes: { ...item.attributes, timezone: e.target.value } })}
- endpoint="/api/server/timezones"
- keyGetter={(it) => it}
- titleGetter={(it) => it}
- label={t('sharedTimezone')}
- />
- <TextField
- value={item.poiLayer || ''}
- onChange={(e) => setItem({ ...item, poiLayer: e.target.value })}
- label={t('mapPoiLayer')}
- />
- <FormGroup>
- <FormControlLabel
- control={<Checkbox checked={item.twelveHourFormat} onChange={(e) => setItem({ ...item, twelveHourFormat: e.target.checked })} />}
- label={t('settingsTwelveHourFormat')}
- />
- </FormGroup>
- </AccordionDetails>
- </Accordion>
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedLocation')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- type="number"
- value={item.latitude || 0}
- onChange={(e) => setItem({ ...item, latitude: Number(e.target.value) })}
- label={t('positionLatitude')}
- />
- <TextField
- type="number"
- value={item.longitude || 0}
- onChange={(e) => setItem({ ...item, longitude: Number(e.target.value) })}
- label={t('positionLongitude')}
- />
- <TextField
- type="number"
- value={item.zoom || 0}
- onChange={(e) => setItem({ ...item, zoom: Number(e.target.value) })}
- label={t('serverZoom')}
- />
- <Button
- variant="outlined"
- color="primary"
- onClick={() => {
- const { lng, lat } = map.getCenter();
- setItem({
- ...item,
- latitude: Number(lat.toFixed(6)),
- longitude: Number(lng.toFixed(6)),
- zoom: Number(map.getZoom().toFixed(1)),
- });
- }}
- >
- {t('mapCurrentLocation')}
- </Button>
- </AccordionDetails>
- </Accordion>
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedPermissions')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- label={t('userExpirationTime')}
- type="date"
- value={(item.expirationTime && dayjs(item.expirationTime).locale('en').format('YYYY-MM-DD')) || '2099-01-01'}
- onChange={(e) => setItem({ ...item, expirationTime: dayjs(e.target.value, 'YYYY-MM-DD').locale('en').format() })}
- disabled={!manager}
- />
- <TextField
- type="number"
- value={item.deviceLimit || 0}
- onChange={(e) => setItem({ ...item, deviceLimit: Number(e.target.value) })}
- label={t('userDeviceLimit')}
- disabled={!admin}
- />
- <TextField
- type="number"
- value={item.userLimit || 0}
- onChange={(e) => setItem({ ...item, userLimit: Number(e.target.value) })}
- label={t('userUserLimit')}
- disabled={!admin}
- />
- <FormGroup>
- <FormControlLabel
- control={<Checkbox checked={item.disabled} onChange={(e) => setItem({ ...item, disabled: e.target.checked })} />}
- label={t('sharedDisabled')}
- disabled={!manager}
- />
- <FormControlLabel
- control={<Checkbox checked={item.administrator} onChange={(e) => setItem({ ...item, administrator: e.target.checked })} />}
- label={t('userAdmin')}
- disabled={!admin}
- />
- <FormControlLabel
- control={<Checkbox checked={item.readonly} onChange={(e) => setItem({ ...item, readonly: e.target.checked })} />}
- label={t('serverReadonly')}
- disabled={!manager}
- />
- <FormControlLabel
- control={<Checkbox checked={item.deviceReadonly} onChange={(e) => setItem({ ...item, deviceReadonly: e.target.checked })} />}
- label={t('userDeviceReadonly')}
- disabled={!manager}
- />
- <FormControlLabel
- control={<Checkbox checked={item.limitCommands} onChange={(e) => setItem({ ...item, limitCommands: e.target.checked })} />}
- label={t('userLimitCommands')}
- disabled={!manager}
- />
- <FormControlLabel
- control={<Checkbox checked={item.disableReports} onChange={(e) => setItem({ ...item, disableReports: e.target.checked })} />}
- label={t('userDisableReports')}
- disabled={!manager}
- />
- <FormControlLabel
- control={<Checkbox checked={item.fixedEmail} onChange={(e) => setItem({ ...item, fixedEmail: e.target.checked })} />}
- label={t('userFixedEmail')}
- disabled={!manager}
- />
- </FormGroup>
- </AccordionDetails>
- </Accordion>
- <EditAttributesAccordion
- attribute={attribute}
- attributes={item.attributes}
- setAttributes={(attributes) => setItem({ ...item, attributes })}
- definitions={{ ...commonUserAttributes, ...userAttributes }}
- focusAttribute={attribute}
- />
- {registrationEnabled && item.id === currentUser.id && !manager && (
- <Accordion>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1" color="error">
- {t('userDeleteAccount')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- <TextField
- value={deleteEmail}
- onChange={(e) => setDeleteEmail(e.target.value)}
- label={t('userEmail')}
- error={deleteFailed}
- />
- <Button
- variant="outlined"
- color="error"
- onClick={handleDelete}
- startIcon={<DeleteForeverIcon />}
- >
- {t('userDeleteAccount')}
- </Button>
- </AccordionDetails>
- </Accordion>
- )}
- </>
- )}
- </EditItemView>
- );
-};
-
-export default UserPage;
diff --git a/modern/src/settings/UsersPage.jsx b/modern/src/settings/UsersPage.jsx
deleted file mode 100644
index 2941965b..00000000
--- a/modern/src/settings/UsersPage.jsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import React, { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-import {
- Table, TableRow, TableCell, TableHead, TableBody, Switch, TableFooter, FormControlLabel,
-} from '@mui/material';
-import LoginIcon from '@mui/icons-material/Login';
-import LinkIcon from '@mui/icons-material/Link';
-import { useCatch, useEffectAsync } from '../reactHelper';
-import { formatBoolean, formatTime } from '../common/util/formatter';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import PageLayout from '../common/components/PageLayout';
-import SettingsMenu from './components/SettingsMenu';
-import CollectionFab from './components/CollectionFab';
-import CollectionActions from './components/CollectionActions';
-import TableShimmer from '../common/components/TableShimmer';
-import { useManager } from '../common/util/permissions';
-import SearchHeader, { filterByKeyword } from './components/SearchHeader';
-import { usePreference } from '../common/util/preferences';
-import useSettingsStyles from './common/useSettingsStyles';
-
-const UsersPage = () => {
- const classes = useSettingsStyles();
- const navigate = useNavigate();
- const t = useTranslation();
-
- const manager = useManager();
-
- const hours12 = usePreference('twelveHourFormat');
-
- const [timestamp, setTimestamp] = useState(Date.now());
- const [items, setItems] = useState([]);
- const [searchKeyword, setSearchKeyword] = useState('');
- const [loading, setLoading] = useState(false);
- const [temporary, setTemporary] = useState(false);
-
- const handleLogin = useCatch(async (userId) => {
- const response = await fetch(`/api/session/${userId}`);
- if (response.ok) {
- window.location.replace('/');
- } else {
- throw Error(await response.text());
- }
- });
-
- const actionLogin = {
- key: 'login',
- title: t('loginLogin'),
- icon: <LoginIcon fontSize="small" />,
- handler: handleLogin,
- };
-
- const actionConnections = {
- key: 'connections',
- title: t('sharedConnections'),
- icon: <LinkIcon fontSize="small" />,
- handler: (userId) => navigate(`/settings/user/${userId}/connections`),
- };
-
- useEffectAsync(async () => {
- setLoading(true);
- try {
- const response = await fetch('/api/users');
- if (response.ok) {
- setItems(await response.json());
- } else {
- throw Error(await response.text());
- }
- } finally {
- setLoading(false);
- }
- }, [timestamp]);
-
- return (
- <PageLayout menu={<SettingsMenu />} breadcrumbs={['settingsTitle', 'settingsUsers']}>
- <SearchHeader keyword={searchKeyword} setKeyword={setSearchKeyword} />
- <Table className={classes.table}>
- <TableHead>
- <TableRow>
- <TableCell>{t('sharedName')}</TableCell>
- <TableCell>{t('userEmail')}</TableCell>
- <TableCell>{t('userAdmin')}</TableCell>
- <TableCell>{t('sharedDisabled')}</TableCell>
- <TableCell>{t('userExpirationTime')}</TableCell>
- <TableCell className={classes.columnAction} />
- </TableRow>
- </TableHead>
- <TableBody>
- {!loading ? items.filter((u) => temporary || !u.temporary).filter(filterByKeyword(searchKeyword)).map((item) => (
- <TableRow key={item.id}>
- <TableCell>{item.name}</TableCell>
- <TableCell>{item.email}</TableCell>
- <TableCell>{formatBoolean(item.administrator, t)}</TableCell>
- <TableCell>{formatBoolean(item.disabled, t)}</TableCell>
- <TableCell>{formatTime(item.expirationTime, 'date', hours12)}</TableCell>
- <TableCell className={classes.columnAction} padding="none">
- <CollectionActions
- itemId={item.id}
- editPath="/settings/user"
- endpoint="users"
- setTimestamp={setTimestamp}
- customActions={manager ? [actionLogin, actionConnections] : [actionConnections]}
- />
- </TableCell>
- </TableRow>
- )) : (<TableShimmer columns={6} endAction />)}
- </TableBody>
- <TableFooter>
- <TableRow>
- <TableCell colSpan={6} align="right">
- <FormControlLabel
- control={(
- <Switch
- value={temporary}
- onChange={(e) => setTemporary(e.target.checked)}
- size="small"
- />
- )}
- label={t('userTemporary')}
- labelPlacement="start"
- />
- </TableCell>
- </TableRow>
- </TableFooter>
- </Table>
- <CollectionFab editPath="/settings/user" />
- </PageLayout>
- );
-};
-
-export default UsersPage;
diff --git a/modern/src/settings/common/useSettingsStyles.js b/modern/src/settings/common/useSettingsStyles.js
deleted file mode 100644
index b276e0b7..00000000
--- a/modern/src/settings/common/useSettingsStyles.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { makeStyles } from '@mui/styles';
-
-export default makeStyles((theme) => ({
- table: {
- marginBottom: theme.spacing(10),
- },
- columnAction: {
- width: '1%',
- paddingRight: theme.spacing(1),
- },
- container: {
- marginTop: theme.spacing(2),
- },
- buttons: {
- marginTop: theme.spacing(2),
- marginBottom: theme.spacing(2),
- display: 'flex',
- justifyContent: 'space-evenly',
- '& > *': {
- flexBasis: '33%',
- },
- },
- details: {
- display: 'flex',
- flexDirection: 'column',
- gap: theme.spacing(2),
- paddingBottom: theme.spacing(3),
- },
- verticalActions: {
- display: 'flex',
- flexDirection: 'column',
- },
-}));
diff --git a/modern/src/settings/components/AddAttributeDialog.jsx b/modern/src/settings/components/AddAttributeDialog.jsx
deleted file mode 100644
index 86ff64ea..00000000
--- a/modern/src/settings/components/AddAttributeDialog.jsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import React, { useState } from 'react';
-import {
- Button, Dialog, DialogActions, DialogContent, FormControl, InputLabel, MenuItem, Select, TextField, Autocomplete,
-} from '@mui/material';
-
-import { createFilterOptions } from '@mui/material/useAutocomplete';
-import { makeStyles } from '@mui/styles';
-import { useTranslation } from '../../common/components/LocalizationProvider';
-
-const useStyles = makeStyles((theme) => ({
- details: {
- display: 'flex',
- flexDirection: 'column',
- gap: theme.spacing(2),
- paddingBottom: theme.spacing(1),
- paddingTop: theme.spacing(3),
- },
-}));
-
-const AddAttributeDialog = ({ open, onResult, definitions }) => {
- const classes = useStyles();
- const t = useTranslation();
-
- const filter = createFilterOptions({
- stringify: (option) => option.name,
- });
-
- const options = Object.entries(definitions).map(([key, value]) => ({
- key,
- name: value.name,
- type: value.type,
- }));
-
- const [key, setKey] = useState();
- const [type, setType] = useState('string');
-
- return (
- <Dialog open={open} fullWidth maxWidth="xs">
- <DialogContent className={classes.details}>
- <Autocomplete
- onChange={(_, option) => {
- setKey(option && typeof option === 'object' ? option.key : option);
- if (option && option.type) {
- setType(option.type);
- }
- }}
- filterOptions={(options, params) => {
- const filtered = filter(options, params);
- if (params.inputValue) {
- filtered.push({
- key: params.inputValue,
- name: params.inputValue,
- });
- }
- return filtered;
- }}
- options={options}
- getOptionLabel={(option) => (option && typeof option === 'object' ? option.name : option)}
- renderOption={(props, option) => (
- <li {...props}>
- {option.name}
- </li>
- )}
- renderInput={(params) => (
- <TextField {...params} label={t('sharedAttribute')} />
- )}
- freeSolo
- />
- <FormControl
- fullWidth
- disabled={key in definitions}
- >
- <InputLabel>{t('sharedType')}</InputLabel>
- <Select
- label={t('sharedType')}
- value={type}
- onChange={(e) => setType(e.target.value)}
- >
- <MenuItem value="string">{t('sharedTypeString')}</MenuItem>
- <MenuItem value="number">{t('sharedTypeNumber')}</MenuItem>
- <MenuItem value="boolean">{t('sharedTypeBoolean')}</MenuItem>
- </Select>
- </FormControl>
- </DialogContent>
- <DialogActions>
- <Button
- color="primary"
- disabled={!key}
- onClick={() => onResult({ key, type })}
- >
- {t('sharedAdd')}
- </Button>
- <Button
- autoFocus
- onClick={() => onResult(null)}
- >
- {t('sharedCancel')}
- </Button>
- </DialogActions>
- </Dialog>
- );
-};
-
-export default AddAttributeDialog;
diff --git a/modern/src/settings/components/BaseCommandView.jsx b/modern/src/settings/components/BaseCommandView.jsx
deleted file mode 100644
index bb70c3b9..00000000
--- a/modern/src/settings/components/BaseCommandView.jsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useSelector } from 'react-redux';
-import {
- TextField, FormControlLabel, Checkbox,
-} from '@mui/material';
-import { useTranslation } from '../../common/components/LocalizationProvider';
-import SelectField from '../../common/components/SelectField';
-import { prefixString } from '../../common/util/stringUtils';
-import useCommandAttributes from '../../common/attributes/useCommandAttributes';
-
-const BaseCommandView = ({ deviceId, item, setItem }) => {
- const t = useTranslation();
-
- const textEnabled = useSelector((state) => state.session.server.textEnabled);
-
- const availableAttributes = useCommandAttributes(t);
-
- const [attributes, setAttributes] = useState([]);
-
- useEffect(() => {
- if (item && item.type) {
- setAttributes(availableAttributes[item.type] || []);
- } else {
- setAttributes([]);
- }
- }, [availableAttributes, item]);
-
- return (
- <>
- <SelectField
- value={item.type}
- onChange={(e) => setItem({ ...item, type: e.target.value, attributes: {} })}
- endpoint={deviceId ? `/api/commands/types?${new URLSearchParams({ deviceId }).toString()}` : '/api/commands/types'}
- keyGetter={(it) => it.type}
- titleGetter={(it) => t(prefixString('command', it.type))}
- label={t('sharedType')}
- />
- {attributes.map(({ key, name, type }) => {
- if (type === 'boolean') {
- return (
- <FormControlLabel
- control={(
- <Checkbox
- checked={item.attributes[key]}
- onChange={(e) => {
- const updateItem = { ...item, attributes: { ...item.attributes } };
- updateItem.attributes[key] = e.target.checked;
- setItem(updateItem);
- }}
- />
- )}
- label={name}
- />
- );
- }
- return (
- <TextField
- type={type === 'number' ? 'number' : 'text'}
- value={item.attributes[key]}
- onChange={(e) => {
- const updateItem = { ...item, attributes: { ...item.attributes } };
- updateItem.attributes[key] = type === 'number' ? Number(e.target.value) : e.target.value;
- setItem(updateItem);
- }}
- label={name}
- />
- );
- })}
- {textEnabled && (
- <FormControlLabel
- control={<Checkbox checked={item.textChannel} onChange={(event) => setItem({ ...item, textChannel: event.target.checked })} />}
- label={t('commandSendSms')}
- />
- )}
- </>
- );
-};
-
-export default BaseCommandView;
diff --git a/modern/src/settings/components/CollectionActions.jsx b/modern/src/settings/components/CollectionActions.jsx
deleted file mode 100644
index 666052d5..00000000
--- a/modern/src/settings/components/CollectionActions.jsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import React, { useState } from 'react';
-import {
- IconButton, Menu, MenuItem, useMediaQuery, useTheme,
-} from '@mui/material';
-import Tooltip from '@mui/material/Tooltip';
-import MoreVertIcon from '@mui/icons-material/MoreVert';
-import EditIcon from '@mui/icons-material/Edit';
-import DeleteIcon from '@mui/icons-material/Delete';
-import { useNavigate } from 'react-router-dom';
-import { makeStyles } from '@mui/styles';
-import RemoveDialog from '../../common/components/RemoveDialog';
-import { useTranslation } from '../../common/components/LocalizationProvider';
-
-const useStyles = makeStyles(() => ({
- row: {
- display: 'flex',
- },
-}));
-
-const CollectionActions = ({
- itemId, editPath, endpoint, setTimestamp, customActions, readonly,
-}) => {
- const theme = useTheme();
- const classes = useStyles();
- const navigate = useNavigate();
- const t = useTranslation();
-
- const phone = useMediaQuery(theme.breakpoints.down('sm'));
-
- const [menuAnchorEl, setMenuAnchorEl] = useState(null);
- const [removing, setRemoving] = useState(false);
-
- const handleEdit = () => {
- navigate(`${editPath}/${itemId}`);
- setMenuAnchorEl(null);
- };
-
- const handleRemove = () => {
- setRemoving(true);
- setMenuAnchorEl(null);
- };
-
- const handleCustom = (action) => {
- action.handler(itemId);
- setMenuAnchorEl(null);
- };
-
- const handleRemoveResult = (removed) => {
- setRemoving(false);
- if (removed) {
- setTimestamp(Date.now());
- }
- };
-
- return (
- <>
- {phone ? (
- <>
- <IconButton size="small" onClick={(event) => setMenuAnchorEl(event.currentTarget)}>
- <MoreVertIcon fontSize="small" />
- </IconButton>
- <Menu open={!!menuAnchorEl} anchorEl={menuAnchorEl} onClose={() => setMenuAnchorEl(null)}>
- {customActions && customActions.map((action) => (
- <MenuItem onClick={() => handleCustom(action)} key={action.key}>{action.title}</MenuItem>
- ))}
- {!readonly && (
- <>
- <MenuItem onClick={handleEdit}>{t('sharedEdit')}</MenuItem>
- <MenuItem onClick={handleRemove}>{t('sharedRemove')}</MenuItem>
- </>
- )}
- </Menu>
- </>
- ) : (
- <div className={classes.row}>
- {customActions && customActions.map((action) => (
- <Tooltip title={action.title} key={action.key}>
- <IconButton size="small" onClick={() => handleCustom(action)}>
- {action.icon}
- </IconButton>
- </Tooltip>
- ))}
- {!readonly && (
- <>
- <Tooltip title={t('sharedEdit')}>
- <IconButton size="small" onClick={handleEdit}>
- <EditIcon fontSize="small" />
- </IconButton>
- </Tooltip>
- <Tooltip title={t('sharedRemove')}>
- <IconButton size="small" onClick={handleRemove}>
- <DeleteIcon fontSize="small" />
- </IconButton>
- </Tooltip>
- </>
- )}
- </div>
- )}
- <RemoveDialog style={{ transform: 'none' }} open={removing} endpoint={endpoint} itemId={itemId} onResult={handleRemoveResult} />
- </>
- );
-};
-
-export default CollectionActions;
diff --git a/modern/src/settings/components/CollectionFab.jsx b/modern/src/settings/components/CollectionFab.jsx
deleted file mode 100644
index 3c1fa783..00000000
--- a/modern/src/settings/components/CollectionFab.jsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react';
-import { Fab } from '@mui/material';
-import makeStyles from '@mui/styles/makeStyles';
-import AddIcon from '@mui/icons-material/Add';
-import { useNavigate } from 'react-router-dom';
-import { useRestriction } from '../../common/util/permissions';
-
-const useStyles = makeStyles((theme) => ({
- fab: {
- position: 'fixed',
- bottom: theme.spacing(2),
- right: theme.spacing(2),
- [theme.breakpoints.down('md')]: {
- bottom: `calc(${theme.dimensions.bottomBarHeight}px + ${theme.spacing(2)})`,
- },
- },
-}));
-
-const CollectionFab = ({ editPath, disabled }) => {
- const classes = useStyles();
- const navigate = useNavigate();
-
- const readonly = useRestriction('readonly');
-
- if (!readonly && !disabled) {
- return (
- <Fab size="medium" color="primary" className={classes.fab} onClick={() => navigate(editPath)}>
- <AddIcon />
- </Fab>
- );
- }
- return '';
-};
-
-export default CollectionFab;
diff --git a/modern/src/settings/components/EditAttributesAccordion.jsx b/modern/src/settings/components/EditAttributesAccordion.jsx
deleted file mode 100644
index 4d4ae254..00000000
--- a/modern/src/settings/components/EditAttributesAccordion.jsx
+++ /dev/null
@@ -1,217 +0,0 @@
-import React, { useState } from 'react';
-
-import {
- Button,
- Checkbox,
- OutlinedInput,
- FormControl,
- FormControlLabel,
- Grid,
- IconButton,
- InputAdornment,
- InputLabel,
- Accordion,
- AccordionSummary,
- Typography,
- AccordionDetails,
-} from '@mui/material';
-import CloseIcon from '@mui/icons-material/Close';
-import AddIcon from '@mui/icons-material/Add';
-import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
-import AddAttributeDialog from './AddAttributeDialog';
-import { useTranslation } from '../../common/components/LocalizationProvider';
-import { useAttributePreference } from '../../common/util/preferences';
-import {
- distanceFromMeters, distanceToMeters, distanceUnitString, speedFromKnots, speedToKnots, speedUnitString, volumeFromLiters, volumeToLiters, volumeUnitString,
-} from '../../common/util/converter';
-import useFeatures from '../../common/util/useFeatures';
-import useSettingsStyles from '../common/useSettingsStyles';
-
-const EditAttributesAccordion = ({ attribute, attributes, setAttributes, definitions, focusAttribute }) => {
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const features = useFeatures();
-
- const speedUnit = useAttributePreference('speedUnit');
- const distanceUnit = useAttributePreference('distanceUnit');
- const volumeUnit = useAttributePreference('volumeUnit');
-
- const [addDialogShown, setAddDialogShown] = useState(false);
-
- const updateAttribute = (key, value, type, subtype) => {
- const updatedAttributes = { ...attributes };
- switch (subtype) {
- case 'speed':
- updatedAttributes[key] = speedToKnots(Number(value), speedUnit);
- break;
- case 'distance':
- updatedAttributes[key] = distanceToMeters(Number(value), distanceUnit);
- break;
- case 'volume':
- updatedAttributes[key] = volumeToLiters(Number(value), volumeUnit);
- break;
- default:
- updatedAttributes[key] = type === 'number' ? Number(value) : value;
- break;
- }
- setAttributes(updatedAttributes);
- };
-
- const deleteAttribute = (key) => {
- const updatedAttributes = { ...attributes };
- delete updatedAttributes[key];
- setAttributes(updatedAttributes);
- };
-
- const getAttributeName = (key, subtype) => {
- const definition = definitions[key];
- const name = definition ? definition.name : key;
- switch (subtype) {
- case 'speed':
- return `${name} (${speedUnitString(speedUnit, t)})`;
- case 'distance':
- return `${name} (${distanceUnitString(distanceUnit, t)})`;
- case 'volume':
- return `${name} (${volumeUnitString(volumeUnit, t)})`;
- default:
- return name;
- }
- };
-
- const getAttributeType = (value) => {
- if (typeof value === 'number') {
- return 'number';
- } if (typeof value === 'boolean') {
- return 'boolean';
- }
- return 'string';
- };
-
- const getAttributeSubtype = (key) => {
- const definition = definitions[key];
- return definition && definition.subtype;
- };
-
- const getDisplayValue = (value, subtype) => {
- if (value) {
- switch (subtype) {
- case 'speed':
- return speedFromKnots(value, speedUnit);
- case 'distance':
- return distanceFromMeters(value, distanceUnit);
- case 'volume':
- return volumeFromLiters(value, volumeUnit);
- default:
- return value;
- }
- }
- return '';
- };
-
- const convertToList = (attributes) => {
- const booleanList = [];
- const otherList = [];
- const excludeAttributes = ['speedUnit', 'distanceUnit', 'volumeUnit', 'timezone'];
- Object.keys(attributes || []).filter((key) => !excludeAttributes.includes(key)).forEach((key) => {
- const value = attributes[key];
- const type = getAttributeType(value);
- const subtype = getAttributeSubtype(key);
- if (type === 'boolean') {
- booleanList.push({
- key, value, type, subtype,
- });
- } else {
- otherList.push({
- key, value, type, subtype,
- });
- }
- });
- return [...otherList, ...booleanList];
- };
-
- const handleAddResult = (definition) => {
- setAddDialogShown(false);
- if (definition) {
- switch (definition.type) {
- case 'number':
- updateAttribute(definition.key, 0);
- break;
- case 'boolean':
- updateAttribute(definition.key, false);
- break;
- default:
- updateAttribute(definition.key, '');
- break;
- }
- }
- };
-
- return features.disableAttributes ? '' : (
- <Accordion defaultExpanded={!!attribute}>
- <AccordionSummary expandIcon={<ExpandMoreIcon />}>
- <Typography variant="subtitle1">
- {t('sharedAttributes')}
- </Typography>
- </AccordionSummary>
- <AccordionDetails className={classes.details}>
- {convertToList(attributes).map(({
- key, value, type, subtype,
- }) => {
- if (type === 'boolean') {
- return (
- <Grid container direction="row" justifyContent="space-between" key={key}>
- <FormControlLabel
- control={(
- <Checkbox
- checked={value}
- onChange={(e) => updateAttribute(key, e.target.checked)}
- />
- )}
- label={getAttributeName(key, subtype)}
- />
- <IconButton size="small" className={classes.removeButton} onClick={() => deleteAttribute(key)}>
- <CloseIcon fontSize="small" />
- </IconButton>
- </Grid>
- );
- }
- return (
- <FormControl key={key}>
- <InputLabel>{getAttributeName(key, subtype)}</InputLabel>
- <OutlinedInput
- label={getAttributeName(key, subtype)}
- type={type === 'number' ? 'number' : 'text'}
- value={getDisplayValue(value, subtype)}
- onChange={(e) => updateAttribute(key, e.target.value, type, subtype)}
- autoFocus={focusAttribute === key}
- endAdornment={(
- <InputAdornment position="end">
- <IconButton size="small" edge="end" onClick={() => deleteAttribute(key)}>
- <CloseIcon fontSize="small" />
- </IconButton>
- </InputAdornment>
- )}
- />
- </FormControl>
- );
- })}
- <Button
- variant="outlined"
- color="primary"
- onClick={() => setAddDialogShown(true)}
- startIcon={<AddIcon />}
- >
- {t('sharedAdd')}
- </Button>
- <AddAttributeDialog
- open={addDialogShown}
- onResult={handleAddResult}
- definitions={definitions}
- />
- </AccordionDetails>
- </Accordion>
- );
-};
-
-export default EditAttributesAccordion;
diff --git a/modern/src/settings/components/EditItemView.jsx b/modern/src/settings/components/EditItemView.jsx
deleted file mode 100644
index 61bc4161..00000000
--- a/modern/src/settings/components/EditItemView.jsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import React from 'react';
-import { useNavigate, useParams } from 'react-router-dom';
-import {
- Container, Button, Accordion, AccordionDetails, AccordionSummary, Skeleton, Typography, TextField,
-} from '@mui/material';
-import { useCatch, useEffectAsync } from '../../reactHelper';
-import { useTranslation } from '../../common/components/LocalizationProvider';
-import PageLayout from '../../common/components/PageLayout';
-import useSettingsStyles from '../common/useSettingsStyles';
-
-const EditItemView = ({
- children, endpoint, item, setItem, defaultItem, validate, onItemSaved, menu, breadcrumbs,
-}) => {
- const navigate = useNavigate();
- const classes = useSettingsStyles();
- const t = useTranslation();
-
- const { id } = useParams();
-
- useEffectAsync(async () => {
- if (!item) {
- if (id) {
- const response = await fetch(`/api/${endpoint}/${id}`);
- if (response.ok) {
- setItem(await response.json());
- } else {
- throw Error(await response.text());
- }
- } else {
- setItem(defaultItem || {});
- }
- }
- }, [id, item, defaultItem]);
-
- const handleSave = useCatch(async () => {
- let url = `/api/${endpoint}`;
- if (id) {
- url += `/${id}`;
- }
-
- const response = await fetch(url, {
- method: !id ? 'POST' : 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(item),
- });
-
- if (response.ok) {
- if (onItemSaved) {
- onItemSaved(await response.json());
- }
- navigate(-1);
- } else {
- throw Error(await response.text());
- }
- });
-
- return (
- <PageLayout menu={menu} breadcrumbs={breadcrumbs}>
- <Container maxWidth="xs" className={classes.container}>
- {item ? children : (
- <Accordion defaultExpanded>
- <AccordionSummary>
- <Typography variant="subtitle1">
- <Skeleton width="10em" />
- </Typography>
- </AccordionSummary>
- <AccordionDetails>
- {[...Array(3)].map((_, i) => (
- <Skeleton key={-i} width="100%">
- <TextField />
- </Skeleton>
- ))}
- </AccordionDetails>
- </Accordion>
- )}
- <div className={classes.buttons}>
- <Button
- type="button"
- color="primary"
- variant="outlined"
- onClick={() => navigate(-1)}
- disabled={!item}
- >
- {t('sharedCancel')}
- </Button>
- <Button
- type="button"
- color="primary"
- variant="contained"
- onClick={handleSave}
- disabled={!item || !validate()}
- >
- {t('sharedSave')}
- </Button>
- </div>
- </Container>
- </PageLayout>
- );
-};
-
-export default EditItemView;
diff --git a/modern/src/settings/components/SearchHeader.jsx b/modern/src/settings/components/SearchHeader.jsx
deleted file mode 100644
index 25757ed2..00000000
--- a/modern/src/settings/components/SearchHeader.jsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-import { TextField, useTheme, useMediaQuery } from '@mui/material';
-import makeStyles from '@mui/styles/makeStyles';
-import { useTranslation } from '../../common/components/LocalizationProvider';
-
-export const filterByKeyword = (keyword) => (item) => !keyword || JSON.stringify(item).toLowerCase().includes(keyword.toLowerCase());
-
-const useStyles = makeStyles((theme) => ({
- header: {
- position: 'sticky',
- left: 0,
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'stretch',
- padding: theme.spacing(3, 2, 2),
- },
-}));
-
-const SearchHeader = ({ keyword, setKeyword }) => {
- const theme = useTheme();
- const classes = useStyles();
- const t = useTranslation();
-
- const phone = useMediaQuery(theme.breakpoints.down('sm'));
-
- return phone ? (
- <div className={classes.header}>
- <TextField
- variant="outlined"
- placeholder={t('sharedSearch')}
- value={keyword}
- onChange={(e) => setKeyword(e.target.value)}
- />
- </div>
- ) : '';
-};
-
-export default SearchHeader;
diff --git a/modern/src/settings/components/SettingsMenu.jsx b/modern/src/settings/components/SettingsMenu.jsx
deleted file mode 100644
index 7085d47a..00000000
--- a/modern/src/settings/components/SettingsMenu.jsx
+++ /dev/null
@@ -1,173 +0,0 @@
-import React from 'react';
-import {
- Divider, List, ListItemButton, ListItemIcon, ListItemText,
-} from '@mui/material';
-import SettingsIcon from '@mui/icons-material/Settings';
-import CreateIcon from '@mui/icons-material/Create';
-import NotificationsIcon from '@mui/icons-material/Notifications';
-import FolderIcon from '@mui/icons-material/Folder';
-import PersonIcon from '@mui/icons-material/Person';
-import StorageIcon from '@mui/icons-material/Storage';
-import BuildIcon from '@mui/icons-material/Build';
-import PeopleIcon from '@mui/icons-material/People';
-import TodayIcon from '@mui/icons-material/Today';
-import PublishIcon from '@mui/icons-material/Publish';
-import SmartphoneIcon from '@mui/icons-material/Smartphone';
-import HelpIcon from '@mui/icons-material/Help';
-import CampaignIcon from '@mui/icons-material/Campaign';
-import { Link, useLocation } from 'react-router-dom';
-import { useSelector } from 'react-redux';
-import { useTranslation } from '../../common/components/LocalizationProvider';
-import {
- useAdministrator, useManager, useRestriction,
-} from '../../common/util/permissions';
-import useFeatures from '../../common/util/useFeatures';
-
-const MenuItem = ({
- title, link, icon, selected,
-}) => (
- <ListItemButton key={link} component={Link} to={link} selected={selected}>
- <ListItemIcon>{icon}</ListItemIcon>
- <ListItemText primary={title} />
- </ListItemButton>
-);
-
-const SettingsMenu = () => {
- const t = useTranslation();
- const location = useLocation();
-
- const readonly = useRestriction('readonly');
- const admin = useAdministrator();
- const manager = useManager();
- const userId = useSelector((state) => state.session.user.id);
- const supportLink = useSelector((state) => state.session.server.attributes.support);
-
- const features = useFeatures();
-
- return (
- <>
- <List>
- <MenuItem
- title={t('sharedPreferences')}
- link="/settings/preferences"
- icon={<SettingsIcon />}
- selected={location.pathname === '/settings/preferences'}
- />
- {!readonly && (
- <>
- <MenuItem
- title={t('sharedNotifications')}
- link="/settings/notifications"
- icon={<NotificationsIcon />}
- selected={location.pathname.startsWith('/settings/notification')}
- />
- <MenuItem
- title={t('settingsUser')}
- link={`/settings/user/${userId}`}
- icon={<PersonIcon />}
- selected={location.pathname === `/settings/user/${userId}`}
- />
- <MenuItem
- title={t('deviceTitle')}
- link="/settings/devices"
- icon={<SmartphoneIcon />}
- selected={location.pathname.startsWith('/settings/device')}
- />
- <MenuItem
- title={t('sharedGeofences')}
- link="/geofences"
- icon={<CreateIcon />}
- selected={location.pathname.startsWith('/settings/geofence')}
- />
- {!features.disableGroups && (
- <MenuItem
- title={t('settingsGroups')}
- link="/settings/groups"
- icon={<FolderIcon />}
- selected={location.pathname.startsWith('/settings/group')}
- />
- )}
- {!features.disableDrivers && (
- <MenuItem
- title={t('sharedDrivers')}
- link="/settings/drivers"
- icon={<PersonIcon />}
- selected={location.pathname.startsWith('/settings/driver')}
- />
- )}
- {!features.disableCalendars && (
- <MenuItem
- title={t('sharedCalendars')}
- link="/settings/calendars"
- icon={<TodayIcon />}
- selected={location.pathname.startsWith('/settings/calendar')}
- />
- )}
- {!features.disableComputedAttributes && (
- <MenuItem
- title={t('sharedComputedAttributes')}
- link="/settings/attributes"
- icon={<StorageIcon />}
- selected={location.pathname.startsWith('/settings/attribute')}
- />
- )}
- {!features.disableMaintenance && (
- <MenuItem
- title={t('sharedMaintenance')}
- link="/settings/maintenances"
- icon={<BuildIcon />}
- selected={location.pathname.startsWith('/settings/maintenance')}
- />
- )}
- {!features.disableSavedCommands && (
- <MenuItem
- title={t('sharedSavedCommands')}
- link="/settings/commands"
- icon={<PublishIcon />}
- selected={location.pathname.startsWith('/settings/command')}
- />
- )}
- {supportLink && (
- <MenuItem
- title={t('settingsSupport')}
- link={supportLink}
- icon={<HelpIcon />}
- />
- )}
- </>
- )}
- </List>
- {manager && (
- <>
- <Divider />
- <List>
- {admin && (
- <>
- <MenuItem
- title={t('serverAnnouncement')}
- link="/settings/announcement"
- icon={<CampaignIcon />}
- selected={location.pathname === '/settings/announcement'}
- />
- <MenuItem
- title={t('settingsServer')}
- link="/settings/server"
- icon={<StorageIcon />}
- selected={location.pathname === '/settings/server'}
- />
- </>
- )}
- <MenuItem
- title={t('settingsUsers')}
- link="/settings/users"
- icon={<PeopleIcon />}
- selected={location.pathname.startsWith('/settings/user') && location.pathname !== `/settings/user/${userId}`}
- />
- </List>
- </>
- )}
- </>
- );
-};
-
-export default SettingsMenu;