import React, { useEffect, useRef, useState } from "react"; import { Link } from "react-router-dom"; import { apiFetch } from "../api"; import { useAuth } from "../state"; const emptyForm = { name: "", host: "", port: 5432, dbname: "postgres", username: "", password: "", sslmode: "prefer", use_pg_stat_statements: true, discover_all_databases: false, owner_user_ids: [], tags: {}, }; const emptyEditForm = { id: null, name: "", host: "", port: 5432, dbname: "", username: "", password: "", sslmode: "prefer", use_pg_stat_statements: true, owner_user_ids: [], }; function toggleOwner(ids, userId) { return ids.includes(userId) ? ids.filter((id) => id !== userId) : [...ids, userId]; } function OwnerPicker({ candidates, selectedIds, onToggle, query, onQueryChange }) { const pickerRef = useRef(null); const [open, setOpen] = useState(false); const filtered = candidates.filter((item) => item.email.toLowerCase().includes(query.trim().toLowerCase()) ); const selected = candidates.filter((item) => selectedIds.includes(item.user_id)); useEffect(() => { const onPointerDown = (event) => { if (!pickerRef.current?.contains(event.target)) { setOpen(false); } }; document.addEventListener("mousedown", onPointerDown); return () => document.removeEventListener("mousedown", onPointerDown); }, []); return (
{selected.length > 0 ? ( selected.map((item) => ( )) ) : ( No owners selected yet. )}
onQueryChange(e.target.value)} onFocus={() => setOpen(true)} onClick={() => setOpen(true)} placeholder="Search users by email..." />
{open && (
{filtered.map((item) => { const active = selectedIds.includes(item.user_id); return ( ); })} {filtered.length === 0 &&
No matching users.
}
)}
); } export function TargetsPage() { const { tokens, refresh, me } = useAuth(); const [targets, setTargets] = useState([]); const [form, setForm] = useState(emptyForm); const [editForm, setEditForm] = useState(emptyEditForm); const [editing, setEditing] = useState(false); const [error, setError] = useState(""); const [loading, setLoading] = useState(true); const [testState, setTestState] = useState({ loading: false, message: "", ok: null }); const [saveState, setSaveState] = useState({ loading: false, message: "" }); const [ownerCandidates, setOwnerCandidates] = useState([]); const [createOwnerQuery, setCreateOwnerQuery] = useState(""); const [editOwnerQuery, setEditOwnerQuery] = useState(""); const canManage = me?.role === "admin" || me?.role === "operator"; const load = async () => { setLoading(true); try { if (canManage) { const [targetRows, candidates] = await Promise.all([ apiFetch("/targets", {}, tokens, refresh), apiFetch("/targets/owner-candidates", {}, tokens, refresh), ]); setTargets(targetRows); setOwnerCandidates(candidates); } else { setTargets(await apiFetch("/targets", {}, tokens, refresh)); } setError(""); } catch (e) { setError(String(e.message || e)); } finally { setLoading(false); } }; useEffect(() => { load(); }, [canManage]); const createTarget = async (e) => { e.preventDefault(); try { await apiFetch("/targets", { method: "POST", body: JSON.stringify(form) }, tokens, refresh); setForm(emptyForm); await load(); } catch (e) { setError(String(e.message || e)); } }; const testConnection = async () => { setTestState({ loading: true, message: "", ok: null }); try { const result = await apiFetch( "/targets/test-connection", { method: "POST", body: JSON.stringify({ host: form.host, port: form.port, dbname: form.dbname, username: form.username, password: form.password, sslmode: form.sslmode, }), }, tokens, refresh ); setTestState({ loading: false, message: `${result.message} (PostgreSQL ${result.server_version})`, ok: true }); } catch (e) { setTestState({ loading: false, message: String(e.message || e), ok: false }); } }; const deleteTarget = async (id) => { if (!confirm("Delete target?")) return; try { await apiFetch(`/targets/${id}`, { method: "DELETE" }, tokens, refresh); await load(); } catch (e) { setError(String(e.message || e)); } }; const startEdit = (target) => { setEditing(true); setSaveState({ loading: false, message: "" }); setEditOwnerQuery(""); setEditForm({ id: target.id, name: target.name, host: target.host, port: target.port, dbname: target.dbname, username: target.username, password: "", sslmode: target.sslmode, use_pg_stat_statements: target.use_pg_stat_statements !== false, owner_user_ids: Array.isArray(target.owner_user_ids) ? target.owner_user_ids : [], }); }; const cancelEdit = () => { setEditing(false); setEditForm(emptyEditForm); setSaveState({ loading: false, message: "" }); }; const saveEdit = async (e) => { e.preventDefault(); if (!editForm.id) return; setSaveState({ loading: true, message: "" }); try { const payload = { name: editForm.name, host: editForm.host, port: Number(editForm.port), dbname: editForm.dbname, username: editForm.username, sslmode: editForm.sslmode, use_pg_stat_statements: !!editForm.use_pg_stat_statements, owner_user_ids: editForm.owner_user_ids || [], }; if (editForm.password.trim()) payload.password = editForm.password; await apiFetch(`/targets/${editForm.id}`, { method: "PUT", body: JSON.stringify(payload) }, tokens, refresh); setSaveState({ loading: false, message: "Target updated." }); setEditing(false); setEditForm(emptyEditForm); await load(); } catch (e) { setSaveState({ loading: false, message: String(e.message || e) }); } }; return (

Targets Management

{error &&
{error}
} {canManage && (

New Target

setForm({ ...form, name: e.target.value })} required />
setForm({ ...form, host: e.target.value })} required />
setForm({ ...form, port: Number(e.target.value) })} type="number" required />
setForm({ ...form, dbname: e.target.value })} required /> {form.discover_all_databases ? "Connection database used to crawl all available databases on this instance." : "Single database to monitor for this target."}
setForm({ ...form, username: e.target.value })} required />
setForm({ ...form, password: e.target.value })} required />
If you see "rejected SSL upgrade", switch to disable.
setForm({ ...form, owner_user_ids: toggleOwner(form.owner_user_ids, userId) })} /> Only selected users will receive email notifications for this target's alerts.
{testState.message && (
{testState.message}
)}
)} {canManage && editing && (

Edit Target

setEditForm({ ...editForm, name: e.target.value })} required />
setEditForm({ ...editForm, host: e.target.value })} required />
setEditForm({ ...editForm, port: Number(e.target.value) })} required />
setEditForm({ ...editForm, dbname: e.target.value })} required />
setEditForm({ ...editForm, username: e.target.value })} required />
setEditForm({ ...editForm, password: e.target.value })} />
setEditForm({ ...editForm, owner_user_ids: toggleOwner(editForm.owner_user_ids, userId) })} /> Only selected users will receive email notifications for this target's alerts.
{saveState.message &&
{saveState.message}
}
)} {canManage && (

Troubleshooting

Quick checks for the most common connection issues.

Connection refused: host/port is wrong or database is unreachable.

rejected SSL upgrade: set SSL mode to disable.

localhost points to the backend container itself, not your host machine.

)}
{loading ? (

Loading targets...

) : ( {targets.map((t) => ( ))}
Name Host DB Owners Query Insights Actions
{t.name} {t.host}:{t.port} {t.dbname} {Array.isArray(t.owner_user_ids) ? t.owner_user_ids.length : 0} {t.use_pg_stat_statements ? "Enabled" : "Disabled"}
Details {canManage && ( )} {canManage && ( )}
)}
); }