mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-23 03:27:43 +09:00
refactor(ui): move ten more pollers onto usePolling
Plan 76 milestone E2, continuing f21abe7a. Poller ratchet 41 sites / 31 files
-> 31 / 22.
Migrated: Jobs, Monitors, MonitorDetail, DeliveryLog, Deployments, Databases,
VulnerabilityTab, and the service-detail Logs/Metrics/Overview tabs.
The trap flagged in the plan bit on four of them, so it is worth restating: the
interval effect's dependency array was doing double duty as the "reload because
the query changed" trigger —
useEffect(() => {
load(); // <- reload on filter change
pollRef.current = setInterval(load, POLL_MS);
return () => clearInterval(pollRef.current);
}, [isAdmin, load]); // <- load() is a useCallback of the filters
usePolling holds the callback in a ref precisely so it does not restart on every
render, which means a naive swap silently drops that reload and nothing fails to
compile. Each of those four keeps an explicit `useEffect(() => load(), [load])`
alongside the poll, with `immediate: false` so mount does not fetch twice.
Six magic interval literals became named constants while their lines were being
touched (LOG_REFRESH_MS, METRICS_REFRESH_MS, SCAN_POLL_MS, DEPLOY_POLL_MS,
ENGINE_POLL_MS).
Not migrated, and not mechanical: useMetrics, useContainerStatus and
useDeployJobStream are socket-fallback pollers driven by imperative
startPolling()/stopPolling() calls from socket event handlers, with a 3s grace
period before falling back. Expressing that as a declarative `enabled` flag is a
real restructure of each hook, and they only poll when the socket is already
down. Left for when they are next touched.
Gate: npm run build clean, npm run lint 0 errors / 953 warnings (one below the
954 baseline — four orphaned useRef imports removed), npm test 72 passed,
boundary check green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
375677ab0c
commit
1503c493f4
11 files changed
+68
-68
No files matched your search
@@ -48,25 +48,16 @@ const LEGACY_POLLERS = new Map(Object.entries({
|
||||
'components/docker/ContainersTab.jsx': 2,
|
||||
'components/monitoring/DoctorPanel.jsx': 1,
|
||||
'components/security/ScannerTab.jsx': 2,
|
||||
'components/security/VulnerabilityTab.jsx': 1,
|
||||
'components/server/OnboardingWizard.jsx': 2,
|
||||
'components/serverdetail/ServicesTab.jsx': 1,
|
||||
'components/servers/LinkPanelForm.jsx': 1,
|
||||
'components/service-detail/LogsTab.jsx': 1,
|
||||
'components/service-detail/MetricsTab.jsx': 1,
|
||||
'components/service-detail/OverviewTab.jsx': 1,
|
||||
'hooks/useContainerStatus.js': 1,
|
||||
'hooks/useDeployJobStream.js': 1,
|
||||
'hooks/useMetrics.js': 1,
|
||||
'pages/Dashboard.jsx': 4,
|
||||
'pages/Databases.jsx': 1,
|
||||
'pages/DeliveryLog.jsx': 1,
|
||||
'pages/DeployConsole.jsx': 1,
|
||||
'pages/Deployments.jsx': 1,
|
||||
'pages/ImportWizard.jsx': 1,
|
||||
'pages/Jobs.jsx': 1,
|
||||
'pages/MonitorDetail.jsx': 1,
|
||||
'pages/Monitors.jsx': 2,
|
||||
'pages/Monitors.jsx': 1,
|
||||
'pages/QueueDetail.jsx': 1,
|
||||
'pages/QueueOperations.jsx': 1,
|
||||
'pages/ServerDetail.jsx': 1,
|
||||
|
||||
@@ -5,6 +5,11 @@ import { useToast } from '../../contexts/ToastContext';
|
||||
import EmptyState from '@/components/EmptyState';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Pill, ScoreGauge } from '@/components/ds';
|
||||
import { usePolling } from '@/hooks/usePolling';
|
||||
|
||||
// Cadence while a Lynis scan is running.
|
||||
const SCAN_POLL_MS = 5000;
|
||||
|
||||
|
||||
const hardeningColor = (score) => {
|
||||
if (score >= 70) return 'var(--green)';
|
||||
@@ -23,13 +28,11 @@ const VulnerabilityTab = () => {
|
||||
loadStatus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let interval;
|
||||
if (scanStatus?.status === 'running') {
|
||||
interval = setInterval(loadScanStatus, 5000);
|
||||
}
|
||||
return () => clearInterval(interval);
|
||||
}, [scanStatus?.status]);
|
||||
// Only while a scan is actually running.
|
||||
usePolling(loadScanStatus, SCAN_POLL_MS, {
|
||||
enabled: scanStatus?.status === 'running',
|
||||
immediate: false,
|
||||
});
|
||||
|
||||
const loadStatus = async () => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -10,6 +10,11 @@ import { Input } from '@/components/ui/input';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { downloadBlob } from '@/utils/downloadBlob';
|
||||
import { usePolling } from '@/hooks/usePolling';
|
||||
|
||||
// Log tail refresh cadence while auto-refresh is on.
|
||||
const LOG_REFRESH_MS = 5000;
|
||||
|
||||
|
||||
const LOG_LEVELS = ['all', 'error', 'warn', 'info', 'debug'];
|
||||
|
||||
@@ -56,11 +61,7 @@ const LogsTab = ({ app }) => {
|
||||
return () => { cancelled = true; };
|
||||
}, [app?.id, deployJobId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRefresh) return;
|
||||
const interval = setInterval(loadLogs, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [autoRefresh, app.id, lineCount]);
|
||||
usePolling(loadLogs, LOG_REFRESH_MS, { enabled: autoRefresh, immediate: false });
|
||||
|
||||
useEffect(() => {
|
||||
if (autoScroll && logRef.current) {
|
||||
|
||||
@@ -2,6 +2,11 @@ import { useState, useEffect } from 'react';
|
||||
import api from '../../services/api';
|
||||
import { Gauge } from '@/components/ds';
|
||||
import EmptyState from '../EmptyState';
|
||||
import { usePolling } from '@/hooks/usePolling';
|
||||
|
||||
// Live metrics cadence.
|
||||
const METRICS_REFRESH_MS = 10000;
|
||||
|
||||
|
||||
const MetricsTab = ({ app }) => {
|
||||
const [stats, setStats] = useState(null);
|
||||
@@ -11,11 +16,9 @@ const MetricsTab = ({ app }) => {
|
||||
const isDocker = app.app_type === 'docker';
|
||||
const isPython = ['flask', 'django'].includes(app.app_type);
|
||||
|
||||
useEffect(() => {
|
||||
loadMetrics();
|
||||
const interval = setInterval(loadMetrics, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, [app.id]);
|
||||
// Load on mount and whenever the app changes; poll on top of that.
|
||||
useEffect(() => { loadMetrics(); }, [app.id]);
|
||||
usePolling(loadMetrics, METRICS_REFRESH_MS, { immediate: false });
|
||||
|
||||
async function loadMetrics() {
|
||||
try {
|
||||
|
||||
@@ -9,6 +9,11 @@ import { formatBytes } from '../../utils/formatBytes';
|
||||
import BandwidthSparkline from '../BandwidthSparkline';
|
||||
import ScheduledTasksCard from '../ScheduledTasksCard';
|
||||
import { KpiBand, MetricCard, Pill, Gauge, EnvTag } from '@/components/ds';
|
||||
import { usePolling } from '@/hooks/usePolling';
|
||||
|
||||
// Live metrics cadence.
|
||||
const METRICS_REFRESH_MS = 10000;
|
||||
|
||||
|
||||
// Deployment status → semantic tone (ds Pill kind / dot modifier)
|
||||
const DEPLOY_TONE = {
|
||||
@@ -36,11 +41,9 @@ const OverviewTab = ({ app, deployConfig }) => {
|
||||
const isDocker = app.app_type === 'docker';
|
||||
const isPython = ['flask', 'django'].includes(app.app_type);
|
||||
|
||||
useEffect(() => {
|
||||
loadMetrics();
|
||||
const interval = setInterval(loadMetrics, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, [app.id]);
|
||||
// Load on mount and whenever the app changes; poll on top of that.
|
||||
useEffect(() => { loadMetrics(); }, [app.id]);
|
||||
usePolling(loadMetrics, METRICS_REFRESH_MS, { immediate: false });
|
||||
|
||||
useEffect(() => {
|
||||
// Best-effort daily rollups; hide the card when there is no data.
|
||||
|
||||
@@ -35,6 +35,11 @@ import {
|
||||
} from '../components/databases/engineHelpers';
|
||||
import { listTables, connKey, connLabel, quoteIdent, ENGINE_META } from '../components/databases/dbAdapter';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { usePolling } from '@/hooks/usePolling';
|
||||
|
||||
// Cadence while an engine install is in flight.
|
||||
const ENGINE_POLL_MS = 4000;
|
||||
|
||||
|
||||
const SIDEBAR_KEY = 'serverkit-dbx-sidebar';
|
||||
|
||||
@@ -168,11 +173,10 @@ export default function Databases() {
|
||||
const installingCount = engineData.installed.filter(
|
||||
(e) => engineTreeStatus(e) === 'installing',
|
||||
).length;
|
||||
useEffect(() => {
|
||||
if (!installingCount) return undefined;
|
||||
const timer = setInterval(loadEngines, 4000);
|
||||
return () => clearInterval(timer);
|
||||
}, [installingCount, loadEngines]);
|
||||
usePolling(loadEngines, ENGINE_POLL_MS, {
|
||||
enabled: installingCount > 0,
|
||||
immediate: false,
|
||||
});
|
||||
|
||||
const openCatalog = useCallback((query = '') => {
|
||||
setCatalogQuery(query);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { Send, Inbox } from 'lucide-react';
|
||||
import api from '../services/api';
|
||||
import {
|
||||
@@ -17,6 +17,7 @@ import { useAuth } from '../contexts/AuthContext';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { timeAgo } from '../utils/timeAgo';
|
||||
import EmailProviders from '../components/EmailProviders';
|
||||
import { usePolling } from '@/hooks/usePolling';
|
||||
|
||||
const STATUSES = ['all', 'pending', 'sent', 'failed', 'skipped'];
|
||||
const CHANNELS = ['all', 'inapp', 'email', 'discord', 'slack', 'telegram', 'webhook'];
|
||||
@@ -83,7 +84,6 @@ export default function DeliveryLog() {
|
||||
const [channel, setChannel] = useState('all');
|
||||
const [filtersOpen, setFiltersOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const pollRef = useRef(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
@@ -99,12 +99,11 @@ export default function DeliveryLog() {
|
||||
}
|
||||
}, [status, channel]);
|
||||
|
||||
// Reload when the filters change; poll on top of that.
|
||||
useEffect(() => {
|
||||
if (!isAdmin) return undefined;
|
||||
load();
|
||||
pollRef.current = setInterval(load, POLL_MS);
|
||||
return () => clearInterval(pollRef.current);
|
||||
if (isAdmin) load();
|
||||
}, [isAdmin, load]);
|
||||
usePolling(load, POLL_MS, { enabled: isAdmin, immediate: false });
|
||||
|
||||
const onRetry = async (id) => {
|
||||
try {
|
||||
|
||||
@@ -14,6 +14,11 @@ import { useTopbarActions, useTopbarChrome } from '@/hooks/useTopbarActions';
|
||||
import { useTableSort } from '@/hooks/useTableSort';
|
||||
import { useColumnVisibility } from '@/hooks/useColumnVisibility';
|
||||
import RequiresDocker from '../components/RequiresDocker';
|
||||
import { usePolling } from '@/hooks/usePolling';
|
||||
|
||||
// Deploy activity cadence while auto-refresh is on.
|
||||
const DEPLOY_POLL_MS = 3000;
|
||||
|
||||
|
||||
// How many jobs one fetch pulls. The column rules run over what has been
|
||||
// fetched, so this is the reach of a filter until the user asks for more.
|
||||
@@ -160,11 +165,7 @@ const Deployments = () => {
|
||||
|
||||
useEffect(() => { loadJobs(); }, [loadJobs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRefresh) return undefined;
|
||||
const timer = setInterval(loadJobs, 3000);
|
||||
return () => clearInterval(timer);
|
||||
}, [autoRefresh, loadJobs]);
|
||||
usePolling(loadJobs, DEPLOY_POLL_MS, { enabled: autoRefresh, immediate: false });
|
||||
|
||||
const searched = useMemo(() => {
|
||||
const q = search.trim().toLowerCase();
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
// Activity shows a view bar, a DataTable of runs, and server-side pagination
|
||||
// over a job store that can hold six figures of scheduler-tick rows.
|
||||
// Wired to the real ApiService job methods (see frontend/src/services/api/jobs.js).
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { ListChecks, RefreshCw, RotateCcw, XCircle, Play, Clock } from 'lucide-react';
|
||||
import api from '../services/api';
|
||||
@@ -29,6 +29,7 @@ import { useColumnVisibility } from '@/hooks/useColumnVisibility';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { timeAgo } from '../utils/timeAgo';
|
||||
import { usePolling } from '@/hooks/usePolling';
|
||||
|
||||
const titleCase = (value = '') => value.charAt(0).toUpperCase() + value.slice(1);
|
||||
|
||||
@@ -188,7 +189,6 @@ export default function Jobs() {
|
||||
const [q, setQ] = useState('');
|
||||
const [page, setPage] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const pollRef = useRef(null);
|
||||
|
||||
// Table sort + column visibility, controlled so saved views can drive
|
||||
// them — same localStorage keys the DataTables used when uncontrolled.
|
||||
@@ -251,12 +251,11 @@ export default function Jobs() {
|
||||
return undefined;
|
||||
}, [isAdmin]);
|
||||
|
||||
// Reload when the query changes; poll on top of that.
|
||||
useEffect(() => {
|
||||
if (!isAdmin) return undefined;
|
||||
load();
|
||||
pollRef.current = setInterval(load, POLL_MS);
|
||||
return () => clearInterval(pollRef.current);
|
||||
if (isAdmin) load();
|
||||
}, [isAdmin, load]);
|
||||
usePolling(load, POLL_MS, { enabled: isAdmin, immediate: false });
|
||||
|
||||
const onFiltersChange = (next) => { setFilters(next); setPage(0); };
|
||||
const onSearch = (value) => { setQ(value.trim()); setPage(0); };
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// list. Its sections are a SegControl, not a tab strip: a nav under the page's
|
||||
// own header would be the same two-competing-headers problem the group layout
|
||||
// exists to avoid.
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import {
|
||||
Activity, ArrowLeft, BarChart3, CheckCircle2, Clock, ExternalLink, Lock,
|
||||
@@ -31,6 +31,7 @@ import PageLayout from '../layouts/PageLayout';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useTableSort } from '@/hooks/useTableSort';
|
||||
import { useColumnVisibility } from '@/hooks/useColumnVisibility';
|
||||
import { usePolling } from '@/hooks/usePolling';
|
||||
|
||||
const POLL_MS = 15000;
|
||||
|
||||
@@ -147,7 +148,6 @@ export default function MonitorDetail() {
|
||||
// row you are reading slides away on the next poll.
|
||||
const [frozen, setFrozen] = useState(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const pollRef = useRef(null);
|
||||
// Sort/column state for the check log lives above the early returns so the
|
||||
// hook order never changes; storageKey keeps the choices across visits.
|
||||
const { sorts, setSorts } = useTableSort({ storageKey: 'serverkit-table-monitor-checks-sort' });
|
||||
@@ -172,11 +172,9 @@ export default function MonitorDetail() {
|
||||
}
|
||||
}, [monitorId, rangeHours]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
pollRef.current = setInterval(load, POLL_MS);
|
||||
return () => clearInterval(pollRef.current);
|
||||
}, [load]);
|
||||
// Reload when the range changes; poll on top of that.
|
||||
useEffect(() => { load(); }, [load]);
|
||||
usePolling(load, POLL_MS, { immediate: false });
|
||||
|
||||
// Oldest-first for the chart; the API returns newest-first for the log.
|
||||
const series = useMemo(() => {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
// rows this client never loaded), while the grid chrome's column rules narrow
|
||||
// the rows already on screen. A saved view carries both — the server pair under
|
||||
// `page.serverFilters`, the rules under `columnFilters`.
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
ChevronRight, Globe, Pause, Play, Plus, Radar, RefreshCw,
|
||||
@@ -33,6 +33,7 @@ import { useTableSort } from '@/hooks/useTableSort';
|
||||
import { useColumnVisibility } from '@/hooks/useColumnVisibility';
|
||||
import useFocusParam from '@/hooks/useFocusParam';
|
||||
import { CHECK_TYPES, MONITOR_STATUS, monitorStateOf } from '../components/monitoring/monitorShared';
|
||||
import { usePolling } from '@/hooks/usePolling';
|
||||
|
||||
const POLL_MS = 15000;
|
||||
|
||||
@@ -193,7 +194,6 @@ export default function Monitors() {
|
||||
// Bumps once a second so "next check in 42s" actually counts down between
|
||||
// polls instead of sitting still for 15 seconds at a time.
|
||||
const [, setTick] = useState(0);
|
||||
const pollRef = useRef(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
@@ -208,11 +208,9 @@ export default function Monitors() {
|
||||
}
|
||||
}, [q, filters.status, filters.type]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
pollRef.current = setInterval(load, POLL_MS);
|
||||
return () => clearInterval(pollRef.current);
|
||||
}, [load]);
|
||||
// Reload when the search/filters change; poll on top of that.
|
||||
useEffect(() => { load(); }, [load]);
|
||||
usePolling(load, POLL_MS, { immediate: false });
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setTick((n) => n + 1), 1000);
|
||||
|
||||
Reference in new issue
Block a user