Skip to content

Commit 6c0d308

Browse files
committed
Paginate servers on frontend; closes pterodactyl#2106
1 parent 03abc17 commit 6c0d308

File tree

5 files changed

+122
-20
lines changed

5 files changed

+122
-20
lines changed

resources/scripts/api/getServers.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import { rawDataToServerObject, Server } from '@/api/server/getServer';
22
import http, { getPaginationSet, PaginatedResult } from '@/api/http';
33

4-
export default (query?: string, includeAdmin?: boolean): Promise<PaginatedResult<Server>> => {
4+
interface QueryParams {
5+
query?: string;
6+
page?: number;
7+
includeAdmin?: boolean;
8+
}
9+
10+
export default ({ query, page = 1, includeAdmin = false }: QueryParams): Promise<PaginatedResult<Server>> => {
511
return new Promise((resolve, reject) => {
612
http.get('/api/client', {
713
params: {
8-
include: [ 'allocation' ],
914
type: includeAdmin ? 'all' : undefined,
1015
'filter[name]': query,
16+
page,
1117
},
1218
})
1319
.then(({ data }) => resolve({

resources/scripts/components/dashboard/DashboardContainer.tsx

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import { Server } from '@/api/server/getServer';
33
import getServers from '@/api/getServers';
44
import ServerRow from '@/components/dashboard/ServerRow';
@@ -11,15 +11,17 @@ import Switch from '@/components/elements/Switch';
1111
import tw from 'twin.macro';
1212
import useSWR from 'swr';
1313
import { PaginatedResult } from '@/api/http';
14+
import Pagination from '@/components/elements/Pagination';
1415

1516
export default () => {
1617
const { clearFlashes, clearAndAddHttpError } = useFlash();
18+
const [ page, setPage ] = useState(1);
1719
const { rootAdmin } = useStoreState(state => state.user.data!);
18-
const [ showAdmin, setShowAdmin ] = usePersistedState('show_all_servers', false);
20+
const [ includeAdmin, setIncludeAdmin ] = usePersistedState('show_all_servers', false);
1921

2022
const { data: servers, error } = useSWR<PaginatedResult<Server>>(
21-
[ '/api/client/servers', showAdmin ],
22-
() => getServers(undefined, showAdmin)
23+
[ '/api/client/servers', includeAdmin, page ],
24+
() => getServers({ includeAdmin, page }),
2325
);
2426

2527
useEffect(() => {
@@ -32,26 +34,34 @@ export default () => {
3234
{rootAdmin &&
3335
<div css={tw`mb-2 flex justify-end items-center`}>
3436
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}>
35-
{showAdmin ? 'Showing all servers' : 'Showing your servers'}
37+
{includeAdmin ? 'Showing all servers' : 'Showing your servers'}
3638
</p>
3739
<Switch
3840
name={'show_all_servers'}
39-
defaultChecked={showAdmin}
40-
onChange={() => setShowAdmin(s => !s)}
41+
defaultChecked={includeAdmin}
42+
onChange={() => setIncludeAdmin(s => !s)}
4143
/>
4244
</div>
4345
}
4446
{!servers ?
4547
<Spinner centered size={'large'}/>
4648
:
47-
servers.items.length > 0 ?
48-
servers.items.map((server, index) => (
49-
<ServerRow key={server.uuid} server={server} css={index > 0 ? tw`mt-2` : undefined}/>
50-
))
51-
:
52-
<p css={tw`text-center text-sm text-neutral-400`}>
53-
There are no servers associated with your account.
54-
</p>
49+
<Pagination data={servers} onPageSelect={setPage}>
50+
{({ items }) => (
51+
items.length > 0 ?
52+
items.map((server, index) => (
53+
<ServerRow
54+
key={server.uuid}
55+
server={server}
56+
css={index > 0 ? tw`mt-2` : undefined}
57+
/>
58+
))
59+
:
60+
<p css={tw`text-center text-sm text-neutral-400`}>
61+
There are no servers associated with your account.
62+
</p>
63+
)}
64+
</Pagination>
5565
}
5666
</PageContentBlock>
5767
);

resources/scripts/components/dashboard/ServerRow.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import React, { useEffect, useRef, useState } from 'react';
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3-
import { faServer, faEthernet, faMicrochip, faMemory, faHdd } from '@fortawesome/free-solid-svg-icons';
3+
import { faEthernet, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';
44
import { Link } from 'react-router-dom';
55
import { Server } from '@/api/server/getServer';
6-
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
76
import getServerResourceUsage, { ServerStats } from '@/api/server/getServerResourceUsage';
87
import { bytesToHuman, megabytesToHuman } from '@/helpers';
98
import tw from 'twin.macro';

resources/scripts/components/dashboard/search/SearchModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export default ({ ...props }: Props) => {
5757
setSubmitting(false);
5858
clearFlashes('search');
5959

60-
getServers(term)
60+
getServers({ query: term })
6161
.then(servers => setServers(servers.items.filter((_, index) => index < 5)))
6262
.catch(error => {
6363
console.error(error);
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import React from 'react';
2+
import { PaginatedResult } from '@/api/http';
3+
import tw from 'twin.macro';
4+
import styled from 'styled-components/macro';
5+
import Button from '@/components/elements/Button';
6+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
7+
import { faAngleDoubleLeft, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons';
8+
9+
interface RenderFuncProps<T> {
10+
items: T[];
11+
isLastPage: boolean;
12+
isFirstPage: boolean;
13+
}
14+
15+
interface Props<T> {
16+
data: PaginatedResult<T>;
17+
showGoToLast?: boolean;
18+
showGoToFirst?: boolean;
19+
onPageSelect: (page: number) => void;
20+
children: (props: RenderFuncProps<T>) => React.ReactNode;
21+
}
22+
23+
const Block = styled(Button)`
24+
${tw`p-0 w-10 h-10`}
25+
26+
&:not(:last-of-type) {
27+
${tw`mr-2`};
28+
}
29+
`;
30+
31+
function Pagination<T> ({ data: { items, pagination }, onPageSelect, children }: Props<T>) {
32+
const isFirstPage = pagination.currentPage === 1;
33+
const isLastPage = pagination.currentPage >= pagination.totalPages;
34+
35+
const pages = [];
36+
37+
// Start two spaces before the current page. If that puts us before the starting page default
38+
// to the first page as the starting point.
39+
const start = Math.max(pagination.currentPage - 2, 1);
40+
const end = Math.min(pagination.totalPages, pagination.currentPage + 5);
41+
42+
for (let i = start; i <= end; i++) {
43+
pages.push(i);
44+
}
45+
46+
return (
47+
<>
48+
{children({ items, isFirstPage, isLastPage })}
49+
{(pages.length > 1) &&
50+
<div css={tw`mt-4 flex justify-center`}>
51+
{(pages[0] > 1 && !isFirstPage) &&
52+
<Block
53+
isSecondary
54+
color={'primary'}
55+
onClick={() => onPageSelect(1)}
56+
>
57+
<FontAwesomeIcon icon={faAngleDoubleLeft}/>
58+
</Block>
59+
}
60+
{
61+
pages.map(i => (
62+
<Block
63+
isSecondary={pagination.currentPage !== i}
64+
color={'primary'}
65+
key={`block_page_${i}`}
66+
onClick={() => onPageSelect(i)}
67+
>
68+
{i}
69+
</Block>
70+
))
71+
}
72+
{(pages[4] < pagination.totalPages && !isLastPage) &&
73+
<Block
74+
isSecondary
75+
color={'primary'}
76+
onClick={() => onPageSelect(pagination.totalPages)}
77+
>
78+
<FontAwesomeIcon icon={faAngleDoubleRight}/>
79+
</Block>
80+
}
81+
</div>
82+
}
83+
</>
84+
);
85+
}
86+
87+
export default Pagination;

0 commit comments

Comments
 (0)