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-classes-per-file": "error",
"max-depth": "error", "max-depth": "error",
"max-len": "off", "max-len": "off",
"max-lines": "error", // "max-lines": "error",
"max-lines-per-function": "off", "max-lines-per-function": "off",
"max-nested-callbacks": "error", "max-nested-callbacks": "error",
"max-params": "off", "max-params": "off",

View File

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

View File

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

View File

View File

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

View File

@ -20,8 +20,6 @@ const Register = () => {
const submit: React.MouseEventHandler = async (event) => { const submit: React.MouseEventHandler = async (event) => {
ref.current?.continuousStart(); ref.current?.continuousStart();
event.preventDefault(); event.preventDefault();
// const username = document.getElementById('username').value;
// const password = document.getElementById('password').value;
const username = usernameRef.current?.value; const username = usernameRef.current?.value;
const password = passwordRef.current?.value; const password = passwordRef.current?.value;
if (!username?.length || !password?.length) { if (!username?.length || !password?.length) {
@ -35,7 +33,6 @@ const Register = () => {
} }
ref.current?.complete(); ref.current?.complete();
navigate('/login'); navigate('/login');
}; };
return <div className="row is-center is-full-screen is-marginless"> 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 { 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';
@ -9,6 +9,23 @@ import { Permissions } from "../../views/PermissionsView";
import { Application as App, Permissions as Perms, User as APIUser, Role } from "../../@types/ApiStructures"; import { Application as App, Permissions as Perms, User as APIUser, Role } from "../../@types/ApiStructures";
import { Dropdown, ToggleSwitch } from "../../components/Selectors"; 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 ApplicationList = ({ apps }: { apps: App[] }) => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -27,42 +44,72 @@ const ApplicationList = ({ apps }: { apps: App[] }) => {
const Application = ({ app }: { app: 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' }; const descProps = { defaultValue: app.description || '', placeholder: 'Describe your application' };
return <div> 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> <p><b>Created: </b>{new Date(app.createdTimestamp).toDateString()}</p>
<textarea {...descProps} > <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>; </div>;
}; };
const RoleComp = ({ role }: {role: Role}) => { const RoleComp = ({ role, onClick }: { role: Role, onClick: React.ReactEventHandler }) => {
return <div className='role'> return <div className='role card' >
{role.name} {role.name} <span className="clickable" onClick={onClick}>X</span>
</div>; </div>;
}; };
const Roles = ({ userRoles, roles }: { userRoles: Role[], roles: Role[] }) => { const Roles = ({ userRoles, roles }: { userRoles: Role[], roles: Role[] }) => {
console.log(roles, userRoles);
const roleSelected = (role: Role, selected: boolean) => { const [unequippedRoles, updateUnequipped] = useState<Role[]>(roles.filter(role => !userRoles.some(r => role.id === r.id)));
console.log(role, selected); 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> return <Dropdown>
<Dropdown.Header> <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.Header>
<Dropdown.ItemList> <Dropdown.ItemList>
{roles.map(role => <Dropdown.Item {unequippedRoles.map(role => <Dropdown.Item
type='multi-select' key={role.id} key={role.id}
selected={userRoles.some(r => r.id === role.id)} onClick={() => {
onChange={(event) => { roleSelected(role);
const elem = event.target as HTMLInputElement;
roleSelected(role, elem.checked);
}} }}
> >
{role.name} {role.name}
@ -75,13 +122,18 @@ const Roles = ({ userRoles, roles }: { userRoles: Role[], roles: Role[] }) => {
const User = ({ user, roles }: { user: APIUser, 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 [perms, updatePerms] = useState<Perms>(user.permissions);
const userContext = useContext(SelectedUserContext);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const appsResponse = await get(`/api/users/${user.id}/applications`); 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); 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'> 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">
<BackButton /> <BackButton />
<h2 className="userId">User {user.id}</h2> <h2 className="userId">User {user.displayName} ({user.id})</h2>
</div> </div>
<p><b>Created: </b>{new Date(user.createdTimestamp).toDateString()}</p> <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> <label>Roles</label>
<Roles userRoles={user.roles} roles={roles} /> <Roles userRoles={user.roles} roles={roles} />
{/* <button className="button primary"> <button className="button primary">
Save User Save User
</button> */} </button>
</div> </div>
@ -137,11 +182,7 @@ const User = ({ user, roles }: { user: APIUser, roles: Role[] }) => {
<div className='row'> <div className='row'>
<div className='col-6-lg col-12'> <div className='col-6-lg col-12'>
<h3>Applications</h3> <h3>Applications</h3>
<BackButton /> {userContext.user && <ApplicationList apps={userContext.user.apps} />}
<Routes>
<Route path='/' element={<ApplicationList apps={apps} />} />
<Route path='/applications/:appid' element={<ApplicationWrapper />} />
</Routes>
</div> </div>
</div> </div>
@ -239,13 +280,35 @@ const Users = () => {
</div>; </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"> return <div className="user-list">
{error && <p>{error}</p>} {error && <p>{error}</p>}
<ErrorBoundary> <ErrorBoundary>
<Routes> <Context>
<Route path='/:id/*' element={<UserWrapper />} /> <Routes>
<Route path='/' element={<UserListWrapper />} /> <Route path='/:id/*' element={<UserWrapper />} />
</Routes> <Route path='/' element={<UserListWrapper />} />
<Route path='/:id/applications/:appid' element={<ApplicationWrapper />} />
</Routes>
</Context>
</ErrorBoundary> </ErrorBoundary>
</div>; </div>;