Paginate servers on frontend; closes #2106

This commit is contained in:
Dane Everitt 2020-07-14 20:48:41 -07:00
parent 03abc1764d
commit 6c0d308348
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
5 changed files with 122 additions and 20 deletions

View file

@ -1,13 +1,19 @@
import { rawDataToServerObject, Server } from '@/api/server/getServer'; import { rawDataToServerObject, Server } from '@/api/server/getServer';
import http, { getPaginationSet, PaginatedResult } from '@/api/http'; import http, { getPaginationSet, PaginatedResult } from '@/api/http';
export default (query?: string, includeAdmin?: boolean): Promise<PaginatedResult<Server>> => { interface QueryParams {
query?: string;
page?: number;
includeAdmin?: boolean;
}
export default ({ query, page = 1, includeAdmin = false }: QueryParams): Promise<PaginatedResult<Server>> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.get('/api/client', { http.get('/api/client', {
params: { params: {
include: [ 'allocation' ],
type: includeAdmin ? 'all' : undefined, type: includeAdmin ? 'all' : undefined,
'filter[name]': query, 'filter[name]': query,
page,
}, },
}) })
.then(({ data }) => resolve({ .then(({ data }) => resolve({

View file

@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { Server } from '@/api/server/getServer'; import { Server } from '@/api/server/getServer';
import getServers from '@/api/getServers'; import getServers from '@/api/getServers';
import ServerRow from '@/components/dashboard/ServerRow'; import ServerRow from '@/components/dashboard/ServerRow';
@ -11,15 +11,17 @@ import Switch from '@/components/elements/Switch';
import tw from 'twin.macro'; import tw from 'twin.macro';
import useSWR from 'swr'; import useSWR from 'swr';
import { PaginatedResult } from '@/api/http'; import { PaginatedResult } from '@/api/http';
import Pagination from '@/components/elements/Pagination';
export default () => { export default () => {
const { clearFlashes, clearAndAddHttpError } = useFlash(); const { clearFlashes, clearAndAddHttpError } = useFlash();
const [ page, setPage ] = useState(1);
const { rootAdmin } = useStoreState(state => state.user.data!); const { rootAdmin } = useStoreState(state => state.user.data!);
const [ showAdmin, setShowAdmin ] = usePersistedState('show_all_servers', false); const [ includeAdmin, setIncludeAdmin ] = usePersistedState('show_all_servers', false);
const { data: servers, error } = useSWR<PaginatedResult<Server>>( const { data: servers, error } = useSWR<PaginatedResult<Server>>(
[ '/api/client/servers', showAdmin ], [ '/api/client/servers', includeAdmin, page ],
() => getServers(undefined, showAdmin) () => getServers({ includeAdmin, page }),
); );
useEffect(() => { useEffect(() => {
@ -32,26 +34,34 @@ export default () => {
{rootAdmin && {rootAdmin &&
<div css={tw`mb-2 flex justify-end items-center`}> <div css={tw`mb-2 flex justify-end items-center`}>
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}> <p css={tw`uppercase text-xs text-neutral-400 mr-2`}>
{showAdmin ? 'Showing all servers' : 'Showing your servers'} {includeAdmin ? 'Showing all servers' : 'Showing your servers'}
</p> </p>
<Switch <Switch
name={'show_all_servers'} name={'show_all_servers'}
defaultChecked={showAdmin} defaultChecked={includeAdmin}
onChange={() => setShowAdmin(s => !s)} onChange={() => setIncludeAdmin(s => !s)}
/> />
</div> </div>
} }
{!servers ? {!servers ?
<Spinner centered size={'large'}/> <Spinner centered size={'large'}/>
: :
servers.items.length > 0 ? <Pagination data={servers} onPageSelect={setPage}>
servers.items.map((server, index) => ( {({ items }) => (
<ServerRow key={server.uuid} server={server} css={index > 0 ? tw`mt-2` : undefined}/> items.length > 0 ?
)) items.map((server, index) => (
: <ServerRow
<p css={tw`text-center text-sm text-neutral-400`}> key={server.uuid}
There are no servers associated with your account. server={server}
</p> css={index > 0 ? tw`mt-2` : undefined}
/>
))
:
<p css={tw`text-center text-sm text-neutral-400`}>
There are no servers associated with your account.
</p>
)}
</Pagination>
} }
</PageContentBlock> </PageContentBlock>
); );

View file

@ -1,9 +1,8 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faServer, faEthernet, faMicrochip, faMemory, faHdd } from '@fortawesome/free-solid-svg-icons'; import { faEthernet, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Server } from '@/api/server/getServer'; import { Server } from '@/api/server/getServer';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import getServerResourceUsage, { ServerStats } from '@/api/server/getServerResourceUsage'; import getServerResourceUsage, { ServerStats } from '@/api/server/getServerResourceUsage';
import { bytesToHuman, megabytesToHuman } from '@/helpers'; import { bytesToHuman, megabytesToHuman } from '@/helpers';
import tw from 'twin.macro'; import tw from 'twin.macro';

View file

@ -57,7 +57,7 @@ export default ({ ...props }: Props) => {
setSubmitting(false); setSubmitting(false);
clearFlashes('search'); clearFlashes('search');
getServers(term) getServers({ query: term })
.then(servers => setServers(servers.items.filter((_, index) => index < 5))) .then(servers => setServers(servers.items.filter((_, index) => index < 5)))
.catch(error => { .catch(error => {
console.error(error); console.error(error);

View file

@ -0,0 +1,87 @@
import React from 'react';
import { PaginatedResult } from '@/api/http';
import tw from 'twin.macro';
import styled from 'styled-components/macro';
import Button from '@/components/elements/Button';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faAngleDoubleLeft, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons';
interface RenderFuncProps<T> {
items: T[];
isLastPage: boolean;
isFirstPage: boolean;
}
interface Props<T> {
data: PaginatedResult<T>;
showGoToLast?: boolean;
showGoToFirst?: boolean;
onPageSelect: (page: number) => void;
children: (props: RenderFuncProps<T>) => React.ReactNode;
}
const Block = styled(Button)`
${tw`p-0 w-10 h-10`}
&:not(:last-of-type) {
${tw`mr-2`};
}
`;
function Pagination<T> ({ data: { items, pagination }, onPageSelect, children }: Props<T>) {
const isFirstPage = pagination.currentPage === 1;
const isLastPage = pagination.currentPage >= pagination.totalPages;
const pages = [];
// Start two spaces before the current page. If that puts us before the starting page default
// to the first page as the starting point.
const start = Math.max(pagination.currentPage - 2, 1);
const end = Math.min(pagination.totalPages, pagination.currentPage + 5);
for (let i = start; i <= end; i++) {
pages.push(i);
}
return (
<>
{children({ items, isFirstPage, isLastPage })}
{(pages.length > 1) &&
<div css={tw`mt-4 flex justify-center`}>
{(pages[0] > 1 && !isFirstPage) &&
<Block
isSecondary
color={'primary'}
onClick={() => onPageSelect(1)}
>
<FontAwesomeIcon icon={faAngleDoubleLeft}/>
</Block>
}
{
pages.map(i => (
<Block
isSecondary={pagination.currentPage !== i}
color={'primary'}
key={`block_page_${i}`}
onClick={() => onPageSelect(i)}
>
{i}
</Block>
))
}
{(pages[4] < pagination.totalPages && !isLastPage) &&
<Block
isSecondary
color={'primary'}
onClick={() => onPageSelect(pagination.totalPages)}
>
<FontAwesomeIcon icon={faAngleDoubleRight}/>
</Block>
}
</div>
}
</>
);
}
export default Pagination;