Changeset d42aac3 for lan-frontend/src/App.jsx
- Timestamp:
- 01/22/26 17:32:29 (6 months ago)
- Branches:
- master
- Children:
- e4c61dd
- Parents:
- 505f39a
- File:
-
- 1 edited
-
lan-frontend/src/App.jsx (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
lan-frontend/src/App.jsx
r505f39a rd42aac3 1 import React, { useEffect, useMemo, useState} from "react";1 import React, { useEffect, useMemo, useState } from "react"; 2 2 import "./styles/theme.css"; 3 3 … … 9 9 10 10 export default function App() { 11 const [stats, setStats] = useState(null); 12 const [computers, setComputers] = useState([]); 13 const [loading, setLoading] = useState(true); 14 const [lastUpdated, setLastUpdated] = useState(null); 15 16 const [query, setQuery] = useState(""); 17 const [status, setStatus] = useState(""); 18 19 const [selectedComputer, setSelectedComputer] = useState(null); 20 const [aiOpen, setAiOpen] = useState(false); 21 const [aiScope, setAiScope] = useState(null); 22 const [selectedEnv, setSelectedEnv] = useState("default"); 23 24 const [me, setMe] = useState(null); 25 const [meLoading, setMeLoading] = useState(true); 26 27 // const [openAI, setOpenAI] = useState(false); 28 // const [scope, setScope] = useState(null); 29 30 31 async function loadMe() { 32 setMeLoading(true); 33 try { 34 const r = await fetch("/api/me", {credentials: "include"}); 35 if (r.ok) { 36 const data = await r.json(); 37 setMe(data.user); 38 } else { 39 setMe(null); 40 } 41 } catch (e) { 42 console.error("loadMe error", e); 43 setMe(null); 44 } finally { 45 setMeLoading(false); 46 } 47 } 48 49 async function refresh() { 50 try { 51 setLoading(true); 52 const [s, c] = await Promise.all([ 53 fetch("/api/stats", {credentials: "include"}).then((r) => r.json()), 54 fetch("/api/computers", { 55 credentials: "include", 56 headers: {"X-Env": selectedEnv}, 57 }).then((r) => r.json()), 58 ]); 59 60 setStats(s); 61 setComputers(Array.isArray(c) ? c : []); 62 setLastUpdated(new Date().toISOString()); 63 } catch (e) { 64 console.error("refresh error", e); 65 } finally { 66 setLoading(false); 67 } 68 } 69 70 // on mount: check session 71 useEffect(() => { 72 loadMe(); 73 }, []); 74 75 // when logged in OR env changed, refresh data 76 useEffect(() => { 77 if (me) refresh(); 78 // eslint-disable-next-line react-hooks/exhaustive-deps 79 }, [me, selectedEnv]); 80 81 const filtered = useMemo(() => { 82 const q = query.trim().toLowerCase(); 83 return (computers || []) 84 .filter((c) => (status ? c.status === status : true)) 85 .filter((c) => { 86 if (!q) return true; 87 const hay = [c.name, c.user, c.ip, c.os, c.status] 88 .filter(Boolean) 89 .join(" ") 90 .toLowerCase(); 91 return hay.includes(q); 92 }); 93 }, [computers, query, status]); 94 95 // Gate UI 96 if (meLoading) return <div style={{padding: 20}}>Loading…</div>; 97 if (!me) return <Login onDone={loadMe}/>; 98 99 return ( 100 <div className="app-wrap"> 101 <TopBar 102 onOpenAI={() => setAiOpen(true)} 103 onRefresh={refresh} 104 lastUpdated={lastUpdated} 105 onEnvChange={(env) => setSelectedEnv(env)} 106 selectedEnv={selectedEnv} 107 me={me} 108 onLoggedOut={loadMe} 11 const [stats, setStats] = useState(null); 12 const [computers, setComputers] = useState([]); 13 const [loading, setLoading] = useState(true); 14 const [lastUpdated, setLastUpdated] = useState(null); 15 16 const [query, setQuery] = useState(""); 17 const [status, setStatus] = useState(""); 18 19 const [selectedComputer, setSelectedComputer] = useState(null); 20 const [aiOpen, setAiOpen] = useState(false); 21 const [aiScope, setAiScope] = useState(null); 22 const [selectedEnv, setSelectedEnv] = useState("default"); 23 24 const [me, setMe] = useState(null); 25 const [meLoading, setMeLoading] = useState(true); 26 27 // NEW: environment setting (switch) 28 const [saveProcHistory, setSaveProcHistory] = useState(false); 29 30 async function loadMe() { 31 setMeLoading(true); 32 try { 33 const r = await fetch("/api/me", { credentials: "include" }); 34 if (r.ok) { 35 const data = await r.json(); 36 setMe(data.user); 37 } else { 38 setMe(null); 39 } 40 } catch (e) { 41 console.error("loadMe error", e); 42 setMe(null); 43 } finally { 44 setMeLoading(false); 45 } 46 } 47 48 async function refresh() { 49 try { 50 setLoading(true); 51 const [s, c] = await Promise.all([ 52 fetch("/api/stats", { credentials: "include" }).then((r) => r.json()), 53 fetch("/api/computers", { 54 credentials: "include", 55 headers: { "X-Env": selectedEnv }, 56 }).then((r) => r.json()), 57 ]); 58 59 setStats(s); 60 setComputers(Array.isArray(c) ? c : []); 61 setLastUpdated(new Date().toISOString()); 62 } catch (e) { 63 console.error("refresh error", e); 64 } finally { 65 setLoading(false); 66 } 67 } 68 69 // NEW: load env settings (switch state) for selected env 70 async function loadEnvSettings(env) { 71 try { 72 // само admin ќе има пристап (ако не си admin, ќе падне во else и ќе стане false) 73 const r = await fetch(`/api/admin/env-settings/${encodeURIComponent(env)}`, { 74 credentials: "include", 75 }); 76 77 if (r.ok) { 78 const data = await r.json(); 79 setSaveProcHistory(!!data.save_process_history); 80 } else { 81 setSaveProcHistory(false); 82 } 83 } catch (e) { 84 console.error("loadEnvSettings error", e); 85 setSaveProcHistory(false); 86 } 87 } 88 89 // NEW: set env setting (toggle) 90 async function setEnvProcessHistory(env, enabled) { 91 // optimistic UI 92 setSaveProcHistory(enabled); 93 94 try { 95 const r = await fetch(`/api/admin/env-settings/${encodeURIComponent(env)}`, { 96 method: "POST", 97 credentials: "include", 98 headers: { "Content-Type": "application/json" }, 99 body: JSON.stringify({ save_process_history: enabled }), 100 }); 101 102 if (r.ok) { 103 const data = await r.json(); 104 setSaveProcHistory(!!data.save_process_history); 105 } else { 106 // rollback 107 setSaveProcHistory(!enabled); 108 console.error("setEnvProcessHistory failed", await r.text()); 109 } 110 } catch (e) { 111 setSaveProcHistory(!enabled); 112 console.error("setEnvProcessHistory error", e); 113 } 114 } 115 116 // on mount: check session 117 useEffect(() => { 118 loadMe(); 119 }, []); 120 121 // when logged in OR env changed, refresh data + load env switch state 122 useEffect(() => { 123 if (!me) return; 124 loadEnvSettings(selectedEnv); 125 refresh(); 126 // eslint-disable-next-line react-hooks/exhaustive-deps 127 }, [me, selectedEnv]); 128 129 const filtered = useMemo(() => { 130 const q = query.trim().toLowerCase(); 131 return (computers || []) 132 .filter((c) => (status ? c.status === status : true)) 133 .filter((c) => { 134 if (!q) return true; 135 const hay = [c.name, c.user, c.ip, c.os, c.status] 136 .filter(Boolean) 137 .join(" ") 138 .toLowerCase(); 139 return hay.includes(q); 140 }); 141 }, [computers, query, status]); 142 143 // Gate UI 144 if (meLoading) return <div style={{ padding: 20 }}>Loading…</div>; 145 if (!me) return <Login onDone={loadMe} />; 146 147 return ( 148 <div className="app-wrap"> 149 <TopBar 150 onOpenAI={() => setAiOpen(true)} 151 onRefresh={refresh} 152 lastUpdated={lastUpdated} 153 onEnvChange={(env) => setSelectedEnv(env)} 154 selectedEnv={selectedEnv} 155 me={me} 156 onLoggedOut={loadMe} 157 // NEW props for switch: 158 saveProcHistory={saveProcHistory} 159 onToggleProcHistory={(enabled) => setEnvProcessHistory(selectedEnv, enabled)} 160 /> 161 162 {/* STATS */} 163 <div className="stats-grid"> 164 <div className="stat-card"> 165 <div className="label">Total Computers</div> 166 <div className="value">{computers.length}</div> 167 <div className="sub">Monitored Systems</div> 168 </div> 169 170 <div className="stat-card"> 171 <div className="label">Sysmon Events (24h)</div> 172 <div className="value">{stats?.total_sysmon_events ?? "—"}</div> 173 <div className="sub">Security Events</div> 174 </div> 175 176 <div className="stat-card"> 177 <div className="label">Network Connections (24h)</div> 178 <div className="value">{stats?.total_network_connections ?? "—"}</div> 179 <div className="sub">Captured Connections</div> 180 </div> 181 </div> 182 183 {/* FILTERS */} 184 <div className="panel" style={{ marginBottom: 14 }}> 185 <div className="panel-head"> 186 <div className="panel-title">Filters</div> 187 <div style={{ color: "var(--dim)", fontSize: 12 }}> 188 {loading ? "Refreshing…" : `${filtered.length} shown`} 189 </div> 190 </div> 191 <div className="panel-body"> 192 <div className="filters"> 193 <input 194 className="input" 195 value={query} 196 onChange={(e) => setQuery(e.target.value)} 197 placeholder="Search by name / user / ip / os…" 109 198 /> 110 199 111 112 {/* STATS */} 113 <div className="stats-grid"> 114 <div className="stat-card"> 115 <div className="label">Total Computers</div> 116 <div className="value">{computers.length}</div> 117 <div className="sub">Monitored Systems</div> 118 </div> 119 120 <div className="stat-card"> 121 <div className="label">Sysmon Events (24h)</div> 122 <div className="value">{stats?.total_sysmon_events ?? "—"}</div> 123 <div className="sub">Security Events</div> 124 </div> 125 126 <div className="stat-card"> 127 <div className="label">Network Connections (24h)</div> 128 <div className="value">{stats?.total_network_connections ?? "—"}</div> 129 <div className="sub">Captured Connections</div> 130 </div> 131 </div> 132 133 {/* FILTERS */} 134 <div className="panel" style={{marginBottom: 14}}> 135 <div className="panel-head"> 136 <div className="panel-title">Filters</div> 137 <div style={{color: "var(--dim)", fontSize: 12}}> 138 {loading ? "Refreshing…" : `${filtered.length} shown`} 139 </div> 140 </div> 141 <div className="panel-body"> 142 <div className="filters"> 143 <input 144 className="input" 145 value={query} 146 onChange={(e) => setQuery(e.target.value)} 147 placeholder="Search by name / user / ip / os…" 148 /> 149 150 <select 151 className="select" 152 value={status} 153 onChange={(e) => setStatus(e.target.value)} 154 > 155 <option value="">All status</option> 156 <option value="online">Online</option> 157 <option value="idle">Idle</option> 158 <option value="offline">Offline</option> 159 </select> 160 161 <button className="btn primary" onClick={refresh}> 162 Refresh 163 </button> 164 </div> 165 </div> 166 </div> 167 168 {/* COMPUTERS */} 169 <div className="panel"> 170 <div className="panel-head"> 171 <div className="panel-title">Connected Computers</div> 172 <div style={{display: "flex", gap: 10, alignItems: "center"}}> 173 <span className="pill">{filtered.length} computers</span> 174 <button 175 className="btn" 176 onClick={() => { 177 setQuery(""); 178 setStatus(""); 179 }} 180 > 181 Reset 182 </button> 183 </div> 184 </div> 185 <div className="panel-body"> 186 <div className="computers-grid"> 187 {filtered.map((c) => ( 188 <ComputerCard 189 key={c.name} 190 c={c} 191 onOpen={(pc) => { 192 setSelectedComputer(pc); 193 setAiScope(pc.name); 194 }} 195 /> 196 ))} 197 {filtered.length === 0 && ( 198 <div style={{color: "var(--dim)"}}> 199 No computers match filter. 200 </div> 201 )} 202 </div> 203 </div> 204 </div> 205 206 {/* DETAILS MODAL */} 207 <ComputerDetailsModal 208 open={!!selectedComputer} 209 computer={selectedComputer} 210 onClose={() => setSelectedComputer(null)} 211 onAskAI={(name) => { 212 setAiScope(name); 213 setAiOpen(true); 200 <select 201 className="select" 202 value={status} 203 onChange={(e) => setStatus(e.target.value)} 204 > 205 <option value="">All status</option> 206 <option value="online">Online</option> 207 <option value="idle">Idle</option> 208 <option value="offline">Offline</option> 209 </select> 210 211 <button className="btn primary" onClick={refresh}> 212 Refresh 213 </button> 214 </div> 215 </div> 216 </div> 217 218 {/* COMPUTERS */} 219 <div className="panel"> 220 <div className="panel-head"> 221 <div className="panel-title">Connected Computers</div> 222 <div style={{ display: "flex", gap: 10, alignItems: "center" }}> 223 <span className="pill">{filtered.length} computers</span> 224 <button 225 className="btn" 226 onClick={() => { 227 setQuery(""); 228 setStatus(""); 229 }} 230 > 231 Reset 232 </button> 233 </div> 234 </div> 235 <div className="panel-body"> 236 <div className="computers-grid"> 237 {filtered.map((c) => ( 238 <ComputerCard 239 key={c.name} 240 c={c} 241 onOpen={(pc) => { 242 setSelectedComputer(pc); 243 setAiScope(pc.name); 214 244 }} 215 /> 216 217 {/* AI DRAWER */} 218 {/*<button onClick={() => setOpenAI(true)}>AI</button>*/} 219 220 {/*<AIAssistantDrawer*/} 221 {/* open={openAI}*/} 222 {/* onClose={() => setOpenAI(false)}*/} 223 {/* computers={computers}*/} 224 {/* scope={scope}*/} 225 {/* setScope={setScope}*/} 226 {/* apiBase={import.meta.env.VITE_API_BASE}*/} 227 {/*/>*/} 228 229 <AIAssistantDrawer 230 open={aiOpen} 231 onClose={() => setAiOpen(false)} 232 computers={computers} 233 scope={aiScope} 234 setScope={setAiScope} 235 apiBase={import.meta.env.VITE_API_BASE} 236 /> 237 238 239 </div> 240 ); 245 /> 246 ))} 247 {filtered.length === 0 && ( 248 <div style={{ color: "var(--dim)" }}>No computers match filter.</div> 249 )} 250 </div> 251 </div> 252 </div> 253 254 {/* DETAILS MODAL */} 255 <ComputerDetailsModal 256 open={!!selectedComputer} 257 computer={selectedComputer} 258 onClose={() => setSelectedComputer(null)} 259 onAskAI={(name) => { 260 setAiScope(name); 261 setAiOpen(true); 262 }} 263 /> 264 265 {/* AI DRAWER */} 266 <AIAssistantDrawer 267 open={aiOpen} 268 onClose={() => setAiOpen(false)} 269 computers={computers} 270 scope={aiScope} 271 setScope={setAiScope} 272 apiBase={import.meta.env.VITE_API_BASE} 273 /> 274 </div> 275 ); 241 276 }
Note:
See TracChangeset
for help on using the changeset viewer.
