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
+10 -1
View File
@@ -13,6 +13,7 @@ import {
Modal,
Pagination,
Popover,
Result,
Row,
Select,
Space,
@@ -191,11 +192,12 @@ export default function ClientsPage() {
summary: serverSummary,
allGroups,
setQuery,
inbounds, onlines, loading, fetched, subSettings,
inbounds, onlines, loading, fetched, fetchError, subSettings,
ipLimitEnable, tgBotEnable, expireDiff, trafficDiff, pageSize,
create, update, remove, bulkDelete, bulkAdjust, bulkAddToGroup, bulkRemoveFromGroup, attach, bulkAttach, detach, bulkDetach,
resetTraffic, resetAllTraffics, delDepleted, setEnable,
applyTrafficEvent, applyClientStatsEvent,
refresh,
hydrate,
} = useClients();
@@ -795,6 +797,13 @@ export default function ClientsPage() {
<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={refresh}>{t('refresh')}</Button>}
/>
) : (
<Row gutter={[isMobile ? 8 : 16, isMobile ? 8 : 12]}>
<Col span={24}>