source: lan-frontend/src/components/ComputerDetailsModal.jsx@ 2058e5c

Last change on this file since 2058e5c was 2058e5c, checked in by istevanoska <ilinastevanoska@…>, 6 months ago

Working / before login

  • Property mode set to 100644
File size: 14.5 KB
Line 
1import React, { useEffect, useMemo, useState } from "react";
2
3function severityForEventId(eventId) {
4 const id = parseInt(eventId, 10);
5 const high = new Set([1, 3, 8, 10]);
6 const med = new Set([5, 6, 7, 11, 22]);
7 if (high.has(id)) return "high";
8 if (med.has(id)) return "medium";
9 return "low";
10}
11
12export default function ComputerDetailsModal({ open, computer, onClose, onAskAI }) {
13 const [tab, setTab] = useState("overview");
14 const [loading, setLoading] = useState(false);
15 const [details, setDetails] = useState(null);
16 const [search, setSearch] = useState("");
17
18 // JSON viewer state
19 const [jsonOpen, setJsonOpen] = useState(false);
20 const [jsonTitle, setJsonTitle] = useState("");
21 const [jsonData, setJsonData] = useState(null);
22
23 useEffect(() => {
24 if (!open || !computer?.name) return;
25 let cancelled = false;
26
27 async function load() {
28 try {
29 setLoading(true);
30 const r = await fetch(`/api/computer/${encodeURIComponent(computer.name)}`);
31 const d = await r.json();
32 if (!cancelled) setDetails(d);
33 } catch (e) {
34 console.error("details error", e);
35 } finally {
36 if (!cancelled) setLoading(false);
37 }
38 }
39
40 load();
41 return () => {
42 cancelled = true;
43 };
44 }, [open, computer?.name]);
45
46 const filteredSysmon = useMemo(() => {
47 const list = details?.sysmon_events || [];
48 const q = search.trim().toLowerCase();
49 if (!q) return list;
50 return list.filter((ev) => {
51 const msg = (ev.message || "").toLowerCase();
52 const type = (ev.event_type || "").toLowerCase();
53 const id = String(ev.event_id ?? "");
54 return msg.includes(q) || type.includes(q) || id.includes(q);
55 });
56 }, [details, search]);
57
58 function openJson(ev) {
59 // server ти враќа: { event_id, event_type, message, timestamp, details }
60 setJsonTitle(`Sysmon Event ${ev?.event_id ?? ""} • ${ev?.event_type ?? ""}`);
61 setJsonData(ev?.details ?? ev); // ако нема details, покажи цел event
62 setJsonOpen(true);
63 }
64
65 function closeJson() {
66 setJsonOpen(false);
67 setJsonTitle("");
68 setJsonData(null);
69 }
70
71 if (!open) return null;
72
73 return (
74 <>
75 <div className="backdrop" onMouseDown={onClose}>
76 <div className="modal" onMouseDown={(e) => e.stopPropagation()}>
77 <div className="modal-head">
78 <div>
79 <div className="modal-title">{computer.name}</div>
80 <div className="modal-sub">
81 {computer.ip || "—"} • {computer.user || "—"} • {computer.status || "—"}
82 </div>
83 </div>
84
85 <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
86 <div className="tabs">
87 <button className={`tab ${tab === "overview" ? "active" : ""}`} onClick={() => setTab("overview")}>
88 Overview
89 </button>
90 <button className={`tab ${tab === "sysmon" ? "active" : ""}`} onClick={() => setTab("sysmon")}>
91 Sysmon
92 </button>
93 <button className={`tab ${tab === "processes" ? "active" : ""}`} onClick={() => setTab("processes")}>
94 Processes
95 </button>
96 <button className={`tab ${tab === "network" ? "active" : ""}`} onClick={() => setTab("network")}>
97 Network
98 </button>
99 </div>
100 <button className="btn" onClick={onClose}>
101 Close
102 </button>
103 </div>
104 </div>
105
106 <div style={{ padding: 16 }}>
107 {loading && <div style={{ color: "#94a3b8" }}>Loading…</div>}
108
109 {!loading && details && tab === "overview" && (
110 <div className="panel">
111 <div className="panel-head">
112 <div className="panel-title">System Overview</div>
113 <button className="btn primary" onClick={() => onAskAI(computer.name)}>
114 Ask AI about this PC
115 </button>
116 </div>
117 <div className="panel-body" style={{ display: "grid", gap: 10 }}>
118 <div>
119 <b>User:</b> {details.computer?.user}
120 </div>
121 <div>
122 <b>IP:</b> {details.computer?.ip}
123 </div>
124 <div>
125 <b>OS:</b> {details.computer?.os}
126 </div>
127 <div>
128 <b>First seen:</b> {details.computer?.first_seen}
129 </div>
130 <div>
131 <b>Last seen:</b> {details.computer?.last_seen}
132 </div>
133 <div>
134 <b>Total logs:</b> {details.computer?.total_logs}
135 </div>
136 </div>
137 </div>
138 )}
139
140 {!loading && details && tab === "sysmon" && (
141 <div className="panel">
142 <div className="panel-head" style={{ alignItems: "center" }}>
143 <div className="panel-title">Sysmon Events</div>
144 <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
145 <span className="pill">{filteredSysmon.length} events</span>
146 <input
147 className="input"
148 style={{ maxWidth: 380 }}
149 placeholder="Search by id / type / message…"
150 value={search}
151 onChange={(e) => setSearch(e.target.value)}
152 />
153 </div>
154 </div>
155
156 <div className="panel-body">
157 <div className="table-wrap">
158 <table>
159 <thead>
160 <tr>
161 <th style={{ width: 90 }}>Time</th>
162 <th style={{ width: 70 }}>ID</th>
163 <th style={{ width: 170 }}>Type</th>
164 <th style={{ width: 110 }}>Severity</th>
165 <th>Message</th>
166 <th style={{ width: 90 }}>View</th>
167 </tr>
168 </thead>
169 <tbody>
170 {filteredSysmon.slice(0, 200).map((ev, idx) => {
171 const sev = severityForEventId(ev.event_id);
172 return (
173 <tr key={idx}>
174 <td>{ev.timestamp ? new Date(ev.timestamp).toLocaleTimeString() : "—"}</td>
175 <td>
176 <b>{ev.event_id}</b>
177 </td>
178 <td>{ev.event_type}</td>
179 <td>
180 <span className={`badge ${sev}`}>{sev.toUpperCase()}</span>
181 </td>
182 <td
183 style={{
184 fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
185 fontSize: 12,
186 }}
187 >
188 {(ev.message || "").slice(0, 140)}
189 </td>
190 <td>
191 <button className="btn" onClick={() => openJson(ev)}>
192 View
193 </button>
194 </td>
195 </tr>
196 );
197 })}
198
199 {filteredSysmon.length === 0 && (
200 <tr>
201 <td colSpan="6" style={{ color: "#94a3b8" }}>
202 No sysmon events.
203 </td>
204 </tr>
205 )}
206 </tbody>
207 </table>
208 </div>
209
210 {filteredSysmon.length > 200 && (
211 <div style={{ marginTop: 10, color: "#94a3b8", fontSize: 12 }}>
212 Showing first 200 (for speed).
213 </div>
214 )}
215 </div>
216 </div>
217 )}
218
219 {!loading && details && tab === "processes" && (
220 <div className="panel">
221 <div className="panel-head">
222 <div className="panel-title">Recent Processes</div>
223 </div>
224 <div className="panel-body">
225 <div className="table-wrap">
226 <table>
227 <thead>
228 <tr>
229 <th>Time</th>
230 <th>PID</th>
231 <th>Name</th>
232 <th>User</th>
233 <th>CPU%</th>
234 <th>RAM MB</th>
235 <th>Cmdline</th>
236 </tr>
237 </thead>
238 <tbody>
239 {(details.recent_processes || []).slice(0, 150).map((p, idx) => (
240 <tr key={idx}>
241 <td>{p.timestamp ? new Date(p.timestamp).toLocaleTimeString() : "—"}</td>
242 <td>
243 <b>{p.pid}</b>
244 </td>
245 <td>{p.name}</td>
246 <td>{p.username || "—"}</td>
247 <td>{p.cpu_percent ?? 0}</td>
248 <td>{p.memory_mb ?? 0}</td>
249 <td
250 style={{
251 fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
252 fontSize: 12,
253 }}
254 >
255 {(p.cmdline || "").slice(0, 120)}
256 </td>
257 </tr>
258 ))}
259 {(details.recent_processes || []).length === 0 && (
260 <tr>
261 <td colSpan="7" style={{ color: "#94a3b8" }}>
262 No process data.
263 </td>
264 </tr>
265 )}
266 </tbody>
267 </table>
268 </div>
269 </div>
270 </div>
271 )}
272
273 {!loading && details && tab === "network" && (
274 <div className="panel">
275 <div className="panel-head">
276 <div className="panel-title">Network Connections</div>
277 </div>
278 <div className="panel-body">
279 <div className="table-wrap">
280 <table>
281 <thead>
282 <tr>
283 <th>Time</th>
284 <th>PID</th>
285 <th>Process</th>
286 <th>Local</th>
287 <th>Remote</th>
288 <th>Status</th>
289 </tr>
290 </thead>
291 <tbody>
292 {(details.network_connections || []).slice(0, 150).map((n, idx) => (
293 <tr key={idx}>
294 <td>{n.timestamp ? new Date(n.timestamp).toLocaleTimeString() : "—"}</td>
295 <td>
296 <b>{n.pid}</b>
297 </td>
298 <td>{n.process_name || "—"}</td>
299 <td
300 style={{
301 fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
302 fontSize: 12,
303 }}
304 >
305 {n.local_address || "—"}
306 </td>
307 <td
308 style={{
309 fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
310 fontSize: 12,
311 }}
312 >
313 {n.remote_address || "—"}
314 </td>
315 <td>{n.status || "—"}</td>
316 </tr>
317 ))}
318 {(details.network_connections || []).length === 0 && (
319 <tr>
320 <td colSpan="6" style={{ color: "#94a3b8" }}>
321 No network data.
322 </td>
323 </tr>
324 )}
325 </tbody>
326 </table>
327 </div>
328 </div>
329 </div>
330 )}
331
332 {!loading && !details && <div style={{ color: "#94a3b8" }}>No details.</div>}
333 </div>
334 </div>
335 </div>
336
337 {/* JSON VIEWER MODAL */}
338 {jsonOpen && (
339 <div className="backdrop" onMouseDown={closeJson}>
340 <div className="modal" onMouseDown={(e) => e.stopPropagation()} style={{ width: "min(900px, 96vw)" }}>
341 <div className="modal-head">
342 <div>
343 <div className="modal-title">View JSON</div>
344 <div className="modal-sub">{jsonTitle}</div>
345 </div>
346 <div style={{ display: "flex", gap: 10 }}>
347 <button
348 className="btn"
349 onClick={() => {
350 const txt = JSON.stringify(jsonData ?? {}, null, 2);
351 navigator.clipboard.writeText(txt);
352 }}
353 >
354 Copy
355 </button>
356 <button className="btn" onClick={closeJson}>
357 Close
358 </button>
359 </div>
360 </div>
361 <div style={{ padding: 16 }}>
362 <pre
363 style={{
364 margin: 0,
365 maxHeight: "70vh",
366 overflow: "auto",
367 padding: 14,
368 borderRadius: 12,
369 border: "1px solid rgba(148,163,184,0.25)",
370 background: "rgba(2,6,23,0.35)",
371 color: "rgba(226,232,240,0.95)",
372 fontSize: 12,
373 lineHeight: 1.5,
374 }}
375 >
376 {JSON.stringify(jsonData ?? {}, null, 2)}
377 </pre>
378 </div>
379 </div>
380 </div>
381 )}
382 </>
383 );
384}
Note: See TracBrowser for help on using the repository browser.