roles WIP & applications view rework
This commit is contained in:
parent
cfc8273a90
commit
a92ee18c0c
@ -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",
|
||||
|
@ -5,5 +5,5 @@ export type DropdownBaseProps = {
|
||||
export type DropdownItemProps = {
|
||||
type?: 'select' | 'multi-select',
|
||||
selected?: boolean,
|
||||
onChange?: React.ReactEventHandler
|
||||
onClick?: React.ReactEventHandler
|
||||
} & DropdownBaseProps
|
@ -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>
|
||||
<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>
|
||||
<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'>
|
||||
return <summary className='card is-vertical-align dropdown-header'>
|
||||
{children}
|
||||
</summary>;
|
||||
};
|
||||
|
||||
const DropdownItem = ({ children, type, selected, onChange }: DropdownItemProps) => {
|
||||
const DropdownItem = ({ children, type, selected, onClick }: DropdownItemProps) => {
|
||||
let InnerElement = null;
|
||||
if (type === 'multi-select')
|
||||
return <ToggleSwitch value={selected || false} onChange={onChange}>
|
||||
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,10 +95,7 @@ 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');
|
||||
|
0
src/css/components/Selectors.css
Normal file
0
src/css/components/Selectors.css
Normal 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 {
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.tree li:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@ -19,21 +22,25 @@ li input {
|
||||
width: 18px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.tree ul li:before {
|
||||
top: -8px;
|
||||
left: -20px;
|
||||
width: 17px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.tree ul li:after {
|
||||
top: 19px;
|
||||
left: -20px;
|
||||
width: 18px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tree .groupName {
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.tree li:after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
@ -44,24 +51,30 @@ li input {
|
||||
width: 18px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tree li:last-child:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tree>li:first-child:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tree input {
|
||||
width: 60px !important;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.actionButtons .button+.button {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.registerCodeWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -71,10 +84,16 @@ li input {
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.registerCodeWrapper p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.userId {
|
||||
overflow-wrap: anywhere;
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
.role {
|
||||
margin: 5px
|
||||
}
|
@ -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">
|
||||
|
@ -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>
|
||||
|
||||
<p><b>Created: </b>{new Date(app.createdTimestamp).toDateString()}</p>
|
||||
<ToggleSwitch value={app.disabled}>
|
||||
Login Disabled:
|
||||
</ToggleSwitch>
|
||||
|
||||
<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>
|
||||
<Context>
|
||||
<Routes>
|
||||
<Route path='/:id/*' element={<UserWrapper />} />
|
||||
<Route path='/' element={<UserListWrapper />} />
|
||||
<Route path='/:id/applications/:appid' element={<ApplicationWrapper />} />
|
||||
</Routes>
|
||||
</Context>
|
||||
</ErrorBoundary>
|
||||
</div>;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user