source: lan-frontend/src/components/TopBar.jsx

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

Added history mmanage

  • Property mode set to 100644
File size: 15.8 KB
Line 
1import React, { useEffect, useMemo, useState } from "react";
2import logo from "../assets/logo_image.png";
3
4export default function TopBar({
5 onOpenAI,
6 onRefresh,
7 lastUpdated,
8 onEnvChange,
9 selectedEnv: selectedEnvProp, // optional (ако сакаш да го контролира App)
10 me,
11 onLoggedOut, // optional callback (App loadMe)
12
13 // NEW: process history switch (controlled by App)
14 saveProcHistory = false,
15 onToggleProcHistory, // (enabled:boolean) => void
16}) {
17 const [open, setOpen] = useState(false);
18
19 // env + tokens
20 const [envs, setEnvs] = useState(["default"]);
21 const [selectedEnv, setSelectedEnv] = useState(selectedEnvProp || "default");
22 const [newEnvName, setNewEnvName] = useState("");
23 const [token, setToken] = useState("");
24
25 const [busy, setBusy] = useState(false);
26 const [error, setError] = useState("");
27
28 const isAdmin = (me?.role || "").toLowerCase() === "admin";
29
30 // keep in sync ако App праќа selectedEnv prop
31 useEffect(() => {
32 if (selectedEnvProp && selectedEnvProp !== selectedEnv) {
33 setSelectedEnv(selectedEnvProp);
34 }
35 // eslint-disable-next-line react-hooks/exhaustive-deps
36 }, [selectedEnvProp]);
37
38 async function loadEnvironments() {
39 const r = await fetch("/api/admin/environments", {
40 credentials: "include",
41 });
42 const data = await r.json();
43 if (!r.ok) throw new Error(data?.error || `HTTP ${r.status}`);
44
45 const list = data.environments?.length ? data.environments : ["default"];
46 setEnvs(list);
47
48 if (!list.includes(selectedEnv)) {
49 setSelectedEnv(list[0]);
50 onEnvChange?.(list[0]);
51 }
52 }
53
54 async function createEnvironment() {
55 setError("");
56 setToken("");
57
58 if (!newEnvName.trim()) {
59 setError("Внеси име за environment.");
60 return;
61 }
62
63 setBusy(true);
64 try {
65 const r = await fetch("/api/admin/environments", {
66 method: "POST",
67 headers: { "Content-Type": "application/json" },
68 credentials: "include",
69 body: JSON.stringify({ name: newEnvName.trim() }),
70 });
71
72 const data = await r.json();
73 if (!r.ok) throw new Error(data?.error || `HTTP ${r.status}`);
74
75 await loadEnvironments();
76 setSelectedEnv(newEnvName.trim());
77 onEnvChange?.(newEnvName.trim());
78 setNewEnvName("");
79 } catch (e) {
80 setError(String(e.message || e));
81 } finally {
82 setBusy(false);
83 }
84 }
85
86 async function generateToken() {
87 setError("");
88 setToken("");
89 if (!selectedEnv) {
90 setError("Избери environment.");
91 return;
92 }
93
94 setBusy(true);
95 try {
96 const r = await fetch("/api/admin/tokens", {
97 method: "POST",
98 headers: { "Content-Type": "application/json" },
99 credentials: "include",
100 body: JSON.stringify({ env: selectedEnv }),
101 });
102
103 const data = await r.json();
104 if (!r.ok) throw new Error(data?.error || `HTTP ${r.status}`);
105
106 setToken(data.token || "");
107 } catch (e) {
108 setError(String(e.message || e));
109 } finally {
110 setBusy(false);
111 }
112 }
113
114 async function copyToken() {
115 if (!token) return;
116 await navigator.clipboard.writeText(token);
117 }
118
119 function changeEnv(val) {
120 setSelectedEnv(val);
121 setToken("");
122 onEnvChange?.(val);
123 }
124
125 async function logout() {
126 try {
127 await fetch("/api/auth/logout", { method: "POST", credentials: "include" });
128 } catch {}
129 onLoggedOut?.();
130 // reset local admin modal state
131 setOpen(false);
132 setToken("");
133 setError("");
134 }
135
136 const prettyTime = useMemo(() => {
137 if (!lastUpdated) return "Loading…";
138 try {
139 return `Updated ${new Date(lastUpdated).toLocaleTimeString()}`;
140 } catch {
141 return "Updated";
142 }
143 }, [lastUpdated]);
144
145 return (
146 <>
147 <style>{`
148 .tb-wrap{ position: sticky; top: 0; z-index: 20; backdrop-filter: blur(10px);
149 background: linear-gradient(180deg, rgba(10,16,32,0.92), rgba(10,16,32,0.75));
150 border-bottom: 1px solid rgba(96,165,250,0.18); }
151 .tb-inner{ max-width: 1200px; margin: 0 auto; padding: 14px 18px;
152 display: flex; align-items: center; justify-content: space-between; gap: 14px; }
153 .tb-actions{ display:flex; align-items:center; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
154 .tb-pill{ display:inline-flex; align-items:center; gap: 8px; padding: 8px 12px; border-radius: 999px;
155 background: rgba(30,41,59,0.65); border: 1px solid rgba(96,165,250,0.22);
156 color: rgba(255,255,255,0.9); font-weight: 800; font-size: 12px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.06); }
157 .tb-pill .dot{ width: 8px; height: 8px; border-radius: 999px; background: rgba(96,165,250,0.95);
158 box-shadow: 0 0 0 3px rgba(96,165,250,0.15); }
159 .tb-btn{ appearance: none; border: 1px solid rgba(96,165,250,0.22); background: rgba(15,23,42,0.55);
160 color: rgba(255,255,255,0.9); padding: 9px 12px; border-radius: 12px; font-weight: 800;
161 font-size: 13px; cursor: pointer; transition: transform .12s ease, background .12s ease, border-color .12s ease, opacity .12s ease;
162 box-shadow: 0 8px 20px rgba(0,0,0,0.18); }
163 .tb-btn:hover{ transform: translateY(-1px); background: rgba(30,41,59,0.65); border-color: rgba(96,165,250,0.38); }
164 .tb-btn:disabled{ opacity: .6; cursor: not-allowed; transform: none; }
165 .tb-btn.primary{ background: linear-gradient(135deg, rgba(37,99,235,0.95), rgba(56,189,248,0.7));
166 border-color: rgba(147,197,253,0.35); color: rgba(5,12,24,0.95); }
167 .tb-btn.primary:hover{ background: linear-gradient(135deg, rgba(59,130,246,0.95), rgba(34,211,238,0.75)); }
168
169 .tb-switch{ display:flex; align-items:center; gap:10px; padding: 8px 12px; border-radius: 999px;
170 background: rgba(15,23,42,0.55); border: 1px solid rgba(96,165,250,0.22);
171 color: rgba(255,255,255,0.9); font-weight: 800; font-size: 12px; }
172 .tb-switch .label{ color: rgba(191,219,254,0.9); }
173 .tb-toggle{ width: 46px; height: 26px; border-radius: 999px; position: relative; border: 1px solid rgba(96,165,250,0.25);
174 background: rgba(30,41,59,0.65); cursor: pointer; transition: background .12s ease, opacity .12s ease; }
175 .tb-toggle.disabled{ opacity: .55; cursor: not-allowed; }
176 .tb-knob{ width: 22px; height: 22px; border-radius: 999px; position: absolute; top: 1px; left: 1px;
177 background: rgba(226,232,240,0.95); transition: transform .14s ease; box-shadow: 0 6px 14px rgba(0,0,0,0.22); }
178 .tb-toggle.on{ background: rgba(37,99,235,0.85); border-color: rgba(147,197,253,0.45); }
179 .tb-toggle.on .tb-knob{ transform: translateX(20px); }
180
181 .tb-backdrop{ position: fixed; inset: 0; background: rgba(2,6,23,0.72); backdrop-filter: blur(6px);
182 display:flex; align-items: center; justify-content: center; padding: 20px; z-index: 50; }
183 .tb-modal{ width: min(860px, 96vw); border-radius: 18px;
184 background: radial-gradient(900px 420px at 20% 0%, rgba(96,165,250,0.18), transparent 60%),
185 radial-gradient(700px 350px at 80% 10%, rgba(34,211,238,0.10), transparent 55%),
186 rgba(10,16,32,0.92);
187 border: 1px solid rgba(96,165,250,0.22); box-shadow: 0 30px 80px rgba(0,0,0,0.45); overflow: hidden; }
188 .tb-modal-head{ padding: 16px 18px; display:flex; align-items: center; justify-content: space-between; gap: 10px;
189 border-bottom: 1px solid rgba(96,165,250,0.18); }
190 .tb-modal-title{ color: rgba(255,255,255,0.96); font-weight: 900; letter-spacing: .3px; }
191 .tb-modal-sub{ color: rgba(191,219,254,0.8); font-size: 12px; margin-top: 2px; }
192 .tb-grid{ padding: 16px; display:grid; gap: 12px; }
193 .tb-card{ border-radius: 16px; background: rgba(15,23,42,0.55); border: 1px solid rgba(96,165,250,0.18); padding: 14px; }
194 .tb-card-head{ display:flex; align-items:center; justify-content: space-between; margin-bottom: 10px; gap: 10px; }
195 .tb-card-title{ color: rgba(255,255,255,0.92); font-weight: 900; }
196 .tb-help{ color: rgba(148,163,184,0.95); font-size: 12px; margin-top: 8px; line-height: 1.35; }
197 .tb-input, .tb-select{ width: 100%; padding: 10px 12px; border-radius: 12px;
198 border: 1px solid rgba(96,165,250,0.18); background: rgba(2,6,23,0.35); color: rgba(255,255,255,0.92);
199 outline: none; font-weight: 700; }
200 .tb-input::placeholder{ color: rgba(148,163,184,0.75); }
201 .tb-row{ display:grid; gap: 10px; }
202 .tb-token{ display:flex; align-items:center; justify-content: space-between; gap: 10px; padding: 10px 12px;
203 border-radius: 14px; background: rgba(2,6,23,0.38); border: 1px dashed rgba(147,197,253,0.32);
204 color: rgba(255,255,255,0.92); }
205 .tb-mono{ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
206 font-weight: 800; color: rgba(191,219,254,0.95); }
207 .tb-error{ border-color: rgba(239,68,68,0.45) !important; background: rgba(127,29,29,0.15) !important;
208 color: rgba(254,202,202,0.95); }
209 `}</style>
210
211 <div className="tb-wrap">
212 <div className="tb-inner">
213 <div>
214 <img
215 src={logo}
216 alt="NETIntel logo"
217 className="netintel-logo"
218 width={150}
219 style={{ marginTop: 10 }}
220 />
221 <div style={{ marginTop: 4, fontSize: 12, color: "rgba(191,219,254,0.8)", fontWeight: 800 }}>
222 <span style={{ color: "rgba(96,165,250,0.95)" }}>{prettyTime}</span>
223 {me?.email ? (
224 <span style={{ marginLeft: 10 }}>
225 • {me.email}
226 {me?.role ? ` • ${me.role}` : ""}
227 </span>
228 ) : null}
229 </div>
230 </div>
231
232 <div className="tb-actions">
233 <span className="tb-pill" title="Selected environment">
234 <span className="dot" />
235 Env: {selectedEnv}
236 </span>
237
238 {/* NEW: Save process history switch */}
239 <div className="tb-switch" title={isAdmin ? "Toggle saving process history for this environment" : "Only admin can change this"}>
240 <span className="label">Proc history</span>
241 <div
242 className={[
243 "tb-toggle",
244 saveProcHistory ? "on" : "",
245 !isAdmin ? "disabled" : "",
246 ].join(" ")}
247 role="switch"
248 aria-checked={saveProcHistory ? "true" : "false"}
249 tabIndex={0}
250 onClick={() => {
251 if (!isAdmin) return;
252 onToggleProcHistory?.(!saveProcHistory);
253 }}
254 onKeyDown={(e) => {
255 if (!isAdmin) return;
256 if (e.key === "Enter" || e.key === " ") {
257 e.preventDefault();
258 onToggleProcHistory?.(!saveProcHistory);
259 }
260 }}
261 >
262 <div className="tb-knob" />
263 </div>
264 </div>
265
266 <button className="tb-btn" onClick={onRefresh} disabled={busy}>
267 Refresh
268 </button>
269
270 <button
271 className="tb-btn"
272 onClick={async () => {
273 setOpen(true);
274 setError("");
275 setToken("");
276 try {
277 await loadEnvironments();
278 } catch (e) {
279 setError(String(e.message || e));
280 }
281 }}
282 >
283 Admin
284 </button>
285
286 <button className="tb-btn primary" onClick={onOpenAI}>
287 AI Assistant
288 </button>
289 </div>
290 </div>
291 </div>
292
293 {open && (
294 <div className="tb-backdrop" onMouseDown={() => setOpen(false)}>
295 <div className="tb-modal" onMouseDown={(e) => e.stopPropagation()}>
296 <div className="tb-modal-head">
297 <div>
298 <div className="tb-modal-title">Tenant Admin</div>
299 <div className="tb-modal-sub">
300 Environments ➜ Generate token (clients send X-Env-Token to /receive)
301 </div>
302 </div>
303 <div style={{ display: "flex", gap: 10 }}>
304 <button className="tb-btn" onClick={logout}>
305 Logout
306 </button>
307 <button className="tb-btn" onClick={() => setOpen(false)}>
308 Close
309 </button>
310 </div>
311 </div>
312
313 <div className="tb-grid">
314 <div className="tb-card">
315 <div className="tb-card-head">
316 <div className="tb-card-title">Choose environment</div>
317 </div>
318 <div className="tb-row">
319 <select
320 className="tb-select"
321 value={selectedEnv}
322 onChange={(e) => changeEnv(e.target.value)}
323 >
324 {envs.map((e) => (
325 <option key={e} value={e}>
326 {e}
327 </option>
328 ))}
329 </select>
330 <div className="tb-help">
331 Овој env ќе се користи за dashboard филтрирање + token generation.
332 <br />
333 <b>Proc history</b> switch горе важи за тековниот env.
334 </div>
335 </div>
336 </div>
337
338 <div className="tb-card">
339 <div className="tb-card-head">
340 <div className="tb-card-title">Create new environment</div>
341 </div>
342 <div className="tb-row">
343 <input
344 className="tb-input"
345 value={newEnvName}
346 onChange={(e) => setNewEnvName(e.target.value)}
347 placeholder="e.g. school-lab / staging"
348 />
349 <button className="tb-btn" onClick={createEnvironment} disabled={busy}>
350 {busy ? "Creating…" : "Create"}
351 </button>
352 </div>
353 </div>
354
355 <div className="tb-card">
356 <div className="tb-card-head">
357 <div className="tb-card-title">Generate token (for clients)</div>
358 </div>
359
360 <div className="tb-row">
361 <button className="tb-btn primary" onClick={generateToken} disabled={busy}>
362 {busy ? "Generating…" : `Generate token for "${selectedEnv}"`}
363 </button>
364
365 {token && (
366 <>
367 <div className="tb-token">
368 <div>
369 <div style={{ fontWeight: 900, color: "rgba(255,255,255,0.92)" }}>
370 Token
371 </div>
372 <div className="tb-mono">
373 {token.slice(0, 12)}…{token.slice(-10)}
374 </div>
375 </div>
376 <button className="tb-btn" onClick={copyToken}>
377 Copy
378 </button>
379 </div>
380
381 <div className="tb-help">
382 Клиентот мора да праќа header: <b>X-Env-Token</b> со овој token на <b>/receive</b>.
383 </div>
384 </>
385 )}
386 </div>
387 </div>
388
389 {error && (
390 <div className="tb-card tb-error">
391 <div style={{ fontWeight: 900 }}>Error</div>
392 <div style={{ marginTop: 6 }}>{error}</div>
393 </div>
394 )}
395 </div>
396 </div>
397 </div>
398 )}
399 </>
400 );
401}
Note: See TracBrowser for help on using the repository browser.