forked from Alex/Pterodactyl-Panel
Add table to admin/UsersContainer.tsx
This commit is contained in:
parent
0ddf806100
commit
95c55e7d28
@ -6,6 +6,7 @@ use Illuminate\Http\JsonResponse;
|
||||
use Pterodactyl\Models\AdminRole;
|
||||
use Pterodactyl\Repositories\Eloquent\AdminRolesRepository;
|
||||
use Pterodactyl\Transformers\Api\Application\AdminRoleTransformer;
|
||||
use Pterodactyl\Http\Requests\Api\Application\Roles\GetRoleRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Application\Roles\GetRolesRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Application\Roles\StoreRoleRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Application\Roles\DeleteRoleRequest;
|
||||
@ -49,13 +50,13 @@ class RoleController extends ApplicationApiController
|
||||
/**
|
||||
* Returns a single role.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Application\Roles\GetRolesRequest $request
|
||||
* @param \Pterodactyl\Http\Requests\Api\Application\Roles\GetRoleRequest $request
|
||||
* @param \Pterodactyl\Models\AdminRole $role
|
||||
*
|
||||
* @return array
|
||||
* @throws \Illuminate\Contracts\Container\BindingResolutionException
|
||||
*/
|
||||
public function view(GetRolesRequest $request, AdminRole $role): array
|
||||
public function view(GetRoleRequest $request, AdminRole $role): array
|
||||
{
|
||||
return $this->fractal->item($role)
|
||||
->transformWith($this->getTransformer(AdminRoleTransformer::class))
|
||||
|
@ -10,6 +10,8 @@ use Pterodactyl\Services\Users\UserCreationService;
|
||||
use Pterodactyl\Services\Users\UserDeletionService;
|
||||
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
|
||||
use Pterodactyl\Transformers\Api\Application\UserTransformer;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Pterodactyl\Http\Requests\Api\Application\Users\GetUserRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Application\Users\GetUsersRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Application\Users\StoreUserRequest;
|
||||
use Pterodactyl\Http\Requests\Api\Application\Users\DeleteUserRequest;
|
||||
@ -18,6 +20,11 @@ use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
|
||||
class UserController extends ApplicationApiController
|
||||
{
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Users\UserCreationService
|
||||
*/
|
||||
@ -28,11 +35,6 @@ class UserController extends ApplicationApiController
|
||||
*/
|
||||
private $deletionService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Users\UserUpdateService
|
||||
*/
|
||||
@ -51,13 +53,12 @@ class UserController extends ApplicationApiController
|
||||
UserCreationService $creationService,
|
||||
UserDeletionService $deletionService,
|
||||
UserUpdateService $updateService
|
||||
)
|
||||
{
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->repository = $repository;
|
||||
$this->creationService = $creationService;
|
||||
$this->deletionService = $deletionService;
|
||||
$this->repository = $repository;
|
||||
$this->updateService = $updateService;
|
||||
}
|
||||
|
||||
@ -73,10 +74,17 @@ class UserController extends ApplicationApiController
|
||||
*/
|
||||
public function index(GetUsersRequest $request): array
|
||||
{
|
||||
$perPage = $request->query('per_page', 10);
|
||||
if ($perPage < 1) {
|
||||
$perPage = 10;
|
||||
} else if ($perPage > 100) {
|
||||
throw new BadRequestHttpException('"per_page" query parameter must be below 100.');
|
||||
}
|
||||
|
||||
$users = QueryBuilder::for(User::query())
|
||||
->allowedFilters(['email', 'uuid', 'username', 'external_id'])
|
||||
->allowedSorts(['id', 'uuid'])
|
||||
->paginate(100);
|
||||
->paginate($perPage);
|
||||
|
||||
return $this->fractal->collection($users)
|
||||
->transformWith($this->getTransformer(UserTransformer::class))
|
||||
@ -87,13 +95,13 @@ class UserController extends ApplicationApiController
|
||||
* Handle a request to view a single user. Includes any relations that
|
||||
* were defined in the request.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Application\Users\GetUsersRequest $request
|
||||
* @param \Pterodactyl\Http\Requests\Api\Application\Users\GetUserRequest $request
|
||||
* @param \Pterodactyl\Models\User $user
|
||||
*
|
||||
* @return array
|
||||
* @throws \Illuminate\Contracts\Container\BindingResolutionException
|
||||
*/
|
||||
public function view(GetUsersRequest $request, User $user): array
|
||||
public function view(GetUserRequest $request, User $user): array
|
||||
{
|
||||
return $this->fractal->item($user)
|
||||
->transformWith($this->getTransformer(UserTransformer::class))
|
||||
@ -122,10 +130,9 @@ class UserController extends ApplicationApiController
|
||||
$this->updateService->setUserLevel(User::USER_LEVEL_ADMIN);
|
||||
$user = $this->updateService->handle($user, $request->validated());
|
||||
|
||||
$response = $this->fractal->item($user)
|
||||
->transformWith($this->getTransformer(UserTransformer::class));
|
||||
|
||||
return $response->toArray();
|
||||
return $this->fractal->item($user)
|
||||
->transformWith($this->getTransformer(UserTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
|
20
app/Http/Requests/Api/Application/Users/GetUserRequest.php
Normal file
20
app/Http/Requests/Api/Application/Users/GetUserRequest.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Application\Users;
|
||||
|
||||
use Pterodactyl\Models\User;
|
||||
|
||||
class GetUserRequest extends GetUsersRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the requested role exists on the Panel.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function resourceExists(): bool
|
||||
{
|
||||
$user = $this->route()->parameter('user');
|
||||
|
||||
return $user instanceof User && $user->exists;
|
||||
}
|
||||
}
|
39
resources/scripts/api/admin/users/getUsers.ts
Normal file
39
resources/scripts/api/admin/users/getUsers.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
|
||||
import { rawDataToUser } from '@/api/transformers';
|
||||
import { createContext, useContext } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
externalId: string;
|
||||
uuid: string;
|
||||
username: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
language: string;
|
||||
rootAdmin: boolean;
|
||||
tfa: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
interface ctx {
|
||||
page: number;
|
||||
setPage: (value: number | ((s: number) => number)) => void;
|
||||
}
|
||||
|
||||
export const Context = createContext<ctx>({ page: 1, setPage: () => 1 });
|
||||
|
||||
export default () => {
|
||||
const { page } = useContext(Context);
|
||||
|
||||
return useSWR<PaginatedResult<User>>([ 'users', page ], async () => {
|
||||
const { data } = await http.get('/api/application/users', { params: { page } });
|
||||
|
||||
return ({
|
||||
items: (data.data || []).map(rawDataToUser),
|
||||
pagination: getPaginationSet(data.meta.pagination),
|
||||
});
|
||||
});
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import { Egg } from '@/api/admin/nests/eggs/getEggs';
|
||||
import { Nest } from '@/api/admin/nests/getNests';
|
||||
import { Role } from '@/api/admin/roles/getRoles';
|
||||
import { User } from '@/api/admin/users/getUsers';
|
||||
import { Allocation } from '@/api/server/getServer';
|
||||
import { FractalResponseData } from '@/api/http';
|
||||
import { FileObject } from '@/api/server/files/loadDirectory';
|
||||
@ -117,3 +118,18 @@ export const rawDataToEgg = ({ attributes }: FractalResponseData): Egg => ({
|
||||
createdAt: new Date(attributes.created_at),
|
||||
updatedAt: new Date(attributes.updated_at),
|
||||
});
|
||||
|
||||
export const rawDataToUser = ({ attributes }: FractalResponseData): User => ({
|
||||
id: attributes.id,
|
||||
externalId: attributes.external_id,
|
||||
uuid: attributes.uuid,
|
||||
username: attributes.username,
|
||||
email: attributes.email,
|
||||
firstName: attributes.first_name,
|
||||
lastName: attributes.last_name,
|
||||
language: attributes.language,
|
||||
rootAdmin: attributes.root_admin,
|
||||
tfa: attributes['2fa'],
|
||||
createdAt: new Date(attributes.created_at),
|
||||
updatedAt: new Date(attributes.updated_at),
|
||||
});
|
||||
|
@ -102,7 +102,7 @@ const NestsContainer = () => {
|
||||
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>{nest.id}</td>
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>
|
||||
<NavLink to={`${match.url}/${nest.id}`}>
|
||||
<NavLink to={`${match.url}/${nest.id}`} css={tw`text-primary-400 hover:text-primary-300`}>
|
||||
{nest.name}
|
||||
</NavLink>
|
||||
</td>
|
||||
|
@ -103,7 +103,7 @@ export default () => {
|
||||
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>{role.id}</td>
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>
|
||||
<NavLink to={`${match.url}/${role.id}`}>
|
||||
<NavLink to={`${match.url}/${role.id}`} css={tw`text-primary-400 hover:text-primary-300`}>
|
||||
{role.name}
|
||||
</NavLink>
|
||||
</td>
|
||||
|
@ -1,12 +1,67 @@
|
||||
import AdminCheckbox from '@/components/admin/AdminCheckbox';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import getUsers, { Context as UsersContext } from '@/api/admin/users/getUsers';
|
||||
import AdminTable, { ContentWrapper, Loading, NoItems, Pagination, TableBody, TableHead, TableHeader, TableRow } from '@/components/admin/AdminTable';
|
||||
import Button from '@/components/elements/Button';
|
||||
import React from 'react';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import useFlash from '@/plugins/useFlash';
|
||||
import { AdminContext } from '@/state/admin';
|
||||
import { NavLink, useRouteMatch } from 'react-router-dom';
|
||||
import tw from 'twin.macro';
|
||||
import AdminContentBlock from '@/components/admin/AdminContentBlock';
|
||||
|
||||
export default () => {
|
||||
const RowCheckbox = ({ id }: { id: number}) => {
|
||||
const isChecked = AdminContext.useStoreState(state => state.nests.selectedNests.indexOf(id) >= 0);
|
||||
const appendSelectedUser = AdminContext.useStoreActions(actions => actions.nests.appendSelectedNest);
|
||||
const removeSelectedUser = AdminContext.useStoreActions(actions => actions.nests.removeSelectedNest);
|
||||
|
||||
return (
|
||||
<AdminCheckbox
|
||||
name={id.toString()}
|
||||
checked={isChecked}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.currentTarget.checked) {
|
||||
appendSelectedUser(id);
|
||||
} else {
|
||||
removeSelectedUser(id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const UsersContainer = () => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
const { page, setPage } = useContext(UsersContext);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||
const { data: users, error, isValidating } = getUsers();
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) {
|
||||
clearFlashes('users');
|
||||
return;
|
||||
}
|
||||
|
||||
clearAndAddHttpError({ error, key: 'users' });
|
||||
}, [ error ]);
|
||||
|
||||
const length = users?.items?.length || 0;
|
||||
|
||||
const setSelectedUsers = AdminContext.useStoreActions(actions => actions.nests.setSelectedNests);
|
||||
const selectedUserLength = AdminContext.useStoreState(state => state.nests.selectedNests.length);
|
||||
|
||||
const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedUsers(e.currentTarget.checked ? (users?.items?.map(user => user.id) || []) : []);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedUsers([]);
|
||||
}, [ page ]);
|
||||
|
||||
return (
|
||||
<AdminContentBlock>
|
||||
<div css={tw`w-full flex flex-row items-center`}>
|
||||
<div css={tw`w-full flex flex-row items-center mb-8`}>
|
||||
<div css={tw`flex flex-col`}>
|
||||
<h2 css={tw`text-2xl text-neutral-50 font-header font-medium`}>Users</h2>
|
||||
<p css={tw`text-base text-neutral-400`}>All registered users on the system.</p>
|
||||
@ -16,6 +71,66 @@ export default () => {
|
||||
New User
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<FlashMessageRender byKey={'users'} css={tw`mb-4`}/>
|
||||
|
||||
<AdminTable>
|
||||
{ users === undefined || (error && isValidating) ?
|
||||
<Loading/>
|
||||
:
|
||||
length < 1 ?
|
||||
<NoItems/>
|
||||
:
|
||||
<ContentWrapper
|
||||
checked={selectedUserLength === (length === 0 ? -1 : length)}
|
||||
onSelectAllClick={onSelectAllClick}
|
||||
>
|
||||
<Pagination data={users} onPageSelect={setPage}>
|
||||
<div css={tw`overflow-x-auto`}>
|
||||
<table css={tw`w-full table-auto`}>
|
||||
<TableHead>
|
||||
<TableHeader name={'ID'}/>
|
||||
<TableHeader name={'Email'}/>
|
||||
<TableHeader name={'Username'}/>
|
||||
<TableHeader name={'Client Name'}/>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{
|
||||
users.items.map(user => (
|
||||
<TableRow key={user.id}>
|
||||
<td css={tw`pl-6`}>
|
||||
<RowCheckbox id={user.id}/>
|
||||
</td>
|
||||
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>{user.id}</td>
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>
|
||||
<NavLink to={`${match.url}/${user.id}`} css={tw`text-primary-400 hover:text-primary-300`}>
|
||||
{user.email}
|
||||
</NavLink>
|
||||
</td>
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>{user.username}</td>
|
||||
<td css={tw`px-6 text-sm text-neutral-200 text-left whitespace-nowrap`}>{user.lastName}, {user.firstName}</td>
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
</TableBody>
|
||||
</table>
|
||||
</div>
|
||||
</Pagination>
|
||||
</ContentWrapper>
|
||||
}
|
||||
</AdminTable>
|
||||
</AdminContentBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const [ page, setPage ] = useState<number>(1);
|
||||
|
||||
return (
|
||||
<UsersContext.Provider value={{ page, setPage }}>
|
||||
<UsersContainer/>
|
||||
</UsersContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -17,6 +17,7 @@ Route::group(['prefix' => '/users'], function () {
|
||||
Route::get('/external/{external_id}', 'Users\ExternalUserController@index')->name('api.application.users.external');
|
||||
|
||||
Route::post('/', 'Users\UserController@store');
|
||||
|
||||
Route::patch('/{user}', 'Users\UserController@update');
|
||||
|
||||
Route::delete('/{user}', 'Users\UserController@delete');
|
||||
@ -36,6 +37,7 @@ Route::group(['prefix' => '/nodes'], function () {
|
||||
Route::get('/{node}/configuration', 'Nodes\NodeConfigurationController');
|
||||
|
||||
Route::post('/', 'Nodes\NodeController@store');
|
||||
|
||||
Route::patch('/{node}', 'Nodes\NodeController@update');
|
||||
|
||||
Route::delete('/{node}', 'Nodes\NodeController@delete');
|
||||
@ -60,6 +62,7 @@ Route::group(['prefix' => '/locations'], function () {
|
||||
Route::get('/{location}', 'Locations\LocationController@view')->name('api.application.locations.view');
|
||||
|
||||
Route::post('/', 'Locations\LocationController@store');
|
||||
|
||||
Route::patch('/{location}', 'Locations\LocationController@update');
|
||||
|
||||
Route::delete('/{location}', 'Locations\LocationController@delete');
|
||||
|
Loading…
Reference in New Issue
Block a user