frontend fixes

This commit is contained in:
Roman 2024-03-29 18:44:31 +01:00
parent 755da257f8
commit 9fc0a19f59
15 changed files with 131 additions and 89 deletions

@ -0,0 +1,16 @@
<?php
return [
"title" => "Systemlog",
"severity" => "Schweregrad",
"timestamp" => "Zeitpunkt",
"module" => "Modul",
"message" => "Nachricht",
"search" => "Suche",
"search_query" => "Suchanfrage",
"no_entries_placeholder" => "Keine Log-Einträge zum Anzeigen",
"timestamp_placeholder" => "Datum und Zeitpunk Auswählen zum Filtern",
// dialog
"fetch_log_error" => "Fehler beim Holen der Log-Einträge",
];

@ -3,6 +3,7 @@
return [ return [
"title" => "Berechtigungen", "title" => "Berechtigungen",
"title_short" => "ACL", "title_short" => "ACL",
"search" => "Suche",
"query" => "Suchanfrage", "query" => "Suchanfrage",
"add_permission" => "Berechtigung hinzufügen", "add_permission" => "Berechtigung hinzufügen",
"permission" => "Berechtigung", "permission" => "Berechtigung",

@ -0,0 +1,16 @@
<?php
return [
"title" => "System Log",
"severity" => "Severity",
"timestamp" => "Timestamp",
"module" => "Module",
"message" => "Message",
"search" => "Search",
"search_query" => "Search query",
"no_entries_placeholder" => "No log entries to display",
"timestamp_placeholder" => "Select date and time to filter",
// dialog
"fetch_log_error" => "Error fetching log entries",
];

@ -3,6 +3,7 @@
return [ return [
"title" => "Permissions", "title" => "Permissions",
"title_short" => "ACL", "title_short" => "ACL",
"search" => "Search",
"query" => "Search query", "query" => "Search query",
"add_permission" => "Add permission", "add_permission" => "Add permission",
"permission" => "Permission", "permission" => "Permission",

@ -9,5 +9,5 @@
<noscript>{{ L("general.noscript") }}</noscript> <noscript>{{ L("general.noscript") }}</noscript>
<div type="module" id="admin-panel"></div> <div type="module" id="admin-panel"></div>
<script src="/react/dist/admin-panel/index.js" nonce="{{ site.csp.nonce }}"></script> <script src="/react/dist/admin-panel/index.js" nonce="{{ site.csp.nonce }}"></script>
<link rel="stylesheet" href="/react/dist/admin-panel/index.css" nonce="{{ site.csp.nonce }}"></link> <link rel="stylesheet" href="/react/dist/admin-panel/index.css" nonce="{{ site.csp.nonce }}" />
{% endblock %} {% endblock %}

@ -37,8 +37,8 @@ export default function AdminDashboard(props) {
const showDialog = useCallback((message, title, options=["Close"], onOption = null) => { const showDialog = useCallback((message, title, options=["Close"], onOption = null) => {
setDialog({ setDialog({
show: true, message: show: true,
message, message: message,
title: title, title: title,
options: options, options: options,
onOption: onOption, onOption: onOption,
@ -80,27 +80,12 @@ export default function AdminDashboard(props) {
<Route path={"/admin/groups"} element={<GroupListView {...controlObj} />}/> <Route path={"/admin/groups"} element={<GroupListView {...controlObj} />}/>
<Route path={"/admin/group/:groupId"} element={<EditGroupView {...controlObj} />}/> <Route path={"/admin/group/:groupId"} element={<EditGroupView {...controlObj} />}/>
<Route path={"/admin/logs"} element={<LogView {...controlObj} />}/> <Route path={"/admin/logs"} element={<LogView {...controlObj} />}/>
<Route path={"/admin/acl"} element={<AccessControlList {...controlObj} />}/> <Route path={"/admin/permissions"} element={<AccessControlList {...controlObj} />}/>
<Route path={"/admin/routes"} element={<RouteListView {...controlObj} />}/> <Route path={"/admin/routes"} element={<RouteListView {...controlObj} />}/>
<Route path={"/admin/routes/:routeId"} element={<RouteEditView {...controlObj} />}/> <Route path={"/admin/routes/:routeId"} element={<RouteEditView {...controlObj} />}/>
<Route path={"*"} element={<View404 />} /> <Route path={"*"} element={<View404 />} />
</Routes> </Routes>
</Suspense> </Suspense>
{/*<Route exact={true} path={"/admin/users"}><UserOverview {...this.controlObj} /></Route>
<Route path={"/admin/user/add"}><CreateUser {...this.controlObj} /></Route>
<Route path={"/admin/user/edit/:userId"} render={(props) => {
let newProps = {...props, ...this.controlObj};
return <EditUser {...newProps} />
}}/>
<Route path={"/admin/user/permissions"}><PermissionSettings {...this.controlObj}/></Route>
<Route path={"/admin/group/add"}><CreateGroup {...this.controlObj} /></Route>
<Route exact={true} path={"/admin/contact/"}><ContactRequestOverview {...this.controlObj} /></Route>
<Route path={"/admin/visitors"}><Visitors {...this.controlObj} /></Route>
<Route path={"/admin/logs"}><Logs {...this.controlObj} notifications={this.state.notifications} /></Route>
<Route path={"/admin/settings"}><Settings {...this.controlObj} /></Route>
<Route path={"/admin/pages"}><PageOverview {...this.controlObj} /></Route>
<Route path={"/admin/help"}><HelpPage {...this.controlObj} /></Route>
<Route path={"*"}><View404 /></Route>*/}
<Dialog {...dialog}/> <Dialog {...dialog}/>
</section> </section>
</div> </div>

@ -44,7 +44,7 @@ export default function Sidebar(props) {
"name": "admin.settings", "name": "admin.settings",
"icon": "tools" "icon": "tools"
}, },
"acl": { "permissions": {
"name": "admin.acl", "name": "admin.acl",
"icon": "door-open" "icon": "door-open"
}, },

@ -12,7 +12,7 @@ import {
TableContainer, TableContainer,
TableHead, TableHead,
TableRow, TableRow,
IconButton, styled IconButton, styled, FormGroup, FormLabel, FormControl, Box
} from "@material-ui/core"; } from "@material-ui/core";
import {Add, Delete, Edit, Refresh} from "@material-ui/icons"; import {Add, Delete, Edit, Refresh} from "@material-ui/icons";
import {USER_GROUP_ADMIN} from "shared/constants"; import {USER_GROUP_ADMIN} from "shared/constants";
@ -224,39 +224,40 @@ export default function AccessControlList(props) {
</div> </div>
</div> </div>
<div className={"row"}> <div className={"row"}>
<div className={"col-6"}> <FormGroup className={"col-6"}>
<div className={"form-group"}> <FormLabel>{L("permissions.search")}</FormLabel>
<label>{L("permissions.query")}</label> <TextField
<TextField placeholder={L("permissions.query") + "…"}
className={"form-control"} value={query}
placeholder={L("permissions.query") + "…"} onChange={e => setQuery(e.target.value)}
value={query} variant={"outlined"}
onChange={e => setQuery(e.target.value)} size={"small"} />
variant={"outlined"} </FormGroup>
size={"small"} />
</div>
</div>
<div className={"col-6 text-right"}> <div className={"col-6 text-right"}>
<label>{L("general.controls")}</label> <Box>
<div className={"form-group"}> <FormLabel>{L("general.controls")}</FormLabel>
<Button variant={"outlined"} color={"primary"} className={"m-1"} startIcon={<Refresh />} onClick={() => onFetchACL(true)}> </Box>
<Box mb={2}>
<Button variant={"outlined"} color={"primary"} className={"mr-1"}
startIcon={<Refresh />} onClick={() => onFetchACL(true)}>
{L("general.reload")} {L("general.reload")}
</Button> </Button>
<Button variant={"outlined"} className={"m-1"} startIcon={<Add />} disabled={!props.api.hasGroup(USER_GROUP_ADMIN)} <Button variant={"outlined"} startIcon={<Add />}
disabled={!props.api.hasGroup(USER_GROUP_ADMIN)}
onClick={() => setDialogData({ onClick={() => setDialogData({
open: true, open: true,
title: L("permissions.add_permission"), title: L("permissions.add_permission"),
inputs: [ inputs: [
{ type: "label", value: L("general.method") + ":" }, { type: "label", value: L("permissions.method") + ":" },
{ type: "text", name: "method", value: "", placeholder: L("general.method") }, { type: "text", name: "method", value: "", placeholder: L("permissions.method") },
{ type: "label", value: L("general.description") + ":" }, { type: "label", value: L("permissions.description") + ":" },
{ type: "text", name: "description", maxLength: 128, placeholder: L("general.description") } { type: "text", name: "description", maxLength: 128, placeholder: L("permissions.description") }
], ],
onOption: (option, inputData) => option === 0 && onUpdatePermission(inputData, []) onOption: (option, inputData) => option === 0 && onUpdatePermission(inputData, [])
})} > })} >
{L("general.add")} {L("general.add")}
</Button> </Button>
</div> </Box>
</div> </div>
</div> </div>
<TableContainer component={Paper} style={{overflowX: "initial"}}> <TableContainer component={Paper} style={{overflowX: "initial"}}>

@ -62,7 +62,6 @@ export default function GroupListView(props) {
<div className={"container-fluid"}> <div className={"container-fluid"}>
<div className={"row mb-2"}> <div className={"row mb-2"}>
<div className={"col-sm-6"}> <div className={"col-sm-6"}>
<h1 className={"m-0 text-dark"}>Users</h1>
</div> </div>
<div className={"col-sm-6"}> <div className={"col-sm-6"}>
<ol className={"breadcrumb float-sm-right"}> <ol className={"breadcrumb float-sm-right"}>

@ -0,0 +1,4 @@
import EditGroupView from "./group-edit";
import GroupListView from "./group-list";
export default { EditGroupView, GroupListView };

@ -4,12 +4,12 @@ import {Link} from "react-router-dom";
import usePagination from "shared/hooks/pagination"; import usePagination from "shared/hooks/pagination";
import {DataColumn, DataTable, DateTimeColumn, NumericColumn, StringColumn} from "shared/elements/data-table"; import {DataColumn, DataTable, DateTimeColumn, NumericColumn, StringColumn} from "shared/elements/data-table";
import {TextField} from "@mui/material"; import {TextField} from "@mui/material";
import {DesktopDateTimePicker} from "@mui/x-date-pickers"; import {DateTimePicker} from "@mui/x-date-pickers";
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import {API_DATETIME_FORMAT} from "shared/constants"; import {API_DATETIME_FORMAT} from "shared/constants";
import {format, toDate} from "date-fns"; import {format, toDate} from "date-fns";
import {Select} from "@material-ui/core"; import {FormControl, FormGroup, FormLabel, MenuItem, Select} from "@material-ui/core";
export default function LogView(props) { export default function LogView(props) {
@ -30,7 +30,7 @@ export default function LogView(props) {
const [forceReload, setForceReload] = useState(0); const [forceReload, setForceReload] = useState(0);
useEffect(() => { useEffect(() => {
requestModules(props.api, ["general"], currentLocale).then(data => { requestModules(props.api, ["general", "logs"], currentLocale).then(data => {
if (!data.success) { if (!data.success) {
props.showDialog("Error fetching translations: " + data.msg); props.showDialog("Error fetching translations: " + data.msg);
} }
@ -38,16 +38,22 @@ export default function LogView(props) {
}, [currentLocale]); }, [currentLocale]);
const onFetchLogs = useCallback((page, count, orderBy, sortOrder) => { const onFetchLogs = useCallback((page, count, orderBy, sortOrder) => {
let apiTimeStamp = null;
try {
if (timestamp) {
apiTimeStamp = format(timestamp, API_DATETIME_FORMAT);
}
} catch (e) {
apiTimeStamp = null;
}
api.fetchLogEntries(page, count, orderBy, sortOrder, api.fetchLogEntries(page, count, orderBy, sortOrder,
LOG_LEVELS[logLevel], LOG_LEVELS[logLevel], apiTimeStamp, query).then((res) => {
timestamp ? format(timestamp, API_DATETIME_FORMAT) : null,
query
).then((res) => {
if (res.success) { if (res.success) {
setLogEntries(res.logs); setLogEntries(res.logs);
pagination.update(res.pagination); pagination.update(res.pagination);
} else { } else {
showDialog(res.msg, "Error fetching log entries"); showDialog(res.msg, L("logs.fetch_logs_error"));
return null; return null;
} }
}); });
@ -59,7 +65,7 @@ export default function LogView(props) {
}, [query, timestamp, logLevel]); }, [query, timestamp, logLevel]);
const messageColumn = (() => { const messageColumn = (() => {
let column = new DataColumn(L("message"), "message"); let column = new DataColumn(L("logs.message"), "message");
column.sortable = false; column.sortable = false;
column.renderData = (L, entry) => { column.renderData = (L, entry) => {
return <pre>{entry.message}</pre> return <pre>{entry.message}</pre>
@ -69,9 +75,9 @@ export default function LogView(props) {
const columnDefinitions = [ const columnDefinitions = [
new NumericColumn(L("general.id"), "id"), new NumericColumn(L("general.id"), "id"),
new StringColumn(L("module"), "module"), new StringColumn(L("logs.module"), "module"),
new StringColumn(L("severity"), "severity"), new StringColumn(L("logs.severity"), "severity"),
new DateTimeColumn(L("timestamp"), "timestamp", { precise: true }), new DateTimeColumn(L("logs.timestamp"), "timestamp", { precise: true }),
messageColumn, messageColumn,
]; ];
@ -80,54 +86,55 @@ export default function LogView(props) {
<div className={"container-fluid"}> <div className={"container-fluid"}>
<div className={"row mb-2"}> <div className={"row mb-2"}>
<div className={"col-sm-6"}> <div className={"col-sm-6"}>
<h1 className={"m-0 text-dark"}>System Log</h1> <h1 className={"m-0 text-dark"}>{L("logs.title")}</h1>
</div> </div>
<div className={"col-sm-6"}> <div className={"col-sm-6"}>
<ol className={"breadcrumb float-sm-right"}> <ol className={"breadcrumb float-sm-right"}>
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li> <li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
<li className="breadcrumb-item active">System Log</li> <li className="breadcrumb-item active">{L("logs.title")}</li>
</ol> </ol>
</div> </div>
</div> </div>
</div> </div>
<div className={"content overflow-auto"}> <div className={"content overflow-auto"}>
<div className={"row p-2"}> <div className={"row p-2"}>
<div className={"col-2"}> <FormGroup className={"col-2"}>
<div className={"form-group"}> <FormLabel>{L("logs.severity")}</FormLabel>
<label>{L("log.severity")}</label> <FormControl>
<Select native className={"form-control"} value={logLevel} onChange={e => setLogLevel(parseInt(e.target.value))}> <TextField select variant={"outlined"} size={"small"} value={logLevel}
onChange={e => setLogLevel(parseInt(e.target.value))}
inputProps={{ size: "small" }}>
{LOG_LEVELS.map((value, index) => {LOG_LEVELS.map((value, index) =>
<option key={"option-" + value} value={index}>{value}</option>) <MenuItem key={"option-" + value} value={index}>{value}</MenuItem>)
} }
</Select> </TextField>
</div> </FormControl>
</div> </FormGroup>
<div className={"col-4"}> <FormGroup className={"col-4"}>
<div className={"form-group"}> <FormLabel>{L("logs.timestamp")}</FormLabel>
<label>{L("log.timestamp")}</label> <FormControl>
<LocalizationProvider dateAdapter={AdapterDateFns}> <LocalizationProvider dateAdapter={AdapterDateFns}>
<DesktopDateTimePicker className={"form-control"} <DateTimePicker label={L("logs.timestamp_placeholder") + "…"}
label={L("Select date time to filter...")}
value={timestamp ? toDate(new Date()) : null} value={timestamp ? toDate(new Date()) : null}
format={L("general.datefns_datetime_format_precise")} format={L("general.datefns_datetime_format_precise")}
onChange={(newValue) => setTimestamp(newValue)} onChange={(newValue) => setTimestamp(newValue)}
slotProps={{ textField: { } }} slotProps={{ textField: { size:'small' } }}
sx={{"& .MuiInputBase-input": { height: "23px", padding: 1 }}}
/> />
</LocalizationProvider> </LocalizationProvider>
</div> </FormControl>
</div> </FormGroup>
<div className={"col-6"}> <FormGroup className={"col-6"}>
<div className={"form-group"}> <FormLabel>{L("logs.search")}</FormLabel>
<label>{L("log.query")}</label> <FormControl>
<TextField <TextField
className={"form-control"} placeholder={L("logs.search_query") + "…"}
placeholder={L("log.search_query") + "…"}
value={query} value={query}
onChange={e => setQuery(e.target.value)} onChange={e => setQuery(e.target.value)}
variant={"outlined"} variant={"outlined"}
size={"small"}/> size={"small"}/>
</div> </FormControl>
</div> </FormGroup>
</div> </div>
<div className={"container-fluid"}> <div className={"container-fluid"}>
<DataTable <DataTable
@ -138,7 +145,7 @@ export default function LogView(props) {
forceReload={forceReload} forceReload={forceReload}
defaultSortColumn={3} defaultSortColumn={3}
defaultSortOrder={"desc"} defaultSortOrder={"desc"}
placeholder={"No log entries to display"} placeholder={L("logs.no_entries_placeholder")}
columns={columnDefinitions} /> columns={columnDefinitions} />
</div> </div>
</div> </div>

@ -0,0 +1,4 @@
import RouteEditView from "./route-edit";
import RouteListView from "./route-list";
export default { RouteEditView, RouteListView };

@ -1,7 +1,8 @@
import {Checkbox, FormControl, FormControlLabel, Select, styled, TextField} from "@material-ui/core"; import {Box, Checkbox, FormControl, FormControlLabel, Select, styled, TextField} from "@material-ui/core";
import * as React from "react"; import * as React from "react";
import {useCallback, useContext, useEffect, useRef} from "react"; import {useCallback, useContext, useEffect, useRef} from "react";
import {LocaleContext} from "shared/locale"; import {LocaleContext} from "shared/locale";
import {CheckCircle, ErrorRounded} from "@material-ui/icons";
const RouteFormControl = styled(FormControl)((props) => ({ const RouteFormControl = styled(FormControl)((props) => ({
"& > label": { "& > label": {
@ -85,11 +86,12 @@ export default function RouteForm(props) {
); );
if (route.type === "dynamic") { if (route.type === "dynamic") {
let extraArgs, type; let extraArgs, type, isValid = false;
try { try {
extraArgs = JSON.parse(route.extra); extraArgs = JSON.parse(route.extra);
type = typeof extraArgs; type = typeof extraArgs;
extraArgs = JSON.stringify(extraArgs, null, 2); extraArgs = JSON.stringify(extraArgs, null, 2);
isValid = type === "object";
} catch (e) { } catch (e) {
extraArgs = null extraArgs = null
} }
@ -97,14 +99,16 @@ export default function RouteForm(props) {
<RouteFormControl key={"form-control-extra"} fullWidth={true}> <RouteFormControl key={"form-control-extra"} fullWidth={true}>
<label htmlFor={"route-extra"}>{L("routes.arguments")}</label> <label htmlFor={"route-extra"}>{L("routes.arguments")}</label>
<textarea id={"route-extra"} <textarea id={"route-extra"}
ref={extraRef} ref={extraRef} style={!isValid ? {borderColor: "red"} : {}}
value={extraArgs ?? route.extra} value={extraArgs ?? route.extra}
onChange={e => setRoute({...route, extra: minifyJson(e.target.value)})}/> onChange={e => setRoute({...route, extra: minifyJson(e.target.value)})}/>
<i>{ <Box mt={1} fontStyle={"italic"} display={"grid"} gridTemplateColumns={"30px auto"}>{
extraArgs === null ? extraArgs === null ?
L("routes.json_err") : <><ErrorRounded color={"secondary"}/><span>{L("routes.json_err")}</span></> :
(type !== "object" ? L("routes.json_not_obj") : L("routes.json_ok")) (type !== "object" ?
}</i> <><ErrorRounded color={"secondary"}/><span>{L("routes.json_not_object")}</span></> :
<><CheckCircle color={"primary"} /><span>{L("routes.json_ok")}</span></>)
}</Box>
</RouteFormControl> </RouteFormControl>
); );
} else if (route.type === "static") { } else if (route.type === "static") {

@ -0,0 +1,4 @@
import UserEditView from "./user-edit";
import UserListView from "./user-list";
export default { UserEditView, UserListView };

@ -1,5 +1,5 @@
import React, {useState} from "react"; import React, {useState} from "react";
import {Box, MenuItem, Select, Pagination as MuiPagination} from "@mui/material"; import {Box, Select, Pagination as MuiPagination} from "@mui/material";
import {sprintf} from "sprintf-js"; import {sprintf} from "sprintf-js";
import {FormControl} from "@material-ui/core"; import {FormControl} from "@material-ui/core";