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 { 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 />
|
||||||
|
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{
|
.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{
|
||||||
|
@ -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
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 { 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">
|
@ -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}`)
|
||||||
|
Loading…
Reference in New Issue
Block a user