Files
NexaPG/frontend/src/App.jsx
nessi 839943d9fd Add navigation and smooth scrolling for alert toasts
This update enables opening specific alerts via toast buttons, utilizing `useNavigate` to redirect and auto-expand the corresponding alert on the Alerts page. Includes enhancements for toast dismissal with animations and adds new styles for smooth transitions and better user interaction.
2026-02-12 13:33:50 +01:00

159 lines
6.4 KiB
JavaScript

import React from "react";
import { NavLink, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
import { useAuth } from "./state";
import { LoginPage } from "./pages/LoginPage";
import { DashboardPage } from "./pages/DashboardPage";
import { TargetsPage } from "./pages/TargetsPage";
import { TargetDetailPage } from "./pages/TargetDetailPage";
import { QueryInsightsPage } from "./pages/QueryInsightsPage";
import { AlertsPage } from "./pages/AlertsPage";
import { AdminUsersPage } from "./pages/AdminUsersPage";
function Protected({ children }) {
const { tokens } = useAuth();
const location = useLocation();
if (!tokens?.accessToken) return <Navigate to="/login" state={{ from: location.pathname }} replace />;
return children;
}
function Layout({ children }) {
const { me, logout, uiMode, setUiMode, alertToasts, dismissAlertToast } = useAuth();
const navigate = useNavigate();
const navClass = ({ isActive }) => `nav-btn${isActive ? " active" : ""}`;
return (
<div className="shell">
<aside className="sidebar">
<div className="brand">
<img src="/nexapg-logo.svg" alt="NexaPG" className="brand-logo" />
<h1>NexaPG</h1>
</div>
<nav className="sidebar-nav">
<NavLink to="/" end className={navClass}>
<span className="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M4 6c0-1.7 3.6-3 8-3s8 1.3 8 3-3.6 3-8 3-8-1.3-8-3zm0 6c0 1.7 3.6 3 8 3s8-1.3 8-3M4 18c0 1.7 3.6 3 8 3s8-1.3 8-3" />
</svg>
</span>
<span className="nav-label">Dashboard</span>
</NavLink>
<NavLink to="/targets" className={navClass}>
<span className="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3zM12 12l8-4.5M12 12L4 7.5M12 12v9" />
</svg>
</span>
<span className="nav-label">Targets</span>
</NavLink>
<NavLink to="/query-insights" className={navClass}>
<span className="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M4 19h16M7 15l3-3 3 2 4-5M18 8h.01" />
</svg>
</span>
<span className="nav-label">Query Insights</span>
</NavLink>
<NavLink to="/alerts" className={navClass}>
<span className="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M15 17h5l-1.4-1.4A2 2 0 0 1 18 14.2V10a6 6 0 0 0-12 0v4.2a2 2 0 0 1-.6 1.4L4 17h5m6 0a3 3 0 0 1-6 0" />
</svg>
</span>
<span className="nav-label">Alerts</span>
</NavLink>
{me?.role === "admin" && (
<NavLink to="/admin/users" className={navClass}>
<span className="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zm-7 8a7 7 0 0 1 14 0" />
</svg>
</span>
<span className="nav-label">Admin</span>
</NavLink>
)}
</nav>
<div className="profile">
<div className="mode-switch-block">
<div className="mode-switch-label">View Mode</div>
<button
className={`mode-toggle ${uiMode === "easy" ? "easy" : "dba"}`}
onClick={() => setUiMode(uiMode === "easy" ? "dba" : "easy")}
type="button"
>
<span className="mode-pill">Easy</span>
<span className="mode-pill">DBA</span>
</button>
<small>{uiMode === "easy" ? "Simple health guidance" : "Advanced DBA metrics"}</small>
</div>
<div>{me?.email}</div>
<div className="role">{me?.role}</div>
<button className="logout-btn" onClick={logout}>Logout</button>
</div>
</aside>
<main className="main">
{children}
<div className="toast-stack" aria-live="polite" aria-atomic="true">
{alertToasts.map((toast) => (
<div key={toast.id} className={`alert-toast ${toast.severity || "warning"}${toast.closing ? " closing" : ""}`}>
<div className="alert-toast-head">
<strong>{toast.severity === "alert" ? "New Alert" : "New Warning"}</strong>
<div className="toast-actions">
<button
type="button"
className="toast-view"
title="Open in Alerts"
onClick={() => {
navigate(`/alerts?open=${encodeURIComponent(toast.alertKey || "")}`);
dismissAlertToast(toast.id);
}}
>
<span aria-hidden="true">
<svg viewBox="0 0 24 24" width="13" height="13">
<path
d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6zm10 3a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"
fill="currentColor"
/>
</svg>
</span>
</button>
<button type="button" className="toast-close" onClick={() => dismissAlertToast(toast.id)}>
x
</button>
</div>
</div>
<div className="alert-toast-title">{toast.title}</div>
<div className="alert-toast-target">{toast.target}</div>
<div className="alert-toast-message">{toast.message}</div>
</div>
))}
</div>
</main>
</div>
);
}
export function App() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="*"
element={
<Protected>
<Layout>
<Routes>
<Route path="/" element={<DashboardPage />} />
<Route path="/targets" element={<TargetsPage />} />
<Route path="/targets/:id" element={<TargetDetailPage />} />
<Route path="/query-insights" element={<QueryInsightsPage />} />
<Route path="/alerts" element={<AlertsPage />} />
<Route path="/admin/users" element={<AdminUsersPage />} />
</Routes>
</Layout>
</Protected>
}
/>
</Routes>
);
}