Skip to content

Commit c120c64

Browse files
authored
feat(node): added node searching and pagination on api
2 parents fc0286d + 5d74acf commit c120c64

File tree

3 files changed

+298
-60
lines changed

3 files changed

+298
-60
lines changed
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { Input } from '@/components/ui/input'
2+
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from '@/components/ui/pagination'
3+
import useDirDetection from '@/hooks/use-dir-detection'
4+
import { cn } from '@/lib/utils'
5+
import { debounce } from 'es-toolkit'
6+
import { SearchIcon, X, RefreshCw } from 'lucide-react'
7+
import { useState, useRef, useEffect } from 'react'
8+
import { useTranslation } from 'react-i18next'
9+
import { RefetchOptions } from '@tanstack/react-query'
10+
import { LoaderCircle } from 'lucide-react'
11+
import { Button } from '@/components/ui/button'
12+
13+
interface NodeFiltersProps {
14+
filters: {
15+
search?: string
16+
limit: number
17+
offset: number
18+
}
19+
onFilterChange: (filters: Partial<NodeFiltersProps['filters']>) => void
20+
refetch?: (options?: RefetchOptions) => Promise<unknown>
21+
isFetching?: boolean
22+
}
23+
24+
export const NodeFilters = ({ filters, onFilterChange, refetch, isFetching }: NodeFiltersProps) => {
25+
const { t } = useTranslation()
26+
const dir = useDirDetection()
27+
const [search, setSearch] = useState(filters.search || '')
28+
29+
const onFilterChangeRef = useRef(onFilterChange)
30+
onFilterChangeRef.current = onFilterChange
31+
32+
const debouncedFilterChangeRef = useRef(
33+
debounce((value: string) => {
34+
onFilterChangeRef.current({
35+
search: value || undefined,
36+
offset: 0,
37+
})
38+
}, 300),
39+
)
40+
41+
useEffect(() => {
42+
return () => {
43+
debouncedFilterChangeRef.current.cancel()
44+
}
45+
}, [])
46+
47+
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
48+
const value = e.target.value
49+
setSearch(value)
50+
debouncedFilterChangeRef.current(value)
51+
}
52+
53+
const clearSearch = () => {
54+
setSearch('')
55+
debouncedFilterChangeRef.current.cancel()
56+
onFilterChange({
57+
search: undefined,
58+
offset: 0,
59+
})
60+
}
61+
62+
const handleManualRefresh = () => {
63+
if (refetch) {
64+
refetch()
65+
}
66+
}
67+
68+
return (
69+
<div dir={dir} className="flex items-center gap-2 py-4 md:gap-4">
70+
<div className="relative w-full md:w-[calc(100%/3-10px)] flex" dir={dir}>
71+
<SearchIcon
72+
className={cn('absolute', dir === 'rtl' ? 'right-2' : 'left-2', 'top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground')}/>
73+
<Input
74+
placeholder={t('search')}
75+
value={search}
76+
onChange={handleSearchChange}
77+
className={cn('pl-8 pr-10', dir === 'rtl' && 'pr-8 pl-10')}
78+
/>
79+
{search && (
80+
<button
81+
onClick={clearSearch}
82+
className={cn('absolute', dir === 'rtl' ? 'left-2' : 'right-2', 'top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground')}
83+
>
84+
<X className="h-4 w-4"/>
85+
</button>
86+
)}
87+
</div>
88+
<Button size="icon-md"
89+
onClick={handleManualRefresh}
90+
variant="ghost"
91+
className={cn(
92+
'relative flex h-9 w-9 items-center justify-center border transition-all duration-200 md:h-10 md:w-10',
93+
isFetching && 'opacity-70',
94+
)}
95+
aria-label={t('autoRefresh.refreshNow')}
96+
title={t('autoRefresh.refreshNow')}
97+
>
98+
<RefreshCw className={cn('h-4 w-4', isFetching && 'animate-spin')}/>
99+
</Button>
100+
</div>
101+
)
102+
}
103+
104+
interface NodePaginationControlsProps {
105+
currentPage: number
106+
totalPages: number
107+
totalNodes: number
108+
isLoading: boolean
109+
onPageChange: (page: number) => void
110+
}
111+
112+
export const NodePaginationControls = ({ currentPage, totalPages, totalNodes, isLoading, onPageChange }: NodePaginationControlsProps) => {
113+
const dir = useDirDetection()
114+
115+
const getPaginationRange = (currentPage: number, totalPages: number) => {
116+
const delta = 2
117+
const range = []
118+
119+
if (totalPages <= 5) {
120+
for (let i = 0; i < totalPages; i++) {
121+
range.push(i)
122+
}
123+
return range
124+
}
125+
126+
range.push(0)
127+
128+
let start = Math.max(1, currentPage - delta)
129+
let end = Math.min(totalPages - 2, currentPage + delta)
130+
131+
if (currentPage - delta <= 1) {
132+
end = Math.min(totalPages - 2, start + 2 * delta)
133+
}
134+
if (currentPage + delta >= totalPages - 2) {
135+
start = Math.max(1, totalPages - 3 - 2 * delta)
136+
}
137+
138+
if (start > 1) {
139+
range.push(-1)
140+
}
141+
142+
for (let i = start; i <= end; i++) {
143+
range.push(i)
144+
}
145+
146+
if (end < totalPages - 2) {
147+
range.push(-1)
148+
}
149+
150+
if (totalPages > 1) {
151+
range.push(totalPages - 1)
152+
}
153+
154+
return range
155+
}
156+
157+
const paginationRange = getPaginationRange(currentPage, totalPages)
158+
159+
return (
160+
<div className="mt-4 flex flex-col-reverse items-center justify-between gap-4 md:flex-row">
161+
162+
<Pagination dir="ltr" className={`${dir === 'rtl' ? 'flex-row-reverse' : ''}`}>
163+
<PaginationContent className={cn('w-full justify-center overflow-x-auto', dir === 'rtl' ? 'md:justify-start' : 'md:justify-end')}>
164+
<PaginationItem>
165+
<PaginationPrevious onClick={() => onPageChange(currentPage - 1)} disabled={currentPage === 0 || isLoading} />
166+
</PaginationItem>
167+
{paginationRange.map((pageNumber, i) =>
168+
pageNumber === -1 ? (
169+
<PaginationItem key={`ellipsis-${i}`}>
170+
<PaginationEllipsis />
171+
</PaginationItem>
172+
) : (
173+
<PaginationItem key={pageNumber}>
174+
<PaginationLink
175+
isActive={currentPage === pageNumber}
176+
onClick={() => onPageChange(pageNumber as number)}
177+
disabled={isLoading}
178+
className={isLoading && currentPage === pageNumber ? 'opacity-70' : ''}
179+
>
180+
{isLoading && currentPage === pageNumber ? (
181+
<div className="flex items-center">
182+
<LoaderCircle className="mr-1 h-3 w-3 animate-spin" />
183+
{(pageNumber as number) + 1}
184+
</div>
185+
) : (
186+
(pageNumber as number) + 1
187+
)}
188+
</PaginationLink>
189+
</PaginationItem>
190+
),
191+
)}
192+
<PaginationItem>
193+
<PaginationNext onClick={() => onPageChange(currentPage + 1)} disabled={currentPage === totalPages - 1 || totalPages === 0 || isLoading} />
194+
</PaginationItem>
195+
</PaginationContent>
196+
</Pagination>
197+
</div>
198+
)
199+
}

0 commit comments

Comments
 (0)