Some refactoring, roles page
This commit is contained in:
parent
e16937dac6
commit
93c145c5e5
15
src/App.js
15
src/App.js
@ -11,7 +11,6 @@ import Login from './pages/Login';
|
||||
import { get, setSession, setSettings } from './util/Util';
|
||||
import { PrivateRoute } from './structures/PrivateRoute';
|
||||
import { UnauthedRoute } from './structures/UnauthedRoute';
|
||||
import Users from './pages/Users';
|
||||
import Admin from './pages/Admin';
|
||||
import TitledPage from './components/TitledPage';
|
||||
import Register from './pages/Register';
|
||||
@ -45,8 +44,12 @@ function App() {
|
||||
{ to: '/applications', label: 'Applications', relative: true }
|
||||
]
|
||||
},
|
||||
{ to: '/users', label: 'Users' },
|
||||
{ to: '/admin', label: 'Admin' }
|
||||
{
|
||||
to: '/admin', label: 'Admin', items: [
|
||||
{ to: '/users', label: 'Users', relative: true },
|
||||
{ to: '/roles', label: 'Roles', relative: true }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
if (loading) return null;
|
||||
@ -79,12 +82,6 @@ function App() {
|
||||
</TitledPage>
|
||||
</PrivateRoute >} />
|
||||
|
||||
<Route path='/users/*' element={<PrivateRoute>
|
||||
<TitledPage title='Users' >
|
||||
<Users />
|
||||
</TitledPage>
|
||||
</PrivateRoute >} />
|
||||
|
||||
<Route path='/admin/*' element={<PrivateRoute>
|
||||
<TitledPage title='Admin'>
|
||||
<Admin />
|
||||
|
19
src/components/PageControls.js
Normal file
19
src/components/PageControls.js
Normal file
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
export const PageButtons = ({setPage, page, length}) => {
|
||||
return <div className='flex is-vertical-align is-center page-controls'>
|
||||
<button className="button dark" onClick={() => setPage(page - 1 || 1)}>Previous</button>
|
||||
<p>Page: {page}</p>
|
||||
<button className="button dark" onClick={() => {
|
||||
if (length === 10) setPage(page + 1);
|
||||
}}>Next</button>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const BackButton = () => {
|
||||
const navigate = useNavigate();
|
||||
return <button className="button dark" onClick={() => {
|
||||
navigate(-1);
|
||||
}}>Back</button>;
|
||||
};
|
64
src/components/PermissionsView.js
Normal file
64
src/components/PermissionsView.js
Normal file
@ -0,0 +1,64 @@
|
||||
import React, { useRef } from "react";
|
||||
|
||||
export const Permission = ({ name, value, chain, updatePerms }) => {
|
||||
const inputRef = useRef();
|
||||
const onChange = () => {
|
||||
const val = inputRef.current.value;
|
||||
updatePerms(chain, val);
|
||||
};
|
||||
|
||||
return <li className="flex is-vertical-align">
|
||||
<label>{name}</label>
|
||||
<input ref={inputRef} onChange={onChange} max={10} min={0} type='number' defaultValue={value}></input>
|
||||
</li>;
|
||||
};
|
||||
|
||||
export const PermissionGroup = ({ name, value, chain, updatePerms }) => {
|
||||
const elements = [];
|
||||
|
||||
for (const [perm, val] of Object.entries(value)) {
|
||||
const props = { key: perm, name: perm, value: val, updatePerms, chain: `${chain}:${perm}` };
|
||||
if(typeof val ==='object') elements.push(<PermissionGroup {...props} />);
|
||||
else elements.push(<Permission {...props} />);
|
||||
}
|
||||
return <li>
|
||||
<div className="groupName">
|
||||
<b>Group: {name}</b>
|
||||
</div>
|
||||
<ul>
|
||||
{elements}
|
||||
</ul>
|
||||
</li>;
|
||||
};
|
||||
|
||||
export const Permissions = ({ perms, updatePerms }) => {
|
||||
|
||||
const elements = [];
|
||||
const keys = Object.keys(perms);
|
||||
for (const perm of keys) {
|
||||
const props = { key: perm, name: perm, value: perms[perm], chain: perm, updatePerms };
|
||||
let Elem = null;
|
||||
switch (typeof perms[perm]) {
|
||||
case 'number':
|
||||
Elem = Permission;
|
||||
break;
|
||||
case 'object':
|
||||
Elem = PermissionGroup;
|
||||
break;
|
||||
default:
|
||||
// eslint-disable-next-line react/display-name
|
||||
Elem = () => {
|
||||
return <p>Uknown permission structure</p>;
|
||||
};
|
||||
break;
|
||||
}
|
||||
elements.push(<Elem {...props} />);
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h3>Permissions</h3>
|
||||
<ul className="tree">
|
||||
{elements}
|
||||
</ul>
|
||||
</div>;
|
||||
};
|
35
src/components/RateLimitsView.js
Normal file
35
src/components/RateLimitsView.js
Normal file
@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
|
||||
const RateLimit = ({route, limit}) => {
|
||||
|
||||
return <div>
|
||||
<p><b>{route}</b></p>
|
||||
<label>Limit</label>
|
||||
<input min={0} defaultValue={limit.limit} type='number' />
|
||||
<label>Time</label>
|
||||
<input min={0} defaultValue={limit.time} type='number' />
|
||||
<label>Enabled</label>
|
||||
<input defaultValue={!limit.disabled} type='checkbox' />
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const RateLimits = ({ rateLimits }) => {
|
||||
|
||||
const routes = Object.keys(rateLimits);
|
||||
const elements = routes.map((route, index) => {
|
||||
return <li key={index}>
|
||||
<RateLimit {...{route, limit:rateLimits[route]}} />
|
||||
</li>;
|
||||
});
|
||||
|
||||
return <div>
|
||||
<h2>Rate Limits</h2>
|
||||
<ul>
|
||||
{elements}
|
||||
</ul>
|
||||
|
||||
<code>
|
||||
{JSON.stringify(rateLimits, null, 4)}
|
||||
</code>
|
||||
</div>;
|
||||
};
|
@ -53,13 +53,13 @@ li input {
|
||||
.tree input{
|
||||
width: 60px !important;
|
||||
}
|
||||
.userActionButtons{
|
||||
.actionButtons{
|
||||
display:flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.userActionButtons .button+.button{
|
||||
.actionButtons .button+.button{
|
||||
margin-left: 0;
|
||||
}
|
||||
.registerCodeWrapper{
|
||||
|
@ -1,9 +1,13 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Route, Routes } from "react-router";
|
||||
import FileSelector from "../components/FileSelector";
|
||||
import '../css/pages/Admin.css';
|
||||
import ErrorBoundary from "../util/ErrorBoundary";
|
||||
import Roles from "./admin/Roles";
|
||||
import Users from "./admin/Users";
|
||||
|
||||
const Admin = () => {
|
||||
|
||||
const Main = () => {
|
||||
const [file, setFile] = useState(null);
|
||||
|
||||
const submit = (event) => {
|
||||
@ -31,4 +35,15 @@ const Admin = () => {
|
||||
</div>;
|
||||
};
|
||||
|
||||
const Admin = () => {
|
||||
|
||||
return <ErrorBoundary>
|
||||
<Routes>
|
||||
<Route path="/" element={<Main />} />
|
||||
<Route path="/users/*" element={<Users />} />
|
||||
<Route path='/roles/*' element={<Roles />} />
|
||||
</Routes>
|
||||
</ErrorBoundary>;
|
||||
};
|
||||
|
||||
export default Admin;
|
124
src/pages/admin/Roles.js
Normal file
124
src/pages/admin/Roles.js
Normal file
@ -0,0 +1,124 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Route, Routes, useNavigate, useParams } from "react-router";
|
||||
import { BackButton, PageButtons } from "../../components/PageControls";
|
||||
import { Permissions } from "../../components/PermissionsView";
|
||||
import { RateLimits } from "../../components/RateLimitsView";
|
||||
import { Table, TableListEntry } from "../../components/Table";
|
||||
import ErrorBoundary from "../../util/ErrorBoundary";
|
||||
import { get, patch } from "../../util/Util";
|
||||
|
||||
const Role = ({ role }) => {
|
||||
|
||||
const perms = { ...role.permissions };
|
||||
const updatePerms = (chain, value) => {
|
||||
value = parseInt(value);
|
||||
if (isNaN(value)) return;
|
||||
|
||||
let selected = perms;
|
||||
const keys = chain.split(':');
|
||||
for (const key of keys) {
|
||||
if (key === keys[keys.length - 1]) selected[key] = value;
|
||||
else selected = selected[key];
|
||||
}
|
||||
};
|
||||
|
||||
const commitPerms = async () => {
|
||||
await patch(`/api/roles/${role.id}`, perms);
|
||||
};
|
||||
|
||||
return <div className='role-card'>
|
||||
<div className='row'>
|
||||
<div className='col-6-lg col-12'>
|
||||
|
||||
<div className='flex is-vertical-align flex-wrap'>
|
||||
<BackButton />
|
||||
<h2>Role {role.id}</h2>
|
||||
</div>
|
||||
|
||||
<div className='actionButtons mt-4'>
|
||||
{role.disabled ? <button className="button success">Enable</button> : <button className="button error">Disable</button>}
|
||||
<button className='button error'>Delete</button>
|
||||
</div>
|
||||
|
||||
<p><b>Created: </b>{new Date(role.createdTimestamp).toDateString()}</p>
|
||||
<p><b>Disabled: </b>{role.disabled.toString()}</p>
|
||||
|
||||
<label htmlFor='username'>Name</label>
|
||||
<input autoComplete='off' id='username' defaultValue={role.name} />
|
||||
|
||||
</div>
|
||||
|
||||
<div className="col-6-lg col-12">
|
||||
<Permissions updatePerms={updatePerms} perms={role.permissions} />
|
||||
<button onClick={commitPerms} className="button primary">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='row'>
|
||||
<RateLimits rateLimits={role.rateLimits} />
|
||||
</div>
|
||||
|
||||
</div>;
|
||||
};
|
||||
|
||||
const Roles = () => {
|
||||
|
||||
const [error, setError] = useState(null);
|
||||
const [roles, setRoles] = useState([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const result = await get('/api/roles', { page });
|
||||
if (result.success) {
|
||||
setError(null);
|
||||
setRoles(result.data);
|
||||
} else {
|
||||
setError(result.message);
|
||||
}
|
||||
setLoading(false);
|
||||
})();
|
||||
}, [page]);
|
||||
|
||||
const RoleWrapper = () => {
|
||||
const { id } = useParams();
|
||||
const role = roles.find(r => r.id === id);
|
||||
if(!role) return <p>Unknown role</p>;
|
||||
return <Role role={role} />;
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
const RoleListWrapper = () => {
|
||||
return <div className="role-list-wrapper row">
|
||||
<div className={`col-6-lg col-12 ld ${loading && 'loading'}`}>
|
||||
<h4>All Roles</h4>
|
||||
<Table headerItems={['Name', 'ID']}>
|
||||
{roles.map(role => <TableListEntry
|
||||
onClick={() => {
|
||||
navigate(role.id);
|
||||
}}
|
||||
key={role.id}
|
||||
item={role}
|
||||
itemKeys={['name', 'id']}
|
||||
/>)}
|
||||
</Table>
|
||||
|
||||
<PageButtons {...{page, setPage, length: roles.length}} />
|
||||
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
return <div>
|
||||
{error && <p>{error}</p>}
|
||||
<ErrorBoundary>
|
||||
<Routes>
|
||||
<Route path='/:id/*' element={<RoleWrapper />} />
|
||||
<Route path='/' element={<RoleListWrapper />}/>
|
||||
</Routes>
|
||||
</ErrorBoundary>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default Roles;
|
@ -1,72 +1,11 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Route, Routes, useNavigate, useParams } from "react-router";
|
||||
import ErrorBoundary from "../util/ErrorBoundary";
|
||||
import { get, post } from '../util/Util';
|
||||
import '../css/pages/Users.css';
|
||||
import { Table, TableListEntry } from "../components/Table";
|
||||
|
||||
const Permission = ({ name, value, chain, updatePerms }) => {
|
||||
const inputRef = useRef();
|
||||
const onChange = () => {
|
||||
const val = inputRef.current.value;
|
||||
updatePerms(chain, val);
|
||||
};
|
||||
|
||||
return <li className="flex is-vertical-align">
|
||||
<label>{name}</label>
|
||||
<input ref={inputRef} onChange={onChange} max={10} min={0} type='number' defaultValue={value}></input>
|
||||
</li>;
|
||||
};
|
||||
|
||||
const PermissionGroup = ({ name, value, chain, updatePerms }) => {
|
||||
const elements = [];
|
||||
|
||||
for (const [perm, val] of Object.entries(value)) {
|
||||
const props = { key: perm, name: perm, value: val, updatePerms, chain: `${chain}:${perm}` };
|
||||
if(typeof val ==='object') elements.push(<PermissionGroup {...props} />);
|
||||
else elements.push(<Permission {...props} />);
|
||||
}
|
||||
return <li>
|
||||
<div className="groupName">
|
||||
<b>Group: {name}</b>
|
||||
</div>
|
||||
<ul>
|
||||
{elements}
|
||||
</ul>
|
||||
</li>;
|
||||
};
|
||||
|
||||
const Permissions = ({ perms, updatePerms }) => {
|
||||
|
||||
const elements = [];
|
||||
const keys = Object.keys(perms);
|
||||
for (const perm of keys) {
|
||||
const props = { key: perm, name: perm, value: perms[perm], chain: perm, updatePerms };
|
||||
let Elem = null;
|
||||
switch (typeof perms[perm]) {
|
||||
case 'number':
|
||||
Elem = Permission;
|
||||
break;
|
||||
case 'object':
|
||||
Elem = PermissionGroup;
|
||||
break;
|
||||
default:
|
||||
// eslint-disable-next-line react/display-name
|
||||
Elem = () => {
|
||||
return <p>Uknown permission structure</p>;
|
||||
};
|
||||
break;
|
||||
}
|
||||
elements.push(<Elem {...props} />);
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h3>Permissions</h3>
|
||||
<ul className="tree">
|
||||
{elements}
|
||||
</ul>
|
||||
</div>;
|
||||
};
|
||||
import ErrorBoundary from "../../util/ErrorBoundary";
|
||||
import { get, post } from '../../util/Util';
|
||||
import '../../css/pages/Users.css';
|
||||
import { Table, TableListEntry } from "../../components/Table";
|
||||
import { BackButton, PageButtons } from "../../components/PageControls";
|
||||
import { Permissions } from "../../components/PermissionsView";
|
||||
|
||||
const ApplicationList = ({ apps }) => {
|
||||
|
||||
@ -99,8 +38,7 @@ const Application = ({ app }) => {
|
||||
|
||||
// TODO: Groups, description, notes
|
||||
const User = ({ user }) => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [apps, updateApps] = useState([]);
|
||||
const perms = {...user.permissions};
|
||||
|
||||
@ -135,16 +73,13 @@ const User = ({ user }) => {
|
||||
};
|
||||
|
||||
return <div className='user-card'>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-6-lg col-12">
|
||||
<div className="flex is-vertical-align flex-wrap">
|
||||
<button className="button dark" onClick={() => {
|
||||
navigate(-1);
|
||||
}}>Back</button>
|
||||
<BackButton />
|
||||
<h2 className="userId">User {user.id}</h2>
|
||||
</div>
|
||||
<div className="userActionButtons mt-4">
|
||||
<div className="actionButtons mt-4">
|
||||
{user.disabled ? <button className="button success">Enable</button> : <button className="button error">Disable</button>}
|
||||
<button className="button error">Delete</button>
|
||||
</div>
|
||||
@ -251,20 +186,14 @@ const Users = () => {
|
||||
<Table headerItems={['Username', 'ID']}>
|
||||
{users.map(user => <TableListEntry
|
||||
onClick={() => {
|
||||
navigate(`/users/${user.id}`);
|
||||
navigate(user.id);
|
||||
}}
|
||||
key={user.id}
|
||||
item={user}
|
||||
itemKeys={['name', 'id']} />)}
|
||||
</Table>
|
||||
|
||||
<div className='flex is-vertical-align is-center page-controls'>
|
||||
<button className="button dark" onClick={() => setPage(page - 1 || 1)}>Previous</button>
|
||||
<p>Page: {page}</p>
|
||||
<button className="button dark" onClick={() => {
|
||||
if (users.length === 10) setPage(page + 1);
|
||||
}}>Next</button>
|
||||
</div>
|
||||
<PageButtons {...{page, setPage, length: users.length}} />
|
||||
</div>
|
||||
|
||||
<div className="col-6-lg col-12">
|
@ -78,6 +78,26 @@ export const post = async (url, body, opts = {}) => {
|
||||
return parseResponse(response);
|
||||
};
|
||||
|
||||
export const patch = async (url, body, opts = {}) => {
|
||||
const options = {
|
||||
method: 'patch',
|
||||
body
|
||||
};
|
||||
|
||||
if (opts.headers !== null) {
|
||||
options.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...opts.headers || {}
|
||||
};
|
||||
}
|
||||
|
||||
if (options.headers && options.headers['Content-Type'] === 'application/json' && body) options.body = JSON.stringify(body);
|
||||
|
||||
const response = await fetch(url, options);
|
||||
return parseResponse(response);
|
||||
|
||||
};
|
||||
|
||||
export const get = async (url, params) => {
|
||||
if (params) url += '?' + Object.entries(params)
|
||||
.map(([key, val]) => `${key}=${val}`)
|
||||
|
Loading…
Reference in New Issue
Block a user