source: lan-frontend/src/App.jsx

Last change on this file was a762898, checked in by istevanoska <ilinastevanoska@…>, 5 months ago

Added visualizations

  • Property mode set to 100644
File size: 14.1 KB
Line 
1import React, {useEffect, useMemo, useState} from "react";
2import "./styles/theme.css";
3import "./styles/netinel.css";
4
5import Login from "./components/Login";
6import TopBar from "./components/TopBar";
7import ComputerCard from "./components/ComputerCard";
8import ComputerDetailsModal from "./components/ComputerDetailsModal";
9import AIAssistantDrawer from "./components/AIAssistantDrawer";
10import NetIntelViz from "./components/NetIntelViz.jsx";
11import SideMenu from "./components/SideMenu";
12
13
14export default function App() {
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 }
54 }
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 }
75 }
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 }
95 }
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 }
122 }
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>
334 </div>
335 );
336}
Note: See TracBrowser for help on using the repository browser.