aboutsummaryrefslogtreecommitdiff
path: root/modern/src/settings/components
diff options
context:
space:
mode:
Diffstat (limited to 'modern/src/settings/components')
-rw-r--r--modern/src/settings/components/BaseCommandView.js58
-rw-r--r--modern/src/settings/components/EditCollectionView.js87
-rw-r--r--modern/src/settings/components/EditItemView.js95
-rw-r--r--modern/src/settings/components/OptionsLayout.js148
4 files changed, 388 insertions, 0 deletions
diff --git a/modern/src/settings/components/BaseCommandView.js b/modern/src/settings/components/BaseCommandView.js
new file mode 100644
index 00000000..b422e153
--- /dev/null
+++ b/modern/src/settings/components/BaseCommandView.js
@@ -0,0 +1,58 @@
+import React, { useEffect, useState } from 'react';
+import {
+ TextField, FormControlLabel, Checkbox,
+} from '@material-ui/core';
+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 = ({ item, setItem }) => {
+ const t = useTranslation();
+
+ const availableAttributes = useCommandAttributes(t);
+
+ const [attributes, setAttributes] = useState([]);
+
+ useEffect(() => {
+ if (item && item.type) {
+ setAttributes(availableAttributes[item.type] || []);
+ } else {
+ setAttributes([]);
+ }
+ }, [availableAttributes, item]);
+
+ return (
+ <>
+ <SelectField
+ margin="normal"
+ value={item.type || ''}
+ 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')}
+ />
+ </>
+ );
+};
+
+export default BaseCommandView;
diff --git a/modern/src/settings/components/EditCollectionView.js b/modern/src/settings/components/EditCollectionView.js
new file mode 100644
index 00000000..9107b68e
--- /dev/null
+++ b/modern/src/settings/components/EditCollectionView.js
@@ -0,0 +1,87 @@
+import React, { useState } from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import { useHistory } from 'react-router-dom';
+import Menu from '@material-ui/core/Menu';
+import MenuItem from '@material-ui/core/MenuItem';
+import Fab from '@material-ui/core/Fab';
+import AddIcon from '@material-ui/icons/Add';
+
+import RemoveDialog from '../../common/components/RemoveDialog';
+import { useTranslation } from '../../common/components/LocalizationProvider';
+import dimensions from '../../common/theme/dimensions';
+import { useEditable } from '../../common/util/permissions';
+
+const useStyles = makeStyles((theme) => ({
+ fab: {
+ position: 'fixed',
+ bottom: theme.spacing(2),
+ right: theme.spacing(2),
+ [theme.breakpoints.down('sm')]: {
+ bottom: dimensions.bottomBarHeight + theme.spacing(2),
+ },
+ },
+}));
+
+const EditCollectionView = ({
+ content, editPath, endpoint, disableAdd, filter,
+}) => {
+ const classes = useStyles();
+ const history = useHistory();
+ const t = useTranslation();
+
+ const editable = useEditable();
+
+ const [selectedId, setSelectedId] = useState(null);
+ const [selectedAnchorEl, setSelectedAnchorEl] = useState(null);
+ const [removeDialogShown, setRemoveDialogShown] = useState(false);
+ const [updateTimestamp, setUpdateTimestamp] = useState(Date.now());
+
+ const menuShow = (anchorId, itemId) => {
+ setSelectedAnchorEl(anchorId);
+ setSelectedId(itemId);
+ };
+
+ const menuHide = () => {
+ setSelectedAnchorEl(null);
+ };
+
+ const handleAdd = () => {
+ history.push(editPath);
+ menuHide();
+ };
+
+ const handleEdit = () => {
+ history.push(`${editPath}/${selectedId}`);
+ menuHide();
+ };
+
+ const handleRemove = () => {
+ setRemoveDialogShown(true);
+ menuHide();
+ };
+
+ const handleRemoveResult = () => {
+ setRemoveDialogShown(false);
+ setUpdateTimestamp(Date.now());
+ };
+
+ const Content = content;
+
+ return (
+ <>
+ <Content updateTimestamp={updateTimestamp} onMenuClick={menuShow} filter={filter} />
+ {editable && !disableAdd && (
+ <Fab size="medium" color="primary" className={classes.fab} onClick={handleAdd}>
+ <AddIcon />
+ </Fab>
+ )}
+ <Menu open={!!selectedAnchorEl} anchorEl={selectedAnchorEl} onClose={menuHide}>
+ <MenuItem onClick={handleEdit}>{t('sharedEdit')}</MenuItem>
+ <MenuItem onClick={handleRemove}>{t('sharedRemove')}</MenuItem>
+ </Menu>
+ <RemoveDialog open={removeDialogShown} endpoint={endpoint} itemId={selectedId} onResult={handleRemoveResult} />
+ </>
+ );
+};
+
+export default EditCollectionView;
diff --git a/modern/src/settings/components/EditItemView.js b/modern/src/settings/components/EditItemView.js
new file mode 100644
index 00000000..90e5294a
--- /dev/null
+++ b/modern/src/settings/components/EditItemView.js
@@ -0,0 +1,95 @@
+import React from 'react';
+import { useHistory, useParams } from 'react-router-dom';
+import { makeStyles } from '@material-ui/core/styles';
+import Container from '@material-ui/core/Container';
+import Button from '@material-ui/core/Button';
+import FormControl from '@material-ui/core/FormControl';
+
+import { useEffectAsync } from '../../reactHelper';
+import OptionsLayout from './OptionsLayout';
+import { useTranslation } from '../../common/components/LocalizationProvider';
+
+const useStyles = makeStyles((theme) => ({
+ container: {
+ marginTop: theme.spacing(2),
+ },
+ buttons: {
+ display: 'flex',
+ justifyContent: 'space-evenly',
+ '& > *': {
+ flexBasis: '33%',
+ },
+ },
+}));
+
+const EditItemView = ({
+ children, endpoint, item, setItem, validate, onItemSaved,
+}) => {
+ const history = useHistory();
+ const classes = useStyles();
+ const t = useTranslation();
+
+ const { id } = useParams();
+
+ useEffectAsync(async () => {
+ if (id) {
+ const response = await fetch(`/api/${endpoint}/${id}`);
+ if (response.ok) {
+ setItem(await response.json());
+ }
+ } else {
+ setItem({});
+ }
+ }, [id]);
+
+ const handleSave = 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());
+ }
+ history.goBack();
+ }
+ };
+
+ return (
+ <OptionsLayout>
+ <Container maxWidth="xs" className={classes.container}>
+ {children}
+ <FormControl fullWidth margin="normal">
+ <div className={classes.buttons}>
+ <Button
+ type="button"
+ color="primary"
+ variant="outlined"
+ onClick={() => history.goBack()}
+ >
+ {t('sharedCancel')}
+ </Button>
+ <Button
+ type="button"
+ color="primary"
+ variant="contained"
+ onClick={handleSave}
+ disabled={!validate()}
+ >
+ {t('sharedSave')}
+ </Button>
+ </div>
+ </FormControl>
+ </Container>
+ </OptionsLayout>
+ );
+};
+
+export default EditItemView;
diff --git a/modern/src/settings/components/OptionsLayout.js b/modern/src/settings/components/OptionsLayout.js
new file mode 100644
index 00000000..9f2a1aac
--- /dev/null
+++ b/modern/src/settings/components/OptionsLayout.js
@@ -0,0 +1,148 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { useHistory, useLocation } from 'react-router-dom';
+import {
+ Typography,
+ Divider,
+ Drawer,
+ makeStyles,
+ IconButton,
+ Hidden,
+} from '@material-ui/core';
+
+import { useSelector } from 'react-redux';
+import SettingsIcon from '@material-ui/icons/Settings';
+import CreateIcon from '@material-ui/icons/Create';
+import ArrowBackIcon from '@material-ui/icons/ArrowBack';
+import NotificationsIcon from '@material-ui/icons/Notifications';
+import FolderIcon from '@material-ui/icons/Folder';
+import PersonIcon from '@material-ui/icons/Person';
+import StorageIcon from '@material-ui/icons/Storage';
+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 '../../common/components/SideNav';
+import NavBar from '../../common/components/NavBar';
+import { useTranslation } from '../../common/components/LocalizationProvider';
+import { useAdministrator, useReadonly } from '../../common/util/permissions';
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ display: 'flex',
+ [theme.breakpoints.down('sm')]: {
+ flexDirection: 'column',
+ },
+ height: '100%',
+ },
+ drawerContainer: {
+ width: theme.dimensions.drawerWidthDesktop,
+ },
+ drawer: {
+ width: theme.dimensions.drawerWidthDesktop,
+ [theme.breakpoints.down('sm')]: {
+ width: theme.dimensions.drawerWidthTablet,
+ },
+ },
+ content: {
+ flex: 1,
+ },
+ drawerHeader: {
+ ...theme.mixins.toolbar,
+ display: 'flex',
+ alignItems: 'center',
+ padding: theme.spacing(0, 1),
+ },
+ toolbar: {
+ [theme.breakpoints.down('sm')]: {
+ ...theme.mixins.toolbar,
+ },
+ },
+}));
+
+const OptionsLayout = ({ children }) => {
+ const t = useTranslation();
+ const classes = useStyles();
+ const location = useLocation();
+ const history = useHistory();
+
+ const [openDrawer, setOpenDrawer] = useState(false);
+ const [optionTitle, setOptionTitle] = useState();
+
+ const readonly = useReadonly();
+ const admin = useAdministrator();
+ const userId = useSelector((state) => state.session.user?.id);
+
+ const readonlyRoutes = useMemo(() => [
+ { name: t('sharedPreferences'), href: '/settings/preferences', icon: <SettingsIcon /> },
+ ], [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('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 adminRoutes = useMemo(() => [
+ { subheader: t('userAdmin') },
+ { name: t('settingsServer'), href: '/admin/server', icon: <StorageIcon /> },
+ { name: t('settingsUsers'), href: '/admin/users', icon: <PeopleIcon /> },
+ { name: t('statisticsTitle'), href: '/admin/statistics', icon: <BarChartIcon /> },
+ ], [t]);
+
+ const routes = useMemo(() => (
+ [...readonlyRoutes, ...(!readonly ? mainRoutes : []), ...(admin ? adminRoutes : [])]
+ ), [readonlyRoutes, readonly, mainRoutes, admin, adminRoutes]);
+
+ useEffect(() => {
+ const activeRoute = routes.find((route) => route.href && location.pathname.includes(route.href));
+ setOptionTitle(activeRoute?.name);
+ }, [location, routes]);
+
+ const title = `${t('settingsTitle')} / ${optionTitle}`;
+
+ return (
+ <div className={classes.root}>
+ <Hidden mdUp>
+ <NavBar setOpenDrawer={setOpenDrawer} title={title} />
+ <Drawer
+ variant="temporary"
+ open={openDrawer}
+ onClose={() => setOpenDrawer(!openDrawer)}
+ classes={{ paper: classes.drawer }}
+ >
+ <SideNav routes={routes} />
+ </Drawer>
+ </Hidden>
+
+ <Hidden smDown>
+ <Drawer
+ variant="permanent"
+ classes={{ root: classes.drawerContainer, paper: classes.drawer }}
+ >
+ <div className={classes.drawerHeader}>
+ <IconButton onClick={() => history.push('/')}>
+ <ArrowBackIcon />
+ </IconButton>
+ <Typography variant="h6" color="inherit" noWrap>
+ {t('settingsTitle')}
+ </Typography>
+ </div>
+ <Divider />
+ <SideNav routes={routes} />
+ </Drawer>
+ </Hidden>
+
+ <div className={classes.content}>{children}</div>
+ </div>
+ );
+};
+
+export default OptionsLayout;