acl view done
This commit is contained in:
parent
aa51380055
commit
a8f4c84f60
@ -24,7 +24,7 @@ class ArrayType extends Parameter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function parseParam($value): bool {
|
public function parseParam($value): bool {
|
||||||
if(!is_array($value)) {
|
if (!is_array($value)) {
|
||||||
if (!$this->canBeOne) {
|
if (!$this->canBeOne) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
|
@ -93,7 +93,7 @@ abstract class Request {
|
|||||||
foreach ($structure as $name => $param) {
|
foreach ($structure as $name => $param) {
|
||||||
$value = $values[$name] ?? NULL;
|
$value = $values[$name] ?? NULL;
|
||||||
|
|
||||||
$isEmpty = (is_string($value) && strlen($value) === 0) || (is_array($value) && empty($value));
|
$isEmpty = is_string($value) && strlen($value) === 0;
|
||||||
if (!$param->optional && (is_null($value) || $isEmpty)) {
|
if (!$param->optional && (is_null($value) || $isEmpty)) {
|
||||||
return $this->createError("Missing parameter: $name");
|
return $this->createError("Missing parameter: $name");
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,23 @@
|
|||||||
import {useCallback, useContext, useEffect, useState} from "react";
|
import {useCallback, useContext, useEffect, useState} from "react";
|
||||||
import {LocaleContext} from "shared/locale";
|
import {LocaleContext} from "shared/locale";
|
||||||
import {Link, useNavigate} from "react-router-dom";
|
import {Link, useNavigate} from "react-router-dom";
|
||||||
import {Button, Checkbox, TextField, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@material-ui/core";
|
import {
|
||||||
import {Add, Refresh} from "@material-ui/icons";
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
TextField,
|
||||||
|
Paper,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
IconButton
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import {Add, Delete, Edit, Refresh} from "@material-ui/icons";
|
||||||
import {USER_GROUP_ADMIN} from "shared/constants";
|
import {USER_GROUP_ADMIN} from "shared/constants";
|
||||||
|
import Dialog from "shared/elements/dialog";
|
||||||
|
import {TableFooter} from "@mui/material";
|
||||||
|
|
||||||
|
|
||||||
export default function AccessControlList(props) {
|
export default function AccessControlList(props) {
|
||||||
@ -20,6 +34,9 @@ export default function AccessControlList(props) {
|
|||||||
// filters
|
// filters
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
// view
|
||||||
|
const [dialogData, setDialogData] = useState({open: false});
|
||||||
|
|
||||||
const onFetchACL = useCallback((force = false) => {
|
const onFetchACL = useCallback((force = false) => {
|
||||||
if (force || fetchACL) {
|
if (force || fetchACL) {
|
||||||
setFetchACL(false);
|
setFetchACL(false);
|
||||||
@ -84,8 +101,33 @@ export default function AccessControlList(props) {
|
|||||||
}
|
}
|
||||||
}, [acl]);
|
}, [acl]);
|
||||||
|
|
||||||
|
const onDeletePermission = useCallback(method => {
|
||||||
|
props.api.deletePermission(method).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
let newACL = acl.filter(acl => acl.method.toLowerCase() !== method.toLowerCase());
|
||||||
|
setACL(newACL);
|
||||||
|
} else {
|
||||||
|
props.showDialog("Error deleting permission: " + data.msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [acl]);
|
||||||
|
|
||||||
|
const onUpdatePermission = useCallback((inputData, groups) => {
|
||||||
|
props.api.updatePermission(inputData.method, groups, inputData.description).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
let newACL = acl.filter(acl => acl.method.toLowerCase() !== inputData.method.toLowerCase());
|
||||||
|
newACL.push({method: inputData.method, groups: groups, description: inputData.description});
|
||||||
|
newACL = newACL.sort((a, b) => a.method.localeCompare(b.method))
|
||||||
|
setACL(newACL);
|
||||||
|
} else {
|
||||||
|
props.showDialog("Error updating permission: " + data.msg);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [acl]);
|
||||||
|
|
||||||
const isRestricted = (method) => {
|
const isRestricted = (method) => {
|
||||||
return ["permissions/update", "permissions/delete"].includes(method.toLowerCase());
|
return ["permissions/update", "permissions/delete"].includes(method.toLowerCase()) &&
|
||||||
|
!props.api.hasGroup(USER_GROUP_ADMIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
const PermissionList = () => {
|
const PermissionList = () => {
|
||||||
@ -104,13 +146,44 @@ export default function AccessControlList(props) {
|
|||||||
rows.push(
|
rows.push(
|
||||||
<TableRow key={"perm-" + index}>
|
<TableRow key={"perm-" + index}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<b>{permission.method}</b><br />
|
<div style={{display: "grid", gridTemplateColumns: "60px auto"}}>
|
||||||
<i>{permission.description}</i>
|
<div style={{alignSelf: "center"}}>
|
||||||
|
<IconButton style={{padding: 0}} size={"small"} color={"primary"}
|
||||||
|
disabled={isRestricted(permission.method)}
|
||||||
|
onClick={() => setDialogData({
|
||||||
|
open: true,
|
||||||
|
title: L("Edit permission"),
|
||||||
|
inputs: [
|
||||||
|
{ type: "label", value: L("general.method") + ":" },
|
||||||
|
{ type: "text", name: "method", value: permission.method, disabled: true },
|
||||||
|
{ type: "label", value: L("general.description") + ":" },
|
||||||
|
{ type: "text", name: "description", value: permission.description, maxLength: 128 }
|
||||||
|
],
|
||||||
|
onOption: (option, inputData) => option === 0 && onUpdatePermission(inputData, permission.groups)
|
||||||
|
})} >
|
||||||
|
<Edit />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton style={{padding: 0}} size={"small"} color={"secondary"}
|
||||||
|
disabled={isRestricted(permission.method)}
|
||||||
|
onClick={() => setDialogData({
|
||||||
|
open: true,
|
||||||
|
title: L("Do you really want to delete this permission?"),
|
||||||
|
message: "Method: " + permission.method,
|
||||||
|
onOption: (option) => option === 0 && onDeletePermission(permission.method)
|
||||||
|
})} >
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b>{permission.method}</b><br />
|
||||||
|
<i>{permission.description}</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{groups.map(group => <TableCell key={"perm-" + index + "-group-" + group.id} align={"center"}>
|
{groups.map(group => <TableCell key={"perm-" + index + "-group-" + group.id} align={"center"}>
|
||||||
<Checkbox checked={!permission.groups.length || permission.groups.includes(group.id)}
|
<Checkbox checked={!permission.groups.length || permission.groups.includes(group.id)}
|
||||||
onChange={(e) => onChangePermission(index, group.id, e.target.checked)}
|
onChange={(e) => onChangePermission(index, group.id, e.target.checked)}
|
||||||
disabled={isRestricted(permission.method) || !props.api.hasGroup(USER_GROUP_ADMIN)} />
|
disabled={isRestricted(permission.method)} />
|
||||||
</TableCell>)}
|
</TableCell>)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
@ -155,18 +228,28 @@ export default function AccessControlList(props) {
|
|||||||
{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"} className={"m-1"} startIcon={<Add />} disabled={!props.api.hasGroup(USER_GROUP_ADMIN)}
|
||||||
onClick={() => navigate("/admin/acl/new")}>
|
onClick={() => setDialogData({
|
||||||
|
open: true,
|
||||||
|
title: L("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") }
|
||||||
|
],
|
||||||
|
onOption: (option, inputData) => option === 0 && onUpdatePermission(inputData, [])
|
||||||
|
})} >
|
||||||
{L("general.add")}
|
{L("general.add")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={Paper} style={{overflowX: "initial"}}>
|
||||||
<Table size={"small"}>
|
<Table stickyHeader size={"small"} className={"table-striped"}>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell sx={{width: "auto"}}>{L("permission")}</TableCell>
|
<TableCell>{L("permission")}</TableCell>
|
||||||
{ groups.map(group => <TableCell key={"group-" + group.id} align={"center"}>
|
{ groups.map(group => <TableCell key={"group-" + group.id} align={"center"}>
|
||||||
{group.name}
|
{group.name}
|
||||||
</TableCell>) }
|
</TableCell>) }
|
||||||
@ -178,5 +261,13 @@ export default function AccessControlList(props) {
|
|||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</div>
|
</div>
|
||||||
|
<Dialog show={dialogData.open}
|
||||||
|
onClose={() => setDialogData({open: false})}
|
||||||
|
title={dialogData.title}
|
||||||
|
message={dialogData.message}
|
||||||
|
onOption={dialogData.onOption}
|
||||||
|
inputs={dialogData.inputs}
|
||||||
|
options={[L("general.ok"), L("general.cancel")]} />
|
||||||
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
@ -7,7 +7,7 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Input, List, ListItem, TextField
|
List, ListItem, TextField
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
export default function Dialog(props) {
|
export default function Dialog(props) {
|
||||||
@ -24,7 +24,9 @@ export default function Dialog(props) {
|
|||||||
if (props.inputs) {
|
if (props.inputs) {
|
||||||
let initialData = {};
|
let initialData = {};
|
||||||
for (const input of props.inputs) {
|
for (const input of props.inputs) {
|
||||||
initialData[input.name] = input.value || "";
|
if (input.type !== "label") {
|
||||||
|
initialData[input.name] = input.value || "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setInputData(initialData);
|
setInputData(initialData);
|
||||||
}
|
}
|
||||||
@ -47,6 +49,9 @@ export default function Dialog(props) {
|
|||||||
delete inputProps.type;
|
delete inputProps.type;
|
||||||
|
|
||||||
switch (input.type) {
|
switch (input.type) {
|
||||||
|
case 'label':
|
||||||
|
inputElements.push(<span {...inputProps}>{input.value}</span>);
|
||||||
|
break;
|
||||||
case 'text':
|
case 'text':
|
||||||
case 'password':
|
case 'password':
|
||||||
inputElements.push(<TextField
|
inputElements.push(<TextField
|
||||||
@ -56,6 +61,7 @@ export default function Dialog(props) {
|
|||||||
size={"small"} fullWidth={true}
|
size={"small"} fullWidth={true}
|
||||||
key={"input-" + input.name}
|
key={"input-" + input.name}
|
||||||
value={inputData[input.name] || ""}
|
value={inputData[input.name] || ""}
|
||||||
|
defaultValue={input.defaultValue || ""}
|
||||||
onChange={e => setInputData({ ...inputData, [input.name]: e.target.value })}
|
onChange={e => setInputData({ ...inputData, [input.name]: e.target.value })}
|
||||||
/>)
|
/>)
|
||||||
break;
|
break;
|
||||||
|
@ -5,6 +5,7 @@ export default function useAsyncSearch(callback, minLength = 1) {
|
|||||||
|
|
||||||
const [searchString, setSearchString] = useState("");
|
const [searchString, setSearchString] = useState("");
|
||||||
const [results, setResults] = useState(null);
|
const [results, setResults] = useState(null);
|
||||||
|
const [isSearching, setSearching] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (minLength > 0 && (!searchString || searchString.length < minLength)) {
|
if (minLength > 0 && (!searchString || searchString.length < minLength)) {
|
||||||
@ -12,9 +13,13 @@ export default function useAsyncSearch(callback, minLength = 1) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(searchString).then(results => {
|
if (!isSearching) {
|
||||||
setResults(results || null);
|
setSearching(true);
|
||||||
});
|
callback(searchString).then(results => {
|
||||||
|
setResults(results || null);
|
||||||
|
setSearching(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
}, [searchString]);
|
}, [searchString]);
|
||||||
|
|
||||||
return [searchString, setSearchString, results];
|
return [searchString, setSearchString, results];
|
||||||
|
Loading…
Reference in New Issue
Block a user