Some refactoring, roles page

This commit is contained in:
Erik 2023-04-01 14:54:10 +03:00
parent e16937dac6
commit 93c145c5e5
Signed by: Navy.gif
GPG Key ID: 2532FBBB61C65A68
9 changed files with 298 additions and 95 deletions

View File

@ -11,7 +11,6 @@ import Login from './pages/Login';
import { get, setSession, setSettings } from './util/Util'; import { get, setSession, setSettings } from './util/Util';
import { PrivateRoute } from './structures/PrivateRoute'; import { PrivateRoute } from './structures/PrivateRoute';
import { UnauthedRoute } from './structures/UnauthedRoute'; import { UnauthedRoute } from './structures/UnauthedRoute';
import Users from './pages/Users';
import Admin from './pages/Admin'; import Admin from './pages/Admin';
import TitledPage from './components/TitledPage'; import TitledPage from './components/TitledPage';
import Register from './pages/Register'; import Register from './pages/Register';
@ -45,8 +44,12 @@ function App() {
{ to: '/applications', label: 'Applications', relative: true } { 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; if (loading) return null;
@ -79,12 +82,6 @@ function App() {
</TitledPage> </TitledPage>
</PrivateRoute >} /> </PrivateRoute >} />
<Route path='/users/*' element={<PrivateRoute>
<TitledPage title='Users' >
<Users />
</TitledPage>
</PrivateRoute >} />
<Route path='/admin/*' element={<PrivateRoute> <Route path='/admin/*' element={<PrivateRoute>
<TitledPage title='Admin'> <TitledPage title='Admin'>
<Admin /> <Admin />

View 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>;
};

View 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>;
};

View 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>;
};

View File

@ -53,13 +53,13 @@ li input {
.tree input{ .tree input{
width: 60px !important; width: 60px !important;
} }
.userActionButtons{ .actionButtons{
display:flex; display:flex;
flex-direction: row; flex-direction: row;
gap: 4px; gap: 4px;
margin-bottom: 1em; margin-bottom: 1em;
} }
.userActionButtons .button+.button{ .actionButtons .button+.button{
margin-left: 0; margin-left: 0;
} }
.registerCodeWrapper{ .registerCodeWrapper{

View File

@ -1,9 +1,13 @@
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { Route, Routes } from "react-router";
import FileSelector from "../components/FileSelector"; import FileSelector from "../components/FileSelector";
import '../css/pages/Admin.css'; 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 [file, setFile] = useState(null);
const submit = (event) => { const submit = (event) => {
@ -31,4 +35,15 @@ const Admin = () => {
</div>; </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; export default Admin;

124
src/pages/admin/Roles.js Normal file
View 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;

View File

@ -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 { Route, Routes, useNavigate, useParams } from "react-router";
import ErrorBoundary from "../util/ErrorBoundary"; import ErrorBoundary from "../../util/ErrorBoundary";
import { get, post } from '../util/Util'; import { get, post } from '../../util/Util';
import '../css/pages/Users.css'; import '../../css/pages/Users.css';
import { Table, TableListEntry } from "../components/Table"; import { Table, TableListEntry } from "../../components/Table";
import { BackButton, PageButtons } from "../../components/PageControls";
const Permission = ({ name, value, chain, updatePerms }) => { import { Permissions } from "../../components/PermissionsView";
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>;
};
const ApplicationList = ({ apps }) => { const ApplicationList = ({ apps }) => {
@ -100,7 +39,6 @@ const Application = ({ app }) => {
// TODO: Groups, description, notes // TODO: Groups, description, notes
const User = ({ user }) => { const User = ({ user }) => {
const navigate = useNavigate();
const [apps, updateApps] = useState([]); const [apps, updateApps] = useState([]);
const perms = {...user.permissions}; const perms = {...user.permissions};
@ -135,16 +73,13 @@ const User = ({ user }) => {
}; };
return <div className='user-card'> return <div className='user-card'>
<div className="row"> <div className="row">
<div className="col-6-lg col-12"> <div className="col-6-lg col-12">
<div className="flex is-vertical-align flex-wrap"> <div className="flex is-vertical-align flex-wrap">
<button className="button dark" onClick={() => { <BackButton />
navigate(-1);
}}>Back</button>
<h2 className="userId">User {user.id}</h2> <h2 className="userId">User {user.id}</h2>
</div> </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>} {user.disabled ? <button className="button success">Enable</button> : <button className="button error">Disable</button>}
<button className="button error">Delete</button> <button className="button error">Delete</button>
</div> </div>
@ -251,20 +186,14 @@ const Users = () => {
<Table headerItems={['Username', 'ID']}> <Table headerItems={['Username', 'ID']}>
{users.map(user => <TableListEntry {users.map(user => <TableListEntry
onClick={() => { onClick={() => {
navigate(`/users/${user.id}`); navigate(user.id);
}} }}
key={user.id} key={user.id}
item={user} item={user}
itemKeys={['name', 'id']} />)} itemKeys={['name', 'id']} />)}
</Table> </Table>
<div className='flex is-vertical-align is-center page-controls'> <PageButtons {...{page, setPage, length: users.length}} />
<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>
</div> </div>
<div className="col-6-lg col-12"> <div className="col-6-lg col-12">

View File

@ -78,6 +78,26 @@ export const post = async (url, body, opts = {}) => {
return parseResponse(response); 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) => { export const get = async (url, params) => {
if (params) url += '?' + Object.entries(params) if (params) url += '?' + Object.entries(params)
.map(([key, val]) => `${key}=${val}`) .map(([key, val]) => `${key}=${val}`)