Should probably invoke the func

This commit is contained in:
Erik 2024-04-01 19:15:12 +03:00
parent a908013b6d
commit 5d0cdab3d0
11 changed files with 142 additions and 79 deletions

View File

@ -56,7 +56,8 @@ export type Application = {
export type Role = { export type Role = {
position: number, position: number,
rateLimits: RateLimits rateLimits: RateLimits,
tag: boolean,
} & APIEntity } & APIEntity
export type FlagType = string | number | boolean | number[] | null export type FlagType = string | number | boolean | number[] | null

View File

@ -6,10 +6,10 @@ export type RequestOptions = {
headers?: { [key: string]: string }, headers?: { [key: string]: string },
} }
export type Res = { export type Res<T> = {
success: boolean, success: boolean,
status: number, status: number,
data?: { [key: string]: any }, data?: T, //{ [key: string]: any },
message?: string message?: string
} }

View File

@ -47,7 +47,7 @@ const App = () =>
{ {
(async () => (async () =>
{ {
const result = await get('/api/user'); const result = await get<{twoFactor: boolean}>('/api/user');
if (result.status === 200) if (result.status === 200)
{ {
setSession(result.data as User); setSession(result.data as User);

View File

@ -83,7 +83,7 @@ export const DataCard = ({ id, className, children = [] }: DataCardProps) =>
}; };
type PaginatedCardUrlProps = { type PaginatedCardUrlProps<T> = {
path: string, path: string,
query?: {[key: string | number]: unknown} query?: {[key: string | number]: unknown}
dataKey?: string, dataKey?: string,
@ -91,14 +91,14 @@ type PaginatedCardUrlProps = {
tableDivClass?: string, tableDivClass?: string,
children?: React.ReactNode | React.ReactNode[], children?: React.ReactNode | React.ReactNode[],
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
formatter: (entry: any, onClick?: (...args: any[]) => void, idx?: number) => JSX.Element formatter: (entry: T, callback?: (...args: any[]) => void, idx?: number) => JSX.Element
// FormatterCallback // FormatterCallback
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
callback?: (...args: any[]) => void, callback?: (...args: any[]) => void,
pageSize?: number, pageSize?: number,
refresh?: boolean refresh?: boolean
} }
export const PaginatedCard = ({ export const PaginatedCard = <T, >({
// id = randomString(), // id = randomString(),
className, className,
tableDivClass, tableDivClass,
@ -108,14 +108,14 @@ export const PaginatedCard = ({
formatter, formatter,
callback, callback,
pageSize = 10, pageSize = 10,
query = {}, query,
refresh refresh
}: PaginatedCardUrlProps) => }: PaginatedCardUrlProps<T>) =>
{ {
if (!(children instanceof Array)) if (!(children instanceof Array))
children = [ children ]; children = [ children ];
const [ data, setData ] = useState<unknown[] | null>(null); const [ data, setData ] = useState<T[] | null>(null);
const [ page, setPage ] = useState(1); const [ page, setPage ] = useState(1);
const [ pages, setPages ] = useState(1); const [ pages, setPages ] = useState(1);
const [ loadState, setLoadState ] = useState(RefresherState.loading); const [ loadState, setLoadState ] = useState(RefresherState.loading);
@ -131,7 +131,7 @@ export const PaginatedCard = ({
setRefresh(val => !val); setRefresh(val => !val);
}; };
const deps: (string | number | object | boolean)[] = [ page, path, refreshLocal ]; const deps: (string | number | object | boolean)[] = [ page, path, refreshLocal ];
if(query && Object.keys(query).length) if(query)
deps.push(query); deps.push(query);
if (typeof refresh !== 'undefined') if (typeof refresh !== 'undefined')
deps.push(refresh); deps.push(refresh);
@ -141,19 +141,28 @@ export const PaginatedCard = ({
(async () => (async () =>
{ {
setLoadState(RefresherState.loading); setLoadState(RefresherState.loading);
if(query)
{
const keys = Object.keys(query); const keys = Object.keys(query);
for (const key of keys) for (const key of keys)
if (typeof query[key] === 'undefined') if (typeof query[key] === 'undefined')
delete query[key]; delete query[key];
const response = await get(path, { page, pageSize, ...query }); }
const response = await get<{ [key: string]: number | T[], pages: number }>(path, { page, pageSize, ...query });
// if (!response.data?.bruh)
// throw new Error('bruh');
if (!response.success || !response.data) if (!response.success || !response.data)
{ {
setLoadState(RefresherState.error); setLoadState(RefresherState.error);
return errorToast(response.message); return errorToast(response.message);
} }
// return setError(response.message ?? null); // return setError(response.message ?? null);
setData(response.data[dataKey]); setData(response.data[dataKey] as T[]);
setPages(response.data.pages ?? 1); setPages(response.data.pages ?? 1);
if(response.data.pages && page > response.data.pages)
setPage(response.data.pages == 0 ? 1 : response.data.pages);
else if(response.data.pages == 0)
setPage(1);
setLoadState(RefresherState.success); setLoadState(RefresherState.success);
})(); })();
}, deps); }, deps);
@ -184,7 +193,9 @@ type BasePaginatedCardProps = {
onRefresh?: () => void, // Callback that is fired whenever the refresher is clicked onRefresh?: () => void, // Callback that is fired whenever the refresher is clicked
// onlineList?: string // onlineList?: string
} }
export const BasePaginatedCard = ({ children, data, className, id, tableDivClass, formatter, callback, page, pages, setPage, loadState, onRefresh }: BasePaginatedCardProps) => export const BasePaginatedCard = ({ children, data,
className, id, tableDivClass, formatter, callback,
page, pages, setPage, loadState, onRefresh }: BasePaginatedCardProps) =>
{ {
if (!(children instanceof Array)) if (!(children instanceof Array))
children = [ children ]; children = [ children ];
@ -235,7 +246,7 @@ export const TableCard = ({ id, children, className = '', tableDivClass = '', da
{/* <p>{'Loading...'}</p> */} {/* <p>{'Loading...'}</p> */}
</DataCard>; </DataCard>;
return <DataCard id={id} className={className}> return <DataCard id={id} className={`${className} content-nm content-bm-2`}>
{cardHeader} {cardHeader}
<div className={tableDivClass}> <div className={tableDivClass}>
<table className='striped table-pd'> <table className='striped table-pd'>

View File

@ -31,7 +31,7 @@ const CredentialFields = () =>
return; return;
button.disabled = true; button.disabled = true;
const result = await post('/api/login', { username, password }); const result = await post<{twoFactor: boolean}>('/api/login', { username, password });
button.disabled = false; button.disabled = false;
if (![ 200, 302 ].includes(result.status)) if (![ 200, 302 ].includes(result.status))
{ {

View File

@ -321,7 +321,7 @@ const Main = () =>
if (listView) if (listView)
query.all = true; query.all = true;
const response = await get('/api/flags', query); const response = await get<{pages: number, flags: APIFlag[], tags: string[]}>('/api/flags', query);
if (!response.success || !response.data) if (!response.success || !response.data)
{ {
setFlags([]); setFlags([]);
@ -329,7 +329,7 @@ const Main = () =>
return errorToast(response.message); return errorToast(response.message);
} }
setFlags(response.data.flags as APIFlag[]); setFlags(response.data.flags);
setPages(response.data.pages); setPages(response.data.pages);
if (availableFilters === null) if (availableFilters === null)
setAvailableFilters(response.data.tags); setAvailableFilters(response.data.tags);

View File

@ -1,11 +1,11 @@
import React, { useContext, useEffect, useRef, useState } from 'react'; import React, { useContext, useEffect, useRef, 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 { Permissions as PermissionsStruct, Role as RoleStruct, User } from '../../@types/ApiStructures'; import { Role as RoleStruct, User } from '../../@types/ApiStructures';
import { BasePaginatedCard, DataCard, PaginatedCard, RefresherState } from '../../components/PageElements'; import { BasePaginatedCard, DataCard, PaginatedCard, RefresherState } from '../../components/PageElements';
import { dateString, del, errorToast, get, patch, post, successToast } from '../../util/Util'; import { dateString, del, errorToast, get, patch, post, successToast } from '../../util/Util';
import { BackButton } from '../../components/PageControls'; import { BackButton } from '../../components/PageControls';
import { ClickToEdit } from '../../components/InputElements'; import { ClickToEdit, ToggleSwitch } from '../../components/InputElements';
import { Permissions } from '../../views/PermissionsView'; import { Permissions } from '../../views/PermissionsView';
import { useSubtitle } from '../../components/TitledPage'; import { useSubtitle } from '../../components/TitledPage';
@ -58,13 +58,13 @@ const Role = ({ refreshList }: RoleProps) =>
<p>Loading...</p> <p>Loading...</p>
</div>; </div>;
const updatePerms = (perms: PermissionsStruct) => const updateRoleStruct = (data: Partial<RoleStruct>) =>
{ {
setRole(role => setRole(role =>
{ {
if (!role) if (!role)
return role; return role;
return { ...role, permissions: perms }; return { ...role, ...data };
}); });
}; };
@ -79,6 +79,14 @@ const Role = ({ refreshList }: RoleProps) =>
successToast('Permissions updated'); successToast('Permissions updated');
}; };
const updateRole = async ({ tag }: Partial<RoleStruct>) =>
{
const response = await patch<RoleStruct>(`/api/roles/${roleId}`, { tag });
if (!response.success || !response.data)
return errorToast(response.message);
updateRoleStruct(response.data);
};
const deleteRole = async (e: React.MouseEvent) => const deleteRole = async (e: React.MouseEvent) =>
{ {
const button = e.target as HTMLButtonElement; const button = e.target as HTMLButtonElement;
@ -136,7 +144,8 @@ const Role = ({ refreshList }: RoleProps) =>
<td>{dateString(role.createdTimestamp)}</td> <td>{dateString(role.createdTimestamp)}</td>
</tr> </tr>
<tr> <tr>
<th>Tag</th>
<td><ToggleSwitch defaultValue={role.tag} onChange={({ target }) => updateRole({ tag: target.checked })} /></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -160,7 +169,7 @@ const Role = ({ refreshList }: RoleProps) =>
<div>Permissions</div> <div>Permissions</div>
<div> <div>
<div className='scrollable h-max-80-vh'> <div className='scrollable h-max-80-vh'>
<Permissions perms={role.permissions} onUpdate={updatePerms} /> <Permissions perms={role.permissions} onUpdate={(permissions) => updateRoleStruct({ permissions })} />
</div> </div>
<hr /> <hr />
<div className='ml-4'> <div className='ml-4'>
@ -289,14 +298,14 @@ const Main = () =>
(async () => (async () =>
{ {
setLoadState(RefresherState.loading); setLoadState(RefresherState.loading);
const response = await get('/api/roles', { page, pageSize: 15, ...filters }); const response = await get<{pages: number, roles: RoleStruct[]}>('/api/roles', { page, pageSize: 15, ...filters });
if (!response.success || !response.data) if (!response.success || !response.data)
{ {
setLoadState(RefresherState.error); setLoadState(RefresherState.error);
return errorToast(response.message); return errorToast(response.message);
} }
setPages(response.data.pages); setPages(response.data.pages);
setRoles(new Map(response.data.roles.map((role: RoleStruct) => [ role.id, role ]))); setRoles(new Map(response.data.roles.map((role) => [ role.id, role ])));
setLoadState(RefresherState.success); setLoadState(RefresherState.success);
})(); })();
}, [ page, filters, refresh ]); }, [ page, filters, refresh ]);

View File

@ -204,7 +204,7 @@ const UserData = ({ user, updateUser }: {user: UserStruct | null, updateUser: (u
{ {
const button = e.target as HTMLButtonElement; const button = e.target as HTMLButtonElement;
button.disabled = true; button.disabled = true;
const response = await get(`/api/users/${user.id}/passwdreset`); const response = await get<{token: string}>(`/api/users/${user.id}/passwdreset`);
button.disabled = false; button.disabled = false;
if (!response.success || !response.data) if (!response.success || !response.data)
return errorToast(response.message); return errorToast(response.message);
@ -418,7 +418,7 @@ const UserList = ({ page, pages, setPage, loadState, onRefresh }: UserListProps)
const getSignupCode = async () => const getSignupCode = async () =>
{ {
const response = await get('/api/register/code'); const response = await get<{code: string}>('/api/register/code');
if (!response.success || !response.data) if (!response.success || !response.data)
return errorToast(response.message); return errorToast(response.message);
const link = `${window.location.origin}/register?code=${response.data.code}`; const link = `${window.location.origin}/register?code=${response.data.code}`;
@ -533,14 +533,14 @@ const Main = () =>
(async () => (async () =>
{ {
setLoadState(RefresherState.loading); setLoadState(RefresherState.loading);
const response = await get('/api/users', { page, pageSize: 15, ...searchFilter }); const response = await get<{pages: number, users: UserStruct[]}>('/api/users', { page, pageSize: 15, ...searchFilter });
if (!response.success || !response.data) if (!response.success || !response.data)
{ {
setLoadState(RefresherState.error); setLoadState(RefresherState.error);
return errorToast(response.message); return errorToast(response.message);
} }
setPages(response.data.pages); setPages(response.data.pages);
setUsers(new Map(response.data.users.map((user: UserStruct) => [ user.id, user ]))); setUsers(new Map(response.data.users.map((user) => [ user.id, user ])));
setLoadState(RefresherState.success); setLoadState(RefresherState.success);
})(); })();
}, [ page, searchFilter, refresh ]); }, [ page, searchFilter, refresh ]);
@ -549,10 +549,10 @@ const Main = () =>
{ {
(async () => (async () =>
{ {
const response = await get('/api/roles', { all: true }); const response = await get<{roles: RoleStruct[]}>('/api/roles', { all: true });
if (!response.success || !response.data) if (!response.success || !response.data)
return errorToast(response.message); return errorToast(response.message);
setRoles(new Map(response.data.roles.map((role: RoleStruct) => [ role.id, role ]))); setRoles(new Map(response.data.roles.map((role) => [ role.id, role ])));
})(); })();
}, [ refresh ]); }, [ refresh ]);

View File

@ -1,20 +1,36 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Route, Routes, useNavigate, useParams } from 'react-router'; import { Route, Routes, useNavigate, useParams } from 'react-router';
import { Table, TableListEntry } from '../../components/Table';
import ErrorBoundary from '../../util/ErrorBoundary'; import ErrorBoundary from '../../util/ErrorBoundary';
import { post, get, del, successToast, patch, errorToast } from '../../util/Util'; import { post, del, successToast, patch, errorToast, get } from '../../util/Util';
import { Application as App } from '../../@types/ApiStructures'; import { Application as App, Permissions as Perms } from '../../@types/ApiStructures';
import { Res } from '../../@types/Other'; import { DataCard, PaginatedCard } from '../../components/PageElements';
import { DataCard } from '../../components/PageElements';
import { BackButton } from '../../components/PageControls'; import { BackButton } from '../../components/PageControls';
import { ClickToEdit } from '../../components/InputElements'; import { ClickToEdit } from '../../components/InputElements';
import { Permissions } from '../../views/PermissionsView'; import { Permissions } from '../../views/PermissionsView';
import { useSubtitle } from '../../components/TitledPage'; import { useSubtitle } from '../../components/TitledPage';
const Application = ({ app }: {app: App}) => const Application = () =>
{ {
const [ perms, updatePerms ] = useState(app.permissions); const { id } = useParams();
const [ app, setApp ] = useState<App | null>(null);
useEffect(() =>
{
(async () =>
{
const response = await get<App>(`/api/users/applications/${id}`);
if (!response.success || !response.data)
return errorToast(response.message);
setApp(response.data);
})();
}, [ id ]);
if (!app)
return <div>
<BackButton />
<p>Loading...</p>
</div>;
const deleteApp = async () => const deleteApp = async () =>
{ {
@ -33,7 +49,7 @@ const Application = ({ app }: {app: App}) =>
const button = e.target as HTMLButtonElement; const button = e.target as HTMLButtonElement;
button.disabled = true; button.disabled = true;
const response = await patch(`/api/user/applications/${app.id}`, { const response = await patch(`/api/user/applications/${app.id}`, {
permissions: perms permissions: app.permissions
}); });
button.disabled = false; button.disabled = false;
if (!response.success) if (!response.success)
@ -41,18 +57,28 @@ const Application = ({ app }: {app: App}) =>
return successToast('Successfully updated permissions'); return successToast('Successfully updated permissions');
}; };
const updatePerms = (permissions: Perms) =>
{
setApp(app =>
{
if (!app)
return app;
return { ...app, permissions };
});
};
const resetPerms = async (e: React.MouseEvent) => const resetPerms = async (e: React.MouseEvent) =>
{ {
const button = e.target as HTMLButtonElement; const button = e.target as HTMLButtonElement;
button.disabled = true; button.disabled = true;
const response = await patch(`/api/user/applications/${app.id}`, { const response = await patch<App>(`/api/user/applications/${app.id}`, {
permissions: null permissions: null
}); });
button.disabled = false; button.disabled = false;
if (!response.success) if (!response.success || !response.data)
return errorToast(response.message); return errorToast(response.message);
successToast('Successfully reset permissions'); successToast('Successfully reset permissions');
updatePerms(response.data?.permissions); updatePerms(response.data.permissions);
}; };
return <div> return <div>
@ -95,7 +121,7 @@ const Application = ({ app }: {app: App}) =>
</div> </div>
<div> <div>
<div className='scrollable h-max-80-vh'> <div className='scrollable h-max-80-vh'>
<Permissions perms={perms} onUpdate={updatePerms} /> <Permissions perms={app.permissions} onUpdate={updatePerms} />
</div> </div>
<hr /> <hr />
<div className='ml-4'> <div className='ml-4'>
@ -109,23 +135,22 @@ const Application = ({ app }: {app: App}) =>
}; };
const applicationFormatter = (app: App, callback?: (id: string) => void) =>
{
return <tr key={app.id} onClick={() => callback && callback(app.id)}>
<td>{app.name}</td>
<td>{app.id}</td>
</tr>;
};
const Applications = () => const Applications = () =>
{ {
const subtitle = useSubtitle(); const subtitle = useSubtitle();
const [ applications, setApplications ] = useState<App[]>([]);
const [ loading, setLoading ] = useState(true);
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => useEffect(() =>
{ {
subtitle.set('Applications'); subtitle.set('Applications');
(async () =>
{
const response = await get('/api/user/applications') as Res;
if (response.status === 200)
setApplications(response.data as App[]);
setLoading(false);
})();
}, []); }, []);
const descField = useRef<HTMLTextAreaElement>(null); const descField = useRef<HTMLTextAreaElement>(null);
@ -151,8 +176,6 @@ const Applications = () =>
const response = await post('/api/user/applications', { name, description }); const response = await post('/api/user/applications', { name, description });
if (response.status !== 200) if (response.status !== 200)
errorToast(response.message); errorToast(response.message);
else
setApplications((apps) => [ ...apps, response.data as App ]);
button.disabled = false; button.disabled = false;
}; };
@ -161,7 +184,26 @@ const Applications = () =>
{ {
return <div className="row"> return <div className="row">
<div className={`col ld ${loading && 'loading'}`}> <PaginatedCard
path='/api/user/applications'
formatter={applicationFormatter}
callback={(id) =>
{
navigate(id);
}}
query={{}}
dataKey='applications'
className='col'
tableDivClass='scrollable-table'
>
Applications
<tr>
<th>Name</th>
<th>ID</th>
</tr>
</PaginatedCard>
{/* <div className={`col ld ${loading && 'loading'}`}>
<h2>Applications</h2> <h2>Applications</h2>
<Table headerItems={[ 'Name', 'ID' ]}> <Table headerItems={[ 'Name', 'ID' ]}>
{applications.map(application => <TableListEntry {applications.map(application => <TableListEntry
@ -171,7 +213,7 @@ const Applications = () =>
itemKeys={[ 'name', 'id' ]} itemKeys={[ 'name', 'id' ]}
/>)} />)}
</Table> </Table>
</div> </div> */}
<div className="col"> <div className="col">
<h2>Create Application</h2> <h2>Create Application</h2>
@ -184,19 +226,19 @@ const Applications = () =>
</div>; </div>;
}; };
const ApplicationWrapper = () => // const ApplicationWrapper = () =>
{ // {
const { id } = useParams(); // const { id } = useParams();
const application = applications.find(app => app.id === id); // const application = applications.find(app => app.id === id);
if(!application) // if(!application)
return <p>Unknown application</p>; // return <p>Unknown application</p>;
return <Application app={application} />; // return <Application app={application} />;
}; // };
return <ErrorBoundary> return <ErrorBoundary>
<Routes> <Routes>
<Route path="/" element={<Main />} /> <Route path="/" element={<Main />} />
<Route path="/:id" element={<ApplicationWrapper />} /> <Route path="/:id" element={<Application />} />
</Routes> </Routes>
</ErrorBoundary>; </ErrorBoundary>;
}; };

View File

@ -155,7 +155,7 @@ const Profile = () =>
} }
button.disabled = true; button.disabled = true;
const response = await post('/api/user/settings', data); const response = await post<{ reAuth: boolean }>('/api/user/settings', data);
button.disabled = false; button.disabled = false;
if (response.data?.reAuth) if (response.data?.reAuth)
{ {

View File

@ -184,7 +184,7 @@ export const setSetting = async (name: string, value: unknown) =>
await post('/api/settings', settings.user); await post('/api/settings', settings.user);
}; };
const parseResponse = async (response: Response): Promise<Res> => const parseResponse = async <T = unknown>(response: Response): Promise<Res<T>> =>
{ {
const { headers: rawHeaders, status } = response; const { headers: rawHeaders, status } = response;
// Fetch returns heders as an interable for some reason // Fetch returns heders as an interable for some reason
@ -214,7 +214,7 @@ const parseResponse = async (response: Response): Promise<Res> =>
return { ...base, message: (await response.text() || response.statusText) }; return { ...base, message: (await response.text() || response.statusText) };
}; };
export const post = async (url: string, body?: object | string, opts: ReqOptions = {}) => export const post = async <T>(url: string, body?: object | string, opts: ReqOptions = {}) =>
{ {
const options: HttpOpts & RequestOptions = { const options: HttpOpts & RequestOptions = {
method: 'POST' method: 'POST'
@ -235,10 +235,10 @@ export const post = async (url: string, body?: object | string, opts: ReqOptions
// console.log(url, options); // console.log(url, options);
const response = await fetch(url, options); const response = await fetch(url, options);
return parseResponse(response); return parseResponse<T>(response);
}; };
export const patch = async (url: string, body: object | string, opts: RequestOptions = {}) => export const patch = async <T>(url: string, body: object | string, opts: RequestOptions = {}) =>
{ {
const options: HttpOpts & RequestOptions = { const options: HttpOpts & RequestOptions = {
method: 'PATCH' method: 'PATCH'
@ -259,18 +259,18 @@ export const patch = async (url: string, body: object | string, opts: RequestOpt
// console.log(url, options); // console.log(url, options);
const response = await fetch(url, options); const response = await fetch(url, options);
return parseResponse(response); return parseResponse<T>(response);
}; };
export const get = async (url: string, params?: {[key: string]: boolean | string | number | string[] | number[]}) => export const get = async <T = unknown>(url: string, params?: {[key: string]: boolean | string | number | string[] | number[]}) =>
{ {
if (params) if (params)
url += '?' + Object.entries(params) url += '?' + Object.entries(params)
.map(([ key, val ]) => `${key}=${val instanceof Array ? val.join(',') : val}`) .map(([ key, val ]) => val !== null && val !== '' && typeof val !== 'undefined' ? `${key}=${val instanceof Array ? val.join(',') : val}` : key)
.join('&'); .join('&');
const response = await fetch(url); const response = await fetch(url);
return parseResponse(response); return parseResponse<T>(response);
}; };
export const del = async (url: string, body?: object | string, opts: RequestOptions = {}) => export const del = async (url: string, body?: object | string, opts: RequestOptions = {}) =>