158 lines
4.8 KiB
JavaScript
158 lines
4.8 KiB
JavaScript
import React, { useEffect, useMemo, useState } from "react";
|
|
import { useParams } from "react-router-dom";
|
|
import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
|
import { apiFetch } from "../api";
|
|
import { useAuth } from "../state";
|
|
|
|
const ranges = {
|
|
"15m": 15 * 60 * 1000,
|
|
"1h": 60 * 60 * 1000,
|
|
"24h": 24 * 60 * 60 * 1000,
|
|
"7d": 7 * 24 * 60 * 60 * 1000,
|
|
};
|
|
|
|
function toQueryRange(range) {
|
|
const to = new Date();
|
|
const from = new Date(to.getTime() - ranges[range]);
|
|
return { from: from.toISOString(), to: to.toISOString() };
|
|
}
|
|
|
|
async function loadMetric(targetId, metric, range, tokens, refresh) {
|
|
const { from, to } = toQueryRange(range);
|
|
return apiFetch(
|
|
`/targets/${targetId}/metrics?metric=${encodeURIComponent(metric)}&from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`,
|
|
{},
|
|
tokens,
|
|
refresh
|
|
);
|
|
}
|
|
|
|
export function TargetDetailPage() {
|
|
const { id } = useParams();
|
|
const { tokens, refresh } = useAuth();
|
|
const [range, setRange] = useState("1h");
|
|
const [series, setSeries] = useState({});
|
|
const [locks, setLocks] = useState([]);
|
|
const [activity, setActivity] = useState([]);
|
|
const [error, setError] = useState("");
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
let active = true;
|
|
(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [connections, xacts, cache, locksTable, activityTable] = 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),
|
|
]);
|
|
if (!active) return;
|
|
setSeries({ connections, xacts, cache });
|
|
setLocks(locksTable);
|
|
setActivity(activityTable);
|
|
setError("");
|
|
} catch (e) {
|
|
if (active) setError(String(e.message || e));
|
|
} finally {
|
|
if (active) setLoading(false);
|
|
}
|
|
})();
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, [id, range, tokens, refresh]);
|
|
|
|
const chartData = useMemo(
|
|
() =>
|
|
(series.connections || []).map((point, idx) => ({
|
|
ts: new Date(point.ts).toLocaleTimeString(),
|
|
connections: point.value,
|
|
xacts: series.xacts?.[idx]?.value || 0,
|
|
cache: series.cache?.[idx]?.value || 0,
|
|
})),
|
|
[series]
|
|
);
|
|
|
|
if (loading) return <div className="card">Lade Target Detail...</div>;
|
|
if (error) return <div className="card error">{error}</div>;
|
|
|
|
return (
|
|
<div>
|
|
<h2>Target Detail #{id}</h2>
|
|
<div className="range-picker">
|
|
{Object.keys(ranges).map((r) => (
|
|
<button key={r} onClick={() => setRange(r)} className={r === range ? "active" : ""}>
|
|
{r}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="card" style={{ height: 320 }}>
|
|
<h3>Connections / TPS approx / Cache hit ratio</h3>
|
|
<ResponsiveContainer width="100%" height="85%">
|
|
<LineChart data={chartData}>
|
|
<XAxis dataKey="ts" hide />
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Line type="monotone" dataKey="connections" stroke="#38bdf8" dot={false} />
|
|
<Line type="monotone" dataKey="xacts" stroke="#22c55e" dot={false} />
|
|
<Line type="monotone" dataKey="cache" stroke="#f59e0b" dot={false} />
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
<div className="grid two">
|
|
<div className="card">
|
|
<h3>Locks</h3>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Type</th>
|
|
<th>Mode</th>
|
|
<th>Granted</th>
|
|
<th>Relation</th>
|
|
<th>PID</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{locks.map((l, i) => (
|
|
<tr key={i}>
|
|
<td>{l.locktype}</td>
|
|
<td>{l.mode}</td>
|
|
<td>{String(l.granted)}</td>
|
|
<td>{l.relation || "-"}</td>
|
|
<td>{l.pid}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div className="card">
|
|
<h3>Activity</h3>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>PID</th>
|
|
<th>User</th>
|
|
<th>State</th>
|
|
<th>Wait</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{activity.map((a) => (
|
|
<tr key={a.pid}>
|
|
<td>{a.pid}</td>
|
|
<td>{a.usename}</td>
|
|
<td>{a.state}</td>
|
|
<td>{a.wait_event_type || "-"}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|