diff --git a/frontend/src/pages/TargetDetailPage.jsx b/frontend/src/pages/TargetDetailPage.jsx index 2a52ef5..a42cd2a 100644 --- a/frontend/src/pages/TargetDetailPage.jsx +++ b/frontend/src/pages/TargetDetailPage.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { useParams } from "react-router-dom"; import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; import { apiFetch } from "../api"; @@ -68,6 +68,7 @@ export function TargetDetailPage() { const { id } = useParams(); const { tokens, refresh, uiMode } = useAuth(); const [range, setRange] = useState("1h"); + const [liveMode, setLiveMode] = useState(false); const [series, setSeries] = useState({}); const [locks, setLocks] = useState([]); const [activity, setActivity] = useState([]); @@ -75,20 +76,27 @@ export function TargetDetailPage() { const [targetMeta, setTargetMeta] = useState(null); const [error, setError] = useState(""); const [loading, setLoading] = useState(true); + const refreshRef = useRef(refresh); + + useEffect(() => { + refreshRef.current = refresh; + }, [refresh]); useEffect(() => { let active = true; - (async () => { - setLoading(true); + const loadAll = async () => { + if (!series.connections?.length) { + setLoading(true); + } try { const [connections, xacts, cache, locksTable, activityTable, overviewData, targetInfo] = await Promise.all([ - loadMetric(id, "connections_total", range, tokens, refresh), - loadMetric(id, "xacts_total", range, tokens, refresh), - loadMetric(id, "cache_hit_ratio", range, tokens, refresh), - apiFetch(`/targets/${id}/locks`, {}, tokens, refresh), - apiFetch(`/targets/${id}/activity`, {}, tokens, refresh), - apiFetch(`/targets/${id}/overview`, {}, tokens, refresh), - apiFetch(`/targets/${id}`, {}, tokens, refresh), + loadMetric(id, "connections_total", range, tokens, refreshRef.current), + loadMetric(id, "xacts_total", range, tokens, refreshRef.current), + loadMetric(id, "cache_hit_ratio", range, tokens, refreshRef.current), + apiFetch(`/targets/${id}/locks`, {}, tokens, refreshRef.current), + apiFetch(`/targets/${id}/activity`, {}, tokens, refreshRef.current), + apiFetch(`/targets/${id}/overview`, {}, tokens, refreshRef.current), + apiFetch(`/targets/${id}`, {}, tokens, refreshRef.current), ]); if (!active) return; setSeries({ connections, xacts, cache }); @@ -102,11 +110,36 @@ export function TargetDetailPage() { } finally { if (active) setLoading(false); } - })(); + }; + + loadAll(); return () => { active = false; }; - }, [id, range, tokens, refresh]); + }, [id, range, tokens?.accessToken, tokens?.refreshToken]); + + useEffect(() => { + if (!liveMode) return; + let active = true; + const intervalId = setInterval(async () => { + try { + const [connections, xacts, cache] = await Promise.all([ + loadMetric(id, "connections_total", "15m", tokens, refreshRef.current), + loadMetric(id, "xacts_total", "15m", tokens, refreshRef.current), + loadMetric(id, "cache_hit_ratio", "15m", tokens, refreshRef.current), + ]); + if (!active) return; + setSeries({ connections, xacts, cache }); + } catch { + // Keep previous chart values if a live tick fails. + } + }, 1000); + + return () => { + active = false; + clearInterval(intervalId); + }; + }, [liveMode, id, tokens?.accessToken, tokens?.refreshToken]); const chartData = useMemo( () => { @@ -362,8 +395,28 @@ export function TargetDetailPage() { )}