import React, { useEffect, useMemo, useState } from "react"; import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, CartesianGrid, } from "recharts"; const VIZ = { STATS: "stats", COMPUTERS: "computers", COMPUTER_DETAILS: "computer_details", NET_GRAPH: "net_graph", }; function fmtTs(ts) { if (!ts) return ""; return String(ts).replace("T", " ").replace("Z", ""); } async function fetchJson(url, { method = "GET", headers = {}, body } = {}) { const res = await fetch(url, { method, credentials: "include", headers: { "Content-Type": "application/json", ...headers, }, body: body ? JSON.stringify(body) : undefined, }); const text = await res.text(); let data; try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; } if (!res.ok) { const msg = data?.error || data?.message || res.statusText; throw new Error(msg); } return data; } /** ✅ Use existing theme.css look: panel / btn / select / input / pill */ function Panel({ title, right, children, subtitle }) { return (
{title}
{subtitle ? (
{subtitle}
) : null}
{right}
{children}
); } function Select({ label, value, onChange, options, disabled }) { return ( ); } function Pill({ children }) { return {children}; } function Table({ columns, rows, emptyText = "Нема податоци." }) { return (
{columns.map((c) => ( ))} {rows.length === 0 ? ( ) : ( rows.map((r, idx) => ( {columns.map((c) => ( ))} )) )}
{c.title}
{emptyText}
{c.render ? c.render(r) : String(r[c.key] ?? "")}
); } export default function NetIntelViz({ baseUrl = "" }) { const [loading, setLoading] = useState(false); const [err, setErr] = useState(""); const [me, setMe] = useState(null); const [envs, setEnvs] = useState(["default"]); const [env, setEnv] = useState("default"); const [viz, setViz] = useState(VIZ.NET_GRAPH); const [stats, setStats] = useState(null); const [computers, setComputers] = useState([]); const [computerName, setComputerName] = useState(""); const [computerDetails, setComputerDetails] = useState(null); const [sinceHours, setSinceHours] = useState("24"); const [graph, setGraph] = useState(null); const api = useMemo(() => (baseUrl || "").replace(/\/+$/, ""), [baseUrl]); const envOptions = useMemo(() => { const uniq = [...new Set(envs)]; return uniq.map((e) => ({ value: e, label: e })); }, [envs]); const computerOptions = useMemo(() => { const opts = computers.map((c) => ({ value: c.name, label: c.name })); return [{ value: "", label: "all" }, ...opts]; }, [computers]); const vizOptions = [ { value: VIZ.STATS, label: "Stats (24h)" }, { value: VIZ.COMPUTERS, label: "Computers" }, { value: VIZ.COMPUTER_DETAILS, label: "Computer details" }, { value: VIZ.NET_GRAPH, label: "Network graph" }, ]; async function loadMe() { const data = await fetchJson(`${api}/api/me`); setMe(data?.user || null); } async function loadEnvs() { try { const data = await fetchJson(`${api}/api/admin/environments`); const list = data?.environments?.length ? data.environments : ["default"]; setEnvs(list); if (!list.includes(env)) setEnv(list[0] || "default"); } catch { setEnvs(["default"]); setEnv("default"); } } async function loadStats(selectedEnv) { const data = await fetchJson(`${api}/api/stats`, { headers: { "X-Env": selectedEnv || env || "default" }, }); setStats(data); } async function loadComputers(selectedEnv) { const data = await fetchJson(`${api}/api/computers`, { headers: { "X-Env": selectedEnv || env || "default" }, }); setComputers(Array.isArray(data) ? data : []); } async function loadComputerDetails(name) { if (!name) { setComputerDetails(null); return; } const data = await fetchJson(`${api}/api/computer/${encodeURIComponent(name)}`, { headers: { "X-Env": env || "default" }, }); setComputerDetails(data); } async function loadNetGraph() { const qs = new URLSearchParams(); qs.set("env", env || "default"); qs.set("since_hours", String(parseInt(sinceHours || "24", 10) || 24)); if (computerName) qs.set("computer", computerName); const data = await fetchJson(`${api}/api/graph/net?${qs.toString()}`, { headers: { "X-Env": env || "default" }, }); setGraph(data); } function MiniBipartiteGraph({ nodes = [], edges = [] }) { const comps = nodes.filter((n) => (n.subTitle || "").toLowerCase().includes("computer")); const ips = nodes.filter((n) => (n.subTitle || "").toLowerCase().includes("remote")); const width = 1100; const height = 520; const pad = 40; const compPos = comps.map((n, i) => ({ ...n, x: pad, y: pad + (i * (height - 2 * pad)) / Math.max(1, comps.length - 1), })); const ipPos = ips.map((n, i) => ({ ...n, x: width - pad, y: pad + (i * (height - 2 * pad)) / Math.max(1, ips.length - 1), })); const pos = new Map(); [...compPos, ...ipPos].forEach((n) => pos.set(n.id, n)); const shownEdges = edges.slice(0, 120); return (
{shownEdges.map((e, idx) => { const s = pos.get(e.source); const t = pos.get(e.target); if (!s || !t) return null; const v = Number(e.mainStat || 1); const strokeWidth = Math.max(1, Math.min(6, Math.log2(v + 1))); return ( ); })} {compPos.map((n) => ( {n.title} ))} {ipPos.map((n) => ( {n.title} ))}
Tip: ако има премногу линии, намали Hours или избери конкретен Computer.
); } async function refreshAll(nextViz = viz, nextEnv = env) { setErr(""); setLoading(true); try { if (nextViz === VIZ.STATS) { await loadStats(nextEnv); } else if (nextViz === VIZ.COMPUTERS) { await loadComputers(nextEnv); } else if (nextViz === VIZ.COMPUTER_DETAILS) { await loadComputers(nextEnv); if (computerName) await loadComputerDetails(computerName); } else if (nextViz === VIZ.NET_GRAPH) { await loadComputers(nextEnv); await loadNetGraph(); } } catch (e) { setErr(e.message || "Грешка при вчитување."); } finally { setLoading(false); } } useEffect(() => { (async () => { setLoading(true); setErr(""); try { await loadMe(); await loadEnvs(); await refreshAll(VIZ.NET_GRAPH, env); } catch (e) { setErr(e.message || "Неуспешно поврзување."); } finally { setLoading(false); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [api]); const historySeries = useMemo(() => { const hist = computerDetails?.history || []; const rows = [...hist].reverse().slice(-80); return rows.map((r) => ({ t: fmtTs(r.timestamp), cpu: Number(r.cpu_usage ?? 0), ram: Number(r.ram_usage ?? 0), disk: Number(r.disk_usage ?? 0), sent: Number(r.network_sent_mb ?? 0), recv: Number(r.network_recv_mb ?? 0), })); }, [computerDetails]); const nodes = graph?.nodes || []; const edges = graph?.edges || []; const meta = graph?.meta || {}; return (
{/* HEADER / CONTROLS */} { setViz(v); refreshAll(v, env); }} options={vizOptions} /> {(viz === VIZ.COMPUTER_DETAILS || viz === VIZ.NET_GRAPH) && ( setSinceHours(v)} options={[ { value: "1", label: "1" }, { value: "6", label: "6" }, { value: "12", label: "12" }, { value: "24", label: "24" }, { value: "48", label: "48" }, { value: "72", label: "72" }, ]} /> )}
} > {err ? (
{err}
) : (
Избери “View” за да смениш што се прикажува.
)} {/* STATS */} {viz === VIZ.STATS && (
Sysmon Events
{stats?.total_sysmon_events ?? "—"}
Last 24 hours
Network Connections
{stats?.total_network_connections ?? "—"}
Last 24 hours
Server Time
{fmtTs(stats?.server_time) || "—"}
Now
)} {/* COMPUTERS */} {viz === VIZ.COMPUTERS && ( fmtTs(r.last_seen) }, ]} rows={computers} emptyText="No computers for this env." /> )} {/* COMPUTER DETAILS */} {viz === VIZ.COMPUTER_DETAILS && ( <> loadComputerDetails(computerName).catch((e) => setErr(e.message))} > Reload } > {!computerName ? (
Select a computer.
) : !computerDetails ? (
Loading…
) : (
Name
{computerDetails.computer?.name}
{computerDetails.computer?.ip} • {computerDetails.computer?.os}
User
{computerDetails.computer?.user || "—"}
{fmtTs(computerDetails.computer?.first_seen)} → {fmtTs(computerDetails.computer?.last_seen)}
Total logs
{computerDetails.computer?.total_logs ?? "—"}
Env: {computerDetails.computer?.env_name || env}
)}
{historySeries.length === 0 ? (
No history data.
) : (
CPU / RAM / Disk
Network MB sent/recv
)}
fmtTs(r.timestamp) }, ]} rows={computerDetails?.recent_processes || []} />
(r.message || "").slice(0, 70) + ((r.message || "").length > 70 ? "…" : ""), }, { key: "timestamp", title: "Time", render: (r) => fmtTs(r.timestamp) }, ]} rows={computerDetails?.sysmon_events || []} />
fmtTs(r.timestamp) }, ]} rows={computerDetails?.network_connections || []} /> )} {/* NET GRAPH */} {viz === VIZ.NET_GRAPH && ( <> loadNetGraph().catch((e) => setErr(e.message))}> Reload graph } >
env: {meta?.env || env} hours: {meta?.since_hours ?? sinceHours} computer: {meta?.computer || "all"} nodes: {nodes.length} edges: {edges.length}
)} ); }