Typings + some refactoring

Refactored applications page WIP
Type params to http method functions
This commit is contained in:
Erik 2024-04-01 19:15:12 +03:00
parent a908013b6d
commit a2ccd0c00e
11 changed files with 142 additions and 79 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -321,7 +321,7 @@ const Main = () =>
if (listView)
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)
{
setFlags([]);
@ -329,7 +329,7 @@ const Main = () =>
return errorToast(response.message);
}
setFlags(response.data.flags as APIFlag[]);
setFlags(response.data.flags);
setPages(response.data.pages);
if (availableFilters === null)
setAvailableFilters(response.data.tags);

View File

@ -1,11 +1,11 @@
import React, { useContext, useEffect, useRef, useState } from 'react';
import { Route, Routes, useNavigate, useParams } from 'react-router';
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 { dateString, del, errorToast, get, patch, post, successToast } from '../../util/Util';
import { BackButton } from '../../components/PageControls';
import { ClickToEdit } from '../../components/InputElements';
import { ClickToEdit, ToggleSwitch } from '../../components/InputElements';
import { Permissions } from '../../views/PermissionsView';
import { useSubtitle } from '../../components/TitledPage';
@ -58,13 +58,13 @@ const Role = ({ refreshList }: RoleProps) =>
<p>Loading...</p>
</div>;
const updatePerms = (perms: PermissionsStruct) =>
const updateRoleStruct = (data: Partial<RoleStruct>) =>
{
setRole(role =>
{
if (!role)
return role;
return { ...role, permissions: perms };
return { ...role, ...data };
});
};
@ -79,6 +79,14 @@ const Role = ({ refreshList }: RoleProps) =>
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 button = e.target as HTMLButtonElement;
@ -136,7 +144,8 @@ const Role = ({ refreshList }: RoleProps) =>
<td>{dateString(role.createdTimestamp)}</td>
</tr>
<tr>
<th>Tag</th>
<td><ToggleSwitch defaultValue={role.tag} onChange={({ target }) => updateRole({ tag: target.checked })} /></td>
</tr>
</tbody>
</table>
@ -160,7 +169,7 @@ const Role = ({ refreshList }: RoleProps) =>
<div>Permissions</div>
<div>
<div className='scrollable h-max-80-vh'>
<Permissions perms={role.permissions} onUpdate={updatePerms} />
<Permissions perms={role.permissions} onUpdate={(permissions) => updateRoleStruct({ permissions })} />
</div>
<hr />
<div className='ml-4'>
@ -289,14 +298,14 @@ const Main = () =>
(async () =>
{
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)
{
setLoadState(RefresherState.error);
return errorToast(response.message);
}
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);
})();
}, [ page, filters, refresh ]);

View File

@ -204,7 +204,7 @@ const UserData = ({ user, updateUser }: {user: UserStruct | null, updateUser: (u
{
const button = e.target as HTMLButtonElement;
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;
if (!response.success || !response.data)
return errorToast(response.message);
@ -418,7 +418,7 @@ const UserList = ({ page, pages, setPage, loadState, onRefresh }: UserListProps)
const getSignupCode = async () =>
{
const response = await get('/api/register/code');
const response = await get<{code: string}>('/api/register/code');
if (!response.success || !response.data)
return errorToast(response.message);
const link = `${window.location.origin}/register?code=${response.data.code}`;
@ -533,14 +533,14 @@ const Main = () =>
(async () =>
{
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)
{
setLoadState(RefresherState.error);
return errorToast(response.message);
}
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);
})();
}, [ page, searchFilter, refresh ]);
@ -549,10 +549,10 @@ const Main = () =>
{
(async () =>
{
const response = await get('/api/roles', { all: true });
const response = await get<{roles: RoleStruct[]}>('/api/roles', { all: true });
if (!response.success || !response.data)
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 ]);

View File

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

View File

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

View File

@ -184,7 +184,7 @@ export const setSetting = async (name: string, value: unknown) =>
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;
// 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) };
};
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 = {
method: 'POST'
@ -235,10 +235,10 @@ export const post = async (url: string, body?: object | string, opts: ReqOptions
// console.log(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 = {
method: 'PATCH'
@ -259,18 +259,18 @@ export const patch = async (url: string, body: object | string, opts: RequestOpt
// console.log(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)
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('&');
const response = await fetch(url);
return parseResponse(response);
return parseResponse<T>(response);
};
export const del = async (url: string, body?: object | string, opts: RequestOptions = {}) =>