From 79570521722a1f4b9e06fc55e295723af1b7cf03 Mon Sep 17 00:00:00 2001 From: nessi Date: Thu, 12 Feb 2026 13:57:32 +0100 Subject: [PATCH] Add live mode toggle for real-time chart updates Introduced a new "Live" button to enable real-time chart updates, refreshing data every second. Refactored data fetching to use `useRef` for `refresh` and updated styles for the live mode button, ensuring a seamless user experience. --- frontend/src/pages/TargetDetailPage.jsx | 79 +++++++++++++++++++++---- frontend/src/styles.css | 15 +++++ 2 files changed, 81 insertions(+), 13 deletions(-) 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() { )}
+ {Object.keys(ranges).map((r) => ( - ))} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 1e12f61..2e81812 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1157,12 +1157,27 @@ td { display: flex; gap: 8px; margin-bottom: 10px; + align-items: center; } .range-picker .active { border-color: var(--accent); } +.live-btn { + font-weight: 800; + letter-spacing: 0.04em; + border-color: #2da55e; + background: linear-gradient(180deg, #103725, #0d2a1d); + color: #bdf7d3; +} + +.live-btn.active { + border-color: #4de08d; + background: linear-gradient(180deg, #1b7a4a, #145f3a); + box-shadow: 0 0 0 2px #2ee68f33; +} + .login-wrap { min-height: 100vh; display: grid;