Skip to content

Commit 62d6ada

Browse files
committed
fix(nodes): update node data handling and improve loading states across components
1 parent c120c64 commit 62d6ada

File tree

8 files changed

+153
-38
lines changed

8 files changed

+153
-38
lines changed

dashboard/src/components/admins/admins-table.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,10 @@ export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetU
192192
if (!isFetching && isAutoRefreshingRef.current) {
193193
isAutoRefreshingRef.current = false
194194
}
195-
}, [isFetching])
195+
if (!isFetching && isChangingPage) {
196+
setIsChangingPage(false)
197+
}
198+
}, [isFetching, isChangingPage])
196199

197200
// When filters change (e.g., search), reset page if needed
198201
const handleFilterChange = (newFilters: Partial<AdminFilters>) => {
@@ -288,7 +291,6 @@ export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetU
288291

289292
setIsChangingPage(true)
290293
setCurrentPage(newPage)
291-
setIsChangingPage(false)
292294
}
293295

294296
const handleItemsPerPageChange = (value: number) => {
@@ -329,7 +331,7 @@ export default function AdminsTable({ onEdit, onDelete, onToggleStatus, onResetU
329331
})
330332

331333
const showLoadingSpinner = isLoading && isFirstLoadRef.current
332-
const isPageLoading = isChangingPage
334+
const isPageLoading = isChangingPage || (isFetching && !isFirstLoadRef.current && !isAutoRefreshingRef.current)
333335

334336
return (
335337
<div>

dashboard/src/components/charts/all-nodes-stacked-bar-chart.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ export function AllNodesStackedBarChart() {
218218

219219
const { t } = useTranslation()
220220
const dir = useDirDetection()
221-
const { data: nodesData } = useGetNodes(undefined, { query: { enabled: true } })
221+
const { data: nodesResponse } = useGetNodes(undefined, { query: { enabled: true } })
222222
const { resolvedTheme } = useTheme()
223223

224224
// Navigation handler for modal
@@ -230,7 +230,7 @@ export function AllNodesStackedBarChart() {
230230
}
231231

232232
// Build color palette for nodes
233-
const nodeList: NodeResponse[] = useMemo(() => (Array.isArray(nodesData) ? nodesData : []), [nodesData])
233+
const nodeList: NodeResponse[] = useMemo(() => (nodesResponse?.nodes || []), [nodesResponse])
234234

235235
// Function to generate distinct colors based on theme
236236
const generateDistinctColor = useCallback((index: number, _totalNodes: number, isDark: boolean): string => {

dashboard/src/components/dialogs/usage-modal.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ const UsageModal = ({ open, onClose, username }: UsageModalProps) => {
272272
}, [is_sudo])
273273

274274
// Fetch nodes list - only for sudo admins
275-
const { data: nodes, isLoading: isLoadingNodes } = useGetNodes(undefined, {
275+
const { data: nodesResponse, isLoading: isLoadingNodes } = useGetNodes(undefined, {
276276
query: {
277277
enabled: open && is_sudo, // Only fetch nodes for sudo admins when modal is open
278278
},
@@ -287,7 +287,7 @@ const UsageModal = ({ open, onClose, username }: UsageModalProps) => {
287287
}
288288

289289
// Build color palette for nodes
290-
const nodeList: NodeResponse[] = useMemo(() => (Array.isArray(nodes) ? nodes : []), [nodes])
290+
const nodeList: NodeResponse[] = useMemo(() => (nodesResponse?.nodes || []), [nodesResponse])
291291

292292
// Function to generate distinct colors based on theme
293293
const generateDistinctColor = useCallback((index: number, _totalNodes: number, isDark: boolean): string => {
@@ -671,7 +671,7 @@ const UsageModal = ({ open, onClose, username }: UsageModalProps) => {
671671
</SelectTrigger>
672672
<SelectContent>
673673
<SelectItem value="all">{t('userDialog.allNodes', { defaultValue: 'All Nodes' })}</SelectItem>
674-
{nodes?.map(node => (
674+
{nodeList.map(node => (
675675
<SelectItem key={node.id} value={node.id.toString()}>
676676
{node.name}
677677
</SelectItem>

dashboard/src/components/dialogs/user-all-ips-modal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,21 +168,22 @@ export default function UserAllIPsModal({ isOpen, onOpenChange, username }: User
168168
}
169169
}, [isOpen])
170170

171-
const { data: nodesData } = useGetNodes(undefined, {
171+
const { data: nodesResponse } = useGetNodes(undefined, {
172172
query: {
173173
enabled: isOpen,
174174
staleTime: 5 * 60 * 1000,
175175
},
176176
})
177177
const nodeNameMap = useMemo(() => {
178178
const map: { [nodeId: string]: string } = {}
179+
const nodesData = nodesResponse?.nodes || []
179180
if (nodesData && Array.isArray(nodesData)) {
180181
nodesData.forEach(node => {
181182
map[String(node.id)] = node.name
182183
})
183184
}
184185
return map
185-
}, [nodesData])
186+
}, [nodesResponse])
186187

187188
const userIPsQueryOptions = useMemo(
188189
() => ({

dashboard/src/components/nodes/nodes-list.tsx

Lines changed: 132 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useCallback, useRef } from 'react'
1+
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
22
import { useTranslation } from 'react-i18next'
33
import Node from '@/components/nodes/node'
44
import {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}

dashboard/src/components/users/users-table.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ const UsersTable = memo(() => {
324324
const totalUsers = usersData?.total || 0
325325
const totalPages = Math.ceil(totalUsers / itemsPerPage)
326326
const showLoadingSpinner = isLoading && isFirstLoadRef.current
327-
const isPageLoading = isChangingPage
327+
const isPageLoading = isChangingPage || (isFetching && !isFirstLoadRef.current && !isAutoRefreshingRef.current)
328328

329329
return (
330330
<div>

dashboard/src/pages/_dashboard.nodes.logs.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ export default function NodeLogs() {
5757

5858
const eventSourceRef = useRef<EventSource | null>(null)
5959

60-
const { data: nodes = [] } = useGetNodes({})
60+
const { data: nodesResponse } = useGetNodes({})
61+
const nodes = nodesResponse?.nodes || []
6162

6263
// Filter to only show connected nodes
6364
const connectedNodes = useMemo(() => nodes.filter(node => node.status === 'connected'), [nodes])

dashboard/src/pages/_dashboard.statistics.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ const Statistics = () => {
1414
const [selectedServer, setSelectedServer] = useState<string>('master')
1515

1616
// Fetch nodes for the selector
17-
const { data: nodesData, isLoading: isLoadingNodes } = useGetNodes(undefined, {
17+
const { data: nodesResponse, isLoading: isLoadingNodes } = useGetNodes(undefined, {
1818
query: {
1919
enabled: true,
2020
},
2121
})
2222

23+
// Extract nodes array from response
24+
const nodesData = nodesResponse?.nodes || []
25+
2326
// Use the getSystemStats API with proper query key and refetch interval
2427
const { data, error, isLoading } = useQuery({
2528
queryKey: getGetSystemStatsQueryKey(),
@@ -60,7 +63,7 @@ const Statistics = () => {
6063
{t('master')}
6164
</SelectItem>
6265
{nodesData
63-
?.filter((node: NodeResponse) => node.status === 'connected')
66+
.filter((node: NodeResponse) => node.status === 'connected')
6467
.map((node: NodeResponse) => (
6568
<SelectItem key={node.id} value={String(node.id)} className="text-xs sm:text-sm">
6669
{node.name}

0 commit comments

Comments
 (0)