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

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

Added OAuth/prototype

  • Property mode set to 100644
File size: 22.6 KB
Line 
1import React, { useEffect, useMemo, useState } from "react";
2
3function severityForEventId(eventId) {
4 const id = Number.parseInt(String(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
12function fmtTime(ts) {
13 if (!ts) return "—";
14 try {
15 return new Date(ts).toLocaleTimeString();
16 } catch {
17 return String(ts);
18 }
19}
20
21export default function ComputerDetailsModal({ open, computer, onClose, onAskAI }) {
22 const [tab, setTab] = useState("overview");
23 const [loading, setLoading] = useState(false);
24 const [details, setDetails] = useState(null);
25 const [error, setError] = useState("");
26 const [search, setSearch] = useState("");
27
28 // JSON viewer
29 const [jsonOpen, setJsonOpen] = useState(false);
30 const [jsonTitle, setJsonTitle] = useState("");
31 const [jsonData, setJsonData] = useState(null);
32
33 // reset when opening / switching pc
34 useEffect(() => {
35 if (!open) return;
36 setTab("overview");
37 setSearch("");
38 setError("");
39 setDetails(null);
40 setJsonOpen(false);
41 setJsonTitle("");
42 setJsonData(null);
43 }, [open, computer?.name]);
44
45 // close on ESC
46 useEffect(() => {
47 if (!open) return;
48 const onKey = (e) => {
49 if (e.key === "Escape") onClose?.();
50 };
51 window.addEventListener("keydown", onKey);
52 return () => window.removeEventListener("keydown", onKey);
53 }, [open, onClose]);
54
55 // load details
56 useEffect(() => {
57 if (!open || !computer?.name) return;
58 let cancelled = false;
59
60 async function load() {
61 setLoading(true);
62 setError("");
63 try {
64 const r = await fetch(`/api/computer/${encodeURIComponent(computer.name)}`, {
65 credentials: "include",
66 });
67 const d = await r.json().catch(() => ({}));
68 if (!r.ok) throw new Error(d?.error || `HTTP ${r.status}`);
69 if (!cancelled) setDetails(d);
70 } catch (e) {
71 if (!cancelled) {
72 setError(String(e?.message || e));
73 setDetails(null);
74 }
75 } finally {
76 if (!cancelled) setLoading(false);
77 }
78 }
79
80 load();
81 return () => {
82 cancelled = true;
83 };
84 }, [open, computer?.name]);
85
86 const filteredSysmon = useMemo(() => {
87 const list = details?.sysmon_events || [];
88 const q = search.trim().toLowerCase();
89 if (!q) return list;
90 return list.filter((ev) => {
91 const msg = (ev.message || "").toLowerCase();
92 const type = (ev.event_type || "").toLowerCase();
93 const id = String(ev.event_id ?? "").toLowerCase();
94 return msg.includes(q) || type.includes(q) || id.includes(q);
95 });
96 }, [details, search]);
97
98 function openJson(ev) {
99 setJsonTitle(`Sysmon Event ${ev?.event_id ?? ""} • ${ev?.event_type ?? ""}`);
100 setJsonData(ev?.details ?? ev);
101 setJsonOpen(true);
102 }
103
104 function closeJson() {
105 setJsonOpen(false);
106 setJsonTitle("");
107 setJsonData(null);
108 }
109
110 if (!open) return null;
111
112 return (
113 <>
114 <style>{`
115 .cdm-backdrop{
116 position: fixed; inset: 0; z-index: 60;
117 background: rgba(2,6,23,0.70);
118 backdrop-filter: blur(6px);
119 display:flex; align-items:center; justify-content:center;
120 padding: 18px;
121 }
122 .cdm-modal{
123 width: min(1100px, 96vw);
124 height: min(86vh, 860px);
125 display:flex; flex-direction:column;
126 border-radius: 18px;
127 background: radial-gradient(900px 420px at 20% 0%, rgba(96,165,250,0.18), transparent 60%),
128 radial-gradient(700px 350px at 80% 10%, rgba(34,211,238,0.10), transparent 55%),
129 rgba(10,16,32,0.92);
130 border: 1px solid rgba(96,165,250,0.20);
131 box-shadow: 0 30px 80px rgba(0,0,0,0.45);
132 overflow: hidden;
133 }
134 .cdm-head{
135 padding: 14px 16px;
136 border-bottom: 1px solid rgba(96,165,250,0.16);
137 display:flex; align-items:center; justify-content:space-between; gap:12px;
138 }
139 .cdm-title{ font-size: 16px; font-weight: 900; color: rgba(255,255,255,0.95); }
140 .cdm-sub{ margin-top: 2px; font-size: 12px; color: rgba(191,219,254,0.80); }
141 .cdm-right{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; justify-content:flex-end; }
142 .cdm-tabs{
143 display:flex; gap:8px; padding: 6px;
144 background: rgba(15,23,42,0.45);
145 border: 1px solid rgba(96,165,250,0.18);
146 border-radius: 999px;
147 }
148 .cdm-tab{
149 appearance:none; border: 0;
150 padding: 8px 12px;
151 border-radius: 999px;
152 background: transparent;
153 color: rgba(226,232,240,0.88);
154 font-weight: 900;
155 font-size: 12px;
156 cursor:pointer;
157 transition: background .12s ease, transform .12s ease, color .12s ease;
158 }
159 .cdm-tab:hover{ transform: translateY(-1px); background: rgba(30,41,59,0.55); }
160 .cdm-tab.active{
161 background: linear-gradient(135deg, rgba(37,99,235,0.95), rgba(56,189,248,0.65));
162 color: rgba(5,12,24,0.95);
163 }
164 .cdm-btn{
165 appearance:none;
166 border: 1px solid rgba(96,165,250,0.22);
167 background: rgba(15,23,42,0.55);
168 color: rgba(255,255,255,0.92);
169 padding: 9px 12px;
170 border-radius: 12px;
171 font-weight: 900;
172 font-size: 13px;
173 cursor: pointer;
174 transition: transform .12s ease, background .12s ease, border-color .12s ease, opacity .12s ease;
175 box-shadow: 0 8px 20px rgba(0,0,0,0.18);
176 }
177 .cdm-btn:hover{ transform: translateY(-1px); background: rgba(30,41,59,0.65); border-color: rgba(96,165,250,0.38); }
178 .cdm-btn:disabled{ opacity: .6; cursor: not-allowed; transform:none; }
179 .cdm-btn.primary{
180 background: linear-gradient(135deg, rgba(37,99,235,0.95), rgba(56,189,248,0.65));
181 border-color: rgba(147,197,253,0.35);
182 color: rgba(5,12,24,0.95);
183 }
184
185 /* body scroll zone */
186 .cdm-body{
187 flex: 1;
188 display:flex;
189 flex-direction:column;
190 min-height: 0; /* important for scroll */
191 }
192 .cdm-content{
193 flex: 1;
194 min-height: 0;
195 overflow: auto;
196 padding: 14px 16px 18px;
197 }
198
199 .cdm-panel{
200 border-radius: 16px;
201 background: rgba(15,23,42,0.55);
202 border: 1px solid rgba(96,165,250,0.16);
203 overflow: hidden;
204 }
205 .cdm-panel-head{
206 padding: 12px 12px;
207 display:flex; align-items:center; justify-content:space-between; gap:10px;
208 border-bottom: 1px solid rgba(96,165,250,0.14);
209 }
210 .cdm-panel-title{ color: rgba(255,255,255,0.92); font-weight: 900; }
211 .cdm-panel-body{ padding: 12px; }
212
213 /* table area scroll fix */
214 .cdm-table-wrap{
215 border-radius: 14px;
216 border: 1px solid rgba(148,163,184,0.18);
217 background: rgba(2,6,23,0.28);
218 overflow: hidden;
219 }
220 .cdm-table-scroll{
221 max-height: 52vh;
222 overflow: auto;
223 }
224 table{ width: 100%; border-collapse: collapse; }
225 thead th{
226 position: sticky; top: 0;
227 background: rgba(10,16,32,0.95);
228 backdrop-filter: blur(8px);
229 text-align:left;
230 font-size: 12px;
231 color: rgba(191,219,254,0.88);
232 padding: 10px 10px;
233 border-bottom: 1px solid rgba(96,165,250,0.16);
234 z-index: 1;
235 }
236 tbody td{
237 padding: 10px 10px;
238 border-bottom: 1px solid rgba(148,163,184,0.12);
239 color: rgba(226,232,240,0.92);
240 font-size: 13px;
241 vertical-align: top;
242 }
243 tbody tr:hover td{ background: rgba(30,41,59,0.35); }
244
245 .cdm-pill{
246 display:inline-flex; align-items:center; gap:8px;
247 padding: 6px 10px;
248 border-radius: 999px;
249 background: rgba(30,41,59,0.55);
250 border: 1px solid rgba(96,165,250,0.18);
251 color: rgba(255,255,255,0.9);
252 font-weight: 900;
253 font-size: 12px;
254 }
255 .cdm-input{
256 width: min(420px, 45vw);
257 padding: 9px 12px;
258 border-radius: 12px;
259 border: 1px solid rgba(96,165,250,0.18);
260 background: rgba(2,6,23,0.35);
261 color: rgba(255,255,255,0.92);
262 outline: none;
263 font-weight: 800;
264 }
265 .cdm-input::placeholder{ color: rgba(148,163,184,0.75); }
266
267 .cdm-badge{
268 display:inline-flex; align-items:center; justify-content:center;
269 padding: 5px 10px;
270 border-radius: 999px;
271 font-weight: 1000;
272 font-size: 11px;
273 letter-spacing: .3px;
274 border: 1px solid rgba(148,163,184,0.18);
275 background: rgba(15,23,42,0.5);
276 color: rgba(226,232,240,0.95);
277 }
278 .cdm-badge.high{ border-color: rgba(239,68,68,0.35); color: rgba(254,202,202,0.95); }
279 .cdm-badge.medium{ border-color: rgba(251,191,36,0.35); color: rgba(254,243,199,0.95); }
280 .cdm-badge.low{ border-color: rgba(34,197,94,0.25); color: rgba(220,252,231,0.95); }
281
282 /* JSON modal */
283 .cdm-json-modal{
284 width: min(980px, 96vw);
285 height: min(82vh, 860px);
286 display:flex; flex-direction:column;
287 border-radius: 18px;
288 background: rgba(10,16,32,0.94);
289 border: 1px solid rgba(96,165,250,0.20);
290 overflow:hidden;
291 }
292 .cdm-json-body{
293 flex:1; min-height:0; overflow:auto; padding: 14px 16px 18px;
294 }
295 .cdm-pre{
296 margin: 0;
297 padding: 14px;
298 border-radius: 12px;
299 border: 1px solid rgba(148,163,184,0.22);
300 background: rgba(2,6,23,0.35);
301 color: rgba(226,232,240,0.95);
302 font-size: 12px;
303 line-height: 1.5;
304 white-space: pre-wrap;
305 word-break: break-word;
306 }
307 `}</style>
308
309 <div className="cdm-backdrop" onMouseDown={onClose}>
310 <div className="cdm-modal" onMouseDown={(e) => e.stopPropagation()}>
311 <div className="cdm-head">
312 <div>
313 <div className="cdm-title">{computer?.name || "Computer"}</div>
314 <div className="cdm-sub">
315 {computer?.ip || "—"} • {computer?.user || "—"} • {computer?.status || "—"}
316 </div>
317 </div>
318
319 <div className="cdm-right">
320 <button className="cdm-btn primary" onClick={() => onAskAI?.(computer?.name)} disabled={!computer?.name}>
321 Ask AI
322 </button>
323
324 <div className="cdm-tabs">
325 <button className={`cdm-tab ${tab === "overview" ? "active" : ""}`} onClick={() => setTab("overview")}>
326 Overview
327 </button>
328 <button className={`cdm-tab ${tab === "sysmon" ? "active" : ""}`} onClick={() => setTab("sysmon")}>
329 Sysmon
330 </button>
331 <button className={`cdm-tab ${tab === "processes" ? "active" : ""}`} onClick={() => setTab("processes")}>
332 Processes
333 </button>
334 <button className={`cdm-tab ${tab === "network" ? "active" : ""}`} onClick={() => setTab("network")}>
335 Network
336 </button>
337 </div>
338
339 <button className="cdm-btn" onClick={onClose}>
340 Close
341 </button>
342 </div>
343 </div>
344
345 <div className="cdm-body">
346 <div className="cdm-content">
347 {loading && <div style={{ color: "rgba(148,163,184,0.95)" }}>Loading…</div>}
348
349 {!loading && error && (
350 <div className="cdm-panel" style={{ borderColor: "rgba(239,68,68,0.35)" }}>
351 <div className="cdm-panel-head">
352 <div className="cdm-panel-title">Error</div>
353 </div>
354 <div className="cdm-panel-body" style={{ color: "rgba(254,202,202,0.95)" }}>
355 {error}
356 </div>
357 </div>
358 )}
359
360 {!loading && !error && details && tab === "overview" && (
361 <div className="cdm-panel">
362 <div className="cdm-panel-head">
363 <div className="cdm-panel-title">System Overview</div>
364 <span className="cdm-pill">Env: {details.computer?.env_name || "default"}</span>
365 </div>
366 <div className="cdm-panel-body" style={{ display: "grid", gap: 10 }}>
367 <div><b>User:</b> {details.computer?.user || "—"}</div>
368 <div><b>IP:</b> {details.computer?.ip || "—"}</div>
369 <div><b>OS:</b> {details.computer?.os || "—"}</div>
370 <div><b>First seen:</b> {details.computer?.first_seen || "—"}</div>
371 <div><b>Last seen:</b> {details.computer?.last_seen || "—"}</div>
372 <div><b>Total logs:</b> {details.computer?.total_logs ?? "—"}</div>
373 </div>
374 </div>
375 )}
376
377 {!loading && !error && details && tab === "sysmon" && (
378 <div className="cdm-panel">
379 <div className="cdm-panel-head">
380 <div className="cdm-panel-title">Sysmon Events</div>
381 <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
382 <span className="cdm-pill">{filteredSysmon.length} events</span>
383 <input
384 className="cdm-input"
385 placeholder="Search by id / type / message…"
386 value={search}
387 onChange={(e) => setSearch(e.target.value)}
388 />
389 </div>
390 </div>
391
392 <div className="cdm-panel-body">
393 <div className="cdm-table-wrap">
394 <div className="cdm-table-scroll">
395 <table>
396 <thead>
397 <tr>
398 <th style={{ width: 90 }}>Time</th>
399 <th style={{ width: 70 }}>ID</th>
400 <th style={{ width: 190 }}>Type</th>
401 <th style={{ width: 110 }}>Severity</th>
402 <th>Message</th>
403 <th style={{ width: 90 }}>View</th>
404 </tr>
405 </thead>
406 <tbody>
407 {filteredSysmon.slice(0, 400).map((ev, idx) => {
408 const sev = severityForEventId(ev.event_id);
409 return (
410 <tr key={idx}>
411 <td>{fmtTime(ev.timestamp)}</td>
412 <td><b>{ev.event_id ?? "—"}</b></td>
413 <td>{ev.event_type || "—"}</td>
414 <td><span className={`cdm-badge ${sev}`}>{sev.toUpperCase()}</span></td>
415 <td style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 12 }}>
416 {(ev.message || "").slice(0, 220)}
417 </td>
418 <td>
419 <button className="cdm-btn" onClick={() => openJson(ev)}>
420 View
421 </button>
422 </td>
423 </tr>
424 );
425 })}
426
427 {filteredSysmon.length === 0 && (
428 <tr>
429 <td colSpan="6" style={{ color: "rgba(148,163,184,0.95)" }}>
430 No sysmon events.
431 </td>
432 </tr>
433 )}
434 </tbody>
435 </table>
436 </div>
437 </div>
438 {filteredSysmon.length > 400 && (
439 <div style={{ marginTop: 10, color: "rgba(148,163,184,0.95)", fontSize: 12 }}>
440 Showing first 400 (for speed).
441 </div>
442 )}
443 </div>
444 </div>
445 )}
446
447 {!loading && !error && details && tab === "processes" && (
448 <div className="cdm-panel">
449 <div className="cdm-panel-head">
450 <div className="cdm-panel-title">Recent Processes</div>
451 <span className="cdm-pill">{(details.recent_processes || []).length} rows</span>
452 </div>
453
454 <div className="cdm-panel-body">
455 <div className="cdm-table-wrap">
456 <div className="cdm-table-scroll">
457 <table>
458 <thead>
459 <tr>
460 <th style={{ width: 90 }}>Time</th>
461 <th style={{ width: 80 }}>PID</th>
462 <th style={{ width: 200 }}>Name</th>
463 <th style={{ width: 140 }}>User</th>
464 <th style={{ width: 90 }}>CPU%</th>
465 <th style={{ width: 110 }}>RAM MB</th>
466 <th>Cmdline</th>
467 </tr>
468 </thead>
469 <tbody>
470 {(details.recent_processes || []).slice(0, 400).map((p, idx) => (
471 <tr key={idx}>
472 <td>{fmtTime(p.timestamp)}</td>
473 <td><b>{p.pid ?? "—"}</b></td>
474 <td>{p.name || "—"}</td>
475 <td>{p.username || "—"}</td>
476 <td>{p.cpu_percent ?? 0}</td>
477 <td>{p.memory_mb ?? 0}</td>
478 <td style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 12 }}>
479 {(p.cmdline || "").slice(0, 220)}
480 </td>
481 </tr>
482 ))}
483 {(details.recent_processes || []).length === 0 && (
484 <tr>
485 <td colSpan="7" style={{ color: "rgba(148,163,184,0.95)" }}>
486 No process data.
487 </td>
488 </tr>
489 )}
490 </tbody>
491 </table>
492 </div>
493 </div>
494 </div>
495 </div>
496 )}
497
498 {!loading && !error && details && tab === "network" && (
499 <div className="cdm-panel">
500 <div className="cdm-panel-head">
501 <div className="cdm-panel-title">Network Connections</div>
502 <span className="cdm-pill">{(details.network_connections || []).length} rows</span>
503 </div>
504
505 <div className="cdm-panel-body">
506 <div className="cdm-table-wrap">
507 <div className="cdm-table-scroll">
508 <table>
509 <thead>
510 <tr>
511 <th style={{ width: 90 }}>Time</th>
512 <th style={{ width: 80 }}>PID</th>
513 <th style={{ width: 180 }}>Process</th>
514 <th style={{ width: 220 }}>Local</th>
515 <th style={{ width: 220 }}>Remote</th>
516 <th style={{ width: 140 }}>Status</th>
517 </tr>
518 </thead>
519 <tbody>
520 {(details.network_connections || []).slice(0, 400).map((n, idx) => (
521 <tr key={idx}>
522 <td>{fmtTime(n.timestamp)}</td>
523 <td><b>{n.pid ?? "—"}</b></td>
524 <td>{n.process_name || "—"}</td>
525 <td style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 12 }}>
526 {n.local_address || "—"}
527 </td>
528 <td style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 12 }}>
529 {n.remote_address || "—"}
530 </td>
531 <td>{n.status || "—"}</td>
532 </tr>
533 ))}
534 {(details.network_connections || []).length === 0 && (
535 <tr>
536 <td colSpan="6" style={{ color: "rgba(148,163,184,0.95)" }}>
537 No network data.
538 </td>
539 </tr>
540 )}
541 </tbody>
542 </table>
543 </div>
544 </div>
545 </div>
546 </div>
547 )}
548
549 {!loading && !error && !details && (
550 <div style={{ color: "rgba(148,163,184,0.95)" }}>No details.</div>
551 )}
552 </div>
553 </div>
554 </div>
555 </div>
556
557 {/* JSON VIEWER */}
558 {jsonOpen && (
559 <div className="cdm-backdrop" onMouseDown={closeJson}>
560 <div className="cdm-json-modal" onMouseDown={(e) => e.stopPropagation()}>
561 <div className="cdm-head">
562 <div>
563 <div className="cdm-title">View JSON</div>
564 <div className="cdm-sub">{jsonTitle}</div>
565 </div>
566 <div className="cdm-right">
567 <button
568 className="cdm-btn"
569 onClick={() => navigator.clipboard.writeText(JSON.stringify(jsonData ?? {}, null, 2))}
570 >
571 Copy
572 </button>
573 <button className="cdm-btn" onClick={closeJson}>
574 Close
575 </button>
576 </div>
577 </div>
578
579 <div className="cdm-json-body">
580 <pre className="cdm-pre">{JSON.stringify(jsonData ?? {}, null, 2)}</pre>
581 </div>
582 </div>
583 </div>
584 )}
585 </>
586 );
587}
Note: See TracBrowser for help on using the repository browser.