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:
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user