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() }; } 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 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)}%
); } 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 [overview, setOverview] = useState(null); const [targetMeta, setTargetMeta] = useState(null); const [error, setError] = useState(""); const [loading, setLoading] = useState(true); useEffect(() => { let active = true; (async () => { setLoading(true); try { const [connections, xacts, cache, locksTable, activityTable, overviewData, targetInfo] = 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), apiFetch(`/targets/${id}/overview`, {}, tokens, refresh), apiFetch(`/targets/${id}`, {}, tokens, refresh), ]); if (!active) return; setSeries({ connections, xacts, cache }); setLocks(locksTable); setActivity(activityTable); setOverview(overviewData); setTargetMeta(targetInfo); 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( () => { 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] ); 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})` : ""}

{overview && (

Database Overview

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{formatBytes(overview.storage.disk_space.free_bytes)}
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 ?? "-"}

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 || "-"}
); }