diff --git a/app/Http/Controllers/Api/Application/Eggs/EggController.php b/app/Http/Controllers/Api/Application/Eggs/EggController.php index 11e9de273..25d3a05a7 100644 --- a/app/Http/Controllers/Api/Application/Eggs/EggController.php +++ b/app/Http/Controllers/Api/Application/Eggs/EggController.php @@ -38,12 +38,13 @@ class EggController extends ApplicationApiController */ public function index(GetEggsRequest $request, Nest $nest): array { - $perPage = $request->query('per_page', 0); + $perPage = $request->query('per_page', 10); if ($perPage > 100) { throw new QueryValueOutOfRangeHttpException('per_page', 1, 100); } $eggs = QueryBuilder::for(Egg::query()) + ->where('nest_id', '=', $nest->id) ->allowedFilters(['id', 'name', 'author']) ->allowedSorts(['id', 'name', 'author']); if ($perPage > 0) { diff --git a/app/Http/Controllers/Api/Application/Locations/LocationController.php b/app/Http/Controllers/Api/Application/Locations/LocationController.php index 326227168..de121c2e8 100644 --- a/app/Http/Controllers/Api/Application/Locations/LocationController.php +++ b/app/Http/Controllers/Api/Application/Locations/LocationController.php @@ -9,7 +9,6 @@ use Spatie\QueryBuilder\QueryBuilder; use Pterodactyl\Services\Locations\LocationUpdateService; use Pterodactyl\Services\Locations\LocationCreationService; use Pterodactyl\Services\Locations\LocationDeletionService; -use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Transformers\Api\Application\LocationTransformer; use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; @@ -24,7 +23,6 @@ class LocationController extends ApplicationApiController private LocationCreationService $creationService; private LocationDeletionService $deletionService; private LocationUpdateService $updateService; - private LocationRepositoryInterface $repository; /** * LocationController constructor. @@ -32,15 +30,13 @@ class LocationController extends ApplicationApiController public function __construct( LocationCreationService $creationService, LocationDeletionService $deletionService, - LocationUpdateService $updateService, - LocationRepositoryInterface $repository + LocationUpdateService $updateService ) { parent::__construct(); $this->creationService = $creationService; $this->deletionService = $deletionService; $this->updateService = $updateService; - $this->repository = $repository; } /** @@ -57,7 +53,7 @@ class LocationController extends ApplicationApiController $locations = QueryBuilder::for(Location::query()) ->allowedFilters(['short', 'long']) - ->allowedSorts(['id']) + ->allowedSorts(['id', 'short', 'long']) ->paginate($perPage); return $this->fractal->collection($locations) diff --git a/app/Http/Controllers/Api/Application/Mounts/MountController.php b/app/Http/Controllers/Api/Application/Mounts/MountController.php index 0d860550b..9070f4155 100644 --- a/app/Http/Controllers/Api/Application/Mounts/MountController.php +++ b/app/Http/Controllers/Api/Application/Mounts/MountController.php @@ -40,8 +40,8 @@ class MountController extends ApplicationApiController } $mounts = QueryBuilder::for(Mount::query()) - ->allowedFilters(['name', 'host']) - ->allowedSorts(['id', 'name', 'host']) + ->allowedFilters(['id', 'name', 'source', 'target']) + ->allowedSorts(['id', 'name', 'source', 'target']) ->paginate($perPage); return $this->fractal->collection($mounts) diff --git a/app/Http/Controllers/Api/Application/Nests/NestController.php b/app/Http/Controllers/Api/Application/Nests/NestController.php index 9fbb5984a..008f4f1db 100644 --- a/app/Http/Controllers/Api/Application/Nests/NestController.php +++ b/app/Http/Controllers/Api/Application/Nests/NestController.php @@ -50,7 +50,7 @@ class NestController extends ApplicationApiController */ public function index(GetNestsRequest $request): array { - $perPage = $request->query('per_page', 0); + $perPage = $request->query('per_page', 10); if ($perPage > 100) { throw new QueryValueOutOfRangeHttpException('per_page', 1, 100); } diff --git a/app/Http/Controllers/Api/Application/Roles/RoleController.php b/app/Http/Controllers/Api/Application/Roles/RoleController.php index caae5c447..94b7fe592 100644 --- a/app/Http/Controllers/Api/Application/Roles/RoleController.php +++ b/app/Http/Controllers/Api/Application/Roles/RoleController.php @@ -3,9 +3,13 @@ namespace Pterodactyl\Http\Controllers\Api\Application\Roles; use Illuminate\Http\Response; +use Pterodactyl\Models\Location; use Illuminate\Http\JsonResponse; use Pterodactyl\Models\AdminRole; +use Spatie\QueryBuilder\QueryBuilder; +use Pterodactyl\Transformers\Api\Application\LocationTransformer; use Pterodactyl\Transformers\Api\Application\AdminRoleTransformer; +use Pterodactyl\Exceptions\Http\QueryValueOutOfRangeHttpException; use Pterodactyl\Http\Requests\Api\Application\Roles\GetRoleRequest; use Pterodactyl\Http\Requests\Api\Application\Roles\GetRolesRequest; use Pterodactyl\Http\Requests\Api\Application\Roles\StoreRoleRequest; @@ -30,7 +34,17 @@ class RoleController extends ApplicationApiController */ public function index(GetRolesRequest $request): array { - return $this->fractal->collection(AdminRole::all()) + $perPage = $request->query('per_page', 10); + if ($perPage < 1 || $perPage > 100) { + throw new QueryValueOutOfRangeHttpException('per_page', 1, 100); + } + + $roles = QueryBuilder::for(AdminRole::query()) + ->allowedFilters(['id', 'name']) + ->allowedSorts(['id', 'name']) + ->paginate($perPage); + + return $this->fractal->collection($roles) ->transformWith($this->getTransformer(AdminRoleTransformer::class)) ->toArray(); } diff --git a/app/Http/Controllers/Api/Application/Users/UserController.php b/app/Http/Controllers/Api/Application/Users/UserController.php index b5b3d7eaf..789b8bf77 100644 --- a/app/Http/Controllers/Api/Application/Users/UserController.php +++ b/app/Http/Controllers/Api/Application/Users/UserController.php @@ -19,7 +19,7 @@ use Pterodactyl\Http\Requests\Api\Application\Users\DeleteUserRequest; use Pterodactyl\Http\Requests\Api\Application\Users\UpdateUserRequest; use Pterodactyl\Http\Controllers\Api\Application\ApplicationApiController; -class UserController extends ApplicationApiController +class UserController extends ApplicationApiController { private UserRepositoryInterface $repository; private UserCreationService $creationService; @@ -58,8 +58,8 @@ class UserController extends ApplicationApiController } $users = QueryBuilder::for(User::query()) - ->allowedFilters(['email', 'uuid', 'username', 'external_id']) - ->allowedSorts(['id', 'uuid']) + ->allowedFilters(['id', 'uuid', 'username', 'email', 'first_name', 'last_name', 'external_id']) + ->allowedSorts(['id', 'uuid', 'username', 'email', 'admin_role_id']) ->paginate($perPage); return $this->fractal->collection($users) diff --git a/resources/scripts/api/admin/databases/getDatabases.ts b/resources/scripts/api/admin/databases/getDatabases.ts index 750597f8e..9a697051a 100644 --- a/resources/scripts/api/admin/databases/getDatabases.ts +++ b/resources/scripts/api/admin/databases/getDatabases.ts @@ -28,18 +28,58 @@ export const rawDataToDatabase = ({ attributes }: FractalResponseData): Database getAddress: () => `${attributes.host}:${attributes.port}`, }); +export interface Filters { + id?: string; + name?: string; + host?: string; +} + interface ctx { page: number; setPage: (value: number | ((s: number) => number)) => void; + + filters: Filters | null; + setFilters: (filters: Filters | null) => void; + + sort: string | null; + setSort: (sort: string | null) => void; + + sortDirection: boolean; + setSortDirection: (direction: boolean) => void; } -export const Context = createContext({ page: 1, setPage: () => 1 }); +export const Context = createContext({ + page: 1, + setPage: () => 1, + + filters: null, + setFilters: () => null, + + sort: null, + setSort: () => null, + + sortDirection: false, + setSortDirection: () => false, +}); export default (include: string[] = []) => { - const { page } = useContext(Context); + const { page, filters, sort, sortDirection } = useContext(Context); - return useSWR>([ 'databases', page ], async () => { - const { data } = await http.get('/api/application/databases', { params: { include: include.join(','), page } }); + const params = {}; + if (filters !== null) { + Object.keys(filters).forEach(key => { + // @ts-ignore + params['filter[' + key + ']'] = filters[key]; + }); + } + + if (sort !== null) { + // @ts-ignore + params.sort = (sortDirection ? '-' : '') + sort; + } + + return useSWR>([ 'databases', page, filters, sort, sortDirection ], async () => { + const { data } = await http.get('/api/application/databases', { params: { include: include.join(','), page, ...params } }); return ({ items: (data.data || []).map(rawDataToDatabase), diff --git a/resources/scripts/api/admin/locations/getLocations.ts b/resources/scripts/api/admin/locations/getLocations.ts index 6c70bab6f..dc6ac6d4d 100644 --- a/resources/scripts/api/admin/locations/getLocations.ts +++ b/resources/scripts/api/admin/locations/getLocations.ts @@ -18,18 +18,58 @@ export const rawDataToLocation = ({ attributes }: FractalResponseData): Location updatedAt: new Date(attributes.updated_at), }); +export interface Filters { + id?: string; + short?: string; + long?: string; +} + interface ctx { page: number; setPage: (value: number | ((s: number) => number)) => void; + + filters: Filters | null; + setFilters: (filters: Filters | null) => void; + + sort: string | null; + setSort: (sort: string | null) => void; + + sortDirection: boolean; + setSortDirection: (direction: boolean) => void; } -export const Context = createContext({ page: 1, setPage: () => 1 }); +export const Context = createContext({ + page: 1, + setPage: () => 1, + + filters: null, + setFilters: () => null, + + sort: null, + setSort: () => null, + + sortDirection: false, + setSortDirection: () => false, +}); export default (include: string[] = []) => { - const { page } = useContext(Context); + const { page, filters, sort, sortDirection } = useContext(Context); - return useSWR>([ 'locations', page ], async () => { - const { data } = await http.get('/api/application/locations', { params: { include: include.join(','), page } }); + const params = {}; + if (filters !== null) { + Object.keys(filters).forEach(key => { + // @ts-ignore + params['filter[' + key + ']'] = filters[key]; + }); + } + + if (sort !== null) { + // @ts-ignore + params.sort = (sortDirection ? '-' : '') + sort; + } + + return useSWR>([ 'locations', page, filters, sort, sortDirection ], async () => { + const { data } = await http.get('/api/application/locations', { params: { include: include.join(','), page, ...params } }); return ({ items: (data.data || []).map(rawDataToLocation), diff --git a/resources/scripts/api/admin/mounts/getMounts.ts b/resources/scripts/api/admin/mounts/getMounts.ts index b05981df9..a8cff9a0f 100644 --- a/resources/scripts/api/admin/mounts/getMounts.ts +++ b/resources/scripts/api/admin/mounts/getMounts.ts @@ -43,18 +43,59 @@ export const rawDataToMount = ({ attributes }: FractalResponseData): Mount => ({ }, }); +export interface Filters { + id?: string; + name?: string; + source?: string; + target?: string; +} + interface ctx { page: number; setPage: (value: number | ((s: number) => number)) => void; + + filters: Filters | null; + setFilters: (filters: Filters | null) => void; + + sort: string | null; + setSort: (sort: string | null) => void; + + sortDirection: boolean; + setSortDirection: (direction: boolean) => void; } -export const Context = createContext({ page: 1, setPage: () => 1 }); +export const Context = createContext({ + page: 1, + setPage: () => 1, + + filters: null, + setFilters: () => null, + + sort: null, + setSort: () => null, + + sortDirection: false, + setSortDirection: () => false, +}); export default (include: string[] = []) => { - const { page } = useContext(Context); + const { page, filters, sort, sortDirection } = useContext(Context); - return useSWR>([ 'mounts', page ], async () => { - const { data } = await http.get('/api/application/mounts', { params: { include: include.join(','), page } }); + const params = {}; + if (filters !== null) { + Object.keys(filters).forEach(key => { + // @ts-ignore + params['filter[' + key + ']'] = filters[key]; + }); + } + + if (sort !== null) { + // @ts-ignore + params.sort = (sortDirection ? '-' : '') + sort; + } + + return useSWR>([ 'mounts', page, filters, sort, sortDirection ], async () => { + const { data } = await http.get('/api/application/mounts', { params: { include: include.join(','), page, ...params } }); return ({ items: (data.data || []).map(rawDataToMount), diff --git a/resources/scripts/api/admin/nests/getEggs.ts b/resources/scripts/api/admin/nests/getEggs.ts index be5703625..9796fc367 100644 --- a/resources/scripts/api/admin/nests/getEggs.ts +++ b/resources/scripts/api/admin/nests/getEggs.ts @@ -1,10 +1,63 @@ -import http from '@/api/http'; +import http, { getPaginationSet, PaginatedResult } from '@/api/http'; +import { createContext, useContext } from 'react'; +import useSWR from 'swr'; import { Egg, rawDataToEgg } from '@/api/admin/eggs/getEgg'; -export default (nestId: number): Promise => { - return new Promise((resolve, reject) => { - http.get(`/api/application/nests/${nestId}/eggs`) - .then(({ data }) => resolve((data.data || []).map(rawDataToEgg))) - .catch(reject); +export interface Filters { + id?: string; + name?: string; +} + +interface ctx { + page: number; + setPage: (value: number | ((s: number) => number)) => void; + + filters: Filters | null; + setFilters: (filters: Filters | null) => void; + + sort: string | null; + setSort: (sort: string | null) => void; + + sortDirection: boolean; + setSortDirection: (direction: boolean) => void; +} + +export const Context = createContext({ + page: 1, + setPage: () => 1, + + filters: null, + setFilters: () => null, + + sort: null, + setSort: () => null, + + sortDirection: false, + setSortDirection: () => false, +}); + +export default (nestId: number, include: string[] = []) => { + const { page, filters, sort, sortDirection } = useContext(Context); + + const params = {}; + if (filters !== null) { + Object.keys(filters).forEach(key => { + // @ts-ignore + params['filter[' + key + ']'] = filters[key]; + }); + } + + if (sort !== null) { + // @ts-ignore + params.sort = (sortDirection ? '-' : '') + sort; + } + + return useSWR>([ nestId, 'eggs', page, filters, sort, sortDirection ], async () => { + const { data } = await http.get(`/api/application/nests/${nestId}/eggs`, { params: { include: include.join(','), page, ...params } }); + + return ({ + items: (data.data || []).map(rawDataToEgg), + pagination: getPaginationSet(data.meta.pagination), + }); }); }; diff --git a/resources/scripts/api/admin/nests/getNests.ts b/resources/scripts/api/admin/nests/getNests.ts index 095c5800b..78bfe964c 100644 --- a/resources/scripts/api/admin/nests/getNests.ts +++ b/resources/scripts/api/admin/nests/getNests.ts @@ -31,24 +31,57 @@ export const rawDataToNest = ({ attributes }: FractalResponseData): Nest => ({ }, }); +export interface Filters { + id?: string; + name?: string; +} + interface ctx { page: number; setPage: (value: number | ((s: number) => number)) => void; + + filters: Filters | null; + setFilters: (filters: Filters | null) => void; + + sort: string | null; + setSort: (sort: string | null) => void; + + sortDirection: boolean; + setSortDirection: (direction: boolean) => void; } -export const Context = createContext({ page: 1, setPage: () => 1 }); +export const Context = createContext({ + page: 1, + setPage: () => 1, + + filters: null, + setFilters: () => null, + + sort: null, + setSort: () => null, + + sortDirection: false, + setSortDirection: () => false, +}); export default (include: string[] = []) => { - const { page } = useContext(Context); + const { page, filters, sort, sortDirection } = useContext(Context); - return useSWR>([ 'nests', page ], async () => { - const { data } = await http.get('/api/application/nests', { - params: { - include: include.join(','), - per_page: 10, - page, - }, + const params = {}; + if (filters !== null) { + Object.keys(filters).forEach(key => { + // @ts-ignore + params['filter[' + key + ']'] = filters[key]; }); + } + + if (sort !== null) { + // @ts-ignore + params.sort = (sortDirection ? '-' : '') + sort; + } + + return useSWR>([ 'nests', page, filters, sort, sortDirection ], async () => { + const { data } = await http.get('/api/application/nests', { params: { include: include.join(','), page, ...params } }); return ({ items: (data.data || []).map(rawDataToNest), diff --git a/resources/scripts/api/admin/nodes/getAllocations.ts b/resources/scripts/api/admin/nodes/getAllocations.ts index 0cb561b7b..791af342c 100644 --- a/resources/scripts/api/admin/nodes/getAllocations.ts +++ b/resources/scripts/api/admin/nodes/getAllocations.ts @@ -1,5 +1,4 @@ -import http from '@/api/http'; -import { rawDataToServerAllocation } from '@/api/transformers'; +import http, { FractalResponseData } from '@/api/http'; export interface Allocation { id: number; @@ -7,17 +6,22 @@ export interface Allocation { alias: string | null; port: number; notes: string | null; - isDefault: boolean; + assigned: boolean; } -export default (uuid: string): Promise<[ Allocation, string[] ]> => { +export const rawDataToAllocation = (data: FractalResponseData): Allocation => ({ + id: data.attributes.id, + ip: data.attributes.ip, + alias: data.attributes.ip_alias, + port: data.attributes.port, + notes: data.attributes.notes, + assigned: data.attributes.assigned, +}); + +export default (uuid: string): Promise => { return new Promise((resolve, reject) => { - http.get(`/api/application/allocations/${uuid}`) - .then(({ data }) => resolve([ - rawDataToServerAllocation(data), - // eslint-disable-next-line camelcase - data.meta?.is_allocation_owner ? [ '*' ] : (data.meta?.user_permissions || []), - ])) + http.get(`/api/application/nodes/${uuid}/allocations`) + .then(({ data }) => resolve((data.data || []).map(rawDataToAllocation))) .catch(reject); }); }; diff --git a/resources/scripts/api/admin/nodes/getNodes.ts b/resources/scripts/api/admin/nodes/getNodes.ts index ddddbfead..0d146e9c7 100644 --- a/resources/scripts/api/admin/nodes/getNodes.ts +++ b/resources/scripts/api/admin/nodes/getNodes.ts @@ -68,6 +68,7 @@ export const rawDataToNode = ({ attributes }: FractalResponseData): Node => ({ }); export interface Filters { + id?: string; uuid?: string; name?: string; image?: string; diff --git a/resources/scripts/api/admin/roles/getRoles.ts b/resources/scripts/api/admin/roles/getRoles.ts index 1bc50ae1c..a8de9ff8e 100644 --- a/resources/scripts/api/admin/roles/getRoles.ts +++ b/resources/scripts/api/admin/roles/getRoles.ts @@ -1,4 +1,6 @@ -import http, { FractalResponseData } from '@/api/http'; +import http, { FractalResponseData, getPaginationSet, PaginatedResult } from '@/api/http'; +import { createContext, useContext } from 'react'; +import useSWR from 'swr'; export interface Role { id: number; @@ -12,10 +14,61 @@ export const rawDataToRole = ({ attributes }: FractalResponseData): Role => ({ description: attributes.description, }); -export default (include: string[] = []): Promise => { - return new Promise((resolve, reject) => { - http.get('/api/application/roles', { params: { include: include.join(',') } }) - .then(({ data }) => resolve((data.data || []).map(rawDataToRole))) - .catch(reject); +export interface Filters { + id?: string; + name?: string; +} + +interface ctx { + page: number; + setPage: (value: number | ((s: number) => number)) => void; + + filters: Filters | null; + setFilters: (filters: Filters | null) => void; + + sort: string | null; + setSort: (sort: string | null) => void; + + sortDirection: boolean; + setSortDirection: (direction: boolean) => void; +} + +export const Context = createContext({ + page: 1, + setPage: () => 1, + + filters: null, + setFilters: () => null, + + sort: null, + setSort: () => null, + + sortDirection: false, + setSortDirection: () => false, +}); + +export default (include: string[] = []) => { + const { page, filters, sort, sortDirection } = useContext(Context); + + const params = {}; + if (filters !== null) { + Object.keys(filters).forEach(key => { + // @ts-ignore + params['filter[' + key + ']'] = filters[key]; + }); + } + + if (sort !== null) { + // @ts-ignore + params.sort = (sortDirection ? '-' : '') + sort; + } + + return useSWR>([ 'roles', page, filters, sort, sortDirection ], async () => { + const { data } = await http.get('/api/application/roles', { params: { include: include.join(','), page, ...params } }); + + return ({ + items: (data.data || []).map(rawDataToRole), + pagination: getPaginationSet(data.meta.pagination), + }); }); }; diff --git a/resources/scripts/api/admin/servers/getServers.ts b/resources/scripts/api/admin/servers/getServers.ts index fccaef91c..1a220dd04 100644 --- a/resources/scripts/api/admin/servers/getServers.ts +++ b/resources/scripts/api/admin/servers/getServers.ts @@ -100,6 +100,7 @@ export const rawDataToServer = ({ attributes }: FractalResponseData): Server => }); export interface Filters { + id?: string; uuid?: string; name?: string; image?: string; diff --git a/resources/scripts/api/admin/users/getUsers.ts b/resources/scripts/api/admin/users/getUsers.ts index ee70fc647..12f1d81af 100644 --- a/resources/scripts/api/admin/users/getUsers.ts +++ b/resources/scripts/api/admin/users/getUsers.ts @@ -36,18 +36,61 @@ export const rawDataToUser = ({ attributes }: FractalResponseData): User => ({ updatedAt: new Date(attributes.updated_at), }); +export interface Filters { + id?: string; + uuid?: string; + username?: string; + email?: string; + firstName?: string; + lastName?: string; +} + interface ctx { page: number; setPage: (value: number | ((s: number) => number)) => void; + + filters: Filters | null; + setFilters: (filters: Filters | null) => void; + + sort: string | null; + setSort: (sort: string | null) => void; + + sortDirection: boolean; + setSortDirection: (direction: boolean) => void; } -export const Context = createContext({ page: 1, setPage: () => 1 }); +export const Context = createContext({ + page: 1, + setPage: () => 1, + + filters: null, + setFilters: () => null, + + sort: null, + setSort: () => null, + + sortDirection: false, + setSortDirection: () => false, +}); export default (include: string[] = []) => { - const { page } = useContext(Context); + const { page, filters, sort, sortDirection } = useContext(Context); - return useSWR>([ 'users', page ], async () => { - const { data } = await http.get('/api/application/users', { params: { include: include.join(','), page } }); + const params = {}; + if (filters !== null) { + Object.keys(filters).forEach(key => { + // @ts-ignore + params['filter[' + key + ']'] = filters[key]; + }); + } + + if (sort !== null) { + // @ts-ignore + params.sort = (sortDirection ? '-' : '') + sort; + } + + return useSWR>([ 'users', page, filters, sort, sortDirection ], async () => { + const { data } = await http.get('/api/application/users', { params: { include: include.join(','), page, ...params } }); return ({ items: (data.data || []).map(rawDataToUser), diff --git a/resources/scripts/components/admin/databases/DatabasesContainer.tsx b/resources/scripts/components/admin/databases/DatabasesContainer.tsx index ac2ebfeac..478128e53 100644 --- a/resources/scripts/components/admin/databases/DatabasesContainer.tsx +++ b/resources/scripts/components/admin/databases/DatabasesContainer.tsx @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useState } from 'react'; -import getDatabases, { Context as DatabasesContext } from '@/api/admin/databases/getDatabases'; +import getDatabases, { Context as DatabasesContext, Filters } from '@/api/admin/databases/getDatabases'; import FlashMessageRender from '@/components/FlashMessageRender'; import useFlash from '@/plugins/useFlash'; import { AdminContext } from '@/state/admin'; @@ -34,7 +34,7 @@ const RowCheckbox = ({ id }: { id: number}) => { const DatabasesContainer = () => { const match = useRouteMatch(); - const { page, setPage } = useContext(DatabasesContext); + const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(DatabasesContext); const { clearFlashes, clearAndAddHttpError } = useFlash(); const { data: databases, error, isValidating } = getDatabases(); @@ -56,6 +56,17 @@ const DatabasesContainer = () => { setSelectedDatabases(e.currentTarget.checked ? (databases?.items?.map(database => database.id) || []) : []); }; + const onSearch = (query: string): Promise => { + return new Promise((resolve) => { + if (query.length < 2) { + setFilters(null); + } else { + setFilters({ id: query, name: query, host: query }); + } + return resolve(); + }); + }; + useEffect(() => { setSelectedDatabases([]); }, [ page ]); @@ -89,15 +100,16 @@ const DatabasesContainer = () => {
- - + setSort('id')}/> + setSort('name')}/> - + setSort('username')}/> @@ -143,9 +155,21 @@ const DatabasesContainer = () => { export default () => { const [ page, setPage ] = useState(1); + const [ filters, setFilters ] = useState(null); + const [ sort, setSortState ] = useState(null); + const [ sortDirection, setSortDirection ] = useState(false); + + const setSort = (newSort: string | null) => { + if (sort === newSort) { + setSortDirection(!sortDirection); + } else { + setSortState(newSort); + setSortDirection(false); + } + }; return ( - + ); diff --git a/resources/scripts/components/admin/locations/LocationsContainer.tsx b/resources/scripts/components/admin/locations/LocationsContainer.tsx index 00b91c688..a14903912 100644 --- a/resources/scripts/components/admin/locations/LocationsContainer.tsx +++ b/resources/scripts/components/admin/locations/LocationsContainer.tsx @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useState } from 'react'; -import getLocations, { Context as LocationsContext } from '@/api/admin/locations/getLocations'; +import getLocations, { Context as LocationsContext, Filters } from '@/api/admin/locations/getLocations'; import FlashMessageRender from '@/components/FlashMessageRender'; import useFlash from '@/plugins/useFlash'; import { AdminContext } from '@/state/admin'; @@ -34,7 +34,7 @@ const RowCheckbox = ({ id }: { id: number}) => { const LocationsContainer = () => { const match = useRouteMatch(); - const { page, setPage } = useContext(LocationsContext); + const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(LocationsContext); const { clearFlashes, clearAndAddHttpError } = useFlash(); const { data: locations, error, isValidating } = getLocations(); @@ -56,6 +56,17 @@ const LocationsContainer = () => { setSelectedLocations(e.currentTarget.checked ? (locations?.items?.map(location => location.id) || []) : []); }; + const onSearch = (query: string): Promise => { + return new Promise((resolve) => { + if (query.length < 2) { + setFilters(null); + } else { + setFilters({ short: query, long: query }); + } + return resolve(); + }); + }; + useEffect(() => { setSelectedLocations([]); }, [ page ]); @@ -85,14 +96,15 @@ const LocationsContainer = () => {
- - - + setSort('id')}/> + setSort('short')}/> + setSort('long')}/> @@ -132,9 +144,21 @@ const LocationsContainer = () => { export default () => { const [ page, setPage ] = useState(1); + const [ filters, setFilters ] = useState(null); + const [ sort, setSortState ] = useState(null); + const [ sortDirection, setSortDirection ] = useState(false); + + const setSort = (newSort: string | null) => { + if (sort === newSort) { + setSortDirection(!sortDirection); + } else { + setSortState(newSort); + setSortDirection(false); + } + }; return ( - + ); diff --git a/resources/scripts/components/admin/mounts/MountsContainer.tsx b/resources/scripts/components/admin/mounts/MountsContainer.tsx index 87240a6e9..66d8c4043 100644 --- a/resources/scripts/components/admin/mounts/MountsContainer.tsx +++ b/resources/scripts/components/admin/mounts/MountsContainer.tsx @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useState } from 'react'; -import getMounts, { Context as MountsContext } from '@/api/admin/mounts/getMounts'; +import getMounts, { Context as MountsContext, Filters } from '@/api/admin/mounts/getMounts'; import FlashMessageRender from '@/components/FlashMessageRender'; import useFlash from '@/plugins/useFlash'; import { AdminContext } from '@/state/admin'; @@ -34,7 +34,7 @@ const RowCheckbox = ({ id }: { id: number}) => { const MountsContainer = () => { const match = useRouteMatch(); - const { page, setPage } = useContext(MountsContext); + const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(MountsContext); const { clearFlashes, clearAndAddHttpError } = useFlash(); const { data: mounts, error, isValidating } = getMounts(); @@ -56,6 +56,17 @@ const MountsContainer = () => { setSelectedMounts(e.currentTarget.checked ? (mounts?.items?.map(mount => mount.id) || []) : []); }; + const onSearch = (query: string): Promise => { + return new Promise((resolve) => { + if (query.length < 2) { + setFilters(null); + } else { + setFilters({ id: query }); + } + return resolve(); + }); + }; + useEffect(() => { setSelectedMounts([]); }, [ page ]); @@ -87,15 +98,16 @@ const MountsContainer = () => {
- - - - + setSort('id')}/> + setSort('name')}/> + setSort('source')}/> + setSort('target')}/> + + )) + } + +
@@ -171,9 +183,21 @@ const MountsContainer = () => { export default () => { const [ page, setPage ] = useState(1); + const [ filters, setFilters ] = useState(null); + const [ sort, setSortState ] = useState(null); + const [ sortDirection, setSortDirection ] = useState(false); + + const setSort = (newSort: string | null) => { + if (sort === newSort) { + setSortDirection(!sortDirection); + } else { + setSortState(newSort); + setSortDirection(false); + } + }; return ( - + ); diff --git a/resources/scripts/components/admin/nests/NestEditContainer.tsx b/resources/scripts/components/admin/nests/NestEditContainer.tsx index 41db82b6e..fb18de43d 100644 --- a/resources/scripts/components/admin/nests/NestEditContainer.tsx +++ b/resources/scripts/components/admin/nests/NestEditContainer.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router'; -import { NavLink, useRouteMatch } from 'react-router-dom'; +import { useRouteMatch } from 'react-router-dom'; import tw from 'twin.macro'; import AdminContentBlock from '@/components/admin/AdminContentBlock'; import Spinner from '@/components/elements/Spinner'; @@ -16,12 +16,11 @@ import { ApplicationStore } from '@/state'; import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy'; import { Form, Formik, FormikHelpers } from 'formik'; import AdminBox from '@/components/admin/AdminBox'; -import AdminCheckbox from '@/components/admin/AdminCheckbox'; -import AdminTable, { ContentWrapper, NoItems, TableBody, TableHead, TableHeader, TableRow } from '@/components/admin/AdminTable'; import CopyOnClick from '@/components/elements/CopyOnClick'; import Input from '@/components/elements/Input'; import Label from '@/components/elements/Label'; import NestDeleteButton from '@/components/admin/nests/NestDeleteButton'; +import NestEggTable from '@/components/admin/nests/NestEggTable'; interface ctx { nest: Nest | undefined; @@ -198,28 +197,8 @@ const ViewDetailsContainer = () => { ); }; -const RowCheckbox = ({ id }: { id: number }) => { - const isChecked = Context.useStoreState(state => state.selectedEggs.indexOf(id) >= 0); - const appendSelectedEggs = Context.useStoreActions(actions => actions.appendSelectedEggs); - const removeSelectedEggs = Context.useStoreActions(actions => actions.removeSelectedEggs); - - return ( - ) => { - if (e.currentTarget.checked) { - appendSelectedEggs(id); - } else { - removeSelectedEggs(id); - } - }} - /> - ); -}; - const NestEditContainer = () => { - const match = useRouteMatch<{ nestId?: string }>(); + const match = useRouteMatch<{ nestId: string }>(); const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); const [ loading, setLoading ] = useState(true); @@ -227,13 +206,10 @@ const NestEditContainer = () => { const nest = Context.useStoreState(state => state.nest); const setNest = Context.useStoreActions(actions => actions.setNest); - const setSelectedEggs = Context.useStoreActions(actions => actions.setSelectedEggs); - const selectedEggsLength = Context.useStoreState(state => state.selectedEggs.length); - useEffect(() => { clearFlashes('nest'); - getNest(Number(match.params?.nestId), [ 'eggs' ]) + getNest(Number(match.params.nestId), [ 'eggs' ]) .then(nest => setNest(nest)) .catch(error => { console.error(error); @@ -254,12 +230,6 @@ const NestEditContainer = () => { ); } - const length = nest.relations.eggs?.length || 0; - - const onSelectAllClick = (e: React.ChangeEvent) => { - setSelectedEggs(e.currentTarget.checked ? (nest.relations.eggs?.map(egg => egg.id) || []) : []); - }; - return (
@@ -289,52 +259,7 @@ const NestEditContainer = () => {
- - { length < 1 ? - - : - -
- - - - - - - - - { - nest.relations.eggs?.map(egg => ( - - - - - - - - - - )) - } - -
- - - - {egg.id} - - - - {egg.name} - - {egg.description}
-
-
- } -
+
); }; diff --git a/resources/scripts/components/admin/nests/NestEggTable.tsx b/resources/scripts/components/admin/nests/NestEggTable.tsx new file mode 100644 index 000000000..e9afdf400 --- /dev/null +++ b/resources/scripts/components/admin/nests/NestEggTable.tsx @@ -0,0 +1,147 @@ +import CopyOnClick from '@/components/elements/CopyOnClick'; +import React, { useContext, useEffect, useState } from 'react'; +import getEggs, { Context as EggsContext, Filters } from '@/api/admin/nests/getEggs'; +import useFlash from '@/plugins/useFlash'; +import { NavLink, useRouteMatch } from 'react-router-dom'; +import tw from 'twin.macro'; +import AdminCheckbox from '@/components/admin/AdminCheckbox'; +import AdminTable, { TableBody, TableHead, TableHeader, TableRow, Pagination, Loading, NoItems, ContentWrapper } from '@/components/admin/AdminTable'; +import { Context } from '@/components/admin/nests/NestEditContainer'; + +const RowCheckbox = ({ id }: { id: number}) => { + const isChecked = Context.useStoreState(state => state.selectedEggs.indexOf(id) >= 0); + const appendSelectedEggs = Context.useStoreActions(actions => actions.appendSelectedEggs); + const removeSelectedEggs = Context.useStoreActions(actions => actions.removeSelectedEggs); + + return ( + ) => { + if (e.currentTarget.checked) { + appendSelectedEggs(id); + } else { + removeSelectedEggs(id); + } + }} + /> + ); +}; + +const EggsTable = () => { + const match = useRouteMatch<{ nestId: string }>(); + + const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(EggsContext); + const { clearFlashes, clearAndAddHttpError } = useFlash(); + const { data: eggs, error, isValidating } = getEggs(Number(match.params.nestId)); + + useEffect(() => { + if (!error) { + clearFlashes('nests'); + return; + } + + clearAndAddHttpError({ key: 'nests', error }); + }, [ error ]); + + const length = eggs?.items?.length || 0; + + const setSelectedEggs = Context.useStoreActions(actions => actions.setSelectedEggs); + const selectedEggsLength = Context.useStoreState(state => state.selectedEggs.length); + + const onSelectAllClick = (e: React.ChangeEvent) => { + setSelectedEggs(e.currentTarget.checked ? (eggs?.items?.map(nest => nest.id) || []) : []); + }; + + const onSearch = (query: string): Promise => { + return new Promise((resolve) => { + if (query.length < 2) { + setFilters(null); + } else { + setFilters({ name: query }); + } + return resolve(); + }); + }; + + useEffect(() => { + setSelectedEggs([]); + }, [ page ]); + + return ( + + { eggs === undefined || (error && isValidating) ? + + : + length < 1 ? + + : + + +
+ + + setSort('id')}/> + setSort('name')}/> + + + + + { + eggs.items.map(egg => ( + + + + + + + + + + )) + } + +
+ + + + {egg.id} + + + + {egg.name} + + {egg.description}
+
+
+
+ } +
+ ); +}; + +export default () => { + const [ page, setPage ] = useState(1); + const [ filters, setFilters ] = useState(null); + const [ sort, setSortState ] = useState(null); + const [ sortDirection, setSortDirection ] = useState(false); + + const setSort = (newSort: string | null) => { + if (sort === newSort) { + setSortDirection(!sortDirection); + } else { + setSortState(newSort); + setSortDirection(false); + } + }; + + return ( + + + + ); +}; diff --git a/resources/scripts/components/admin/nests/NestsContainer.tsx b/resources/scripts/components/admin/nests/NestsContainer.tsx index 6696ced29..651016398 100644 --- a/resources/scripts/components/admin/nests/NestsContainer.tsx +++ b/resources/scripts/components/admin/nests/NestsContainer.tsx @@ -1,6 +1,6 @@ import CopyOnClick from '@/components/elements/CopyOnClick'; import React, { useContext, useEffect, useState } from 'react'; -import getNests, { Context as NestsContext } from '@/api/admin/nests/getNests'; +import getNests, { Context as NestsContext, Filters } from '@/api/admin/nests/getNests'; import NewNestButton from '@/components/admin/nests/NewNestButton'; import FlashMessageRender from '@/components/FlashMessageRender'; import useFlash from '@/plugins/useFlash'; @@ -34,7 +34,7 @@ const RowCheckbox = ({ id }: { id: number}) => { const NestsContainer = () => { const match = useRouteMatch(); - const { page, setPage } = useContext(NestsContext); + const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(NestsContext); const { clearFlashes, clearAndAddHttpError } = useFlash(); const { data: nests, error, isValidating } = getNests(); @@ -56,6 +56,17 @@ const NestsContainer = () => { setSelectedNests(e.currentTarget.checked ? (nests?.items?.map(nest => nest.id) || []) : []); }; + const onSearch = (query: string): Promise => { + return new Promise((resolve) => { + if (query.length < 2) { + setFilters(null); + } else { + setFilters({ id: query }); + } + return resolve(); + }); + }; + useEffect(() => { setSelectedNests([]); }, [ page ]); @@ -85,13 +96,14 @@ const NestsContainer = () => {
- - + setSort('id')}/> + setSort('name')}/> @@ -132,9 +144,21 @@ const NestsContainer = () => { export default () => { const [ page, setPage ] = useState(1); + const [ filters, setFilters ] = useState(null); + const [ sort, setSortState ] = useState(null); + const [ sortDirection, setSortDirection ] = useState(false); + + const setSort = (newSort: string | null) => { + if (sort === newSort) { + setSortDirection(!sortDirection); + } else { + setSortState(newSort); + setSortDirection(false); + } + }; return ( - + ); diff --git a/resources/scripts/components/admin/nodes/NodeAllocationContainer.tsx b/resources/scripts/components/admin/nodes/NodeAllocationContainer.tsx new file mode 100644 index 000000000..27edde883 --- /dev/null +++ b/resources/scripts/components/admin/nodes/NodeAllocationContainer.tsx @@ -0,0 +1,90 @@ +import Label from '@/components/elements/Label'; +import React, { useEffect, useState } from 'react'; +import AdminBox from '@/components/admin/AdminBox'; +import Creatable from 'react-select/creatable'; +import { ActionMeta, GroupTypeBase, InputActionMeta, ValueType } from 'react-select/src/types'; +import { SelectStyle } from '@/components/elements/Select2'; +import tw from 'twin.macro'; +import getAllocations from '@/api/admin/nodes/getAllocations'; +import { useRouteMatch } from 'react-router-dom'; + +interface Option { + value: string; + label: string; +} + +const distinct = (value: any, index: any, self: any) => { + return self.indexOf(value) === index; +}; + +export default () => { + const match = useRouteMatch<{ id: string }>(); + + const [ ips, setIPs ] = useState([]); + const [ ports, setPorts ] = useState([]); + + useEffect(() => { + getAllocations(match.params.id) + .then(allocations => { + setIPs(allocations.map(a => a.ip).filter(distinct).map(ip => { + return { value: ip, label: ip }; + })); + }); + }, []); + + const onChange = (value: ValueType, action: ActionMeta) => { + console.log({ + event: 'onChange', + value, + action, + }); + }; + + const onInputChange = (newValue: string, actionMeta: InputActionMeta) => { + console.log({ + event: 'onInputChange', + newValue, + actionMeta, + }); + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const isValidNewOption1 = (inputValue: string, selectValue: ValueType, selectOptions: ReadonlyArray
- - - - - + +
+
+ + setSort('id')}/> + setSort('name')}/> + + - - { - roles.map(role => ( - - + + { + roles.items.map(role => ( + + - + - + - - - )) - } - -
- - + + - - {role.id} - - + + {role.id} + + - - {role.name} - - + + {role.name} + + {role.description}
-
+
{role.description}
+
+
} ); }; + +export default () => { + const [ page, setPage ] = useState(1); + const [ filters, setFilters ] = useState(null); + const [ sort, setSortState ] = useState(null); + const [ sortDirection, setSortDirection ] = useState(false); + + const setSort = (newSort: string | null) => { + if (sort === newSort) { + setSortDirection(!sortDirection); + } else { + setSortState(newSort); + setSortDirection(false); + } + }; + + return ( + + + + ); +}; diff --git a/resources/scripts/components/admin/users/UsersContainer.tsx b/resources/scripts/components/admin/users/UsersContainer.tsx index b0502385c..66e9dfea9 100644 --- a/resources/scripts/components/admin/users/UsersContainer.tsx +++ b/resources/scripts/components/admin/users/UsersContainer.tsx @@ -1,7 +1,7 @@ import React, { useContext, useEffect, useState } from 'react'; import AdminCheckbox from '@/components/admin/AdminCheckbox'; import CopyOnClick from '@/components/elements/CopyOnClick'; -import getUsers, { Context as UsersContext } from '@/api/admin/users/getUsers'; +import getUsers, { Context as UsersContext, Filters } from '@/api/admin/users/getUsers'; import AdminTable, { ContentWrapper, Loading, NoItems, Pagination, TableBody, TableHead, TableHeader } from '@/components/admin/AdminTable'; import Button from '@/components/elements/Button'; import FlashMessageRender from '@/components/FlashMessageRender'; @@ -34,7 +34,7 @@ const RowCheckbox = ({ id }: { id: number }) => { const UsersContainer = () => { const match = useRouteMatch(); - const { page, setPage } = useContext(UsersContext); + const { page, setPage, setFilters, sort, setSort, sortDirection } = useContext(UsersContext); const { clearFlashes, clearAndAddHttpError } = useFlash(); const { data: users, error, isValidating } = getUsers(); @@ -56,6 +56,17 @@ const UsersContainer = () => { setSelectedUsers(e.currentTarget.checked ? (users?.items?.map(user => user.id) || []) : []); }; + const onSearch = (query: string): Promise => { + return new Promise((resolve) => { + if (query.length < 2) { + setFilters(null); + } else { + setFilters({ username: query }); + } + return resolve(); + }); + }; + useEffect(() => { setSelectedUsers([]); }, [ page ]); @@ -89,16 +100,17 @@ const UsersContainer = () => {
- - - + setSort('id')}/> + setSort('email')}/> + setSort('username')}/> - + setSort('admin_role_id')}/> @@ -160,9 +172,21 @@ const UsersContainer = () => { export default () => { const [ page, setPage ] = useState(1); + const [ filters, setFilters ] = useState(null); + const [ sort, setSortState ] = useState(null); + const [ sortDirection, setSortDirection ] = useState(false); + + const setSort = (newSort: string | null) => { + if (sort === newSort) { + setSortDirection(!sortDirection); + } else { + setSortState(newSort); + setSortDirection(false); + } + }; return ( - + ); diff --git a/resources/scripts/components/elements/Select2.ts b/resources/scripts/components/elements/Select2.ts new file mode 100644 index 000000000..7c73b3e53 --- /dev/null +++ b/resources/scripts/components/elements/Select2.ts @@ -0,0 +1,101 @@ +import { CSSObject } from '@emotion/serialize'; +import { ContainerProps, ControlProps, InputProps, MenuProps, MultiValueProps, OptionProps, PlaceholderProps, SingleValueProps, StylesConfig, ValueContainerProps } from 'react-select'; +import { theme } from 'twin.macro'; + +type T = any; + +export const SelectStyle: StylesConfig = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + container: (base: CSSObject, props: ContainerProps): CSSObject => { + return { + ...base, + }; + }, + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + control: (base: CSSObject, props: ControlProps): CSSObject => { + return { + ...base, + height: '2.75rem', + /* paddingTop: '0.75rem', + paddingBottom: '0.75rem', + paddingLeft: '4rem', + paddingRight: '4rem', */ + background: theme`colors.neutral.600`, + borderColor: theme`colors.neutral.500`, + borderWidth: '2px', + color: theme`colors.neutral.200`, + }; + }, + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + input: (base: CSSObject, props: InputProps): CSSObject => { + return { + ...base, + color: theme`colors.neutral.200`, + fontSize: '0.875rem', + }; + }, + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + menu: (base: CSSObject, props: MenuProps): CSSObject => { + return { + ...base, + background: theme`colors.neutral.900`, + color: theme`colors.neutral.200`, + }; + }, + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + multiValue: (base: CSSObject, props: MultiValueProps): CSSObject => { + return { + ...base, + background: theme`colors.neutral.900`, + color: theme`colors.neutral.200`, + }; + }, + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + multiValueLabel: (base: CSSObject, props: MultiValueProps): CSSObject => { + return { + ...base, + color: theme`colors.neutral.200`, + }; + }, + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + option: (base: CSSObject, props: OptionProps): CSSObject => { + return { + ...base, + background: theme`colors.neutral.900`, + ':hover': { + background: theme`colors.neutral.700`, + cursor: 'pointer', + }, + }; + }, + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + placeholder: (base: CSSObject, props: PlaceholderProps): CSSObject => { + return { + ...base, + color: theme`colors.neutral.300`, + fontSize: '0.875rem', + }; + }, + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + singleValue: (base: CSSObject, props: SingleValueProps): CSSObject => { + return { + ...base, + color: '#00000', + }; + }, + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + valueContainer: (base: CSSObject, props: ValueContainerProps): CSSObject => { + return { + ...base, + }; + }, +};