feat: Added pagination
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
/*
|
||||
* Client-side pagination.
|
||||
*
|
||||
* The fleet endpoints answer with the whole result set, and a few thousand rows
|
||||
* rendered at once is what locks the tab up. Slicing in the browser is enough:
|
||||
* the payload was never the problem, the DOM node count was. If a result set
|
||||
* ever outgrows the response itself, this is the seam a server-side cursor
|
||||
* would replace.
|
||||
*/
|
||||
|
||||
export const PAGE_SIZES = [25, 50, 100, 200];
|
||||
|
||||
export function usePagination<T>(items: T[], initialSize = 50) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [size, setSize] = useState(initialSize);
|
||||
|
||||
const pageCount = Math.max(1, Math.ceil(items.length / size));
|
||||
|
||||
// A filter change shortens the list under a page that no longer exists;
|
||||
// clamping here rather than in every caller keeps the empty state honest.
|
||||
useEffect(() => {
|
||||
if (page > pageCount) setPage(1);
|
||||
}, [page, pageCount]);
|
||||
|
||||
const slice = useMemo(() => {
|
||||
const start = (page - 1) * size;
|
||||
return items.slice(start, start + size);
|
||||
}, [items, page, size]);
|
||||
|
||||
return {
|
||||
slice,
|
||||
page,
|
||||
size,
|
||||
pageCount,
|
||||
total: items.length,
|
||||
setPage,
|
||||
setSize: (n: number) => {
|
||||
setSize(n);
|
||||
setPage(1);
|
||||
},
|
||||
reset: () => setPage(1),
|
||||
};
|
||||
}
|
||||
|
||||
export function Pagination({
|
||||
page,
|
||||
pageCount,
|
||||
size,
|
||||
total,
|
||||
onPage,
|
||||
onSize,
|
||||
unit = "rows",
|
||||
}: {
|
||||
page: number;
|
||||
pageCount: number;
|
||||
size: number;
|
||||
total: number;
|
||||
onPage: (n: number) => void;
|
||||
onSize: (n: number) => void;
|
||||
unit?: string;
|
||||
}) {
|
||||
if (total === 0) return null;
|
||||
|
||||
const first = (page - 1) * size + 1;
|
||||
const last = Math.min(page * size, total);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 border-t border-border px-4 py-3 text-sm text-text-secondary sm:flex-row sm:items-center sm:justify-between sm:px-6">
|
||||
<span className="tabular-nums">
|
||||
{first}–{last} of {total} {unit}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
aria-label="Rows per page"
|
||||
value={size}
|
||||
onChange={(e) => onSize(Number(e.target.value))}
|
||||
className="rounded border border-border bg-surface-2 px-2 py-1 text-sm text-text-primary focus:border-accent focus:outline-none"
|
||||
>
|
||||
{PAGE_SIZES.map((n) => (
|
||||
<option key={n} value={n}>
|
||||
{n} / page
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={() => onPage(page - 1)}
|
||||
disabled={page <= 1}
|
||||
className="rounded border border-border px-2.5 py-1 text-text-secondary transition-colors hover:text-text-primary disabled:opacity-40 disabled:hover:text-text-secondary"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span className="tabular-nums">
|
||||
{page} / {pageCount}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => onPage(page + 1)}
|
||||
disabled={page >= pageCount}
|
||||
className="rounded border border-border px-2.5 py-1 text-text-secondary transition-colors hover:text-text-primary disabled:opacity-40 disabled:hover:text-text-secondary"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export { Badge } from "./Badge";
|
||||
export { Card, CardHeader, CardTitle } from "./Card";
|
||||
export { Table, Thead, Tbody, Tr, Th, Td } from "./Table";
|
||||
export { Modal } from "./Modal";
|
||||
export { Pagination, usePagination, PAGE_SIZES } from "./Pagination";
|
||||
|
||||
Reference in New Issue
Block a user