From 1af12891a7204d8de3fea9284cba7a9387794342 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Sat, 1 Aug 2026 03:02:35 +0500 Subject: [PATCH] paginate views --- frontend/src/components/Pagination.tsx | 114 +++++++++++++++++++++++++ frontend/src/pages/Clients.tsx | 32 ++++++- frontend/src/pages/Devices.tsx | 14 ++- frontend/src/pages/Vlans.tsx | 14 ++- 4 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/Pagination.tsx diff --git a/frontend/src/components/Pagination.tsx b/frontend/src/components/Pagination.tsx new file mode 100644 index 0000000..4abdeb8 --- /dev/null +++ b/frontend/src/components/Pagination.tsx @@ -0,0 +1,114 @@ +import { useEffect, useMemo, useState } from 'react' +import { ChevronLeft, ChevronRight } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' + +export const PAGE_SIZE_OPTIONS = [10, 20, 50, 100, 200] as const +const DEFAULT_PAGE_SIZE = 10 +const STORAGE_KEY = 'radui.pageSize' + +/** Rows-per-page preference, shared across tables and persisted in localStorage. */ +export function usePageSize() { + const [pageSize, setPageSizeState] = useState(() => { + const raw = Number(localStorage.getItem(STORAGE_KEY)) + return (PAGE_SIZE_OPTIONS as readonly number[]).includes(raw) ? raw : DEFAULT_PAGE_SIZE + }) + const setPageSize = (n: number) => { + setPageSizeState(n) + localStorage.setItem(STORAGE_KEY, String(n)) + } + return [pageSize, setPageSize] as const +} + +/** Client-side pagination over an already-loaded array. */ +export function usePagination(items: T[], pageSize: number) { + const [page, setPage] = useState(1) + const pageCount = Math.max(1, Math.ceil(items.length / pageSize)) + + // Reset to the first page when the page size changes. + useEffect(() => setPage(1), [pageSize]) + // Clamp the current page when the list shrinks (e.g. after a delete/filter). + useEffect(() => { + if (page > pageCount) setPage(pageCount) + }, [page, pageCount]) + + const pageItems = useMemo( + () => items.slice((page - 1) * pageSize, page * pageSize), + [items, page, pageSize], + ) + + return { page, setPage, pageCount, pageItems, total: items.length } +} + +export function TablePagination({ + page, + pageCount, + total, + pageSize, + onPageChange, + onPageSizeChange, +}: { + page: number + pageCount: number + total: number + pageSize: number + onPageChange: (page: number) => void + onPageSizeChange: (size: number) => void +}) { + const from = total === 0 ? 0 : (page - 1) * pageSize + 1 + const to = Math.min(page * pageSize, total) + + return ( +
+
+ Rows per page + +
+
+ + {from}–{to} of {total} + + + + Page {page} of {pageCount} + + +
+
+ ) +} diff --git a/frontend/src/pages/Clients.tsx b/frontend/src/pages/Clients.tsx index 37e8e97..8f9734c 100644 --- a/frontend/src/pages/Clients.tsx +++ b/frontend/src/pages/Clients.tsx @@ -3,6 +3,7 @@ import { Loader2, Pencil, Plus, RefreshCw, Trash2 } from 'lucide-react' import { toast } from 'sonner' import { api, ApiError, type Client, type ClientStatus, type Vlan } from '@/lib/api' import { normalizePhone, phoneError } from '@/lib/phone' +import { TablePagination, usePageSize } from '@/components/Pagination' import { Button } from '@/components/ui/button' import { ClientImportExport } from '@/components/ClientImportExport' import { Badge } from '@/components/ui/badge' @@ -43,16 +44,26 @@ function StatusBadge({ status }: { status: ClientStatus | null }) { export function Clients() { const [devices, setDevices] = useState([]) const [vlans, setVlans] = useState([]) + const [total, setTotal] = useState(0) const [loading, setLoading] = useState(true) const [addOpen, setAddOpen] = useState(false) const [editing, setEditing] = useState(null) const [deleting, setDeleting] = useState(null) + const [pageSize, setPageSize] = usePageSize() + const [page, setPage] = useState(1) + const pageCount = Math.max(1, Math.ceil(total / pageSize)) + + // Server-side pagination: fetch only the current page's rows. const load = useCallback(async () => { setLoading(true) try { - const [d, v] = await Promise.all([api.clients.list(200), api.vlans.list()]) + const [d, v] = await Promise.all([ + api.clients.list(pageSize, (page - 1) * pageSize), + api.vlans.list(), + ]) setDevices(d.items) + setTotal(d.total) setVlans(v) } catch (err) { if (!(err instanceof ApiError && err.status === 401)) @@ -60,18 +71,25 @@ export function Clients() { } finally { setLoading(false) } - }, []) + }, [page, pageSize]) useEffect(() => { load() }, [load]) + // Reset to the first page when the page size changes. + useEffect(() => setPage(1), [pageSize]) + // Clamp the page when the total shrinks (e.g. after a delete). + useEffect(() => { + if (page > pageCount) setPage(pageCount) + }, [page, pageCount]) + return (

Clients

-

{devices.length} registered

+

{total} registered

@@ -108,7 +112,7 @@ export function Devices() { ) : ( - devices.map((d) => ( + pageItems.map((d) => ( {d.ap_mac} @@ -130,6 +134,14 @@ export function Devices() { )} +
setEditing(null)} onSaved={load} /> diff --git a/frontend/src/pages/Vlans.tsx b/frontend/src/pages/Vlans.tsx index ae99a8b..aabae0f 100644 --- a/frontend/src/pages/Vlans.tsx +++ b/frontend/src/pages/Vlans.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState, type ReactNode } from 'react' import { Loader2, Pencil, Plus, RefreshCw, Trash2 } from 'lucide-react' import { toast } from 'sonner' import { api, ApiError, type Vlan } from '@/lib/api' +import { TablePagination, usePageSize, usePagination } from '@/components/Pagination' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' @@ -46,6 +47,9 @@ export function Vlans() { load() }, [load]) + const [pageSize, setPageSize] = usePageSize() + const { page, setPage, pageCount, pageItems, total } = usePagination(vlans, pageSize) + return (
@@ -86,7 +90,7 @@ export function Vlans() { ) : ( - vlans.map((v) => ( + pageItems.map((v) => ( VLAN {v.vlanid} @@ -113,6 +117,14 @@ export function Vlans() { )} +