From 95c55e7d28fe52430811175a8c7ccbe6f6a3e0be Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Mon, 4 Jan 2021 11:50:43 -0700 Subject: [PATCH] Add table to admin/UsersContainer.tsx --- .../Api/Application/Roles/RoleController.php | 5 +- .../Api/Application/Users/UserController.php | 37 +++--- .../Api/Application/Users/GetUserRequest.php | 20 +++ resources/scripts/api/admin/users/getUsers.ts | 39 ++++++ resources/scripts/api/transformers.ts | 16 +++ .../components/admin/nests/NestsContainer.tsx | 2 +- .../components/admin/roles/RolesContainer.tsx | 2 +- .../components/admin/users/UsersContainer.tsx | 121 +++++++++++++++++- routes/api-application.php | 3 + 9 files changed, 223 insertions(+), 22 deletions(-) create mode 100644 app/Http/Requests/Api/Application/Users/GetUserRequest.php create mode 100644 resources/scripts/api/admin/users/getUsers.ts diff --git a/app/Http/Controllers/Api/Application/Roles/RoleController.php b/app/Http/Controllers/Api/Application/Roles/RoleController.php index bc7363994..f80243b13 100644 --- a/app/Http/Controllers/Api/Application/Roles/RoleController.php +++ b/app/Http/Controllers/Api/Application/Roles/RoleController.php @@ -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)) diff --git a/app/Http/Controllers/Api/Application/Users/UserController.php b/app/Http/Controllers/Api/Application/Users/UserController.php index 495d92fd3..905abeb73 100644 --- a/app/Http/Controllers/Api/Application/Users/UserController.php +++ b/app/Http/Controllers/Api/Application/Users/UserController.php @@ -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(); } /** diff --git a/app/Http/Requests/Api/Application/Users/GetUserRequest.php b/app/Http/Requests/Api/Application/Users/GetUserRequest.php new file mode 100644 index 000000000..e5b99daf3 --- /dev/null +++ b/app/Http/Requests/Api/Application/Users/GetUserRequest.php @@ -0,0 +1,20 @@ +route()->parameter('user'); + + return $user instanceof User && $user->exists; + } +} diff --git a/resources/scripts/api/admin/users/getUsers.ts b/resources/scripts/api/admin/users/getUsers.ts new file mode 100644 index 000000000..135d3c9b3 --- /dev/null +++ b/resources/scripts/api/admin/users/getUsers.ts @@ -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({ page: 1, setPage: () => 1 }); + +export default () => { + const { page } = useContext(Context); + + return useSWR>([ 'users', page ], async () => { + const { data } = await http.get('/api/application/users', { params: { page } }); + + return ({ + items: (data.data || []).map(rawDataToUser), + pagination: getPaginationSet(data.meta.pagination), + }); + }); +}; diff --git a/resources/scripts/api/transformers.ts b/resources/scripts/api/transformers.ts index 614bebce1..c6a5ef18f 100644 --- a/resources/scripts/api/transformers.ts +++ b/resources/scripts/api/transformers.ts @@ -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), +}); diff --git a/resources/scripts/components/admin/nests/NestsContainer.tsx b/resources/scripts/components/admin/nests/NestsContainer.tsx index 0450d6b87..2977215da 100644 --- a/resources/scripts/components/admin/nests/NestsContainer.tsx +++ b/resources/scripts/components/admin/nests/NestsContainer.tsx @@ -102,7 +102,7 @@ const NestsContainer = () => { {nest.id} - + {nest.name} diff --git a/resources/scripts/components/admin/roles/RolesContainer.tsx b/resources/scripts/components/admin/roles/RolesContainer.tsx index 5c054bcb2..6af5a5cab 100644 --- a/resources/scripts/components/admin/roles/RolesContainer.tsx +++ b/resources/scripts/components/admin/roles/RolesContainer.tsx @@ -103,7 +103,7 @@ export default () => { {role.id} - + {role.name} diff --git a/resources/scripts/components/admin/users/UsersContainer.tsx b/resources/scripts/components/admin/users/UsersContainer.tsx index 4ceb6420a..ff16d219e 100644 --- a/resources/scripts/components/admin/users/UsersContainer.tsx +++ b/resources/scripts/components/admin/users/UsersContainer.tsx @@ -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 ( + ) => { + 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) => { + setSelectedUsers(e.currentTarget.checked ? (users?.items?.map(user => user.id) || []) : []); + }; + + useEffect(() => { + setSelectedUsers([]); + }, [ page ]); + return ( -
+

Users

All registered users on the system.

@@ -16,6 +71,66 @@ export default () => { New User
+ + + + + { users === undefined || (error && isValidating) ? + + : + length < 1 ? + + : + + +
+ + + + + + + + + + { + users.items.map(user => ( + + + + + + + + + )) + } + +
+ + {user.id} + + {user.email} + + {user.username}{user.lastName}, {user.firstName}
+
+
+
+ } +
); }; + +export default () => { + const [ page, setPage ] = useState(1); + + return ( + + + + ); +}; diff --git a/routes/api-application.php b/routes/api-application.php index 66c12a33e..28175de54 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -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');