Files
NexaPG/frontend/src/App.jsx
nessi cd1795b9ff
All checks were successful
PostgreSQL Compatibility Matrix / PG14 smoke (pull_request) Successful in 12s
PostgreSQL Compatibility Matrix / PG15 smoke (pull_request) Successful in 11s
PostgreSQL Compatibility Matrix / PG16 smoke (pull_request) Successful in 9s
PostgreSQL Compatibility Matrix / PG17 smoke (pull_request) Successful in 10s
PostgreSQL Compatibility Matrix / PG18 smoke (pull_request) Successful in 11s
Add first and last name fields for users
This commit introduces optional `first_name` and `last_name` fields to the user model, including database migrations, backend, and frontend support. It enhances user profiles, updates user creation and editing flows, and refines the UI to display full names where available.
2026-02-13 10:57:10 +01:00

182 lines
7.7 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";
import { ServiceInfoPage } from "./pages/ServiceInfoPage";
import { UserSettingsPage } from "./pages/UserSettingsPage";
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, serviceUpdateAvailable } = useAuth();
const navigate = useNavigate();
const navClass = ({ isActive }) => `nav-btn${isActive ? " active" : ""}`;
const fullName = [me?.first_name, me?.last_name].filter(Boolean).join(" ").trim();
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>
<NavLink
to="/service-info"
className={({ isActive }) => `nav-btn${isActive ? " active" : ""}${serviceUpdateAvailable ? " update-available" : ""}`}
>
<span className="nav-icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zm0-11v6m0-10h.01" />
</svg>
</span>
<span className="nav-label">Service Information</span>
</NavLink>
{me?.role === "admin" && (
<>
<div className="sidebar-nav-spacer" aria-hidden="true" />
<NavLink to="/admin/users" className={({ isActive }) => `nav-btn admin-nav${isActive ? " active" : ""}`}>
<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 className="profile-name">{fullName || me?.email}</div>
{fullName && <div className="profile-email">{me?.email}</div>}
<div className="role profile-role">{me?.role}</div>
<NavLink to="/user-settings" className={({ isActive }) => `profile-btn${isActive ? " active" : ""}`}>
User Settings
</NavLink>
<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="/service-info" element={<ServiceInfoPage />} />
<Route path="/user-settings" element={<UserSettingsPage />} />
<Route path="/admin/users" element={<AdminUsersPage />} />
</Routes>
</Layout>
</Protected>
}
/>
</Routes>
);
}