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.warnings.length > 0 && ( <> Things to watch )}

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

{(overview.storage.all_databases || []).map((d) => ( ))}
DatabaseSize
{d.name}{formatBytes(d.size_bytes)}

Largest Tables (Top 5)

{(overview.storage.largest_tables || []).map((t, i) => ( ))}
TableSize
{t.schema}.{t.table}{formatBytes(t.size_bytes)}

Replication Clients

{(overview.replication.clients || []).map((c, i) => ( ))}
ClientStateLag
{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

{locks.map((l, i) => ( ))}
Type Mode Granted Relation PID
{l.locktype} {l.mode} {String(l.granted)} {l.relation || "-"} {l.pid}

Activity

{activity.map((a) => ( ))}
PID User State Wait
{a.pid} {a.usename} {a.state} {a.wait_event_type || "-"}
); }