Include footer on the table

This commit is contained in:
Dane Everitt 2022-02-27 16:15:11 -05:00
parent 37ce7b08b7
commit 93febff5e8
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
5 changed files with 84 additions and 34 deletions

View file

@ -43,7 +43,7 @@ class DataValidationException extends PterodactylException implements HttpExcept
* *
* @return int * @return int
*/ */
public function getStatusCode() public function getStatusCode(): int
{ {
return 500; return 500;
} }
@ -51,7 +51,7 @@ class DataValidationException extends PterodactylException implements HttpExcept
/** /**
* @return array * @return array
*/ */
public function getHeaders() public function getHeaders(): array
{ {
return []; return [];
} }

View file

@ -20,23 +20,15 @@ use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
class UserController extends ApplicationApiController class UserController extends ApplicationApiController
{ {
private UserCreationService $creationService;
private UserDeletionService $deletionService;
private UserUpdateService $updateService;
/** /**
* UserController constructor. * UserController constructor.
*/ */
public function __construct( public function __construct(
UserCreationService $creationService, protected UserCreationService $creationService,
UserDeletionService $deletionService, protected UserDeletionService $deletionService,
UserUpdateService $updateService protected UserUpdateService $updateService
) { ) {
parent::__construct(); parent::__construct();
$this->creationService = $creationService;
$this->deletionService = $deletionService;
$this->updateService = $updateService;
} }
/** /**
@ -84,8 +76,6 @@ class UserController extends ApplicationApiController
* Revocation errors are returned under the 'revocation_errors' key in the response * Revocation errors are returned under the 'revocation_errors' key in the response
* meta. If there are no errors this is an empty array. * meta. If there are no errors this is an empty array.
* *
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Illuminate\Contracts\Container\BindingResolutionException * @throws \Illuminate\Contracts\Container\BindingResolutionException
*/ */
public function update(UpdateUserRequest $request, User $user): array public function update(UpdateUserRequest $request, User $user): array

View file

@ -1,5 +1,6 @@
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
import { store } from '@/state'; import { store } from '@/state';
import { Model } from '@/api/definitions';
const http: AxiosInstance = axios.create({ const http: AxiosInstance = axios.create({
timeout: 20000, timeout: 20000,
@ -88,6 +89,20 @@ export interface FractalResponseList {
data: FractalResponseData[]; data: FractalResponseData[];
} }
export interface FractalPaginatedResponse extends FractalResponseList {
meta: {
pagination: {
total: number;
count: number;
/* eslint-disable camelcase */
per_page: number;
current_page: number;
total_pages: number;
/* eslint-enable camelcase */
};
};
}
export interface PaginatedResult<T> { export interface PaginatedResult<T> {
items: T[]; items: T[];
pagination: PaginationDataSet; pagination: PaginationDataSet;
@ -150,3 +165,13 @@ export const withQueryBuilderParams = (data?: QueryBuilderParams): Record<string
sorts: !sorts.length ? undefined : sorts.join(','), sorts: !sorts.length ? undefined : sorts.join(','),
}; };
}; };
type TransformerFunc = (data: FractalResponseData) => Model;
export const toPaginatedSet = <T extends TransformerFunc> (
response: FractalPaginatedResponse,
transformer: T,
): PaginatedResult<ReturnType<T>> => ({
items: response.data.map(transformer) as ReturnType<T>[],
pagination: getPaginationSet(response.meta.pagination),
});

View file

@ -1,5 +1,12 @@
import React, { Fragment, useEffect, useState } from 'react'; import React, { Fragment, useEffect, useState } from 'react';
import http from '@/api/http'; import http, {
FractalPaginatedResponse,
PaginatedResult,
PaginationDataSet,
QueryBuilderParams,
toPaginatedSet,
withQueryBuilderParams,
} from '@/api/http';
import { UUID } from '@/api/definitions'; import { UUID } from '@/api/definitions';
import { Transformers, User } from '@definitions/admin'; import { Transformers, User } from '@definitions/admin';
import { Transition } from '@/components/elements/transitions'; import { Transition } from '@/components/elements/transitions';
@ -7,31 +14,58 @@ import { LockOpenIcon, PlusIcon, SupportIcon, TrashIcon } from '@heroicons/react
import { Button } from '@/components/elements/button/index'; import { Button } from '@/components/elements/button/index';
import { Checkbox, InputField } from '@/components/elements/inputs'; import { Checkbox, InputField } from '@/components/elements/inputs';
import UserTableRow from '@/components/admin/users/UserTableRow'; import UserTableRow from '@/components/admin/users/UserTableRow';
import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
import { AxiosError } from 'axios';
const UsersContainerV2 = () => { const useGetUsers = (
const [ users, setUsers ] = useState<User[]>([]); params?: QueryBuilderParams<'id' | 'email' | 'uuid'>,
config?: SWRConfiguration,
): SWRResponse<PaginatedResult<User>, AxiosError> => {
return useSWR<PaginatedResult<User>>([ '/api/application/users', params ], async () => {
const { data } = await http.get<FractalPaginatedResponse>(
'/api/application/users',
{ params: withQueryBuilderParams(params) },
);
return toPaginatedSet(data, Transformers.toUser);
}, config || { revalidateOnMount: true, revalidateOnFocus: false });
};
const PaginationFooter = ({ pagination, span }: { span: number; pagination: PaginationDataSet }) => {
const start = (pagination.currentPage - 1) * pagination.perPage;
const end = ((pagination.currentPage - 1) * pagination.perPage) + pagination.count;
return (
<tfoot>
<tr className={'bg-neutral-800'}>
<td scope={'col'} colSpan={span} className={'px-4 py-2'}>
<p className={'text-sm text-neutral-500'}>
Showing <span className={'font-semibold text-neutral-400'}>{Math.max(start, 1)}</span> to&nbsp;
<span className={'font-semibold text-neutral-400'}>{end}</span> of&nbsp;
<span className={'font-semibold text-neutral-400'}>{pagination.total}</span> results.
</p>
</td>
</tr>
</tfoot>
);
};
const UsersContainer = () => {
const { data: users } = useGetUsers();
const [ selected, setSelected ] = useState<UUID[]>([]); const [ selected, setSelected ] = useState<UUID[]>([]);
useEffect(() => { useEffect(() => {
document.title = 'Admin | Users'; document.title = 'Admin | Users';
}, []); }, []);
useEffect(() => {
http.get('/api/application/users')
.then(({ data }) => {
setUsers(data.data.map(Transformers.toUser));
})
.catch(console.error);
}, []);
const onRowChange = (user: User, checked: boolean) => { const onRowChange = (user: User, checked: boolean) => {
setSelected((state) => { setSelected((state) => {
return checked ? [ ...state, user.uuid ] : selected.filter((uuid) => uuid !== user.uuid); return checked ? [ ...state, user.uuid ] : selected.filter((uuid) => uuid !== user.uuid);
}); });
}; };
const selectAllChecked = users.length > 0 && selected.length > 0; const selectAllChecked = users && users.items.length > 0 && selected.length > 0;
const onSelectAll = () => setSelected((state) => state.length > 0 ? [] : users.map(({ uuid }) => uuid)); const onSelectAll = () => setSelected((state) => state.length > 0 ? [] : users?.items.map(({ uuid }) => uuid) || []);
return ( return (
<div> <div>
@ -44,7 +78,7 @@ const UsersContainerV2 = () => {
<div className={'mr-6'}> <div className={'mr-6'}>
<Checkbox <Checkbox
checked={selectAllChecked} checked={selectAllChecked}
indeterminate={selected.length !== users.length} indeterminate={selected.length !== users?.items.length}
onChange={onSelectAll} onChange={onSelectAll}
/> />
</div> </div>
@ -61,7 +95,7 @@ const UsersContainerV2 = () => {
<div className={'flex-1'}> <div className={'flex-1'}>
<Checkbox <Checkbox
checked={selectAllChecked} checked={selectAllChecked}
indeterminate={selected.length !== users.length} indeterminate={selected.length !== users?.items.length}
onChange={onSelectAll} onChange={onSelectAll}
/> />
</div> </div>
@ -87,7 +121,7 @@ const UsersContainerV2 = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{users.map(user => ( {users?.items.map(user => (
<UserTableRow <UserTableRow
key={user.uuid} key={user.uuid}
user={user} user={user}
@ -96,9 +130,10 @@ const UsersContainerV2 = () => {
/> />
))} ))}
</tbody> </tbody>
{users && <PaginationFooter span={4} pagination={users.pagination}/>}
</table> </table>
</div> </div>
); );
}; };
export default UsersContainerV2; export default UsersContainer;

View file

@ -45,7 +45,7 @@ import {
import CollapsedIcon from '@/assets/images/pterodactyl.svg'; import CollapsedIcon from '@/assets/images/pterodactyl.svg';
import Sidebar from '@/components/admin/Sidebar'; import Sidebar from '@/components/admin/Sidebar';
import useUserPersistedState from '@/plugins/useUserPersistedState'; import useUserPersistedState from '@/plugins/useUserPersistedState';
import UsersContainerV2 from '@/components/admin/users/UsersContainerV2'; import UsersContainer from '@/components/admin/users/UsersContainer';
const AdminRouter = ({ location, match }: RouteComponentProps) => { const AdminRouter = ({ location, match }: RouteComponentProps) => {
const email = useStoreState((state: State<ApplicationStore>) => state.user.data!.email); const email = useStoreState((state: State<ApplicationStore>) => state.user.data!.email);
@ -132,7 +132,7 @@ const AdminRouter = ({ location, match }: RouteComponentProps) => {
<Route path={`${match.path}/servers`} component={ServersContainer} exact/> <Route path={`${match.path}/servers`} component={ServersContainer} exact/>
<Route path={`${match.path}/servers/new`} component={NewServerContainer} exact/> <Route path={`${match.path}/servers/new`} component={NewServerContainer} exact/>
<Route path={`${match.path}/servers/:id`} component={ServerRouter}/> <Route path={`${match.path}/servers/:id`} component={ServerRouter}/>
<Route path={`${match.path}/users`} component={UsersContainerV2} exact/> <Route path={`${match.path}/users`} component={UsersContainer} exact/>
<Route path={`${match.path}/users/new`} component={NewUserContainer} exact/> <Route path={`${match.path}/users/new`} component={NewUserContainer} exact/>
<Route path={`${match.path}/users/:id`} component={UserRouter}/> <Route path={`${match.path}/users/:id`} component={UserRouter}/>
<Route path={`${match.path}/roles`} component={RolesContainer} exact/> <Route path={`${match.path}/roles`} component={RolesContainer} exact/>