fix(ui): exit infinite spinner with a retry card on failed initial load

List pages wrapped content in <Spin spinning={!fetched}> where 'fetched' only flipped true once data arrived. With staleTime: Infinity + retry: 1, a transient network error on first load left the query in a permanent error state and the spinner stuck forever.

Now 'fetched' also settles on query.isError, and a failed load shows a Result error card with a Refresh button that self-heals when the backend returns, mirroring the existing XrayPage pattern. Applied to clients, inbounds, groups, nodes, and the dashboard.

Fixes #4723
This commit is contained in:
MHSanaei
2026-06-01 07:43:32 +02:00
parent dd14e9b3b0
commit b9cbc0c1e8
9 changed files with 60 additions and 9 deletions
+9 -2
View File
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { Card, Col, ConfigProvider, Layout, Modal, Row, Spin, Statistic, message } from 'antd';
import { Button, Card, Col, ConfigProvider, Layout, Modal, Result, Row, Spin, Statistic, message } from 'antd';
import {
CheckCircleOutlined,
CloseCircleOutlined,
@@ -29,7 +29,7 @@ export default function NodesPage() {
const [messageApi, messageContextHolder] = message.useMessage();
useEffect(() => { setMessageInstance(messageApi); }, [messageApi]);
const { nodes, loading, fetched, totals } = useNodesQuery();
const { nodes, loading, fetched, fetchError, refetch, totals } = useNodesQuery();
const { create, update, remove, setEnable, testConnection, probe, updatePanels } = useNodeMutations();
const { data: latestVersion = '' } = useQuery({
@@ -159,6 +159,13 @@ export default function NodesPage() {
<Spin spinning={!fetched} delay={200} description={t('loading')} size="large">
{!fetched ? (
<div className="loading-spacer" />
) : fetchError ? (
<Result
status="error"
title={t('somethingWentWrong')}
subTitle={fetchError}
extra={<Button type="primary" loading={loading} onClick={() => refetch()}>{t('refresh')}</Button>}
/>
) : (
<Row gutter={[isMobile ? 8 : 16, isMobile ? 8 : 12]}>
<Col span={24}>