Removed AdminLTE, some minor improvements
This commit is contained in:
@@ -12,11 +12,14 @@ import {
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
IconButton, styled, FormGroup, FormLabel, Box
|
||||
IconButton, styled, FormGroup, FormLabel, Box, Grid
|
||||
} from "@mui/material";
|
||||
import {Add, Delete, Edit, Refresh} from "@mui/icons-material";
|
||||
import {USER_GROUP_ADMIN} from "shared/constants";
|
||||
import Dialog from "shared/elements/dialog";
|
||||
import ViewContent from "../elements/view-content";
|
||||
import ButtonBar from "../elements/button-bar";
|
||||
import TableBodyStriped from "shared/elements/table-body-striped";
|
||||
|
||||
const BorderedColumn = styled(TableCell)({
|
||||
borderLeft: "1px dotted #666",
|
||||
@@ -206,73 +209,68 @@ export default function AccessControlList(props) {
|
||||
return <>{rows}</>
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<h1 className={"m-0 text-dark"}>{L("permissions.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">{L("permissions.title_short")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"row"}>
|
||||
<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"}>
|
||||
return <ViewContent title={L("permissions.title")} path={[
|
||||
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||
<span key={"permissions"}>{L("permissions.title_short")}</span>,
|
||||
]}>
|
||||
<Grid container>
|
||||
<Grid item xs={6}>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("permissions.search")}</FormLabel>
|
||||
<TextField
|
||||
placeholder={L("permissions.query") + "…"}
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
variant={"outlined"}
|
||||
size={"small"}/>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
<Grid item xs={6} textAlign={"end"}>
|
||||
<Box>
|
||||
<FormLabel>{L("general.controls")}</FormLabel>
|
||||
</Box>
|
||||
<Box mb={2}>
|
||||
<Button variant={"outlined"} color={"primary"} className={"mr-1"} size={"small"}
|
||||
startIcon={<Refresh />} onClick={() => onFetchACL(true)}>
|
||||
<ButtonBar mb={2}>
|
||||
<Button variant={"outlined"} color={"primary"} size={"small"}
|
||||
startIcon={<Refresh/>} onClick={() => onFetchACL(true)}>
|
||||
{L("general.reload")}
|
||||
</Button>
|
||||
<Button variant={"outlined"} startIcon={<Add />} size={"small"}
|
||||
<Button variant={"outlined"} startIcon={<Add/>} size={"small"}
|
||||
disabled={!props.api.hasGroup(USER_GROUP_ADMIN)}
|
||||
onClick={() => setDialogData({
|
||||
open: true,
|
||||
title: L("permissions.add_permission"),
|
||||
inputs: [
|
||||
{ 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") }
|
||||
{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, []) : true
|
||||
})} >
|
||||
})}>
|
||||
{L("general.add")}
|
||||
</Button>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
</ButtonBar>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<TableContainer component={Paper} style={{overflowX: "initial"}}>
|
||||
<Table stickyHeader size={"small"} className={"table-striped"}>
|
||||
<Table stickyHeader size={"small"}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{L("permissions.permission")}</TableCell>
|
||||
<BorderedColumn align={"center"}><i>{L("permissions.everyone")}</i></BorderedColumn>
|
||||
{ groups.map(group => <TableCell key={"group-" + group.id} align={"center"}>
|
||||
{groups.map(group => <TableCell key={"group-" + group.id} align={"center"}>
|
||||
{group.name}
|
||||
</TableCell>) }
|
||||
</TableCell>)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<PermissionList />
|
||||
</TableBody>
|
||||
<TableBodyStriped>
|
||||
<PermissionList/>
|
||||
</TableBodyStriped>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Dialog show={dialogData.open}
|
||||
@@ -281,6 +279,6 @@ export default function AccessControlList(props) {
|
||||
message={dialogData.message}
|
||||
onOption={dialogData.onOption}
|
||||
inputs={dialogData.inputs}
|
||||
options={[L("general.ok"), L("general.cancel")]} />
|
||||
</>
|
||||
options={[L("general.ok"), L("general.cancel")]}/>
|
||||
</ViewContent>
|
||||
}
|
||||
@@ -8,10 +8,12 @@ import {DataTable, ControlsColumn, NumericColumn, StringColumn} from "shared/ele
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import usePagination from "shared/hooks/pagination";
|
||||
import Dialog from "shared/elements/dialog";
|
||||
import {FormControl, FormGroup, FormLabel, TextField, Button, CircularProgress, Box} from "@mui/material";
|
||||
import {FormControl, FormLabel, TextField, Button, CircularProgress, Box, Grid} from "@mui/material";
|
||||
import {Add, Delete, KeyboardArrowLeft, Save} from "@mui/icons-material";
|
||||
import {MuiColorInput} from "mui-color-input";
|
||||
import ButtonBar from "../../elements/button-bar";
|
||||
import ViewContent from "../../elements/view-content";
|
||||
import FormGroup from "../../elements/form-group";
|
||||
|
||||
const defaultGroupData = {
|
||||
name: "",
|
||||
@@ -175,91 +177,75 @@ export default function EditGroupView(props) {
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<h1 className={"m-0 text-dark"}>
|
||||
{ isNewGroup ? L("account.new_group") : L("account.group") + ": " + group.name }
|
||||
</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"><Link to={"/admin/groups"}>{L("account.group")}</Link></li>
|
||||
<li className="breadcrumb-item active">{ isNewGroup ? L("general.new") : groupId }</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<div className={"row"}>
|
||||
<div className={"col-4 pl-5 pr-5"}>
|
||||
<FormGroup className={"my-2"}>
|
||||
<FormLabel htmlFor={"name"}>
|
||||
{L("account.group_name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<TextField maxLength={32} value={group.name}
|
||||
size={"small"} name={"name"}
|
||||
placeholder={L("account.name")}
|
||||
onChange={e => setGroup({...group, name: e.target.value})}/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<ViewContent title={ isNewGroup ? L("account.new_group") : L("account.group") + ": " + group.name }
|
||||
path={[
|
||||
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||
<Link key={"group"} to={"/admin/groups"}>{L("account.group")}</Link>,
|
||||
<span key={"action"} >{isNewGroup ? L("general.new") : groupId}</span>,
|
||||
]}>
|
||||
<Grid container>
|
||||
<Grid item xs={6}>
|
||||
<FormGroup>
|
||||
<FormLabel htmlFor={"name"}>
|
||||
{L("account.group_name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<TextField maxLength={32} value={group.name}
|
||||
size={"small"} name={"name"}
|
||||
placeholder={L("account.name")}
|
||||
onChange={e => setGroup({...group, name: e.target.value})}/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup className={"my-2"}>
|
||||
<FormLabel htmlFor={"color"}>
|
||||
{L("account.color")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<MuiColorInput
|
||||
format={"hex"}
|
||||
value={group.color}
|
||||
size={"small"}
|
||||
variant={"outlined"}
|
||||
onChange={color => setGroup({...group, color: color})}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<FormLabel htmlFor={"color"}>
|
||||
{L("account.color")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<MuiColorInput
|
||||
format={"hex"}
|
||||
value={group.color}
|
||||
size={"small"}
|
||||
variant={"outlined"}
|
||||
onChange={color => setGroup({...group, color: color})}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<ButtonBar mt={2}>
|
||||
<Button startIcon={<KeyboardArrowLeft />}
|
||||
variant={"outlined"}
|
||||
onClick={() => navigate("/admin/groups")}>
|
||||
{L("general.go_back")}
|
||||
</Button>
|
||||
<Button startIcon={isSaving ? <CircularProgress size={14} /> : <Save />}
|
||||
color={"primary"}
|
||||
variant={"outlined"}
|
||||
disabled={isSaving || (!api.hasPermission(isNewGroup ? "groups/create" : "groups/update"))}
|
||||
onClick={onSave}>
|
||||
{isSaving ? L("general.saving") + "…" : L("general.save")}
|
||||
</Button>
|
||||
{ !isNewGroup &&
|
||||
<Button startIcon={<Delete/>} disabled={!api.hasPermission("groups/delete")}
|
||||
variant={"outlined"} color={"secondary"}
|
||||
onClick={() => setDialogData({
|
||||
open: true,
|
||||
title: L("account.delete_group_title"),
|
||||
message: L("account.delete_group_text"),
|
||||
onOption: option => option === 0 ? onDeleteGroup() : true
|
||||
})}>
|
||||
{L("general.delete")}
|
||||
</Button>
|
||||
}
|
||||
</ButtonBar>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonBar mt={2}>
|
||||
<Button startIcon={<KeyboardArrowLeft />}
|
||||
variant={"outlined"}
|
||||
onClick={() => navigate("/admin/groups")}>
|
||||
{L("general.go_back")}
|
||||
</Button>
|
||||
<Button startIcon={isSaving ? <CircularProgress size={14} /> : <Save />}
|
||||
color={"primary"}
|
||||
variant={"outlined"}
|
||||
disabled={isSaving || (!api.hasPermission(isNewGroup ? "groups/create" : "groups/update"))}
|
||||
onClick={onSave}>
|
||||
{isSaving ? L("general.saving") + "…" : L("general.save")}
|
||||
</Button>
|
||||
{ !isNewGroup &&
|
||||
<Button startIcon={<Delete/>} disabled={!api.hasPermission("groups/delete")}
|
||||
variant={"outlined"} color={"secondary"}
|
||||
onClick={() => setDialogData({
|
||||
open: true,
|
||||
title: L("account.delete_group_title"),
|
||||
message: L("account.delete_group_text"),
|
||||
onOption: option => option === 0 ? onDeleteGroup() : true
|
||||
})}>
|
||||
{L("general.delete")}
|
||||
</Button>
|
||||
}
|
||||
</ButtonBar>
|
||||
{!isNewGroup && api.hasPermission("groups/getMembers") ?
|
||||
<Box m={3} className={"col-6"}>
|
||||
<Box mt={3}>
|
||||
<h4>{L("account.members")}</h4>
|
||||
<DataTable
|
||||
data={members}
|
||||
pagination={pagination}
|
||||
defaultSortOrder={"asc"}
|
||||
defaultSortColumn={0}
|
||||
className={"table table-striped"}
|
||||
fetchData={onFetchMembers}
|
||||
placeholder={L("account.no_members")}
|
||||
columns={[
|
||||
@@ -298,7 +284,9 @@ export default function EditGroupView(props) {
|
||||
</Box>
|
||||
: <></>
|
||||
}
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ViewContent>
|
||||
<Dialog show={dialogData.open}
|
||||
onClose={() => setDialogData({open: false})}
|
||||
title={dialogData.title}
|
||||
@@ -306,6 +294,5 @@ export default function EditGroupView(props) {
|
||||
onOption={dialogData.onOption}
|
||||
inputs={dialogData.inputs}
|
||||
options={[L("general.ok"), L("general.cancel")]} />
|
||||
</>
|
||||
|
||||
</>
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {LocaleContext} from "shared/locale";
|
||||
import {ControlsColumn, DataTable, NumericColumn, StringColumn} from "shared/elements/data-table";
|
||||
import {Add, Edit} from "@mui/icons-material";
|
||||
import usePagination from "shared/hooks/pagination";
|
||||
import ViewContent from "../../elements/view-content";
|
||||
|
||||
|
||||
export default function GroupListView(props) {
|
||||
@@ -46,42 +47,25 @@ export default function GroupListView(props) {
|
||||
]),
|
||||
];
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<h1 className={"m-0 text-dark"}>{L("account.groups")}</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">{L("account.groups")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<div className={"container-fluid"}>
|
||||
<DataTable
|
||||
data={groups}
|
||||
pagination={pagination}
|
||||
defaultSortOrder={"asc"}
|
||||
defaultSortColumn={0}
|
||||
className={"table table-striped"}
|
||||
fetchData={onFetchGroups}
|
||||
placeholder={"No groups to display"}
|
||||
columns={columnDefinitions}
|
||||
buttons={[{
|
||||
key: "btn-create-group",
|
||||
color: "primary",
|
||||
startIcon: <Add />,
|
||||
onClick: () => navigate("/admin/group/new"),
|
||||
disabled: !api.hasPermission("groups/create"),
|
||||
children: L("general.create_new")
|
||||
}]}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
return <ViewContent title={L("account.groups")} path={[
|
||||
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||
<span key={"groups"} >{L("account.groups")}</span>,
|
||||
]}>
|
||||
<DataTable
|
||||
data={groups}
|
||||
pagination={pagination}
|
||||
defaultSortOrder={"asc"}
|
||||
defaultSortColumn={0}
|
||||
fetchData={onFetchGroups}
|
||||
placeholder={"No groups to display"}
|
||||
columns={columnDefinitions}
|
||||
buttons={[{
|
||||
key: "btn-create-group",
|
||||
color: "primary",
|
||||
startIcon: <Add />,
|
||||
onClick: () => navigate("/admin/group/new"),
|
||||
disabled: !api.hasPermission("groups/create"),
|
||||
children: L("general.create_new")
|
||||
}]}/>
|
||||
</ViewContent>
|
||||
}
|
||||
@@ -3,13 +3,14 @@ import {LocaleContext} from "shared/locale";
|
||||
import {Link} from "react-router-dom";
|
||||
import usePagination from "shared/hooks/pagination";
|
||||
import {DataColumn, DataTable, DateTimeColumn, NumericColumn, StringColumn} from "shared/elements/data-table";
|
||||
import {Box, FormControl, FormGroup, FormLabel, IconButton, MenuItem, TextField} from "@mui/material";
|
||||
import {Box, FormControl, FormGroup, FormLabel, Grid, IconButton, MenuItem, TextField} from "@mui/material";
|
||||
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 {ExpandLess, ExpandMore} from "@mui/icons-material";
|
||||
import ViewContent from "../elements/view-content";
|
||||
|
||||
export default function LogView(props) {
|
||||
|
||||
@@ -106,74 +107,63 @@ export default function LogView(props) {
|
||||
messageColumn,
|
||||
];
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<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">{L("logs.title")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content overflow-auto"}>
|
||||
<div className={"row p-2"}>
|
||||
<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) =>
|
||||
<MenuItem key={"option-" + value} value={index}>{value}</MenuItem>)
|
||||
}
|
||||
</TextField>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<FormGroup className={"col-4"}>
|
||||
<FormLabel>{L("logs.timestamp")}</FormLabel>
|
||||
<FormControl>
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||
<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: { size:'small' } }}
|
||||
sx={{"& .MuiInputBase-input": { height: "23px", padding: 1 }}}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<FormGroup className={"col-6"}>
|
||||
<FormLabel>{L("logs.search")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField
|
||||
placeholder={L("logs.search_query") + "…"}
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
variant={"outlined"}
|
||||
size={"small"}/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</div>
|
||||
<div className={"container-fluid"}>
|
||||
<DataTable
|
||||
data={logEntries}
|
||||
pagination={pagination}
|
||||
className={"table table-striped"}
|
||||
fetchData={onFetchLogs}
|
||||
forceReload={forceReload}
|
||||
defaultSortColumn={3}
|
||||
defaultSortOrder={"desc"}
|
||||
placeholder={L("logs.no_entries_placeholder")}
|
||||
columns={columnDefinitions} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
return <ViewContent title={L("logs.title")} path={[
|
||||
<Link key={"dashboard"} to={"/admin/dashboard"}>Home</Link>,
|
||||
<span key={"logs"}>{L("logs.title")}</span>
|
||||
]}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={2}>
|
||||
<FormGroup>
|
||||
<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) =>
|
||||
<MenuItem key={"option-" + value} value={index}>{value}</MenuItem>)
|
||||
}
|
||||
</TextField>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("logs.timestamp")}</FormLabel>
|
||||
<FormControl>
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||
<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: {size: 'small'}}}
|
||||
sx={{"& .MuiInputBase-input": {height: "23px", padding: 1}}}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormGroup>
|
||||
<FormLabel>{L("logs.search")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField
|
||||
placeholder={L("logs.search_query") + "…"}
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
variant={"outlined"}
|
||||
size={"small"}/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<DataTable
|
||||
data={logEntries}
|
||||
pagination={pagination}
|
||||
fetchData={onFetchLogs}
|
||||
forceReload={forceReload}
|
||||
defaultSortColumn={3}
|
||||
defaultSortOrder={"desc"}
|
||||
placeholder={L("logs.no_entries_placeholder")}
|
||||
columns={columnDefinitions}/>
|
||||
</ViewContent>
|
||||
}
|
||||
@@ -13,26 +13,65 @@ import {
|
||||
LibraryBooks,
|
||||
People
|
||||
} from "@mui/icons-material";
|
||||
import {Box, CircularProgress, Paper, Table, TableBody, TableCell, TableRow} from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
Grid,
|
||||
Paper,
|
||||
styled,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow
|
||||
} from "@mui/material";
|
||||
import ViewContent from "../elements/view-content";
|
||||
import {Alert} from "@mui/lab";
|
||||
|
||||
const StatBox = (props) => <div className={"col-lg-3 col-6"}>
|
||||
<div className={"small-box bg-" + props.color}>
|
||||
<div className={"inner"}>
|
||||
const StyledStatBox = styled(Alert)((props) => ({
|
||||
position: "relative",
|
||||
padding: 0,
|
||||
"& > div": {
|
||||
padding: 0,
|
||||
width: "100%",
|
||||
"& a": {
|
||||
color: "white",
|
||||
},
|
||||
"& div:nth-of-type(1)": {
|
||||
padding: props.theme.spacing(2),
|
||||
},
|
||||
"& div:nth-of-type(2) > svg": {
|
||||
position: "absolute",
|
||||
top: props.theme.spacing(1),
|
||||
right: props.theme.spacing(1),
|
||||
opacity: 0.6,
|
||||
fontSize: "5em"
|
||||
},
|
||||
"& div:nth-of-type(3)": {
|
||||
backdropFilter: "brightness(70%)",
|
||||
textAlign: "right",
|
||||
padding: props.theme.spacing(0.5),
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const StatBox = (props) => <StyledStatBox variant={"filled"} icon={false}
|
||||
severity={props.color}>
|
||||
<Box>
|
||||
{!isNaN(props.count) ?
|
||||
<>
|
||||
<h3>{props.count}</h3>
|
||||
<h2>{props.count}</h2>
|
||||
<p>{props.text}</p>
|
||||
</> : <CircularProgress variant={"determinate"} />
|
||||
}
|
||||
</div>
|
||||
<div className={"icon"}>
|
||||
{props.icon}
|
||||
</div>
|
||||
<Link to={props.link} className={"small-box-footer text-right p-1"}>
|
||||
More info <ArrowCircleRight />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
<Box>{props.icon}</Box>
|
||||
<Box>
|
||||
<Link to={props.link}>
|
||||
More info <ArrowCircleRight />
|
||||
</Link>
|
||||
</Box>
|
||||
</StyledStatBox>
|
||||
|
||||
const StatusLine = (props) => {
|
||||
const {enabled, text, ...other} = props;
|
||||
@@ -84,96 +123,88 @@ export default function Overview(props) {
|
||||
loadAvg = loadAvg.map(v => sprintf("%.1f", v)).join(", ");
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<h1 className={"m-0 text-dark"}>{L("admin.dashboard")}</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">{L("admin.dashboard")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section className={"content"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row"}>
|
||||
<StatBox color={"info"} count={stats?.userCount}
|
||||
text={L("admin.users_registered")}
|
||||
icon={<People />}
|
||||
link={"/admin/users"} />
|
||||
<StatBox color={"success"} count={stats?.groupCount}
|
||||
text={L("admin.available_groups")}
|
||||
icon={<Groups />}
|
||||
link={"/admin/groups"} />
|
||||
<StatBox color={"warning"} count={stats?.pageCount}
|
||||
text={L("admin.routes_defined")}
|
||||
icon={<LibraryBooks />}
|
||||
link={"/admin/routes"} />
|
||||
<StatBox color={"danger"} count={stats?.errorCount}
|
||||
text={L("admin.error_count")}
|
||||
icon={<BugReport />}
|
||||
link={"/admin/logs"} />
|
||||
</div>
|
||||
</div>
|
||||
<Box m={2} p={2} component={Paper}>
|
||||
<h4>Server Stats</h4><hr />
|
||||
{stats === null ? <CircularProgress /> :
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Web-Base Version</TableCell>
|
||||
<TableCell>{stats.server.version}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Server</TableCell>
|
||||
<TableCell>{stats.server.server ?? "Unknown"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Load Average</TableCell>
|
||||
<TableCell>{loadAvg}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Memory Usage</TableCell>
|
||||
<TableCell>{humanReadableSize(stats.server.memoryUsage)}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Database</TableCell>
|
||||
<TableCell>{stats.server.database}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Captcha</TableCell>
|
||||
<TableCell>
|
||||
<StatusLine enabled={!!stats.server.captcha}
|
||||
text={L("settings." + (stats.server.captcha ? stats.server.captcha.name : "disabled"))}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Mail</TableCell>
|
||||
<TableCell>
|
||||
<StatusLine enabled={!!stats.server.mail}
|
||||
text={L("settings." + (stats.server.mail ? "enabled" : "disabled"))}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Rate-Limiting</TableCell>
|
||||
<TableCell>
|
||||
<StatusLine enabled={!!stats.server.rateLimiting}
|
||||
text={L("settings." + (stats.server.rateLimiting ? "enabled" : "disabled"))}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</Box>
|
||||
</section>
|
||||
</>
|
||||
return <ViewContent title={L("admin.dashboard")} path={[
|
||||
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>
|
||||
]}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={3}>
|
||||
<StatBox color={"info"} count={stats?.userCount}
|
||||
text={L("admin.users_registered")}
|
||||
icon={<People/>}
|
||||
link={"/admin/users"}/>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<StatBox color={"success"} count={stats?.groupCount}
|
||||
text={L("admin.available_groups")}
|
||||
icon={<Groups/>}
|
||||
link={"/admin/groups"}/>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<StatBox color={"warning"} count={stats?.pageCount}
|
||||
text={L("admin.routes_defined")}
|
||||
icon={<LibraryBooks/>}
|
||||
link={"/admin/routes"}/>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<StatBox color={"error"} count={stats?.errorCount}
|
||||
text={L("admin.error_count")}
|
||||
icon={<BugReport />}
|
||||
link={"/admin/logs"}/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Box m={2} p={2} component={Paper}>
|
||||
<h4>Server Stats</h4>
|
||||
<Divider />
|
||||
{stats === null ? <CircularProgress/> :
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Web-Base Version</TableCell>
|
||||
<TableCell>{stats.server.version}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Server</TableCell>
|
||||
<TableCell>{stats.server.server ?? "Unknown"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Load Average</TableCell>
|
||||
<TableCell>{loadAvg}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Memory Usage</TableCell>
|
||||
<TableCell>{humanReadableSize(stats.server.memoryUsage)}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Database</TableCell>
|
||||
<TableCell>{stats.server.database}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Captcha</TableCell>
|
||||
<TableCell>
|
||||
<StatusLine enabled={!!stats.server.captcha}
|
||||
text={L("settings." + (stats.server.captcha ? stats.server.captcha.name : "disabled"))}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Mail</TableCell>
|
||||
<TableCell>
|
||||
<StatusLine enabled={!!stats.server.mail}
|
||||
text={L("settings." + (stats.server.mail ? "enabled" : "disabled"))}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Rate-Limiting</TableCell>
|
||||
<TableCell>
|
||||
<StatusLine enabled={!!stats.server.rateLimiting}
|
||||
text={L("settings." + (stats.server.rateLimiting ? "enabled" : "disabled"))}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</Box>
|
||||
</ViewContent>
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function ChangePasswordBox(props) {
|
||||
onChange={e => setChangePassword({...changePassword, confirm: e.target.value })} />
|
||||
</FormControl>
|
||||
</SpacedFormGroup>
|
||||
<Box className={"w-50"}>
|
||||
<Box sx={{width: "30%", minWidth: 300}}>
|
||||
<PasswordStrength password={changePassword.new} minLength={6} />
|
||||
</Box>
|
||||
</CollapseBox>
|
||||
|
||||
@@ -18,6 +18,7 @@ import ChangePasswordBox from "./change-password-box";
|
||||
import GpgBox from "./gpg-box";
|
||||
import MultiFactorBox from "./mfa-box";
|
||||
import EditProfilePicture from "./edit-picture";
|
||||
import ViewContent from "../../elements/view-content";
|
||||
|
||||
export default function ProfileView(props) {
|
||||
|
||||
@@ -78,24 +79,10 @@ export default function ProfileView(props) {
|
||||
}, [profile, changePassword, api, showDialog, isSaving]);
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<h1 className={"m-0 text-dark"}>
|
||||
{L("account.edit_profile")}
|
||||
</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">{L("account.profile")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Box>
|
||||
<ViewContent title={L("account.edit_profile")} path={[
|
||||
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||
<span key={"profile"}>{L("account.profile")}</span>
|
||||
]}>
|
||||
<Box display={"grid"} gridTemplateColumns={"300px auto"}>
|
||||
<EditProfilePicture api={api} showDialog={showDialog} setProfile={setProfile}
|
||||
profile={profile} setDialogData={setDialogData} />
|
||||
@@ -104,9 +91,9 @@ export default function ProfileView(props) {
|
||||
<FormLabel>{L("account.username")}</FormLabel>
|
||||
<FormControl>
|
||||
<TextField variant={"outlined"}
|
||||
size={"small"}
|
||||
value={profile.name}
|
||||
onChange={e => setProfile({...profile, name: e.target.value })} />
|
||||
size={"small"}
|
||||
value={profile.name}
|
||||
onChange={e => setProfile({...profile, name: e.target.value })} />
|
||||
</FormControl>
|
||||
</SpacedFormGroup>
|
||||
<SpacedFormGroup>
|
||||
@@ -148,14 +135,13 @@ export default function ProfileView(props) {
|
||||
|
||||
<Box mt={2}>
|
||||
<Button variant={"outlined"} color={"primary"}
|
||||
disabled={isSaving || !api.hasPermission("user/updateProfile")}
|
||||
startIcon={isSaving ? <CircularProgress size={12} /> : <Save />}
|
||||
onClick={onUpdateProfile}>
|
||||
{isSaving ? L("general.saving") + "…" : L("general.save")}
|
||||
disabled={isSaving || !api.hasPermission("user/updateProfile")}
|
||||
startIcon={isSaving ? <CircularProgress size={12} /> : <Save />}
|
||||
onClick={onUpdateProfile}>
|
||||
{isSaving ? L("general.saving") + "…" : L("general.save")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
</ViewContent>
|
||||
<Dialog show={dialogData.show}
|
||||
title={dialogData.title}
|
||||
message={dialogData.message}
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as React from "react";
|
||||
import RouteForm from "./route-form";
|
||||
import {KeyboardArrowLeft, Save} from "@mui/icons-material";
|
||||
import ButtonBar from "../../elements/button-bar";
|
||||
import ViewContent from "../../elements/view-content";
|
||||
|
||||
const MonoSpaceTextField = styled(TextField)((props) => ({
|
||||
"& input": {
|
||||
@@ -113,42 +114,35 @@ export default function RouteEditView(props) {
|
||||
return <CircularProgress/>
|
||||
}
|
||||
|
||||
return <div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<ol className={"breadcrumb"}>
|
||||
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
||||
<li className="breadcrumb-item active"><Link to={"/admin/routes"}>{L("routes.title")}</Link></li>
|
||||
<li className="breadcrumb-item active">{isNewRoute ? L("general.new") : L("general.edit")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<div className={"container-fluid"}>
|
||||
<h3>{L(isNewRoute ? "routes.create_route_title" : "routes.edit_route_title")}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<RouteForm route={route} setRoute={setRoute} />
|
||||
<ButtonBar mt={2}>
|
||||
<Button startIcon={<KeyboardArrowLeft />}
|
||||
variant={"outlined"}
|
||||
onClick={() => navigate("/admin/routes")}>
|
||||
{L("general.cancel")}
|
||||
</Button>
|
||||
<Button startIcon={isSaving ? <CircularProgress size={14} /> : <Save />}
|
||||
color={"primary"}
|
||||
variant={"outlined"}
|
||||
disabled={isSaving}
|
||||
onClick={onSave}>
|
||||
{isSaving ? L("general.saving") + "…" : L("general.save")}
|
||||
</Button>
|
||||
</ButtonBar>
|
||||
<Box mt={3}>
|
||||
<h5>{L("routes.validate_route")}</h5>
|
||||
<MonoSpaceTextField value={routeTest} onChange={e => setRouteTest(e.target.value)}
|
||||
variant={"outlined"} size={"small"} fullWidth={true}
|
||||
placeholder={L("routes.validate_route_placeholder") + "…"} />
|
||||
<pre>
|
||||
Match: {JSON.stringify(routeTestResult)}
|
||||
</pre>
|
||||
</Box>
|
||||
</div>
|
||||
return <ViewContent title={L(isNewRoute ? "routes.create_route_title" : "routes.edit_route_title")}
|
||||
path={[
|
||||
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||
<Link key={"routes"} to={"/admin/routes"}>{L("routes.title")}</Link>,
|
||||
<span key={"action"}>{isNewRoute ? L("general.new") : L("general.edit")}</span>,
|
||||
]}>
|
||||
<RouteForm route={route} setRoute={setRoute} />
|
||||
<ButtonBar mt={2}>
|
||||
<Button startIcon={<KeyboardArrowLeft />}
|
||||
variant={"outlined"}
|
||||
onClick={() => navigate("/admin/routes")}>
|
||||
{L("general.cancel")}
|
||||
</Button>
|
||||
<Button startIcon={isSaving ? <CircularProgress size={14} /> : <Save />}
|
||||
color={"primary"}
|
||||
variant={"outlined"}
|
||||
disabled={isSaving}
|
||||
onClick={onSave}>
|
||||
{isSaving ? L("general.saving") + "…" : L("general.save")}
|
||||
</Button>
|
||||
</ButtonBar>
|
||||
<Box mt={3}>
|
||||
<h5>{L("routes.validate_route")}</h5>
|
||||
<MonoSpaceTextField value={routeTest} onChange={e => setRouteTest(e.target.value)}
|
||||
variant={"outlined"} size={"small"} fullWidth={true}
|
||||
placeholder={L("routes.validate_route_placeholder") + "…"} />
|
||||
<pre>
|
||||
Match: {JSON.stringify(routeTestResult)}
|
||||
</pre>
|
||||
</Box>
|
||||
</ViewContent>
|
||||
}
|
||||
@@ -9,12 +9,15 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
Button,
|
||||
IconButton, Checkbox
|
||||
IconButton, Checkbox, Box
|
||||
} from "@mui/material";
|
||||
import {useCallback, useContext, useEffect, useState} from "react";
|
||||
import {LocaleContext} from "shared/locale";
|
||||
import {Add, Cached, Delete, Edit, Refresh} from "@mui/icons-material";
|
||||
import Dialog from "shared/elements/dialog";
|
||||
import ViewContent from "../../elements/view-content";
|
||||
import ButtonBar from "../../elements/button-bar";
|
||||
import TableBodyStriped from "shared/elements/table-body-striped";
|
||||
|
||||
const RouteTableRow = styled(TableRow)((props) => ({
|
||||
"& td": {
|
||||
@@ -22,7 +25,6 @@ const RouteTableRow = styled(TableRow)((props) => ({
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
export default function RouteListView(props) {
|
||||
|
||||
// meta
|
||||
@@ -118,101 +120,89 @@ export default function RouteListView(props) {
|
||||
const BoolCell = (props) => props.checked ? L("general.yes") : L("general.no")
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<h1 className={"m-0 text-dark"}>{L("routes.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">{L("routes.title")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"row"}>
|
||||
<div className={"col-6"} />
|
||||
<div className={"col-6 text-right"}>
|
||||
<div className={"form-group"}>
|
||||
<Button variant={"outlined"} color={"primary"} className={"m-1"} size={"small"}
|
||||
startIcon={<Refresh />} onClick={() => onFetchRoutes(true)}>
|
||||
{L("general.reload")}
|
||||
</Button>
|
||||
<Button variant={"outlined"} className={"m-1"} startIcon={<Add />} size={"small"}
|
||||
disabled={!props.api.hasPermission("routes/add")}
|
||||
onClick={() => navigate("/admin/routes/new")} >
|
||||
{L("general.add")}
|
||||
</Button>
|
||||
<Button variant={"outlined"} className={"m-1"} startIcon={<Cached />} size={"small"}
|
||||
disabled={!props.api.hasPermission("routes/generateCache") || isGeneratingCache}
|
||||
onClick={onRegenerateCache} >
|
||||
{isGeneratingCache ? L("routes.regenerating_cache") + "…" : L("routes.regenerate_cache")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TableContainer component={Paper} style={{overflowX: "initial"}}>
|
||||
<Table stickyHeader size={"small"} className={"table-striped"}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{L("general.id")}</TableCell>
|
||||
<TableCell>{L("routes.route")}</TableCell>
|
||||
<TableCell>{L("routes.type")}</TableCell>
|
||||
<TableCell>{L("routes.target")}</TableCell>
|
||||
<TableCell>{L("routes.extra")}</TableCell>
|
||||
<TableCell align={"center"}>{L("routes.active")}</TableCell>
|
||||
<TableCell align={"center"}>{L("routes.exact")}</TableCell>
|
||||
<TableCell align={"center"}>{L("general.controls")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.entries(routes).map(([id, route]) =>
|
||||
<RouteTableRow key={"route-" + id}>
|
||||
<TableCell>{route.id}</TableCell>
|
||||
<TableCell>{route.pattern}</TableCell>
|
||||
<TableCell>{route.type}</TableCell>
|
||||
<TableCell>{route.target}</TableCell>
|
||||
<TableCell>{route.extra}</TableCell>
|
||||
<TableCell align={"center"}>
|
||||
<Checkbox checked={route.active}
|
||||
size={"small"}
|
||||
disabled={!api.hasPermission(route.active ? "routes/disable" : "routes/enable")}
|
||||
onChange={(e) => onToggleRoute(route.id, e.target.checked)} />
|
||||
</TableCell>
|
||||
<TableCell align={"center"}><BoolCell checked={route.exact} /></TableCell>
|
||||
<TableCell align={"center"}>
|
||||
<IconButton size={"small"} title={L("general.edit")}
|
||||
disabled={!api.hasPermission("routes/add")}
|
||||
color={"primary"}
|
||||
onClick={() => navigate("/admin/routes/" + id)}>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
<IconButton size={"small"} title={L("general.delete")}
|
||||
disabled={!api.hasPermission("routes/remove")}
|
||||
color={"secondary"}
|
||||
onClick={() => setDialogData({
|
||||
open: true,
|
||||
title: L("routes.delete_route_title"),
|
||||
message: L("routes.delete_route_text"),
|
||||
inputs: [
|
||||
{ type: "text", name: "pattern", value: route.pattern, disabled: true}
|
||||
],
|
||||
options: [L("general.ok"), L("general.cancel")],
|
||||
onOption: btn => btn === 0 ? onDeleteRoute(route.id) : true
|
||||
})}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</RouteTableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<ViewContent title={L("routes.title")} path={[
|
||||
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||
<span key={"routes"}>{L("routes.title")}</span>
|
||||
]}>
|
||||
<ButtonBar mb={1}>
|
||||
<Button variant={"outlined"} color={"primary"} size={"small"}
|
||||
startIcon={<Refresh />} onClick={() => onFetchRoutes(true)}>
|
||||
{L("general.reload")}
|
||||
</Button>
|
||||
<Button variant={"outlined"} startIcon={<Add />} size={"small"}
|
||||
disabled={!props.api.hasPermission("routes/add")}
|
||||
onClick={() => navigate("/admin/routes/new")} >
|
||||
{L("general.add")}
|
||||
</Button>
|
||||
<Button variant={"outlined"} startIcon={<Cached />} size={"small"}
|
||||
disabled={!props.api.hasPermission("routes/generateCache") || isGeneratingCache}
|
||||
onClick={onRegenerateCache} >
|
||||
{isGeneratingCache ? L("routes.regenerating_cache") + "…" : L("routes.regenerate_cache")}
|
||||
</Button>
|
||||
</ButtonBar>
|
||||
<TableContainer component={Paper} sx={{overflowX: "initial"}}>
|
||||
<Table stickyHeader size={"small"}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{L("general.id")}</TableCell>
|
||||
<TableCell>{L("routes.route")}</TableCell>
|
||||
<TableCell>{L("routes.type")}</TableCell>
|
||||
<TableCell>{L("routes.target")}</TableCell>
|
||||
<TableCell>{L("routes.extra")}</TableCell>
|
||||
<TableCell align={"center"}>{L("routes.active")}</TableCell>
|
||||
<TableCell align={"center"}>{L("routes.exact")}</TableCell>
|
||||
<TableCell align={"center"}>{L("general.controls")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBodyStriped>
|
||||
{Object.entries(routes).map(([id, route]) =>
|
||||
<RouteTableRow key={"route-" + id}>
|
||||
<TableCell>{route.id}</TableCell>
|
||||
<TableCell>{route.pattern}</TableCell>
|
||||
<TableCell>{route.type}</TableCell>
|
||||
<TableCell>{route.target}</TableCell>
|
||||
<TableCell>{route.extra}</TableCell>
|
||||
<TableCell align={"center"}>
|
||||
<Checkbox checked={route.active}
|
||||
size={"small"}
|
||||
disabled={!api.hasPermission(route.active ? "routes/disable" : "routes/enable")}
|
||||
onChange={(e) => onToggleRoute(route.id, e.target.checked)} />
|
||||
</TableCell>
|
||||
<TableCell align={"center"}><BoolCell checked={route.exact} /></TableCell>
|
||||
<TableCell align={"center"}>
|
||||
<IconButton size={"small"} title={L("general.edit")}
|
||||
disabled={!api.hasPermission("routes/add")}
|
||||
color={"primary"}
|
||||
onClick={() => navigate("/admin/routes/" + id)}>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
<IconButton size={"small"} title={L("general.delete")}
|
||||
disabled={!api.hasPermission("routes/remove")}
|
||||
color={"secondary"}
|
||||
onClick={() => setDialogData({
|
||||
open: true,
|
||||
title: L("routes.delete_route_title"),
|
||||
message: L("routes.delete_route_text"),
|
||||
inputs: [
|
||||
{ type: "text", name: "pattern", value: route.pattern, disabled: true}
|
||||
],
|
||||
options: [L("general.ok"), L("general.cancel")],
|
||||
onOption: btn => btn === 0 ? onDeleteRoute(route.id) : true
|
||||
})}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</RouteTableRow>
|
||||
)}
|
||||
</TableBodyStriped>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</ViewContent>
|
||||
<Dialog show={dialogData.open}
|
||||
onClose={() => { setDialogData({open: false}); dialogData.onClose && dialogData.onClose() }}
|
||||
onClose={() => {
|
||||
setDialogData({open: false});
|
||||
dialogData.onClose && dialogData.onClose()
|
||||
}}
|
||||
title={dialogData.title}
|
||||
message={dialogData.message}
|
||||
onOption={dialogData.onOption}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import {Link} from "react-router-dom";
|
||||
import {
|
||||
Add,
|
||||
Delete,
|
||||
Delete, DownloadDone,
|
||||
LibraryBooks,
|
||||
Mail,
|
||||
RestartAlt,
|
||||
@@ -33,6 +33,7 @@ import SettingsNumberInput from "./input-number";
|
||||
import SettingsPasswordInput from "./input-password";
|
||||
import SettingsTextInput from "./input-text";
|
||||
import SettingsSelection from "./input-selection";
|
||||
import ViewContent from "../../elements/view-content";
|
||||
|
||||
export default function SettingsView(props) {
|
||||
|
||||
@@ -197,6 +198,16 @@ export default function SettingsView(props) {
|
||||
}
|
||||
}, [api, showDialog, testMailAddress, isSending]);
|
||||
|
||||
const onTestRedis = useCallback(data => {
|
||||
api.testRedis().then(data => {
|
||||
if (!data.success) {
|
||||
showDialog(data.msg, L("settings.redis_test_error"));
|
||||
} else {
|
||||
showDialog(L("settings.redis_test_success"), L("general.success"));
|
||||
}
|
||||
});
|
||||
}, [api, showDialog]);
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
setFetchSettings(true);
|
||||
setNewKey("");
|
||||
@@ -266,7 +277,8 @@ export default function SettingsView(props) {
|
||||
<Button startIcon={isSending ? <CircularProgress size={14} /> : <Send />}
|
||||
variant={"outlined"} onClick={onSendTestMail}
|
||||
fullWidth={true}
|
||||
disabled={!settings.mail_enabled || isSending || !api.hasPermission("mail/test")}>
|
||||
disabled={!settings.mail_enabled || isSending || !api.hasPermission("mail/test") || hasChanged}
|
||||
title={hasChanged ? L("general.unsaved_changes") : ""}>
|
||||
{isSending ? L("general.sending") + "…" : L("general.send")}
|
||||
</Button>
|
||||
</Grid>
|
||||
@@ -300,6 +312,12 @@ export default function SettingsView(props) {
|
||||
renderTextInput("redis_host", !settings.rate_limiting_enabled),
|
||||
renderNumberInput("redis_port", 1, 65535, !settings.rate_limiting_enabled),
|
||||
renderPasswordInput("redis_password", !settings.rate_limiting_enabled),
|
||||
<Button startIcon={<DownloadDone />}
|
||||
variant={"outlined"} onClick={onTestRedis}
|
||||
disabled={!settings.rate_limiting_enabled || !api.hasPermission("testRedis") || hasChanged}
|
||||
title={hasChanged ? L("general.unsaved_changes") : ""}>
|
||||
{L("settings.redis_test")}
|
||||
</Button>
|
||||
];
|
||||
} else if (selectedTab === "uncategorized") {
|
||||
return <TableContainer component={Paper}>
|
||||
@@ -357,57 +375,42 @@ export default function SettingsView(props) {
|
||||
return <CircularProgress />
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<h1 className={"m-0 text-dark"}>{L("settings.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">{L("settings.title")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<Tabs value={selectedTab} onChange={(e, v) => setSelectedTab(v)} component={Paper}>
|
||||
<Tab value={"general"} label={L("settings.general")}
|
||||
icon={<SettingsApplications />} iconPosition={"start"} />
|
||||
<Tab value={"mail"} label={L("settings.mail")}
|
||||
icon={<Mail />} iconPosition={"start"} />
|
||||
<Tab value={"captcha"} label={L("settings.captcha")}
|
||||
icon={<SmartToy />} iconPosition={"start"} />
|
||||
<Tab value={"redis"} label={L("settings.rate_limit")}
|
||||
icon={<Storage />} iconPosition={"start"} />
|
||||
<Tab value={"uncategorized"} label={L("settings.uncategorized")}
|
||||
icon={<LibraryBooks />} iconPosition={"start"} />
|
||||
</Tabs>
|
||||
<Box p={2}>
|
||||
return <ViewContent title={L("settings.title")} path={[
|
||||
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||
<span key={"settings"}>{L("settings.title")}</span>,
|
||||
]}>
|
||||
<Tabs value={selectedTab} onChange={(e, v) => setSelectedTab(v)} component={Paper}>
|
||||
<Tab value={"general"} label={L("settings.general")}
|
||||
icon={<SettingsApplications />} iconPosition={"start"} />
|
||||
<Tab value={"mail"} label={L("settings.mail")}
|
||||
icon={<Mail />} iconPosition={"start"} />
|
||||
<Tab value={"captcha"} label={L("settings.captcha")}
|
||||
icon={<SmartToy />} iconPosition={"start"} />
|
||||
<Tab value={"redis"} label={L("settings.rate_limit")}
|
||||
icon={<Storage />} iconPosition={"start"} />
|
||||
<Tab value={"uncategorized"} label={L("settings.uncategorized")}
|
||||
icon={<LibraryBooks />} iconPosition={"start"} />
|
||||
</Tabs>
|
||||
<Box p={2}>
|
||||
{
|
||||
renderTab()
|
||||
}
|
||||
</Box>
|
||||
<ButtonBar>
|
||||
<Button color={"primary"}
|
||||
onClick={onSaveSettings}
|
||||
disabled={isSaving || !api.hasPermission("settings/set")}
|
||||
startIcon={isSaving ? <CircularProgress size={14} /> : <Save />}
|
||||
variant={"outlined"} title={L(hasChanged ? "general.unsaved_changes" : "general.save")}>
|
||||
{isSaving ? L("general.saving") + "…" : (L("general.save") + (hasChanged ? " *" : ""))}
|
||||
</Button>
|
||||
<Button color={"secondary"}
|
||||
onClick={onReset}
|
||||
disabled={isSaving}
|
||||
startIcon={<RestartAlt />}
|
||||
variant={"outlined"} title={L("general.reset")}>
|
||||
{L("general.reset")}
|
||||
</Button>
|
||||
</ButtonBar>
|
||||
</div>
|
||||
</>
|
||||
|
||||
</Box>
|
||||
<ButtonBar>
|
||||
<Button color={"primary"}
|
||||
onClick={onSaveSettings}
|
||||
disabled={isSaving || !api.hasPermission("settings/set")}
|
||||
startIcon={isSaving ? <CircularProgress size={14} /> : <Save />}
|
||||
variant={"outlined"} title={L(hasChanged ? "general.unsaved_changes" : "general.save")}>
|
||||
{isSaving ? L("general.saving") + "…" : (L("general.save") + (hasChanged ? " *" : ""))}
|
||||
</Button>
|
||||
<Button color={"secondary"}
|
||||
onClick={onReset}
|
||||
disabled={isSaving}
|
||||
startIcon={<RestartAlt />}
|
||||
variant={"outlined"} title={L("general.reset")}>
|
||||
{L("general.reset")}
|
||||
</Button>
|
||||
</ButtonBar>
|
||||
</ViewContent>
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import {useCallback, useContext, useEffect, useState} from "react";
|
||||
import {CircularProgress} from "@mui/material";
|
||||
import {LocaleContext} from "shared/locale";
|
||||
import * as React from "react";
|
||||
|
||||
import ViewContent from "../../elements/view-content";
|
||||
|
||||
export default function UserEditView(props) {
|
||||
|
||||
@@ -56,34 +56,11 @@ export default function UserEditView(props) {
|
||||
return <CircularProgress />
|
||||
}
|
||||
|
||||
return <div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<ol className={"breadcrumb"}>
|
||||
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
||||
<li className="breadcrumb-item active"><Link to={"/admin/users"}>User</Link></li>
|
||||
<li className="breadcrumb-item active">{ isNewUser ? "New" : "Edit" }</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<div className={"container-fluid"}>
|
||||
<h3>{L(isNewUser ? "Create new User" : "Edit User")}</h3>
|
||||
<div className={"col-sm-12 col-lg-6"}>
|
||||
<div className={"row"}>
|
||||
<div className={"col-sm-6 form-group"}>
|
||||
<label htmlFor={"username"}>{L("account.username")}</label>
|
||||
<input type={"text"} className={"form-control"} placeholder={L("account.username")}
|
||||
name={"username"} id={"username"} maxLength={32} value={user.name}
|
||||
onChange={(e) => setUser({...user, name: e.target.value})}/>
|
||||
</div>
|
||||
<div className={"col-sm-6 form-group"}>
|
||||
<label htmlFor={"fullName"}>{L("account.full_name")}</label>
|
||||
<input type={"text"} className={"form-control"} placeholder={L("account.full_name")}
|
||||
name={"fullName"} id={"fullName"} maxLength={32} value={user.fullName}
|
||||
onChange={(e) => setUser({...user, fullName: e.target.value})}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
return <ViewContent title={L(isNewUser ? "account.new_user" : "account.edit_user")} path={[
|
||||
<Link key={"dashboard"} to={"/admin/dashboard"}>Home</Link>,
|
||||
<Link key={"users"} to={"/admin/users"}>User</Link>,
|
||||
<span key={"action"}>{isNewUser ? "New" : "Edit"}</span>
|
||||
]}>
|
||||
|
||||
</ViewContent>
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import {Chip} from "@mui/material";
|
||||
import {Edit, Add} from "@mui/icons-material";
|
||||
import usePagination from "shared/hooks/pagination";
|
||||
|
||||
import ViewContent from "../../elements/view-content";
|
||||
|
||||
export default function UserListView(props) {
|
||||
|
||||
@@ -69,43 +69,25 @@ export default function UserListView(props) {
|
||||
]),
|
||||
];
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<h1 className={"m-0 text-dark"}>{L("account.users")}</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">{L("account.users")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<div className={"container-fluid"}>
|
||||
|
||||
<DataTable
|
||||
data={users}
|
||||
pagination={pagination}
|
||||
defaultSortOrder={"asc"}
|
||||
defaultSortColumn={0}
|
||||
className={"table table-striped"}
|
||||
fetchData={onFetchUsers}
|
||||
placeholder={"No users to display"}
|
||||
columns={columnDefinitions}
|
||||
buttons={[{
|
||||
key: "btn-create",
|
||||
color: "primary",
|
||||
startIcon: <Add />,
|
||||
children: L("general.create_new"),
|
||||
disabled: !api.hasPermission("user/create") && !api.hasPermission("user/invite"),
|
||||
onClick: () => navigate("/admin/user/new")
|
||||
}]}/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
return <ViewContent title={L("account.users")} path={[
|
||||
<Link key={"home"} to={"/admin/dashboard"}>Home</Link>,
|
||||
<span key={"users"}>{L("account.users")}</span>
|
||||
]}>
|
||||
<DataTable
|
||||
data={users}
|
||||
pagination={pagination}
|
||||
defaultSortOrder={"asc"}
|
||||
defaultSortColumn={0}
|
||||
fetchData={onFetchUsers}
|
||||
placeholder={L("account.user_list_placeholder")}
|
||||
columns={columnDefinitions}
|
||||
buttons={[{
|
||||
key: "btn-create",
|
||||
color: "primary",
|
||||
startIcon: <Add />,
|
||||
children: L("general.create_new"),
|
||||
disabled: !api.hasPermission("user/create") && !api.hasPermission("user/invite"),
|
||||
onClick: () => navigate("/admin/user/new")
|
||||
}]}/>
|
||||
</ViewContent>
|
||||
}
|
||||
Reference in New Issue
Block a user