Changeset 505f39a for lan-frontend/src/components/ComputerDetailsModal.jsx
- Timestamp:
- 01/21/26 13:35:44 (6 months ago)
- Branches:
- master
- Children:
- d42aac3
- Parents:
- 2058e5c
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
lan-frontend/src/components/ComputerDetailsModal.jsx
r2058e5c r505f39a 2 2 3 3 function severityForEventId(eventId) { 4 const id = parseInt(eventId, 10);4 const id = Number.parseInt(String(eventId ?? ""), 10); 5 5 const high = new Set([1, 3, 8, 10]); 6 6 const med = new Set([5, 6, 7, 11, 22]); … … 10 10 } 11 11 12 function fmtTime(ts) { 13 if (!ts) return "—"; 14 try { 15 return new Date(ts).toLocaleTimeString(); 16 } catch { 17 return String(ts); 18 } 19 } 20 12 21 export default function ComputerDetailsModal({ open, computer, onClose, onAskAI }) { 13 22 const [tab, setTab] = useState("overview"); 14 23 const [loading, setLoading] = useState(false); 15 24 const [details, setDetails] = useState(null); 25 const [error, setError] = useState(""); 16 26 const [search, setSearch] = useState(""); 17 27 18 // JSON viewer state28 // JSON viewer 19 29 const [jsonOpen, setJsonOpen] = useState(false); 20 30 const [jsonTitle, setJsonTitle] = useState(""); 21 31 const [jsonData, setJsonData] = useState(null); 22 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 23 56 useEffect(() => { 24 57 if (!open || !computer?.name) return; … … 26 59 27 60 async function load() { 61 setLoading(true); 62 setError(""); 28 63 try { 29 setLoading(true); 30 const r = await fetch(`/api/computer/${encodeURIComponent(computer.name)}`); 31 const d = await r.json(); 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}`); 32 69 if (!cancelled) setDetails(d); 33 70 } catch (e) { 34 console.error("details error", e); 71 if (!cancelled) { 72 setError(String(e?.message || e)); 73 setDetails(null); 74 } 35 75 } finally { 36 76 if (!cancelled) setLoading(false); … … 51 91 const msg = (ev.message || "").toLowerCase(); 52 92 const type = (ev.event_type || "").toLowerCase(); 53 const id = String(ev.event_id ?? "") ;93 const id = String(ev.event_id ?? "").toLowerCase(); 54 94 return msg.includes(q) || type.includes(q) || id.includes(q); 55 95 }); … … 57 97 58 98 function openJson(ev) { 59 // server ти враќа: { event_id, event_type, message, timestamp, details }60 99 setJsonTitle(`Sysmon Event ${ev?.event_id ?? ""} • ${ev?.event_type ?? ""}`); 61 setJsonData(ev?.details ?? ev); // ако нема details, покажи цел event100 setJsonData(ev?.details ?? ev); 62 101 setJsonOpen(true); 63 102 } … … 73 112 return ( 74 113 <> 75 <div className="backdrop" onMouseDown={onClose}> 76 <div className="modal" onMouseDown={(e) => e.stopPropagation()}> 77 <div className="modal-head"> 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"> 78 312 <div> 79 <div className=" modal-title">{computer.name}</div>80 <div className=" modal-sub">81 {computer .ip || "—"} • {computer.user || "—"} • {computer.status || "—"}313 <div className="cdm-title">{computer?.name || "Computer"}</div> 314 <div className="cdm-sub"> 315 {computer?.ip || "—"} • {computer?.user || "—"} • {computer?.status || "—"} 82 316 </div> 83 317 </div> 84 318 85 <div style={{ display: "flex", gap: 10, alignItems: "center" }}> 86 <div className="tabs"> 87 <button className={`tab ${tab === "overview" ? "active" : ""}`} onClick={() => setTab("overview")}> 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")}> 88 326 Overview 89 327 </button> 90 <button className={` tab ${tab === "sysmon" ? "active" : ""}`} onClick={() => setTab("sysmon")}>328 <button className={`cdm-tab ${tab === "sysmon" ? "active" : ""}`} onClick={() => setTab("sysmon")}> 91 329 Sysmon 92 330 </button> 93 <button className={` tab ${tab === "processes" ? "active" : ""}`} onClick={() => setTab("processes")}>331 <button className={`cdm-tab ${tab === "processes" ? "active" : ""}`} onClick={() => setTab("processes")}> 94 332 Processes 95 333 </button> 96 <button className={` tab ${tab === "network" ? "active" : ""}`} onClick={() => setTab("network")}>334 <button className={`cdm-tab ${tab === "network" ? "active" : ""}`} onClick={() => setTab("network")}> 97 335 Network 98 336 </button> 99 337 </div> 100 <button className="btn" onClick={onClose}> 338 339 <button className="cdm-btn" onClick={onClose}> 101 340 Close 102 341 </button> … … 104 343 </div> 105 344 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> 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> 116 357 </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} 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> 135 373 </div> 136 374 </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 /> 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 )} 153 443 </div> 154 444 </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> 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> 195 467 </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). 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> 213 493 </div> 214 )}494 </div> 215 495 </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> 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> 223 546 </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>} 547 )} 548 549 {!loading && !error && !details && ( 550 <div style={{ color: "rgba(148,163,184,0.95)" }}>No details.</div> 551 )} 552 </div> 333 553 </div> 334 554 </div> 335 555 </div> 336 556 337 {/* JSON VIEWER MODAL*/}557 {/* JSON VIEWER */} 338 558 {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">559 <div className="cdm-backdrop" onMouseDown={closeJson}> 560 <div className="cdm-json-modal" onMouseDown={(e) => e.stopPropagation()}> 561 <div className="cdm-head"> 342 562 <div> 343 <div className=" modal-title">View JSON</div>344 <div className=" modal-sub">{jsonTitle}</div>563 <div className="cdm-title">View JSON</div> 564 <div className="cdm-sub">{jsonTitle}</div> 345 565 </div> 346 <div style={{ display: "flex", gap: 10 }}>566 <div className="cdm-right"> 347 567 <button 348 className="btn" 349 onClick={() => { 350 const txt = JSON.stringify(jsonData ?? {}, null, 2); 351 navigator.clipboard.writeText(txt); 352 }} 568 className="cdm-btn" 569 onClick={() => navigator.clipboard.writeText(JSON.stringify(jsonData ?? {}, null, 2))} 353 570 > 354 571 Copy 355 572 </button> 356 <button className=" btn" onClick={closeJson}>573 <button className="cdm-btn" onClick={closeJson}> 357 574 Close 358 575 </button> 359 576 </div> 360 577 </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> 578 579 <div className="cdm-json-body"> 580 <pre className="cdm-pre">{JSON.stringify(jsonData ?? {}, null, 2)}</pre> 378 581 </div> 379 582 </div>
Note:
See TracChangeset
for help on using the changeset viewer.
