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.
This commit is contained in:
2026-02-12 13:57:32 +01:00
parent 5674f2ea45
commit 7957052172
2 changed files with 81 additions and 13 deletions

View File

@@ -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 { useParams } from "react-router-dom";
import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import { apiFetch } from "../api"; import { apiFetch } from "../api";
@@ -68,6 +68,7 @@ export function TargetDetailPage() {
const { id } = useParams(); const { id } = useParams();
const { tokens, refresh, uiMode } = useAuth(); const { tokens, refresh, uiMode } = useAuth();
const [range, setRange] = useState("1h"); const [range, setRange] = useState("1h");
const [liveMode, setLiveMode] = useState(false);
const [series, setSeries] = useState({}); const [series, setSeries] = useState({});
const [locks, setLocks] = useState([]); const [locks, setLocks] = useState([]);
const [activity, setActivity] = useState([]); const [activity, setActivity] = useState([]);
@@ -75,20 +76,27 @@ export function TargetDetailPage() {
const [targetMeta, setTargetMeta] = useState(null); const [targetMeta, setTargetMeta] = useState(null);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const refreshRef = useRef(refresh);
useEffect(() => {
refreshRef.current = refresh;
}, [refresh]);
useEffect(() => { useEffect(() => {
let active = true; let active = true;
(async () => { const loadAll = async () => {
setLoading(true); if (!series.connections?.length) {
setLoading(true);
}
try { try {
const [connections, xacts, cache, locksTable, activityTable, overviewData, targetInfo] = await Promise.all([ const [connections, xacts, cache, locksTable, activityTable, overviewData, targetInfo] = await Promise.all([
loadMetric(id, "connections_total", range, tokens, refresh), loadMetric(id, "connections_total", range, tokens, refreshRef.current),
loadMetric(id, "xacts_total", range, tokens, refresh), loadMetric(id, "xacts_total", range, tokens, refreshRef.current),
loadMetric(id, "cache_hit_ratio", range, tokens, refresh), loadMetric(id, "cache_hit_ratio", range, tokens, refreshRef.current),
apiFetch(`/targets/${id}/locks`, {}, tokens, refresh), apiFetch(`/targets/${id}/locks`, {}, tokens, refreshRef.current),
apiFetch(`/targets/${id}/activity`, {}, tokens, refresh), apiFetch(`/targets/${id}/activity`, {}, tokens, refreshRef.current),
apiFetch(`/targets/${id}/overview`, {}, tokens, refresh), apiFetch(`/targets/${id}/overview`, {}, tokens, refreshRef.current),
apiFetch(`/targets/${id}`, {}, tokens, refresh), apiFetch(`/targets/${id}`, {}, tokens, refreshRef.current),
]); ]);
if (!active) return; if (!active) return;
setSeries({ connections, xacts, cache }); setSeries({ connections, xacts, cache });
@@ -102,11 +110,36 @@ export function TargetDetailPage() {
} finally { } finally {
if (active) setLoading(false); if (active) setLoading(false);
} }
})(); };
loadAll();
return () => { return () => {
active = false; 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( const chartData = useMemo(
() => { () => {
@@ -362,8 +395,28 @@ export function TargetDetailPage() {
</div> </div>
)} )}
<div className="range-picker"> <div className="range-picker">
<button
type="button"
className={`live-btn ${liveMode ? "active" : ""}`}
onClick={() => {
setLiveMode((prev) => {
const next = !prev;
if (next) setRange("15m");
return next;
});
}}
>
LIVE
</button>
{Object.keys(ranges).map((r) => ( {Object.keys(ranges).map((r) => (
<button key={r} onClick={() => setRange(r)} className={r === range ? "active" : ""}> <button
key={r}
onClick={() => {
setLiveMode(false);
setRange(r);
}}
className={r === range ? "active" : ""}
>
{r} {r}
</button> </button>
))} ))}

View File

@@ -1157,12 +1157,27 @@ td {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin-bottom: 10px; margin-bottom: 10px;
align-items: center;
} }
.range-picker .active { .range-picker .active {
border-color: var(--accent); 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 { .login-wrap {
min-height: 100vh; min-height: 100vh;
display: grid; display: grid;