aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modern/src/App.js4
-rw-r--r--modern/src/attributes/EditAttributesView.js2
-rw-r--r--modern/src/attributes/useCommandAttributes.js11
-rw-r--r--modern/src/settings/CommandPage.js85
-rw-r--r--modern/src/settings/CommandsPage.js69
-rw-r--r--modern/src/settings/ComputedAttributePage.js2
-rw-r--r--modern/src/settings/OptionsLayout.js4
7 files changed, 174 insertions, 3 deletions
diff --git a/modern/src/App.js b/modern/src/App.js
index 9417feab..1d7b2cae 100644
--- a/modern/src/App.js
+++ b/modern/src/App.js
@@ -30,6 +30,8 @@ import ComputedAttributesPage from './settings/ComputedAttributesPage';
import ComputedAttributePage from './settings/ComputedAttributePage';
import MaintenancesPage from './settings/MaintenancesPage';
import MaintenancePage from './settings/MaintenancePage';
+import CommandsPage from './settings/CommandsPage';
+import CommandPage from './settings/CommandPage';
import StatisticsPage from './admin/StatisticsPage';
import CachingController from './CachingController';
@@ -111,6 +113,8 @@ const App = () => {
<Route exact path="/settings/attribute/:id?" component={ComputedAttributePage} />
<Route exact path="/settings/maintenances" component={MaintenancesPage} />
<Route exact path="/settings/maintenance/:id?" component={MaintenancePage} />
+ <Route exact path="/settings/commands" component={CommandsPage} />
+ <Route exact path="/settings/command/:id?" component={CommandPage} />
<Route exact path="/admin/server" component={ServerPage} />
<Route exact path="/admin/users" component={UsersPage} />
<Route exact path="/admin/statistics" component={StatisticsPage} />
diff --git a/modern/src/attributes/EditAttributesView.js b/modern/src/attributes/EditAttributesView.js
index e8ab83e1..3a18a9a3 100644
--- a/modern/src/attributes/EditAttributesView.js
+++ b/modern/src/attributes/EditAttributesView.js
@@ -116,7 +116,7 @@ const EditAttributesView = ({ attributes, setAttributes, definitions }) => {
<CloseIcon />
</IconButton>
</InputAdornment>
- )}
+ )}
/>
</FormControl>
);
diff --git a/modern/src/attributes/useCommandAttributes.js b/modern/src/attributes/useCommandAttributes.js
new file mode 100644
index 00000000..1212d283
--- /dev/null
+++ b/modern/src/attributes/useCommandAttributes.js
@@ -0,0 +1,11 @@
+import { useMemo } from 'react';
+
+export default (t) => useMemo(() => ({
+ custom: [
+ {
+ key: 'data',
+ name: t('commandData'),
+ type: 'string',
+ },
+ ],
+}), [t]);
diff --git a/modern/src/settings/CommandPage.js b/modern/src/settings/CommandPage.js
new file mode 100644
index 00000000..8a7f5b94
--- /dev/null
+++ b/modern/src/settings/CommandPage.js
@@ -0,0 +1,85 @@
+import React, { useEffect, useState } from 'react';
+import {
+ Accordion, AccordionSummary, AccordionDetails, makeStyles, Typography, TextField, FormControlLabel, Checkbox,
+} from '@material-ui/core';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import EditItemView from '../EditItemView';
+import { useTranslation } from '../LocalizationProvider';
+import SelectField from '../form/SelectField';
+import { prefixString } from '../common/stringUtils';
+import useCommandAttributes from '../attributes/useCommandAttributes';
+
+const useStyles = makeStyles(() => ({
+ details: {
+ flexDirection: 'column',
+ },
+}));
+
+const CommandPage = () => {
+ const classes = useStyles();
+ const t = useTranslation();
+
+ const availableAttributes = useCommandAttributes(t);
+
+ const [item, setItem] = useState();
+ const [attributes, setAttributes] = useState([]);
+
+ useEffect(() => {
+ if (item && item.type) {
+ setAttributes(availableAttributes[item.type] || []);
+ }
+ }, [availableAttributes, item]);
+
+ return (
+ <EditItemView endpoint="commands" item={item} setItem={setItem}>
+ {item && (
+ <Accordion defaultExpanded>
+ <AccordionSummary expandIcon={<ExpandMoreIcon />}>
+ <Typography variant="subtitle1">
+ {t('sharedRequired')}
+ </Typography>
+ </AccordionSummary>
+ <AccordionDetails className={classes.details}>
+ <TextField
+ margin="normal"
+ value={item.description || ''}
+ onChange={(event) => setItem({ ...item, description: event.target.value })}
+ label={t('sharedDescription')}
+ variant="filled"
+ />
+ <SelectField
+ margin="normal"
+ value={item.type || 'custom'}
+ emptyValue={null}
+ onChange={(e) => setItem({ ...item, type: e.target.value, attributes: {} })}
+ endpoint="/api/commands/types"
+ keyGetter={(it) => it.type}
+ titleGetter={(it) => t(prefixString('command', it.type))}
+ label={t('sharedType')}
+ variant="filled"
+ />
+ {attributes.map((attribute) => (
+ <TextField
+ margin="normal"
+ value={item.attributes[attribute.key]}
+ onChange={(e) => {
+ const updateItem = { ...item, attributes: { ...item.attributes } };
+ updateItem.attributes[attribute.key] = e.target.value;
+ setItem(updateItem);
+ }}
+ label={attribute.name}
+ variant="filled"
+ />
+ ))}
+ <FormControlLabel
+ control={<Checkbox checked={item.textChannel} onChange={(event) => setItem({ ...item, textChannel: event.target.checked })} />}
+ label={t('commandSendSms')}
+ />
+ </AccordionDetails>
+ </Accordion>
+ )}
+ </EditItemView>
+ );
+};
+
+export default CommandPage;
diff --git a/modern/src/settings/CommandsPage.js b/modern/src/settings/CommandsPage.js
new file mode 100644
index 00000000..e8422467
--- /dev/null
+++ b/modern/src/settings/CommandsPage.js
@@ -0,0 +1,69 @@
+import React, { useState } from 'react';
+import {
+ TableContainer, Table, TableRow, TableCell, TableHead, TableBody, makeStyles, IconButton,
+} from '@material-ui/core';
+import MoreVertIcon from '@material-ui/icons/MoreVert';
+import { useEffectAsync } from '../reactHelper';
+import EditCollectionView from '../EditCollectionView';
+import OptionsLayout from './OptionsLayout';
+import { useTranslation } from '../LocalizationProvider';
+import { formatBoolean } from '../common/formatter';
+import { prefixString } from '../common/stringUtils';
+
+const useStyles = makeStyles((theme) => ({
+ columnAction: {
+ width: theme.spacing(1),
+ padding: theme.spacing(0, 1),
+ },
+}));
+
+const CommandsView = ({ updateTimestamp, onMenuClick }) => {
+ const classes = useStyles();
+ const t = useTranslation();
+
+ const [items, setItems] = useState([]);
+
+ useEffectAsync(async () => {
+ const response = await fetch('/api/commands');
+ if (response.ok) {
+ setItems(await response.json());
+ }
+ }, [updateTimestamp]);
+
+ return (
+ <TableContainer>
+ <Table>
+ <TableHead>
+ <TableRow>
+ <TableCell className={classes.columnAction} />
+ <TableCell>{t('sharedDescription')}</TableCell>
+ <TableCell>{t('sharedType')}</TableCell>
+ <TableCell>{t('commandSendSms')}</TableCell>
+ </TableRow>
+ </TableHead>
+ <TableBody>
+ {items.map((item) => (
+ <TableRow key={item.id}>
+ <TableCell className={classes.columnAction} padding="none">
+ <IconButton onClick={(event) => onMenuClick(event.currentTarget, item.id)}>
+ <MoreVertIcon />
+ </IconButton>
+ </TableCell>
+ <TableCell>{item.description}</TableCell>
+ <TableCell>{t(prefixString('command', item.type))}</TableCell>
+ <TableCell>{formatBoolean(item.textChannel, t)}</TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ </TableContainer>
+ );
+};
+
+const CommandsPage = () => (
+ <OptionsLayout>
+ <EditCollectionView content={CommandsView} editPath="/settings/command" endpoint="commands" />
+ </OptionsLayout>
+);
+
+export default CommandsPage;
diff --git a/modern/src/settings/ComputedAttributePage.js b/modern/src/settings/ComputedAttributePage.js
index e8efd096..99bbfadb 100644
--- a/modern/src/settings/ComputedAttributePage.js
+++ b/modern/src/settings/ComputedAttributePage.js
@@ -40,7 +40,7 @@ const ComputedAttributePage = () => {
};
return (
- <EditItemView endpoint="/attributes/computed" item={item} setItem={setItem}>
+ <EditItemView endpoint="attributes/computed" item={item} setItem={setItem}>
{item
&& (
<Accordion defaultExpanded>
diff --git a/modern/src/settings/OptionsLayout.js b/modern/src/settings/OptionsLayout.js
index e588cafb..f2d8c5da 100644
--- a/modern/src/settings/OptionsLayout.js
+++ b/modern/src/settings/OptionsLayout.js
@@ -20,6 +20,7 @@ import BuildIcon from '@material-ui/icons/Build';
import PeopleIcon from '@material-ui/icons/People';
import BarChartIcon from '@material-ui/icons/BarChart';
import TodayIcon from '@material-ui/icons/Today';
+import ExitToAppIcon from '@material-ui/icons/ExitToApp';
import SideNav from '../components/SideNav';
import NavBar from '../components/NavBar';
@@ -79,14 +80,15 @@ const OptionsLayout = ({ children }) => {
], [t]);
const mainRoutes = useMemo(() => [
+ { name: t('sharedNotifications'), href: '/settings/notifications', icon: <NotificationsIcon /> },
{ name: t('settingsUser'), href: `/user/${userId}`, icon: <PersonIcon /> },
{ name: t('sharedGeofences'), href: '/geofences', icon: <CreateIcon /> },
- { name: t('sharedNotifications'), href: '/settings/notifications', icon: <NotificationsIcon /> },
{ name: t('settingsGroups'), href: '/settings/groups', icon: <FolderIcon /> },
{ name: t('sharedDrivers'), href: '/settings/drivers', icon: <PersonIcon /> },
{ name: t('sharedCalendars'), href: '/settings/calendars', icon: <TodayIcon /> },
{ name: t('sharedComputedAttributes'), href: '/settings/attributes', icon: <StorageIcon /> },
{ name: t('sharedMaintenance'), href: '/settings/maintenances', icon: <BuildIcon /> },
+ { name: t('sharedSavedCommands'), href: '/settings/commands', icon: <ExitToAppIcon /> },
], [t, userId]);
const routes = useMemo(() => [...mainRoutes, ...(admin ? adminRoutes : [])], [mainRoutes, admin, adminRoutes]);