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.
159 lines
6.4 KiB
JavaScript
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>
|
|
);
|
|
}
|