roles WIP & applications view rework

This commit is contained in:
Erik 2023-05-08 17:35:36 +03:00
parent cfc8273a90
commit a92ee18c0c
Signed by: Navy.gif
GPG Key ID: 2532FBBB61C65A68
7 changed files with 162 additions and 77 deletions

View File

@ -121,7 +121,7 @@
"max-classes-per-file": "error",
"max-depth": "error",
"max-len": "off",
"max-lines": "error",
// "max-lines": "error",
"max-lines-per-function": "off",
"max-nested-callbacks": "error",
"max-params": "off",

View File

@ -5,5 +5,5 @@ export type DropdownBaseProps = {
export type DropdownItemProps = {
type?: 'select' | 'multi-select',
selected?: boolean,
onChange?: React.ReactEventHandler
onClick?: React.ReactEventHandler
} & DropdownBaseProps

View File

@ -1,6 +1,7 @@
import React, { Children, useRef, useState } from "react";
import ClickDetector from "../util/ClickDetector";
import { DropdownBaseProps, DropdownItemProps } from "../@types/Components";
import '../css/components/Selectors.css';
export const FileSelector = ({ cb }: { cb: (file: File) => void }) => {
@ -40,37 +41,45 @@ export const FileSelector = ({ cb }: { cb: (file: File) => void }) => {
export const ToggleSwitch = ({ value, onChange, ref, children }:
{ value: boolean, ref?: React.RefObject<HTMLInputElement>, onChange?: React.ChangeEventHandler, children: string }) => {
return <p>
<span className="check-box check-box-row">
<b>{children}</b>
<input ref={ref} defaultChecked={value} onChange={onChange} type='checkbox' />
</span>
<label>
<span className="check-box check-box-row">
<b>{children}</b>
<input ref={ref} defaultChecked={value} onChange={onChange} type='checkbox' />
</span>
</label>
</p>;
};
export const VerticalToggleSwitch = ({ value, onChange, ref, children }:
{ value: boolean, ref?: React.RefObject<HTMLInputElement>, onChange?: React.ChangeEventHandler, children: string }) => {
return <p>
<b>{children}</b>
<span className="check-box">
<input ref={ref} defaultChecked={value} onChange={onChange} type='checkbox' />
</span>
<label>
<b>{children}</b>
<span className="check-box">
<input ref={ref} defaultChecked={value} onChange={onChange} type='checkbox' />
</span>
</label>
</p>;
};
const DropdownHeader = ({children}: DropdownBaseProps) => {
return <summary className='is-vertical-align'>
const DropdownHeader = ({ children }: DropdownBaseProps) => {
return <summary className='card is-vertical-align dropdown-header'>
{children}
</summary>;
};
const DropdownItem = ({ children, type, selected, onChange }: DropdownItemProps) => {
if (type === 'multi-select')
return <ToggleSwitch value={selected || false} onChange={onChange}>
const DropdownItem = ({ children, type, selected, onClick }: DropdownItemProps) => {
let InnerElement = null;
if (type === 'multi-select')
InnerElement = <ToggleSwitch value={selected || false} onChange={onClick}>
{children as string}
</ToggleSwitch>;
return <div>
else InnerElement = <div onClick={onClick}>
{children}
</div>;
return <div className='dropdown-item'>
{InnerElement}
</div>;
};
const DropdownItemList = ({ children }: DropdownBaseProps) => {
@ -86,21 +95,18 @@ type DropdownProps = {
children: React.ReactNode[]
}
const DropdownComp = ({children, ...props}: DropdownProps) => {
console.log(props);
console.log(children);
const DropdownComp = ({ children }: DropdownProps) => {
if (!children)
throw new Error('Missing children');
if (!children.some(element => element && (element as React.ReactElement).type === DropdownHeader))
throw new Error('Missing a header');
const detailsRef = useRef<HTMLDetailsElement>(null);
return <ClickDetector callback={() => {
if(detailsRef.current) detailsRef.current.open = false;
if (detailsRef.current) detailsRef.current.open = false;
}}>
<details ref={detailsRef} className='dropdown'>
{children}

View File

View File

@ -1,14 +1,17 @@
li input {
margin: 0;
}
.tree li {
list-style-type: none;
margin: 2px 0 2px 2px;
position: relative;
}
.tree span *:hover{
.tree span *:hover {
cursor: pointer
}
.tree li:before {
content: "";
position: absolute;
@ -19,21 +22,25 @@ li input {
width: 18px;
height: 15px;
}
.tree ul li:before{
.tree ul li:before {
top: -8px;
left: -20px;
width: 17px;
height: 28px;
}
.tree ul li:after{
.tree ul li:after {
top: 19px;
left: -20px;
width: 18px;
height: 100%;
}
.tree .groupName{
.tree .groupName {
padding-top: 3px;
}
.tree li:after {
position: absolute;
content: "";
@ -44,25 +51,31 @@ li input {
width: 18px;
height: 100%;
}
.tree li:last-child:after {
display: none;
}
.tree > li:first-child:before {
.tree>li:first-child:before {
display: none;
}
.tree input{
.tree input {
width: 60px !important;
}
.actionButtons{
display:flex;
.actionButtons {
display: flex;
flex-direction: row;
gap: 4px;
margin-bottom: 1em;
}
.actionButtons .button+.button{
.actionButtons .button+.button {
margin-left: 0;
}
.registerCodeWrapper{
.registerCodeWrapper {
display: flex;
align-items: center;
align-content: center;
@ -71,10 +84,16 @@ li input {
justify-content: center;
cursor: pointer;
}
.registerCodeWrapper p{
.registerCodeWrapper p {
margin-bottom: 0;
}
.userId{
.userId {
overflow-wrap: anywhere;
line-height: initial;
}
.role {
margin: 5px
}

View File

@ -20,8 +20,6 @@ const Register = () => {
const submit: React.MouseEventHandler = async (event) => {
ref.current?.continuousStart();
event.preventDefault();
// const username = document.getElementById('username').value;
// const password = document.getElementById('password').value;
const username = usernameRef.current?.value;
const password = passwordRef.current?.value;
if (!username?.length || !password?.length) {
@ -35,7 +33,6 @@ const Register = () => {
}
ref.current?.complete();
navigate('/login');
};
return <div className="row is-center is-full-screen is-marginless">

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useContext, useEffect, useState } from "react";
import { Route, Routes, useNavigate, useParams } from "react-router";
import ErrorBoundary from "../../util/ErrorBoundary";
import { get, post } from '../../util/Util';
@ -9,6 +9,23 @@ import { Permissions } from "../../views/PermissionsView";
import { Application as App, Permissions as Perms, User as APIUser, Role } from "../../@types/ApiStructures";
import { Dropdown, ToggleSwitch } from "../../components/Selectors";
type PartialUser = {
apps: App[]
}
const SelectedUserContext = React.createContext<{
user: PartialUser | null,
updateUser:(user: PartialUser) => void
}>({
user: null,
updateUser: () => { /** */ }
});
const Context = ({ children }: { children: React.ReactNode }) => {
const [user, updateUser] = useState<PartialUser | null>(null);
return <SelectedUserContext.Provider value={{ user, updateUser }}>
{children}
</SelectedUserContext.Provider>;
};
const ApplicationList = ({ apps }: { apps: App[] }) => {
const navigate = useNavigate();
@ -27,42 +44,72 @@ const ApplicationList = ({ apps }: { apps: App[] }) => {
const Application = ({ app }: { app: App }) => {
const commitPerms = async () => {
await post(`/api/applications/${app.id}/permissions`, perms);
};
const [perms, updatePerms] = useState<Perms>(app.permissions);
const descProps = { defaultValue: app.description || '', placeholder: 'Describe your application' };
return <div>
<h4>{app.name} ({app.id})</h4>
<div className='row'>
<div className='col-6-lg col-12'>
<div className='flex is-vertical-align flex-wrap'>
<BackButton />
<h2 className='userId'>Application {app.name} ({app.id})</h2>
</div>
<b>Description</b>
<textarea {...descProps} >
<p><b>Created: </b>{new Date(app.createdTimestamp).toDateString()}</p>
<ToggleSwitch value={app.disabled}>
Login Disabled:
</ToggleSwitch>
</textarea>
<b>Description</b>
<textarea {...descProps} >
</textarea>
</div>
<div className="col-6-lg col-12">
<Permissions onUpdate={updatePerms} perms={perms} />
<button onClick={commitPerms} className="button primary">Save Permissions</button>
</div>
</div>
</div>;
};
const RoleComp = ({ role }: {role: Role}) => {
return <div className='role'>
{role.name}
const RoleComp = ({ role, onClick }: { role: Role, onClick: React.ReactEventHandler }) => {
return <div className='role card' >
{role.name} <span className="clickable" onClick={onClick}>X</span>
</div>;
};
const Roles = ({ userRoles, roles }: { userRoles: Role[], roles: Role[] }) => {
console.log(roles, userRoles);
const roleSelected = (role: Role, selected: boolean) => {
console.log(role, selected);
const [unequippedRoles, updateUnequipped] = useState<Role[]>(roles.filter(role => !userRoles.some(r => role.id === r.id)));
const [equippedRoles, updateEquipped] = useState<Role[]>(userRoles);
const roleSelected = (role: Role) => {
updateEquipped([...equippedRoles, role]);
updateUnequipped(unequippedRoles.filter(r => r.id !== role.id));
};
const roleDeselected = (role: Role) => {
updateEquipped(equippedRoles.filter(r => r.id !== role.id));
updateUnequipped([...unequippedRoles, role]);
};
return <Dropdown>
<Dropdown.Header>
{userRoles.map(role => <RoleComp key={role.id} role={role} />)}
{equippedRoles.map(role => <RoleComp key={role.id} role={role} onClick={() => {
roleDeselected(role);
}} />)}
</Dropdown.Header>
<Dropdown.ItemList>
{roles.map(role => <Dropdown.Item
type='multi-select' key={role.id}
selected={userRoles.some(r => r.id === role.id)}
onChange={(event) => {
const elem = event.target as HTMLInputElement;
roleSelected(role, elem.checked);
{unequippedRoles.map(role => <Dropdown.Item
key={role.id}
onClick={() => {
roleSelected(role);
}}
>
{role.name}
@ -75,13 +122,18 @@ const Roles = ({ userRoles, roles }: { userRoles: Role[], roles: Role[] }) => {
const User = ({ user, roles }: { user: APIUser, roles: Role[] }) => {
const [apps, updateApps] = useState<App[]>([]);
// const [apps, updateApps] = useState<App[]>([]);
const [perms, updatePerms] = useState<Perms>(user.permissions);
const userContext = useContext(SelectedUserContext);
useEffect(() => {
(async () => {
const appsResponse = await get(`/api/users/${user.id}/applications`);
if (appsResponse.success) updateApps(appsResponse.data as App[]);
if (appsResponse.success) {
const a = appsResponse.data as App[];
// updateApps(a);
userContext.updateUser({ apps: a });
}
})();
}, []);
@ -89,19 +141,12 @@ const User = ({ user, roles }: { user: APIUser, roles: Role[] }) => {
await post(`/api/users/${user.id}/permissions`, perms);
};
const ApplicationWrapper = () => {
const { appid } = useParams();
const app = apps.find(a => a.id === appid);
if (!app) return null;
return <Application app={app} />;
};
return <div className='user-card'>
<div className="row">
<div className="col-6-lg col-12">
<div className="flex is-vertical-align flex-wrap">
<BackButton />
<h2 className="userId">User {user.id}</h2>
<h2 className="userId">User {user.displayName} ({user.id})</h2>
</div>
<p><b>Created: </b>{new Date(user.createdTimestamp).toDateString()}</p>
@ -122,9 +167,9 @@ const User = ({ user, roles }: { user: APIUser, roles: Role[] }) => {
<label>Roles</label>
<Roles userRoles={user.roles} roles={roles} />
{/* <button className="button primary">
<button className="button primary">
Save User
</button> */}
</button>
</div>
@ -137,11 +182,7 @@ const User = ({ user, roles }: { user: APIUser, roles: Role[] }) => {
<div className='row'>
<div className='col-6-lg col-12'>
<h3>Applications</h3>
<BackButton />
<Routes>
<Route path='/' element={<ApplicationList apps={apps} />} />
<Route path='/applications/:appid' element={<ApplicationWrapper />} />
</Routes>
{userContext.user && <ApplicationList apps={userContext.user.apps} />}
</div>
</div>
@ -239,13 +280,35 @@ const Users = () => {
</div>;
};
const ApplicationWrapper = () => {
const { appid } = useParams();
if (!appid) return null;
const userContext = useContext(SelectedUserContext);
const [app, setApp] = useState<App | null>(userContext.user?.apps.find(app => app.id === appid) || null);
useEffect(() => {
(async () => {
if (userContext.user) return;
const result = await get(`/api/applications/${appid}`);
if (result.success)
setApp(result.data as App);
})();
}, [appid]);
if (!app) return <p>Loading...</p>;
return <Application app={app} />;
};
return <div className="user-list">
{error && <p>{error}</p>}
<ErrorBoundary>
<Routes>
<Route path='/:id/*' element={<UserWrapper />} />
<Route path='/' element={<UserListWrapper />} />
</Routes>
<Context>
<Routes>
<Route path='/:id/*' element={<UserWrapper />} />
<Route path='/' element={<UserListWrapper />} />
<Route path='/:id/applications/:appid' element={<ApplicationWrapper />} />
</Routes>
</Context>
</ErrorBoundary>
</div>;