Include footer on the table
This commit is contained in:
parent
37ce7b08b7
commit
93febff5e8
5 changed files with 84 additions and 34 deletions
|
@ -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 [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
<span className={'font-semibold text-neutral-400'}>{end}</span> of
|
||||||
|
<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;
|
|
@ -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/>
|
||||||
|
|
Loading…
Reference in a new issue