import React, { useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, 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() };
}
function formatBytes(value) {
if (value === null || value === undefined) return "-";
const units = ["B", "KB", "MB", "GB", "TB"];
let n = Number(value);
let i = 0;
while (n >= 1024 && i < units.length - 1) {
n /= 1024;
i += 1;
}
return `${n.toFixed(i >= 2 ? 2 : 0)} ${units[i]}`;
}
function formatSeconds(value) {
if (value === null || value === undefined) return "-";
if (value < 60) return `${value.toFixed(1)}s`;
if (value < 3600) return `${(value / 60).toFixed(1)}m`;
return `${(value / 3600).toFixed(1)}h`;
}
function formatNumber(value, digits = 2) {
if (value === null || value === undefined || Number.isNaN(Number(value))) return "-";
return Number(value).toFixed(digits);
}
function formatHostMetricUnavailable() {
return "N/A (agentless)";
}
function formatDiskSpaceAgentless(diskSpace) {
if (!diskSpace) return formatHostMetricUnavailable();
if (diskSpace.free_bytes !== null && diskSpace.free_bytes !== undefined) {
return formatBytes(diskSpace.free_bytes);
}
if (diskSpace.status === "unavailable") return formatHostMetricUnavailable();
return "-";
}
function MetricsTooltip({ active, payload, label }) {
if (!active || !payload || payload.length === 0) return null;
const row = payload[0]?.payload || {};
return (
{label}
connections: {formatNumber(row.connections, 0)}
tps: {formatNumber(row.tps, 2)}
cache: {formatNumber(row.cache, 2)}%
);
}
function didMetricSeriesChange(prev = [], next = []) {
if (!Array.isArray(prev) || !Array.isArray(next)) return true;
if (prev.length !== next.length) return true;
if (prev.length === 0 && next.length === 0) return false;
const prevLast = prev[prev.length - 1];
const nextLast = next[next.length - 1];
return prevLast?.ts !== nextLast?.ts || Number(prevLast?.value) !== Number(nextLast?.value);
}
function isTargetUnreachableError(err) {
return err?.code === "target_unreachable" || err?.status === 503;
}
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 navigate = useNavigate();
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([]);
const [overview, setOverview] = useState(null);
const [targetMeta, setTargetMeta] = useState(null);
const [owners, setOwners] = useState([]);
const [groupTargets, setGroupTargets] = useState([]);
const [offlineState, setOfflineState] = useState(null);
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const refreshRef = useRef(refresh);
useEffect(() => {
refreshRef.current = refresh;
}, [refresh]);
useEffect(() => {
let active = true;
const loadAll = async () => {
if (!series.connections?.length) {
setLoading(true);
}
try {
const [connections, xacts, cache, targetInfo, ownerRows, allTargets] = await Promise.all([
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}`, {}, tokens, refreshRef.current),
apiFetch(`/targets/${id}/owners`, {}, tokens, refreshRef.current),
apiFetch("/targets", {}, tokens, refreshRef.current),
]);
if (!active) return;
setSeries({ connections, xacts, cache });
setTargetMeta(targetInfo);
setOwners(ownerRows);
const groupId = targetInfo?.tags?.monitor_group_id;
if (groupId) {
const sameGroup = allTargets
.filter((item) => item?.tags?.monitor_group_id === groupId)
.sort((a, b) => (a.dbname || "").localeCompare(b.dbname || ""));
setGroupTargets(sameGroup);
} else {
setGroupTargets([]);
}
try {
const [locksTable, activityTable, overviewData] = await Promise.all([
apiFetch(`/targets/${id}/locks`, {}, tokens, refreshRef.current),
apiFetch(`/targets/${id}/activity`, {}, tokens, refreshRef.current),
apiFetch(`/targets/${id}/overview`, {}, tokens, refreshRef.current),
]);
if (!active) return;
setLocks(locksTable);
setActivity(activityTable);
setOverview(overviewData);
setOfflineState(null);
} catch (liveErr) {
if (!active) return;
if (isTargetUnreachableError(liveErr)) {
setLocks([]);
setActivity([]);
setOverview(null);
setOfflineState({
message:
"Target is currently unreachable. Check host/port, network route, SSL mode, and database availability.",
host: liveErr?.details?.host || targetInfo?.host || "-",
port: liveErr?.details?.port || targetInfo?.port || "-",
requestId: liveErr?.requestId || null,
});
} else {
throw liveErr;
}
}
setError("");
} catch (e) {
if (active) setError(String(e.message || e));
} finally {
if (active) setLoading(false);
}
};
loadAll();
return () => {
active = false;
};
}, [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;
const nextSeries = { connections, xacts, cache };
setSeries((prev) => {
const changed =
didMetricSeriesChange(prev.connections, nextSeries.connections) ||
didMetricSeriesChange(prev.xacts, nextSeries.xacts) ||
didMetricSeriesChange(prev.cache, nextSeries.cache);
return changed ? nextSeries : prev;
});
} catch {
// Keep previous chart values if a live tick fails.
}
}, 3000);
return () => {
active = false;
clearInterval(intervalId);
};
}, [liveMode, id, tokens?.accessToken, tokens?.refreshToken]);
const chartData = useMemo(
() => {
const con = series.connections || [];
const xacts = series.xacts || [];
const cache = series.cache || [];
return con.map((point, idx) => {
const prev = xacts[idx - 1];
const curr = xacts[idx];
let tps = 0;
if (prev && curr) {
const dt = (new Date(curr.ts).getTime() - new Date(prev.ts).getTime()) / 1000;
const dx = (curr.value || 0) - (prev.value || 0);
if (dt > 0 && dx >= 0) {
tps = dx / dt;
}
}
return {
ts: new Date(point.ts).toLocaleTimeString(),
connections: point.value,
tps,
cache: (cache[idx]?.value || 0) * 100,
};
});
},
[series]
);
const easySummary = useMemo(() => {
if (!overview) return null;
const latest = chartData[chartData.length - 1];
const issues = [];
const warnings = [];
if (overview.partial_failures?.length > 0) warnings.push("Some advanced metrics are currently unavailable.");
if ((overview.performance?.deadlocks || 0) > 0) issues.push("Deadlocks were detected.");
if ((overview.replication?.mode === "standby") && (overview.replication?.replay_lag_seconds || 0) > 5) {
issues.push("Replication lag is above 5 seconds.");
}
if ((latest?.cache || 0) > 0 && latest.cache < 90) warnings.push("Cache hit ratio is below 90%.");
if ((latest?.connections || 0) > 120) warnings.push("Connection count is relatively high.");
if ((locks?.length || 0) > 150) warnings.push("High number of active locks.");
const health = issues.length > 0 ? "problem" : warnings.length > 0 ? "warning" : "ok";
const message =
health === "ok"
? "Everything looks healthy. No major risks detected right now."
: health === "warning"
? "System is operational, but there are signals you should watch."
: "Attention required. Critical signals need investigation.";
return {
health,
message,
issues,
warnings,
latest,
};
}, [overview, chartData, locks]);
if (loading) return Loading target detail...
;
if (error) return {error}
;
const role = overview?.instance?.role || "-";
const isPrimary = role === "primary";
const isStandby = role === "standby";
return (
Target Detail {targetMeta?.name || `#${id}`}
{targetMeta?.dbname ? ` (${targetMeta.dbname})` : ""}
{groupTargets.length > 1 && (
)}
Responsible users:
{owners.length > 0 ? owners.map((item) => {item.email}) : none assigned}
{offlineState && (
Target Offline
{offlineState.message}
Host: {offlineState.host}
Port: {offlineState.port}
{offlineState.requestId ? Request ID: {offlineState.requestId} : null}
)}
{uiMode === "easy" && overview && easySummary && (
<>
System Health
{easySummary.message}
{easySummary.health === "ok" ? "OK" : easySummary.health === "warning" ? "Warning" : "Problem"}
{formatNumber(easySummary.latest?.connections, 0)}
Current Connections
{formatNumber(easySummary.latest?.tps, 2)}
Transactions/sec (approx)
{formatNumber(easySummary.latest?.cache, 2)}%
Cache Hit Ratio
Quick Explanation
{easySummary.issues.length === 0 && easySummary.warnings.length === 0 && (
No immediate problems were detected. Keep monitoring over time.
)}
{easySummary.issues.length > 0 && (
<>
Problems
{easySummary.issues.map((item, idx) => - {item}
)}
>
)}
{easySummary.warnings.length > 0 && (
<>
Things to watch
{easySummary.warnings.map((item, idx) => - {item}
)}
>
)}
Instance Summary
Role{overview.instance.role}
Uptime{formatSeconds(overview.instance.uptime_seconds)}
Target Port{targetMeta?.port ?? "-"}
Current DB Size{formatBytes(overview.storage.current_database_size_bytes)}
Replication Clients{overview.replication.active_replication_clients ?? "-"}
Autovacuum Workers{overview.performance.autovacuum_workers ?? "-"}
Activity Snapshot
Running Sessions{activity.filter((a) => a.state === "active").length}
Total Sessions{activity.length}
Current Locks{locks.length}
Deadlocks{overview.performance.deadlocks ?? 0}
>
)}
{uiMode === "dba" && overview && (
Database Overview
Agentless mode: host-level CPU, RAM, and free-disk metrics are not available.
PostgreSQL Version{overview.instance.server_version || "-"}
Role
{role}
Uptime{formatSeconds(overview.instance.uptime_seconds)}
Database{overview.instance.current_database || "-"}
Target Port
{targetMeta?.port ?? "-"}
Current DB Size{formatBytes(overview.storage.current_database_size_bytes)}
WAL Size{formatBytes(overview.storage.wal_directory_size_bytes)}
Free Disk{formatDiskSpaceAgentless(overview.storage.disk_space)}
Replay Lag
5 ? "lag-bad" : ""}>
{formatSeconds(overview.replication.replay_lag_seconds)}
Replication Slots{overview.replication.replication_slots_count ?? "-"}
Repl Clients{overview.replication.active_replication_clients ?? "-"}
Autovacuum Workers{overview.performance.autovacuum_workers ?? "-"}
Host CPU{formatHostMetricUnavailable()}
Host RAM{formatHostMetricUnavailable()}
Size all databases
| Database | Size |
{(overview.storage.all_databases || []).map((d) => (
| {d.name} | {formatBytes(d.size_bytes)} |
))}
Largest Tables (Top 5)
| Table | Size |
{(overview.storage.largest_tables || []).map((t, i) => (
| {t.schema}.{t.table} | {formatBytes(t.size_bytes)} |
))}
Replication Clients
| Client | State | Lag |
{(overview.replication.clients || []).map((c, i) => (
| {c.client_addr || c.application_name || "-"} |
{c.state || "-"} |
5 ? "lag-bad" : ""}>
{c.replay_lag_seconds != null ? formatSeconds(c.replay_lag_seconds) : formatBytes(c.replay_lag_bytes)}
|
))}
Performance Core Metrics
xact_commit{overview.performance.xact_commit ?? "-"}
xact_rollback{overview.performance.xact_rollback ?? "-"}
deadlocks{overview.performance.deadlocks ?? "-"}
temp_files{overview.performance.temp_files ?? "-"}
temp_bytes{formatBytes(overview.performance.temp_bytes)}
blk_read_time{overview.performance.blk_read_time ?? "-"} ms
blk_write_time{overview.performance.blk_write_time ?? "-"} ms
checkpoints_timed{overview.performance.checkpoints_timed ?? "-"}
checkpoints_req{overview.performance.checkpoints_req ?? "-"}
{overview.partial_failures?.length > 0 && (
Partial data warnings:
{overview.partial_failures.map((w, i) => - {w}
)}
)}
)}
{Object.keys(ranges).map((r) => (
))}
Connections / TPS (approx) / Cache Hit Ratio
} />
Locks
| Type |
Mode |
Granted |
Relation |
PID |
{locks.map((l, i) => (
| {l.locktype} |
{l.mode} |
{String(l.granted)} |
{l.relation || "-"} |
{l.pid} |
))}
Activity
| PID |
User |
State |
Wait |
{activity.map((a) => (
| {a.pid} |
{a.usename} |
{a.state} |
{a.wait_event_type || "-"} |
))}
);
}