paginate views
build-and-push / build (push) Failing after 34s

This commit is contained in:
2026-08-01 03:02:35 +05:00
parent 141d380abf
commit 1af12891a7
4 changed files with 169 additions and 5 deletions
+114
View File
@@ -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<number>(() => {
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<T>(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 (
<div className="flex flex-wrap items-center justify-between gap-2 border-t px-4 py-2 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<span>Rows per page</span>
<Select value={String(pageSize)} onValueChange={(v) => onPageSizeChange(Number(v))}>
<SelectTrigger className="h-8 w-[4.5rem]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{PAGE_SIZE_OPTIONS.map((n) => (
<SelectItem key={n} value={String(n)}>
{n}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<span>
{from}{to} of {total}
</span>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => onPageChange(page - 1)}
disabled={page <= 1}
title="Previous page"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span>
Page {page} of {pageCount}
</span>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => onPageChange(page + 1)}
disabled={page >= pageCount}
title="Next page"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)
}
+29 -3
View File
@@ -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<Client[]>([])
const [vlans, setVlans] = useState<Vlan[]>([])
const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(true)
const [addOpen, setAddOpen] = useState(false)
const [editing, setEditing] = useState<Client | null>(null)
const [deleting, setDeleting] = useState<Client | null>(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 (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold">Clients</h1>
<p className="text-sm text-muted-foreground">{devices.length} registered</p>
<p className="text-sm text-muted-foreground">{total} registered</p>
</div>
<div className="flex gap-2">
<Button variant="outline" size="icon" onClick={load} title="Refresh">
@@ -142,6 +160,14 @@ export function Clients() {
)}
</TableBody>
</Table>
<TablePagination
page={page}
pageCount={pageCount}
total={total}
pageSize={pageSize}
onPageChange={setPage}
onPageSizeChange={setPageSize}
/>
</div>
<AddClientDialog
+13 -1
View File
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react'
import { Loader2, Pencil, RefreshCw, Wifi } from 'lucide-react'
import { toast } from 'sonner'
import { api, ApiError, type Device } 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'
@@ -69,6 +70,9 @@ export function Devices() {
load()
}, [load])
const [pageSize, setPageSize] = usePageSize()
const { page, setPage, pageCount, pageItems, total } = usePagination(devices, pageSize)
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
@@ -108,7 +112,7 @@ export function Devices() {
</TableCell>
</TableRow>
) : (
devices.map((d) => (
pageItems.map((d) => (
<TableRow key={d.ap_mac}>
<TableCell className="font-mono text-xs">{d.ap_mac}</TableCell>
<TableCell className="font-mono text-xs">
@@ -130,6 +134,14 @@ export function Devices() {
)}
</TableBody>
</Table>
<TablePagination
page={page}
pageCount={pageCount}
total={total}
pageSize={pageSize}
onPageChange={setPage}
onPageSizeChange={setPageSize}
/>
</div>
<EditAliasDialog device={editing} onClose={() => setEditing(null)} onSaved={load} />
+13 -1
View File
@@ -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 (
<div className="space-y-4">
<div className="flex items-center justify-between">
@@ -86,7 +90,7 @@ export function Vlans() {
</TableCell>
</TableRow>
) : (
vlans.map((v) => (
pageItems.map((v) => (
<TableRow key={v.vlanid}>
<TableCell>
<Badge variant="outline">VLAN {v.vlanid}</Badge>
@@ -113,6 +117,14 @@ export function Vlans() {
)}
</TableBody>
</Table>
<TablePagination
page={page}
pageCount={pageCount}
total={total}
pageSize={pageSize}
onPageChange={setPage}
onPageSizeChange={setPageSize}
/>
</div>
<AddVlanDialog open={addOpen} onOpenChange={setAddOpen} onSaved={load} />