1- import { useState , useEffect , useCallback , useRef } from 'react'
1+ import { useState , useEffect , useCallback , useRef , useMemo } from 'react'
22import { useTranslation } from 'react-i18next'
33import Node from '@/components/nodes/node'
44import { useGetNodes , useModifyNode , NodeResponse , NodeConnectionType } from '@/service/api'
@@ -30,8 +30,12 @@ export default function NodesList() {
3030 const [ editingNode , setEditingNode ] = useState < NodeResponse | null > ( null )
3131 const [ currentPage , setCurrentPage ] = useState ( 0 )
3232 const [ isChangingPage , setIsChangingPage ] = useState ( false )
33+ const wasFetchingRef = useRef ( false )
3334 const isFirstLoadRef = useRef ( true )
35+ const previousTotalPagesRef = useRef ( 0 )
3436 const modifyNodeMutation = useModifyNode ( )
37+ const [ allNodes , setAllNodes ] = useState < NodeResponse [ ] > ( [ ] )
38+ const [ localSearchTerm , setLocalSearchTerm ] = useState < string > ( '' )
3539
3640 const [ filters , setFilters ] = useState < {
3741 limit : number
@@ -55,20 +59,40 @@ export default function NodesList() {
5559 refetch,
5660 } = useGetNodes ( filters , {
5761 query : {
58- refetchInterval : false ,
62+ refetchInterval : 10000 ,
5963 staleTime : 0 ,
6064 gcTime : 0 ,
6165 retry : 1 ,
6266 refetchOnMount : true ,
63- refetchOnWindowFocus : false ,
67+ refetchOnWindowFocus : true ,
6468 } ,
6569 } )
6670
71+ // Check if we should use local search (only one page of nodes)
72+ const totalNodesFromResponse = nodesResponse ?. total || 0
73+ const shouldUseLocalSearch = totalNodesFromResponse > 0 && totalNodesFromResponse <= NODES_PER_PAGE && ! filters . search
74+
6775 useEffect ( ( ) => {
6876 if ( nodesResponse && isFirstLoadRef . current ) {
6977 isFirstLoadRef . current = false
7078 }
71- } , [ nodesResponse ] )
79+ // Store all nodes when fetched without search/pagination and there's only one page
80+ if ( nodesResponse && shouldUseLocalSearch && ! filters . search && filters . offset === 0 ) {
81+ setAllNodes ( nodesResponse . nodes || [ ] )
82+ }
83+ } , [ nodesResponse , shouldUseLocalSearch , filters . search , filters . offset ] )
84+
85+ useEffect ( ( ) => {
86+ // Track when fetching starts
87+ if ( isFetching ) {
88+ wasFetchingRef . current = true
89+ }
90+ // Only reset isChangingPage when fetching completes (was fetching, now not fetching)
91+ if ( ! isFetching && wasFetchingRef . current && isChangingPage ) {
92+ setIsChangingPage ( false )
93+ wasFetchingRef . current = false
94+ }
95+ } , [ isFetching , isChangingPage ] )
7296
7397 useEffect ( ( ) => {
7498 const handleOpenDialog = ( ) => setIsDialogOpen ( true )
@@ -77,25 +101,39 @@ export default function NodesList() {
77101 } , [ ] )
78102
79103 const handleFilterChange = useCallback ( ( newFilters : Partial < typeof filters > ) => {
104+ const searchValue = newFilters . search !== undefined ? newFilters . search : filters . search
105+ setLocalSearchTerm ( searchValue || '' )
106+
107+ // If using local search, don't update API filters
108+ if ( shouldUseLocalSearch && searchValue ) {
109+ setCurrentPage ( 0 ) // Reset to first page when searching locally
110+ return
111+ }
112+
80113 setFilters ( prev => ( {
81114 ...prev ,
82115 ...newFilters ,
83116 } ) )
84117 if ( newFilters . offset === 0 ) {
85118 setCurrentPage ( 0 )
86119 }
87- } , [ ] )
120+ } , [ filters . search , shouldUseLocalSearch ] )
88121
89122 const handlePageChange = ( newPage : number ) => {
90123 if ( newPage === currentPage || isChangingPage ) return
91124
125+ // If using local search, just update page without API call
126+ if ( shouldUseLocalSearch && localSearchTerm ) {
127+ setCurrentPage ( newPage )
128+ return
129+ }
130+
92131 setIsChangingPage ( true )
93132 setCurrentPage ( newPage )
94133 setFilters ( prev => ( {
95134 ...prev ,
96135 offset : newPage * NODES_PER_PAGE ,
97136 } ) )
98- setIsChangingPage ( false )
99137 }
100138
101139 const handleEdit = ( node : NodeResponse ) => {
@@ -151,11 +189,60 @@ export default function NodesList() {
151189 }
152190 }
153191
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 || isFetching
158- const isPageLoading = isChangingPage
192+ // Filter nodes locally when using local search
193+ const filteredNodes = useMemo ( ( ) => {
194+ if ( shouldUseLocalSearch && localSearchTerm && allNodes . length > 0 ) {
195+ const searchLower = localSearchTerm . toLowerCase ( )
196+ return allNodes . filter ( ( node : NodeResponse ) =>
197+ node . name . toLowerCase ( ) . includes ( searchLower ) ||
198+ node . address . toLowerCase ( ) . includes ( searchLower ) ||
199+ node . port ?. toString ( ) . includes ( searchLower )
200+ )
201+ }
202+ return nodesResponse ?. nodes || [ ]
203+ } , [ shouldUseLocalSearch , localSearchTerm , allNodes , nodesResponse ?. nodes ] )
204+
205+ // Calculate pagination for local search
206+ const paginatedNodes = useMemo ( ( ) => {
207+ if ( shouldUseLocalSearch && localSearchTerm ) {
208+ const start = currentPage * NODES_PER_PAGE
209+ const end = start + NODES_PER_PAGE
210+ return filteredNodes . slice ( start , end )
211+ }
212+ return filteredNodes
213+ } , [ shouldUseLocalSearch , localSearchTerm , filteredNodes , currentPage ] )
214+
215+ const nodesData = paginatedNodes
216+ // Calculate total nodes - use filtered count for local search, otherwise use API response
217+ const totalNodes = shouldUseLocalSearch && localSearchTerm
218+ ? filteredNodes . length
219+ : ( nodesResponse ?. total || 0 )
220+ const showLoadingSpinner = isLoading && isFirstLoadRef . current
221+ const isPageLoading = isChangingPage || ( isFetching && ! isFirstLoadRef . current && ! shouldUseLocalSearch )
222+ const showPageLoadingSkeletons = isPageLoading && ! showLoadingSpinner
223+
224+ const calculatedTotalPages = Math . ceil ( totalNodes / NODES_PER_PAGE )
225+ // Preserve totalPages during loading to prevent pagination from disappearing
226+ const totalPages = calculatedTotalPages > 0 ? calculatedTotalPages : ( isPageLoading ? previousTotalPagesRef . current : 0 )
227+
228+ // Update previous total pages when we have valid data
229+ useEffect ( ( ) => {
230+ if ( calculatedTotalPages > 0 ) {
231+ previousTotalPagesRef . current = calculatedTotalPages
232+ }
233+ } , [ calculatedTotalPages ] )
234+
235+ // Navigate to last available page if current page becomes invalid (e.g., after deleting all nodes on current page)
236+ useEffect ( ( ) => {
237+ if ( calculatedTotalPages > 0 && currentPage >= calculatedTotalPages ) {
238+ const lastPage = calculatedTotalPages - 1
239+ setCurrentPage ( lastPage )
240+ setFilters ( prev => ( {
241+ ...prev ,
242+ offset : lastPage * NODES_PER_PAGE ,
243+ } ) )
244+ }
245+ } , [ calculatedTotalPages , currentPage ] )
159246
160247 return (
161248 < div className = "flex w-full flex-col items-start gap-2" >
@@ -166,26 +253,47 @@ export default function NodesList() {
166253 className = " grid transform-gpu animate-slide-up grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"
167254 style = { { animationDuration : '500ms' , animationDelay : '100ms' , animationFillMode : 'both' } }
168255 >
169- { showLoadingSpinner
256+ { showLoadingSpinner || showPageLoadingSkeletons
170257 ? [ ...Array ( 6 ) ] . map ( ( _ , i ) => (
171- < Card key = { i } className = "p-4" >
172- < div className = "space-y-3" >
173- < div className = "flex items-center justify-between" >
174- < Skeleton className = "h-5 w-24 sm:w-32" />
175- < Skeleton className = "h-6 w-6 shrink-0 rounded-full" />
258+ < Card key = { i } className = "group relative h-full p-4" >
259+ < div className = "flex items-center gap-3" >
260+ < div className = "min-w-0 flex-1" >
261+ { /* Status dot + Node name */ }
262+ < div className = "flex items-center gap-2 mb-1" >
263+ < Skeleton className = "h-2 w-2 rounded-full shrink-0" />
264+ < Skeleton className = "h-5 w-32 sm:w-40" />
265+ </ div >
266+ { /* Address:port */ }
267+ < Skeleton className = "h-4 w-28 sm:w-36 mb-1" />
268+ { /* Version info (optional, sometimes shown) */ }
269+ { i % 3 === 0 && < Skeleton className = "h-3 w-40 sm:w-48 mt-1 mb-2" /> }
270+ { /* Usage display section */ }
271+ < div className = "mt-2 space-y-1.5" >
272+ { /* Progress bar */ }
273+ < Skeleton className = "h-1.5 w-full rounded-full" />
274+ { /* Usage stats */ }
275+ < div className = "flex items-center justify-between gap-2" >
276+ < Skeleton className = "h-3 w-20" />
277+ < Skeleton className = "h-3 w-16" />
278+ </ div >
279+ { /* Uplink/Downlink */ }
280+ < div className = "flex items-center gap-3" >
281+ < Skeleton className = "h-2.5 w-16" />
282+ < Skeleton className = "h-2.5 w-16" />
283+ </ div >
284+ </ div >
176285 </ div >
177- < Skeleton className = "h-4 w-20 sm:w-24" />
178- < div className = "flex gap-2" >
179- < Skeleton className = "h-8 flex-1" />
180- < Skeleton className = "h-8 w-8 shrink-0" />
286+ { /* Dropdown menu button */ }
287+ < div >
288+ < Skeleton className = "h-9 w-9 rounded-md shrink-0" />
181289 </ div >
182290 </ div >
183291 </ Card >
184292 ) )
185293 : nodesData . map ( node => < Node key = { node . id } node = { node } onEdit = { handleEdit } onToggleStatus = { handleToggleStatus } /> ) }
186294 </ div >
187295
188- { ! showLoadingSpinner && nodesData . length === 0 && ! filters . search && (
296+ { ! showLoadingSpinner && ! showPageLoadingSkeletons && nodesData . length === 0 && ! filters . search && totalNodes === 0 && (
189297 < Card className = "mb-12" >
190298 < CardContent className = "p-8 text-center" >
191299 < div className = "space-y-4" >
@@ -202,7 +310,7 @@ export default function NodesList() {
202310 </ Card >
203311 ) }
204312
205- { ! showLoadingSpinner && nodesData . length === 0 && ( filters . search ) && (
313+ { ! showLoadingSpinner && ! showPageLoadingSkeletons && nodesData . length === 0 && ( filters . search || localSearchTerm ) && (
206314 < Card className = "mb-12" >
207315 < CardContent className = "p-8 text-center" >
208316 < div className = "space-y-4" >
@@ -215,7 +323,7 @@ export default function NodesList() {
215323 </ Card >
216324 ) }
217325 </ div >
218- { ! showLoadingSpinner && nodesData . length > 0 && totalNodes > NODES_PER_PAGE && (
326+ { totalPages > 1 && (
219327 < NodePaginationControls
220328 currentPage = { currentPage }
221329 totalPages = { totalPages }
0 commit comments