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