perf(admin): migrate AdminBanSystem tab fetching to React Query

All 10 tab data fetches + the initial status load move from manual useState +
useEffect + loadTabData(switch) pattern to per-tab useQuery hooks with enabled
gating. Each query syncs into the existing state vars via useEffect so the JSX
and mutation handlers stay unchanged; handleSearch/handleUnban/handleToggle/handleSet
now call query.refetch() directly; refresh button uses refetchActiveTab helper.

Net effect: tab switches return to cached data instantly (background revalidate),
no duplicate fetches, no manual loading/error wiring. The reports query has
reportHours in its queryKey so changing the period auto-refetches without the
extra useEffect that previously handled it.
This commit is contained in:
c0mrade
2026-05-26 12:55:28 +03:00
parent 7e9e51ba5a
commit 1e4d6dad47

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { AdminBackButton } from '../components/admin/AdminBackButton'; import { AdminBackButton } from '../components/admin/AdminBackButton';
import { useFocusTrap } from '../hooks/useFocusTrap'; import { useFocusTrap } from '../hooks/useFocusTrap';
import { import {
@@ -211,8 +212,6 @@ export default function AdminBanSystem() {
const [report, setReport] = useState<BanReportResponse | null>(null); const [report, setReport] = useState<BanReportResponse | null>(null);
const [health, setHealth] = useState<BanHealthResponse | null>(null); const [health, setHealth] = useState<BanHealthResponse | null>(null);
const [reportHours, setReportHours] = useState(24); const [reportHours, setReportHours] = useState(24);
const reportHoursRef = useRef(reportHours);
reportHoursRef.current = reportHours;
const [settingLoading, setSettingLoading] = useState<string | null>(null); const [settingLoading, setSettingLoading] = useState<string | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -247,101 +246,143 @@ export default function AdminBanSystem() {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [actionLoading, setActionLoading] = useState<string | null>(null); const [actionLoading, setActionLoading] = useState<string | null>(null);
const loadStatus = useCallback(async () => { // React Query: status once at mount; each tab fetches lazily via `enabled`.
try { // Caching means switching tabs returns to cached data instantly (with background revalidate).
setLoading(true); const statusQuery = useQuery({
const data = await banSystemApi.getStatus(); queryKey: ['ban-status'] as const,
setStatus(data); queryFn: () => banSystemApi.getStatus(),
if (!data.enabled || !data.configured) { });
const isReady = !!(status?.enabled && status?.configured);
const dashboardQuery = useQuery({
queryKey: ['ban-stats'] as const,
queryFn: () => banSystemApi.getStats(),
enabled: isReady && activeTab === 'dashboard',
});
const usersQuery = useQuery({
queryKey: ['ban-users'] as const,
queryFn: () => banSystemApi.getUsers({ limit: 50 }),
enabled: isReady && activeTab === 'users',
});
const punishmentsQuery = useQuery({
queryKey: ['ban-punishments'] as const,
queryFn: () => banSystemApi.getPunishments(),
enabled: isReady && activeTab === 'punishments',
});
const nodesQuery = useQuery({
queryKey: ['ban-nodes'] as const,
queryFn: () => banSystemApi.getNodes(),
enabled: isReady && activeTab === 'nodes',
});
const agentsQuery = useQuery({
queryKey: ['ban-agents'] as const,
queryFn: () => banSystemApi.getAgents(),
enabled: isReady && activeTab === 'agents',
});
const violationsQuery = useQuery({
queryKey: ['ban-violations'] as const,
queryFn: () => banSystemApi.getTrafficViolations(),
enabled: isReady && activeTab === 'violations',
});
const settingsQuery = useQuery({
queryKey: ['ban-settings'] as const,
queryFn: () => banSystemApi.getSettings(),
enabled: isReady && activeTab === 'settings',
});
const trafficQuery = useQuery({
queryKey: ['ban-traffic'] as const,
queryFn: () => banSystemApi.getTraffic(),
enabled: isReady && activeTab === 'traffic',
});
const reportsQuery = useQuery({
queryKey: ['ban-report', reportHours] as const,
queryFn: () => banSystemApi.getReport(reportHours),
enabled: isReady && activeTab === 'reports',
});
const healthQuery = useQuery({
queryKey: ['ban-health'] as const,
queryFn: () => banSystemApi.getHealth(),
enabled: isReady && activeTab === 'health',
});
// Sync query data into the existing state vars so the JSX + handlers stay unchanged
// (handleSearch overrides `users` with search results; useEffect re-syncs on next refetch).
useEffect(() => {
if (statusQuery.data) {
setStatus(statusQuery.data);
if (!statusQuery.data.enabled || !statusQuery.data.configured) {
setError(t('banSystem.notConfigured')); setError(t('banSystem.notConfigured'));
} }
} catch {
setError(t('banSystem.loadError'));
} finally {
setLoading(false);
} }
}, [t]); if (statusQuery.isError) setError(t('banSystem.loadError'));
}, [statusQuery.data, statusQuery.isError, t]);
const loadTabData = useCallback(
async (tab: TabType) => {
try {
setLoading(true);
setError(null);
switch (tab) {
case 'dashboard': {
const statsData = await banSystemApi.getStats();
setStats(statsData);
break;
}
case 'users': {
const usersData = await banSystemApi.getUsers({ limit: 50 });
setUsers(usersData);
break;
}
case 'punishments': {
const punishmentsData = await banSystemApi.getPunishments();
setPunishments(punishmentsData);
break;
}
case 'nodes': {
const nodesData = await banSystemApi.getNodes();
setNodes(nodesData);
break;
}
case 'agents': {
const agentsData = await banSystemApi.getAgents();
setAgents(agentsData);
break;
}
case 'violations': {
const violationsData = await banSystemApi.getTrafficViolations();
setViolations(violationsData);
break;
}
case 'settings': {
const settingsData = await banSystemApi.getSettings();
setSettings(settingsData);
break;
}
case 'traffic': {
const trafficData = await banSystemApi.getTraffic();
setTraffic(trafficData);
break;
}
case 'reports': {
const reportData = await banSystemApi.getReport(reportHoursRef.current);
setReport(reportData);
break;
}
case 'health': {
const healthData = await banSystemApi.getHealth();
setHealth(healthData);
break;
}
}
} catch {
setError(t('banSystem.loadError'));
} finally {
setLoading(false);
}
},
[t],
);
useEffect(() => { useEffect(() => {
loadStatus(); if (dashboardQuery.data) setStats(dashboardQuery.data);
}, [loadStatus]); }, [dashboardQuery.data]);
useEffect(() => { useEffect(() => {
if (status?.enabled && status?.configured) { if (usersQuery.data) setUsers(usersQuery.data);
loadTabData(activeTab); }, [usersQuery.data]);
} useEffect(() => {
}, [activeTab, status, loadTabData]); if (punishmentsQuery.data) setPunishments(punishmentsQuery.data);
}, [punishmentsQuery.data]);
useEffect(() => {
if (nodesQuery.data) setNodes(nodesQuery.data);
}, [nodesQuery.data]);
useEffect(() => {
if (agentsQuery.data) setAgents(agentsQuery.data);
}, [agentsQuery.data]);
useEffect(() => {
if (violationsQuery.data) setViolations(violationsQuery.data);
}, [violationsQuery.data]);
useEffect(() => {
if (settingsQuery.data) setSettings(settingsQuery.data);
}, [settingsQuery.data]);
useEffect(() => {
if (trafficQuery.data) setTraffic(trafficQuery.data);
}, [trafficQuery.data]);
useEffect(() => {
if (reportsQuery.data) setReport(reportsQuery.data);
}, [reportsQuery.data]);
useEffect(() => {
if (healthQuery.data) setHealth(healthQuery.data);
}, [healthQuery.data]);
// Map activeTab → its query (used for `loading` derivation and refetchActiveTab below).
const activeTabQuery =
activeTab === 'dashboard'
? dashboardQuery
: activeTab === 'users'
? usersQuery
: activeTab === 'punishments'
? punishmentsQuery
: activeTab === 'nodes'
? nodesQuery
: activeTab === 'agents'
? agentsQuery
: activeTab === 'violations'
? violationsQuery
: activeTab === 'settings'
? settingsQuery
: activeTab === 'traffic'
? trafficQuery
: activeTab === 'reports'
? reportsQuery
: healthQuery;
// Derive `loading` from status + active tab query.
useEffect(() => {
setLoading(statusQuery.isLoading || activeTabQuery.isFetching);
if (activeTabQuery.isError) setError(t('banSystem.loadError'));
}, [statusQuery.isLoading, activeTabQuery.isFetching, activeTabQuery.isError, t]);
const refetchActiveTab = useCallback(() => {
void activeTabQuery.refetch();
}, [activeTabQuery]);
const handleSearch = async () => { const handleSearch = async () => {
if (!searchQuery.trim()) { if (!searchQuery.trim()) {
loadTabData('users'); void usersQuery.refetch();
return; return;
} }
try { try {
@@ -371,7 +412,7 @@ export default function AdminBanSystem() {
try { try {
setActionLoading(userId); setActionLoading(userId);
await banSystemApi.unbanUser(userId); await banSystemApi.unbanUser(userId);
loadTabData('punishments'); void punishmentsQuery.refetch();
} catch { } catch {
setError(t('banSystem.loadError')); setError(t('banSystem.loadError'));
} finally { } finally {
@@ -383,7 +424,7 @@ export default function AdminBanSystem() {
try { try {
setSettingLoading(key); setSettingLoading(key);
await banSystemApi.toggleSetting(key); await banSystemApi.toggleSetting(key);
loadTabData('settings'); void settingsQuery.refetch();
} catch { } catch {
setError(t('banSystem.loadError')); setError(t('banSystem.loadError'));
} finally { } finally {
@@ -395,7 +436,7 @@ export default function AdminBanSystem() {
try { try {
setSettingLoading(key); setSettingLoading(key);
await banSystemApi.setSetting(key, value); await banSystemApi.setSetting(key, value);
loadTabData('settings'); void settingsQuery.refetch();
} catch { } catch {
setError(t('banSystem.loadError')); setError(t('banSystem.loadError'));
} finally { } finally {
@@ -407,11 +448,7 @@ export default function AdminBanSystem() {
setReportHours(hours); setReportHours(hours);
}; };
useEffect(() => { // (reports query auto-refetches when reportHours changes — it's in the queryKey)
if (activeTab === 'reports' && status?.enabled) {
loadTabData('reports');
}
}, [reportHours, activeTab, status, loadTabData]);
const formatBytes = (bytes: number) => { const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 B'; if (bytes === 0) return '0 B';
@@ -572,7 +609,7 @@ export default function AdminBanSystem() {
</div> </div>
</div> </div>
<button <button
onClick={() => loadTabData(activeTab)} onClick={refetchActiveTab}
disabled={loading} disabled={loading}
className="flex items-center gap-2 rounded-lg bg-dark-800 px-4 py-2 text-dark-300 transition-colors hover:bg-dark-700 hover:text-dark-100 disabled:opacity-50" className="flex items-center gap-2 rounded-lg bg-dark-800 px-4 py-2 text-dark-300 transition-colors hover:bg-dark-700 hover:text-dark-100 disabled:opacity-50"
> >