diff --git a/app/Http/Controllers/Api/Application/Nests/NestController.php b/app/Http/Controllers/Api/Application/Nests/NestController.php index 42c0511a..0979c453 100644 --- a/app/Http/Controllers/Api/Application/Nests/NestController.php +++ b/app/Http/Controllers/Api/Application/Nests/NestController.php @@ -37,7 +37,7 @@ class NestController extends ApplicationApiController */ public function index(GetNestsRequest $request): array { - $nests = $this->repository->paginated(50); + $nests = $this->repository->paginated(2); return $this->fractal->collection($nests) ->transformWith($this->getTransformer(NestTransformer::class)) diff --git a/resources/scripts/api/swr/getNests.ts b/resources/scripts/api/swr/getNests.ts new file mode 100644 index 00000000..ece682d6 --- /dev/null +++ b/resources/scripts/api/swr/getNests.ts @@ -0,0 +1,25 @@ +import { Nest } from '@/api/admin/nests/getNests'; +import useSWR from 'swr'; +import http, { getPaginationSet, PaginatedResult } from '@/api/http'; +import { rawDataToNest } from '@/api/transformers'; +import { createContext, useContext } from 'react'; + +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>([ 'nests', page ], async () => { + const { data } = await http.get(`/api/application/nests`, { params: { page } }); + + return ({ + items: (data.data || []).map(rawDataToNest), + pagination: getPaginationSet(data.meta.pagination), + }); + }); +}; diff --git a/resources/scripts/components/admin/AdminTable.tsx b/resources/scripts/components/admin/AdminTable.tsx index c5fd5564..aeb7fa69 100644 --- a/resources/scripts/components/admin/AdminTable.tsx +++ b/resources/scripts/components/admin/AdminTable.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { TableCheckbox } from '@/components/admin/AdminCheckbox'; import Spinner from '@/components/elements/Spinner'; +import styled from 'styled-components/macro'; import tw from 'twin.macro'; +import { PaginatedResult } from '@/api/http'; export const TableHead = ({ children }: { children: React.ReactNode }) => { return ( @@ -47,116 +49,175 @@ export const TableRow = ({ children }: { children: React.ReactNode }) => { ); }; -interface Params { - loading: boolean; - hasItems: boolean; - checked: boolean; - onSelectAllClick(e: React.ChangeEvent): void; + +interface Props { + data: PaginatedResult; + onPageSelect: (page: number) => void; children: React.ReactNode; } -export default ({ loading, hasItems, checked, onSelectAllClick, children }: Params) => { +const PaginationButton = styled.button<{ active?: boolean }>` + ${tw`relative items-center px-3 py-1 -ml-px text-sm font-normal leading-5 transition duration-150 ease-in-out border border-neutral-500 focus:z-10 focus:outline-none focus:border-primary-300 inline-flex`}; + + ${props => props.active ? tw`bg-neutral-500 text-neutral-50` : tw`bg-neutral-600 text-neutral-200 hover:text-neutral-300`}; +`; + +const PaginationArrow = styled.button` + ${tw`relative inline-flex items-center px-1 py-1 text-sm font-medium leading-5 transition duration-150 ease-in-out border border-neutral-500 bg-neutral-600 text-neutral-400 hover:text-neutral-200 focus:z-10 focus:outline-none focus:border-primary-300 active:bg-neutral-100 active:text-neutral-500`}; +`; + +export function Pagination ({ data: { pagination }, onPageSelect, children }: Props) { + const isFirstPage = pagination.currentPage === 1; + const isLastPage = pagination.currentPage >= pagination.totalPages; + + /* const pages = []; + + const start = Math.max(pagination.currentPage - 2, 1); + const end = Math.min(pagination.totalPages, pagination.currentPage + 5); + + for (let i = start; i <= start + 3; i++) { + pages.push(i); + } + + for (let i = end; i >= end - 3; i--) { + pages.push(i); + } */ + + const setPage = (page: number) => { + if (page < 1 || page > pagination.totalPages) { + return; + } + + onPageSelect(page); + }; + + return ( + <> + {children} + +
+

+ Showing {((pagination.currentPage - 1) * pagination.perPage) + 1} to {((pagination.currentPage - 1) * pagination.perPage) + pagination.count} of {pagination.total} results +

+ + { isFirstPage && isLastPage ? + null + : +
+ +
+ } +
+ + ); +} + +export const Loading = () => { + return ( +
+ +
+ ); +}; + +export const NoItems = () => { + return ( +
+
+ {'No +
+ +

No items could be found, it's almost like they are hiding.

+
+ ); +}; + +interface Params { + checked: boolean; + onSelectAllClick: (e: React.ChangeEvent) => void; + + children: React.ReactNode; +} + +export const ContentWrapper = ({ checked, onSelectAllClick, children }: Params) => { + return ( + <> +
+
+ + + + + +
+ +
+ + + + + + + +
+
+ + {children} + + ); +}; + +export default ({ children }: { children: React.ReactNode }) => { return (
- { loading ? -
- -
- : - !hasItems ? -
-
- {'No -
- -

No items could be found, it's almost like they are hiding.

-
- : - <> -
-
- - - - - -
- -
- - - - - - - -
-
- -
- - {children} -
-
- -
-

- Showing 1 to 10 of 97 results -

- -
- -
-
- - } + {children}
); diff --git a/resources/scripts/components/admin/nests/NestsContainer.tsx b/resources/scripts/components/admin/nests/NestsContainer.tsx index ffd7d01d..29b920db 100644 --- a/resources/scripts/components/admin/nests/NestsContainer.tsx +++ b/resources/scripts/components/admin/nests/NestsContainer.tsx @@ -1,16 +1,14 @@ -import React, { useEffect, useState } from 'react'; -import getNests from '@/api/admin/nests/getNests'; -import { httpErrorToHuman } from '@/api/http'; +import React, { useContext, useEffect, useState } from 'react'; +import getNests, { Context as NestsContext } from '@/api/swr/getNests'; import NewNestButton from '@/components/admin/nests/NewNestButton'; import FlashMessageRender from '@/components/FlashMessageRender'; -import { useDeepMemoize } from '@/plugins/useDeepMemoize'; 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'; import AdminCheckbox from '@/components/admin/AdminCheckbox'; -import AdminTable, { TableBody, TableHead, TableHeader, TableRow } from '@/components/admin/AdminTable'; +import AdminTable, { TableBody, TableHead, TableHeader, TableRow, Pagination, Loading, NoItems, ContentWrapper } from '@/components/admin/AdminTable'; const RowCheckbox = ({ id }: { id: number}) => { const isChecked = AdminContext.useStoreState(state => state.nests.selectedNests.indexOf(id) >= 0); @@ -32,35 +30,35 @@ const RowCheckbox = ({ id }: { id: number}) => { ); }; -export default () => { +const NestsContainer = () => { const match = useRouteMatch(); - const { addError, clearFlashes } = useFlash(); - const [ loading, setLoading ] = useState(true); + const { page, setPage } = useContext(NestsContext); + const { clearFlashes, clearAndAddHttpError } = useFlash(); + const { data: nests, error, isValidating } = getNests(); - const nests = useDeepMemoize(AdminContext.useStoreState(state => state.nests.data)); - const setNests = AdminContext.useStoreActions(state => state.nests.setNests); + useEffect(() => { + if (!error) { + clearFlashes('backups'); + return; + } + + clearAndAddHttpError({ error, key: 'backups' }); + }, [ error ]); + + const length = nests?.items?.length || 0; const setSelectedNests = AdminContext.useStoreActions(actions => actions.nests.setSelectedNests); const selectedNestsLength = AdminContext.useStoreState(state => state.nests.selectedNests.length); - useEffect(() => { - setLoading(!nests.length); - clearFlashes('nests'); - - getNests() - .then(nests => setNests(nests)) - .catch(error => { - console.error(error); - addError({ message: httpErrorToHuman(error), key: 'nests' }); - }) - .then(() => setLoading(false)); - }, []); - const onSelectAllClick = (e: React.ChangeEvent) => { - setSelectedNests(e.currentTarget.checked ? (nests.map(nest => nest.id) || []) : []); + setSelectedNests(e.currentTarget.checked ? (nests?.items?.map(nest => nest.id) || []) : []); }; + useEffect(() => { + setSelectedNests([]); + }, [ page ]); + return (
@@ -74,38 +72,61 @@ export default () => { - 0} - checked={selectedNestsLength === (nests.length === 0 ? -1 : nests.length)} - onSelectAllClick={onSelectAllClick} - > - - - - - + + { nests === undefined || (error && isValidating) ? + + : + length < 1 ? + + : + + +
+ + + + + + - - { - nests.map(nest => ( - - + + { + nests.items.map(nest => ( + + - - - - - )) - } - + + + + + )) + } + +
- - + + {nest.id} - - {nest.name} - - {nest.description}{nest.id} + + {nest.name} + + {nest.description}
+
+
+
+ }
); }; + +export default () => { + const [ page, setPage ] = useState(1); + + return ( + + + + ); +}; diff --git a/resources/scripts/components/admin/roles/RolesContainer.tsx b/resources/scripts/components/admin/roles/RolesContainer.tsx index 27d809c7..c0c99260 100644 --- a/resources/scripts/components/admin/roles/RolesContainer.tsx +++ b/resources/scripts/components/admin/roles/RolesContainer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +/* import React, { useEffect, useState } from 'react'; import { useDeepMemoize } from '@/plugins/useDeepMemoize'; import { AdminContext } from '@/state/admin'; import { httpErrorToHuman } from '@/api/http'; @@ -108,4 +108,13 @@ export default () => {
); +}; */ + +import React from 'react'; + +export default () => { + return ( + <> + + ); }; diff --git a/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php b/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php index 2c150f16..8e81a995 100644 --- a/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php +++ b/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php @@ -64,7 +64,8 @@ abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase * Creates a new default API key and refreshes the headers using it. * * @param \Pterodactyl\Models\User $user - * @param array $permissions + * @param array $permissions + * * @return \Pterodactyl\Models\ApiKey */ protected function createNewDefaultApiKey(User $user, array $permissions = []): ApiKey @@ -101,7 +102,8 @@ abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase * Create a new application API key for a given user model. * * @param \Pterodactyl\Models\User $user - * @param array $permissions + * @param array $permissions + * * @return \Pterodactyl\Models\ApiKey */ protected function createApiKey(User $user, array $permissions = []): ApiKey @@ -125,7 +127,9 @@ abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase * Return a transformer that can be used for testing purposes. * * @param string $abstract + * * @return \Pterodactyl\Transformers\Api\Application\BaseTransformer + * @throws \Illuminate\Contracts\Container\BindingResolutionException */ protected function getTransformer(string $abstract): BaseTransformer {