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
@@ -1,11 +1,13 @@
import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Button,
Card,
Col,
ConfigProvider,
Layout,
Modal,
Result,
Row,
Spin,
Statistic,
@@ -74,6 +76,7 @@ export default function InboundsPage() {
const {
fetched,
fetchError,
dbInbounds,
clientCount,
onlineClients,
@@ -559,6 +562,13 @@ export default function InboundsPage() {
<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" onClick={refresh}>{t('refresh')}</Button>}
/>
) : (
<Row gutter={[isMobile ? 8 : 16, 12]}>
<Col span={24}>
+4 -1
View File
@@ -248,7 +248,9 @@ export function useInbounds() {
if (lastOnlineQuery.data) setLastOnlineMap(lastOnlineQuery.data);
}, [lastOnlineQuery.data]);
const fetched = slimQuery.data !== undefined && defaultsQuery.data !== undefined;
const fetched = (slimQuery.data !== undefined || slimQuery.isError) && (defaultsQuery.data !== undefined || defaultsQuery.isError);
const fetchErrorSource = slimQuery.error || defaultsQuery.error;
const fetchError = fetchErrorSource ? (fetchErrorSource as Error).message : '';
const refresh = useCallback(async () => {
// Invalidate at the inbounds root so both `slim` (this page's list)
@@ -373,6 +375,7 @@ export function useInbounds() {
return {
fetched,
fetchError,
dbInbounds,
clientCount,
onlineClients,