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 [
"title" => "Berechtigungen",
"title_short" => "ACL",
"search" => "Suche",
"query" => "Suchanfrage",
"add_permission" => "Berechtigung hinzufügen",
"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 [
"title" => "Permissions",
"title_short" => "ACL",
"search" => "Search",
"query" => "Search query",
"add_permission" => "Add permission",
"permission" => "Permission",

@ -9,5 +9,5 @@
<noscript>{{ L("general.noscript") }}</noscript>
<div type="module" id="admin-panel"></div>
<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 %}

@ -37,8 +37,8 @@ export default function AdminDashboard(props) {
const showDialog = useCallback((message, title, options=["Close"], onOption = null) => {
setDialog({
show: true, message:
message,
show: true,
message: message,
title: title,
options: options,
onOption: onOption,
@ -80,27 +80,12 @@ export default function AdminDashboard(props) {
<Route path={"/admin/groups"} element={<GroupListView {...controlObj} />}/>
<Route path={"/admin/group/:groupId"} element={<EditGroupView {...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/:routeId"} element={<RouteEditView {...controlObj} />}/>
<Route path={"*"} element={<View404 />} />
</Routes>
</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}/>
</section>
</div>

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

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

@ -62,7 +62,6 @@ export default function GroupListView(props) {
<div className={"container-fluid"}>
<div className={"row mb-2"}>
<div className={"col-sm-6"}>
<h1 className={"m-0 text-dark"}>Users</h1>
</div>
<div className={"col-sm-6"}>
<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 {DataColumn, DataTable, DateTimeColumn, NumericColumn, StringColumn} from "shared/elements/data-table";
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 { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import {API_DATETIME_FORMAT} from "shared/constants";
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) {
@ -30,7 +30,7 @@ export default function LogView(props) {
const [forceReload, setForceReload] = useState(0);
useEffect(() => {
requestModules(props.api, ["general"], currentLocale).then(data => {
requestModules(props.api, ["general", "logs"], currentLocale).then(data => {
if (!data.success) {
props.showDialog("Error fetching translations: " + data.msg);
}
@ -38,16 +38,22 @@ export default function LogView(props) {
}, [currentLocale]);
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,
LOG_LEVELS[logLevel],
timestamp ? format(timestamp, API_DATETIME_FORMAT) : null,
query
).then((res) => {
LOG_LEVELS[logLevel], apiTimeStamp, query).then((res) => {
if (res.success) {
setLogEntries(res.logs);
pagination.update(res.pagination);
} else {
showDialog(res.msg, "Error fetching log entries");
showDialog(res.msg, L("logs.fetch_logs_error"));
return null;
}
});
@ -59,7 +65,7 @@ export default function LogView(props) {
}, [query, timestamp, logLevel]);
const messageColumn = (() => {
let column = new DataColumn(L("message"), "message");
let column = new DataColumn(L("logs.message"), "message");
column.sortable = false;
column.renderData = (L, entry) => {
return <pre>{entry.message}</pre>
@ -69,9 +75,9 @@ export default function LogView(props) {
const columnDefinitions = [
new NumericColumn(L("general.id"), "id"),
new StringColumn(L("module"), "module"),
new StringColumn(L("severity"), "severity"),
new DateTimeColumn(L("timestamp"), "timestamp", { precise: true }),
new StringColumn(L("logs.module"), "module"),
new StringColumn(L("logs.severity"), "severity"),
new DateTimeColumn(L("logs.timestamp"), "timestamp", { precise: true }),
messageColumn,
];
@ -80,54 +86,55 @@ export default function LogView(props) {
<div className={"container-fluid"}>
<div className={"row mb-2"}>
<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 className={"col-sm-6"}>
<ol className={"breadcrumb float-sm-right"}>
<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>
</div>
</div>
</div>
<div className={"content overflow-auto"}>
<div className={"row p-2"}>
<div className={"col-2"}>
<div className={"form-group"}>
<label>{L("log.severity")}</label>
<Select native className={"form-control"} value={logLevel} onChange={e => setLogLevel(parseInt(e.target.value))}>
<FormGroup className={"col-2"}>
<FormLabel>{L("logs.severity")}</FormLabel>
<FormControl>
<TextField select variant={"outlined"} size={"small"} value={logLevel}
onChange={e => setLogLevel(parseInt(e.target.value))}
inputProps={{ size: "small" }}>
{LOG_LEVELS.map((value, index) =>
<option key={"option-" + value} value={index}>{value}</option>)
<MenuItem key={"option-" + value} value={index}>{value}</MenuItem>)
}
</Select>
</div>
</div>
<div className={"col-4"}>
<div className={"form-group"}>
<label>{L("log.timestamp")}</label>
</TextField>
</FormControl>
</FormGroup>
<FormGroup className={"col-4"}>
<FormLabel>{L("logs.timestamp")}</FormLabel>
<FormControl>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DesktopDateTimePicker className={"form-control"}
label={L("Select date time to filter...")}
<DateTimePicker label={L("logs.timestamp_placeholder") + "…"}
value={timestamp ? toDate(new Date()) : null}
format={L("general.datefns_datetime_format_precise")}
onChange={(newValue) => setTimestamp(newValue)}
slotProps={{ textField: { } }}
slotProps={{ textField: { size:'small' } }}
sx={{"& .MuiInputBase-input": { height: "23px", padding: 1 }}}
/>
</LocalizationProvider>
</div>
</div>
<div className={"col-6"}>
<div className={"form-group"}>
<label>{L("log.query")}</label>
</FormControl>
</FormGroup>
<FormGroup className={"col-6"}>
<FormLabel>{L("logs.search")}</FormLabel>
<FormControl>
<TextField
className={"form-control"}
placeholder={L("log.search_query") + "…"}
placeholder={L("logs.search_query") + "…"}
value={query}
onChange={e => setQuery(e.target.value)}
variant={"outlined"}
size={"small"}/>
</div>
</div>
</FormControl>
</FormGroup>
</div>
<div className={"container-fluid"}>
<DataTable
@ -138,7 +145,7 @@ export default function LogView(props) {
forceReload={forceReload}
defaultSortColumn={3}
defaultSortOrder={"desc"}
placeholder={"No log entries to display"}
placeholder={L("logs.no_entries_placeholder")}
columns={columnDefinitions} />
</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 {useCallback, useContext, useEffect, useRef} from "react";
import {LocaleContext} from "shared/locale";
import {CheckCircle, ErrorRounded} from "@material-ui/icons";
const RouteFormControl = styled(FormControl)((props) => ({
"& > label": {
@ -85,11 +86,12 @@ export default function RouteForm(props) {
);
if (route.type === "dynamic") {
let extraArgs, type;
let extraArgs, type, isValid = false;
try {
extraArgs = JSON.parse(route.extra);
type = typeof extraArgs;
extraArgs = JSON.stringify(extraArgs, null, 2);
isValid = type === "object";
} catch (e) {
extraArgs = null
}
@ -97,14 +99,16 @@ export default function RouteForm(props) {
<RouteFormControl key={"form-control-extra"} fullWidth={true}>
<label htmlFor={"route-extra"}>{L("routes.arguments")}</label>
<textarea id={"route-extra"}
ref={extraRef}
ref={extraRef} style={!isValid ? {borderColor: "red"} : {}}
value={extraArgs ?? route.extra}
onChange={e => setRoute({...route, extra: minifyJson(e.target.value)})}/>
<i>{
<Box mt={1} fontStyle={"italic"} display={"grid"} gridTemplateColumns={"30px auto"}>{
extraArgs === null ?
L("routes.json_err") :
(type !== "object" ? L("routes.json_not_obj") : L("routes.json_ok"))
}</i>
<><ErrorRounded color={"secondary"}/><span>{L("routes.json_err")}</span></> :
(type !== "object" ?
<><ErrorRounded color={"secondary"}/><span>{L("routes.json_not_object")}</span></> :
<><CheckCircle color={"primary"} /><span>{L("routes.json_ok")}</span></>)
}</Box>
</RouteFormControl>
);
} 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 {Box, MenuItem, Select, Pagination as MuiPagination} from "@mui/material";
import {Box, Select, Pagination as MuiPagination} from "@mui/material";
import {sprintf} from "sprintf-js";
import {FormControl} from "@material-ui/core";