source: lan-frontend/src/components/Visualisations.jsx@ a762898

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

Prototype 1.1

  • Property mode set to 100644
File size: 9.7 KB
Line 
1// // src/pages/Visualizations.jsx
2// import { useState } from "react";
3// import SankeyPanel from "../components/SankeyPanel";
4//
5// export default function Visualizations() {
6// const [env, setEnv] = useState("office1");
7//
8// // ✅ All = празни низи
9// const [computers, setComputers] = useState([]);
10// const [ips, setIps] = useState([]);
11//
12// return (
13// <div>
14// <h2>Network Sankey Visualization</h2>
15//
16// <SankeyPanel
17// env={env}
18// computers={computers}
19// ips={ips}
20// from="now-2d"
21// to="now"
22// />
23// </div>
24// );
25// }
26
27
28import React, { useEffect, useMemo, useState } from "react";
29import SankeyViz from "../components/SankeyViz";
30
31function MultiSelect({ options, value, onChange, label }) {
32 return (
33 <div style={{ display: "grid", gap: 6 }}>
34 <div style={{ fontSize: 12, color: "rgba(255,255,255,0.7)" }}>{label}</div>
35 <select
36 multiple
37 value={value}
38 onChange={(e) => {
39 const vals = Array.from(e.target.selectedOptions).map((o) => o.value);
40 onChange(vals);
41 }}
42 className="select"
43 style={{ minHeight: 90 }}
44 >
45 {options.map((o) => (
46 <option key={o} value={o}>
47 {o}
48 </option>
49 ))}
50 </select>
51 <div style={{ fontSize: 11, color: "rgba(255,255,255,0.5)" }}>
52 (држи Ctrl за multi-select)
53 </div>
54 </div>
55 );
56}
57
58export default function Visualizations() {
59 const [env, setEnv] = useState("office1");
60
61 // 👇 ако сакаш env листа динамички, додај endpoint.
62 const envOptions = ["default", "office1"];
63
64 const [computerOptions, setComputerOptions] = useState(["All"]);
65 const [ipOptions, setIpOptions] = useState(["All"]);
66
67 const [computers, setComputers] = useState(["All"]);
68 const [ips, setIps] = useState(["All"]);
69
70 const [hours, setHours] = useState(48);
71 const [limit, setLimit] = useState(200);
72
73 const [vizType, setVizType] = useState("sankey"); // sankey | table | topips
74 const [rows, setRows] = useState([]);
75 const [loading, setLoading] = useState(false);
76
77 // ✅ 1) load computers for env (користи твојот постоечки /api/computers)
78 async function loadComputers() {
79 const r = await fetch("/api/computers", {
80 credentials: "include",
81 headers: { "X-Env": env },
82 });
83 const data = await r.json();
84 const names = (Array.isArray(data) ? data : []).map((c) => c.name).filter(Boolean);
85 setComputerOptions(["All", ...names]);
86 }
87
88 // ✅ 2) load ips list (ОВА треба да го имаш на backend како /api/ips)
89 async function loadIps() {
90 const r = await fetch(`/api/ips?env=${encodeURIComponent(env)}&hours=${hours}&limit=${limit}`, {
91 credentials: "include",
92 });
93 const data = await r.json();
94 const list = Array.isArray(data?.ips) ? data.ips : Array.isArray(data) ? data : [];
95 setIpOptions(["All", ...list]);
96 }
97
98 // ✅ 3) load sankey rows (ОВА треба да го имаш на backend како /api/sankey)
99 async function loadSankey() {
100 setLoading(true);
101 try {
102 const params = new URLSearchParams();
103 params.set("env", env);
104 params.set("hours", String(hours));
105 params.set("limit", String(limit));
106 params.set("computers", (computers?.length ? computers : ["All"]).join(","));
107 params.set("ips", (ips?.length ? ips : ["All"]).join(","));
108
109 const r = await fetch(`/api/sankey?${params.toString()}`, { credentials: "include" });
110 const data = await r.json();
111 setRows(Array.isArray(data) ? data : []);
112 } finally {
113 setLoading(false);
114 }
115 }
116
117 useEffect(() => {
118 loadComputers();
119 loadIps();
120 // eslint-disable-next-line react-hooks/exhaustive-deps
121 }, [env, hours, limit]);
122
123 useEffect(() => {
124 loadSankey();
125 // eslint-disable-next-line react-hooks/exhaustive-deps
126 }, [env, hours, limit, computers, ips]);
127
128 // Table view
129 const tableRows = useMemo(() => rows.slice(0, 200), [rows]);
130
131 // Top IPs view
132 const topIps = useMemo(() => {
133 const map = new Map();
134 for (const r of rows) {
135 map.set(r.target, (map.get(r.target) || 0) + Number(r.value || 0));
136 }
137 return Array.from(map.entries())
138 .sort((a, b) => b[1] - a[1])
139 .slice(0, 20);
140 }, [rows]);
141
142 return (
143 <div className="panel" style={{ marginTop: 14 }}>
144 <div className="panel-head">
145 <div className="panel-title">Visualizations</div>
146 <div style={{ color: "var(--dim)", fontSize: 12 }}>
147 {loading ? "Loading…" : `${rows.length} links`}
148 </div>
149 </div>
150
151 <div className="panel-body" style={{ display: "grid", gap: 14 }}>
152 {/* Controls */}
153 <div
154 style={{
155 display: "grid",
156 gridTemplateColumns: "1fr 1fr 1fr",
157 gap: 12,
158 alignItems: "start",
159 }}
160 >
161 <div style={{ display: "grid", gap: 8 }}>
162 <div style={{ fontSize: 12, color: "rgba(255,255,255,0.7)" }}>Environment</div>
163 <select className="select" value={env} onChange={(e) => setEnv(e.target.value)}>
164 {envOptions.map((x) => (
165 <option key={x} value={x}>
166 {x}
167 </option>
168 ))}
169 </select>
170
171 <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
172 <div>
173 <div style={{ fontSize: 12, color: "rgba(255,255,255,0.7)" }}>Hours</div>
174 <input
175 className="input"
176 type="number"
177 value={hours}
178 min={1}
179 max={720}
180 onChange={(e) => setHours(Number(e.target.value))}
181 />
182 </div>
183 <div>
184 <div style={{ fontSize: 12, color: "rgba(255,255,255,0.7)" }}>Limit</div>
185 <input
186 className="input"
187 type="number"
188 value={limit}
189 min={10}
190 max={2000}
191 onChange={(e) => setLimit(Number(e.target.value))}
192 />
193 </div>
194 </div>
195
196 <div style={{ display: "grid", gap: 6 }}>
197 <div style={{ fontSize: 12, color: "rgba(255,255,255,0.7)" }}>Visualization</div>
198 <select className="select" value={vizType} onChange={(e) => setVizType(e.target.value)}>
199 <option value="sankey">Sankey</option>
200 <option value="table">Table</option>
201 <option value="topips">Top IPs</option>
202 </select>
203 </div>
204
205 <button
206 className="btn"
207 onClick={() => {
208 setComputers(["All"]);
209 setIps(["All"]);
210 }}
211 >
212 Reset filters
213 </button>
214 </div>
215
216 <MultiSelect
217 label="Computers"
218 options={computerOptions}
219 value={computers}
220 onChange={(vals) => setComputers(vals.length ? vals : ["All"])}
221 />
222
223 <MultiSelect
224 label="IPs"
225 options={ipOptions}
226 value={ips}
227 onChange={(vals) => setIps(vals.length ? vals : ["All"])}
228 />
229 </div>
230
231 {/* Viz */}
232 {vizType === "sankey" && <SankeyViz dataRows={rows} height={520} />}
233
234 {vizType === "table" && (
235 <div style={{ overflow: "auto", border: "1px solid rgba(255,255,255,0.08)", borderRadius: 12 }}>
236 <table style={{ width: "100%", borderCollapse: "collapse" }}>
237 <thead>
238 <tr>
239 <th style={{ textAlign: "left", padding: 10, color: "rgba(255,255,255,0.8)" }}>Source</th>
240 <th style={{ textAlign: "left", padding: 10, color: "rgba(255,255,255,0.8)" }}>Target</th>
241 <th style={{ textAlign: "right", padding: 10, color: "rgba(255,255,255,0.8)" }}>Value</th>
242 </tr>
243 </thead>
244 <tbody>
245 {tableRows.map((r, idx) => (
246 <tr key={idx} style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
247 <td style={{ padding: 10, color: "rgba(255,255,255,0.85)" }}>{r.source}</td>
248 <td style={{ padding: 10, color: "rgba(255,255,255,0.85)" }}>{r.target}</td>
249 <td style={{ padding: 10, textAlign: "right", color: "rgba(255,255,255,0.85)" }}>{r.value}</td>
250 </tr>
251 ))}
252 </tbody>
253 </table>
254 </div>
255 )}
256
257 {vizType === "topips" && (
258 <div style={{ display: "grid", gap: 8 }}>
259 {topIps.map(([ip, v]) => (
260 <div
261 key={ip}
262 style={{
263 display: "grid",
264 gridTemplateColumns: "220px 1fr 80px",
265 gap: 10,
266 alignItems: "center",
267 padding: "8px 10px",
268 border: "1px solid rgba(255,255,255,0.08)",
269 borderRadius: 12,
270 }}
271 >
272 <div style={{ color: "rgba(255,255,255,0.85)" }}>{ip}</div>
273 <div style={{ height: 10, background: "rgba(255,255,255,0.08)", borderRadius: 99, overflow: "hidden" }}>
274 <div
275 style={{
276 height: "100%",
277 width: `${Math.min(100, (v / (topIps[0]?.[1] || 1)) * 100)}%`,
278 background: "rgba(80,160,255,0.7)",
279 }}
280 />
281 </div>
282 <div style={{ textAlign: "right", color: "rgba(255,255,255,0.8)" }}>{v}</div>
283 </div>
284 ))}
285 </div>
286 )}
287 </div>
288 </div>
289 );
290}
Note: See TracBrowser for help on using the repository browser.