1- import { useState , useEffect , useMemo } from 'react'
1+ import { useState , useEffect , useCallback , useRef } from 'react'
22import { useTranslation } from 'react-i18next'
33import Node from '@/components/nodes/node'
4- import { useGetNodes , useModifyNode , NodeResponse , NodeConnectionType } from '@/service/api'
4+ import { useGetNodes , useModifyNode , NodeResponse , NodeConnectionType } from '@/service/api'
55import { toast } from 'sonner'
66import { queryClient } from '@/utils/query-client'
77import NodeModal from '@/components/dialogs/node-modal'
@@ -10,10 +10,9 @@ import { zodResolver } from '@hookform/resolvers/zod'
1010import { nodeFormSchema , NodeFormValues } from '@/components/dialogs/node-modal'
1111import { Card , CardContent } from '@/components/ui/card'
1212import { Skeleton } from '@/components/ui/skeleton'
13- import { Input } from '@/components/ui/input'
14- import { Search , X } from 'lucide-react'
15- import useDirDetection from '@/hooks/use-dir-detection'
16- import { cn } from '@/lib/utils'
13+ import { NodeFilters , NodePaginationControls } from '@/components/nodes/node-filters'
14+
15+ const NODES_PER_PAGE = 15
1716
1817const initialDefaultValues : Partial < NodeFormValues > = {
1918 name : '' ,
@@ -29,29 +28,76 @@ export default function NodesList() {
2928 const { t } = useTranslation ( )
3029 const [ isDialogOpen , setIsDialogOpen ] = useState ( false )
3130 const [ editingNode , setEditingNode ] = useState < NodeResponse | null > ( null )
32- const [ searchQuery , setSearchQuery ] = useState ( '' )
31+ const [ currentPage , setCurrentPage ] = useState ( 0 )
32+ const [ isChangingPage , setIsChangingPage ] = useState ( false )
33+ const isFirstLoadRef = useRef ( true )
3334 const modifyNodeMutation = useModifyNode ( )
34- const dir = useDirDetection ( )
3535
36- const { data : nodesData , isLoading } = useGetNodes ( undefined , {
37- query : {
38- refetchInterval : isDialogOpen && editingNode ? false : 5000 ,
39- staleTime : 0 ,
40- gcTime : 0 ,
41- } ,
36+ const [ filters , setFilters ] = useState < {
37+ limit : number
38+ offset : number
39+ search ?: string
40+ } > ( {
41+ limit : NODES_PER_PAGE ,
42+ offset : 0 ,
43+ search : undefined ,
4244 } )
4345
4446 const form = useForm < NodeFormValues > ( {
4547 resolver : zodResolver ( nodeFormSchema ) ,
4648 defaultValues : initialDefaultValues ,
4749 } )
4850
51+ const {
52+ data : nodesResponse ,
53+ isLoading,
54+ isFetching,
55+ refetch,
56+ } = useGetNodes ( filters , {
57+ query : {
58+ refetchInterval : false ,
59+ staleTime : 0 ,
60+ gcTime : 0 ,
61+ retry : 1 ,
62+ refetchOnMount : true ,
63+ refetchOnWindowFocus : false ,
64+ } ,
65+ } )
66+
67+ useEffect ( ( ) => {
68+ if ( nodesResponse && isFirstLoadRef . current ) {
69+ isFirstLoadRef . current = false
70+ }
71+ } , [ nodesResponse ] )
72+
4973 useEffect ( ( ) => {
5074 const handleOpenDialog = ( ) => setIsDialogOpen ( true )
5175 window . addEventListener ( 'openNodeDialog' , handleOpenDialog )
5276 return ( ) => window . removeEventListener ( 'openNodeDialog' , handleOpenDialog )
5377 } , [ ] )
5478
79+ const handleFilterChange = useCallback ( ( newFilters : Partial < typeof filters > ) => {
80+ setFilters ( prev => ( {
81+ ...prev ,
82+ ...newFilters ,
83+ } ) )
84+ if ( newFilters . offset === 0 ) {
85+ setCurrentPage ( 0 )
86+ }
87+ } , [ ] )
88+
89+ const handlePageChange = ( newPage : number ) => {
90+ if ( newPage === currentPage || isChangingPage ) return
91+
92+ setIsChangingPage ( true )
93+ setCurrentPage ( newPage )
94+ setFilters ( prev => ( {
95+ ...prev ,
96+ offset : newPage * NODES_PER_PAGE ,
97+ } ) )
98+ setIsChangingPage ( false )
99+ }
100+
55101 const handleEdit = ( node : NodeResponse ) => {
56102 setEditingNode ( node )
57103 form . reset ( {
@@ -105,44 +151,22 @@ export default function NodesList() {
105151 }
106152 }
107153
108- const filteredNodes = useMemo ( ( ) => {
109- if ( ! nodesData || ! searchQuery . trim ( ) ) return nodesData
110- const query = searchQuery . toLowerCase ( ) . trim ( )
111- return nodesData . filter (
112- node =>
113- node . name ?. toLowerCase ( ) . includes ( query ) ||
114- node . address ?. toLowerCase ( ) . includes ( query ) ||
115- node . connection_type ?. toLowerCase ( ) . includes ( query ) ,
116- )
117- } , [ nodesData , searchQuery ] )
154+ const nodesData = nodesResponse ?. nodes || [ ]
155+ const totalNodes = nodesResponse ?. total || 0
156+ const totalPages = Math . ceil ( totalNodes / NODES_PER_PAGE )
157+ const showLoadingSpinner = isLoading && isFirstLoadRef . current
158+ const isPageLoading = isChangingPage
118159
119160 return (
120161 < div className = "flex w-full flex-col items-start gap-2" >
121162 < div className = "w-full flex-1 space-y-4 pt-6" >
122- { /* Search Input */ }
123- < div className = "relative w-full md:w-[calc(100%/3-10px)]" dir = { dir } >
124- < Search className = { cn ( 'absolute' , dir === 'rtl' ? 'right-2' : 'left-2' , 'top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground' ) } />
125- < Input
126- placeholder = { t ( 'search' ) }
127- value = { searchQuery }
128- onChange = { e => setSearchQuery ( e . target . value ) }
129- className = { cn ( 'pl-8 pr-10' , dir === 'rtl' && 'pr-8 pl-10' ) }
130- />
131- { searchQuery && (
132- < button
133- onClick = { ( ) => setSearchQuery ( '' ) }
134- className = { cn ( 'absolute' , dir === 'rtl' ? 'left-2' : 'right-2' , 'top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground' ) }
135- >
136- < X className = "h-4 w-4" />
137- </ button >
138- ) }
139- </ div >
163+ < NodeFilters filters = { filters } onFilterChange = { handleFilterChange } refetch = { refetch } isFetching = { isFetching } />
140164
141165 < div
142166 className = "mb-12 grid transform-gpu animate-slide-up grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
143167 style = { { animationDuration : '500ms' , animationDelay : '100ms' , animationFillMode : 'both' } }
144168 >
145- { isLoading
169+ { showLoadingSpinner
146170 ? [ ...Array ( 6 ) ] . map ( ( _ , i ) => (
147171 < Card key = { i } className = "p-4" >
148172 < div className = "space-y-3" >
@@ -158,10 +182,10 @@ export default function NodesList() {
158182 </ div >
159183 </ Card >
160184 ) )
161- : filteredNodes ? .map ( node => < Node key = { node . id } node = { node } onEdit = { handleEdit } onToggleStatus = { handleToggleStatus } /> ) }
185+ : nodesData . map ( node => < Node key = { node . id } node = { node } onEdit = { handleEdit } onToggleStatus = { handleToggleStatus } /> ) }
162186 </ div >
163187
164- { ! isLoading && ( ! nodesData || nodesData . length === 0 ) && (
188+ { ! showLoadingSpinner && nodesData . length === 0 && ! filters . search && (
165189 < Card className = "mb-12" >
166190 < CardContent className = "p-8 text-center" >
167191 < div className = "space-y-4" >
@@ -178,7 +202,7 @@ export default function NodesList() {
178202 </ Card >
179203 ) }
180204
181- { ! isLoading && nodesData && nodesData . length > 0 && ( ! filteredNodes || filteredNodes . length === 0 ) && (
205+ { ! showLoadingSpinner && nodesData . length === 0 && ( filters . search ) && (
182206 < Card className = "mb-12" >
183207 < CardContent className = "p-8 text-center" >
184208 < div className = "space-y-4" >
@@ -191,6 +215,16 @@ export default function NodesList() {
191215 </ Card >
192216 ) }
193217
218+ { totalNodes > NODES_PER_PAGE && (
219+ < NodePaginationControls
220+ currentPage = { currentPage }
221+ totalPages = { totalPages }
222+ totalNodes = { totalNodes }
223+ isLoading = { isPageLoading }
224+ onPageChange = { handlePageChange }
225+ />
226+ ) }
227+
194228 < NodeModal
195229 isDialogOpen = { isDialogOpen }
196230 onOpenChange = { open => {
0 commit comments