Files
Homelab-manager/web/src/layout/AppShell.tsx
T

109 lines
4.0 KiB
TypeScript

import { useState, type ReactNode } from "react";
import { NavLink } from "react-router-dom";
import {
IconLayoutDashboard,
IconServer2,
IconWorld,
IconNetwork,
IconKey,
IconPlugConnected,
IconBrandDocker,
IconAffiliate,
IconPlayerPlay,
IconBrandGit,
IconServerCog,
IconDatabase,
IconUsers,
IconHistory,
IconStethoscope,
IconSettings,
IconMenu2,
} from "@tabler/icons-react";
import type { CurrentUser } from "../api/client";
interface NavItem {
to: string;
label: string;
icon: ReactNode;
minRole?: "operator" | "admin";
}
const NAV_ITEMS: NavItem[] = [
{ to: "/", label: "Dashboard", icon: <IconLayoutDashboard size={20} /> },
{ to: "/servers", label: "Servers", icon: <IconServer2 size={20} /> },
{ to: "/proxmox", label: "Proxmox", icon: <IconServerCog size={20} /> },
{ to: "/docker", label: "Docker", icon: <IconBrandDocker size={20} /> },
{ to: "/synology", label: "Synology", icon: <IconDatabase size={20} /> },
{ to: "/secrets", label: "Secrets", icon: <IconKey size={20} /> },
{ to: "/dns", label: "DNS", icon: <IconWorld size={20} /> },
{ to: "/ipam", label: "IP Addresses", icon: <IconNetwork size={20} /> },
{ to: "/tailscale", label: "Tailscale", icon: <IconAffiliate size={20} /> },
{ to: "/semaphore", label: "Semaphore", icon: <IconPlayerPlay size={20} /> },
{ to: "/gitea", label: "Gitea", icon: <IconBrandGit size={20} /> },
{ to: "/integrations", label: "Integrations", icon: <IconPlugConnected size={20} /> },
{ to: "/users", label: "Users", icon: <IconUsers size={20} />, minRole: "admin" },
{ to: "/audit-log", label: "Audit Log", icon: <IconHistory size={20} />, minRole: "operator" },
{ to: "/diag-log", label: "Diagnostic Log", icon: <IconStethoscope size={20} />, minRole: "admin" },
{ to: "/settings", label: "Settings", icon: <IconSettings size={20} />, minRole: "admin" },
];
const roleRank: Record<CurrentUser["role"], number> = { viewer: 0, operator: 1, admin: 2 };
export default function AppShell({ user, children }: { user: CurrentUser; children: ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const visibleItems = NAV_ITEMS.filter(
(item) => !item.minRole || roleRank[user.role] >= roleRank[item.minRole],
);
return (
<div className="page">
<aside className="navbar navbar-vertical navbar-expand-lg" data-bs-theme="dark">
<div className="container-fluid">
<button
type="button"
className="navbar-toggler"
onClick={() => setSidebarOpen((open) => !open)}
aria-label="Toggle navigation"
>
<IconMenu2 size={20} />
</button>
<h1 className="navbar-brand navbar-brand-autodark">
<NavLink to="/">Homelab Manager</NavLink>
</h1>
<div className={`navbar-collapse collapse ${sidebarOpen ? "show" : ""}`}>
<ul className="navbar-nav pt-lg-3">
{visibleItems.map((item) => (
<li className="nav-item" key={item.to}>
<NavLink
to={item.to}
end={item.to === "/"}
className="nav-link"
onClick={() => setSidebarOpen(false)}
>
<span className="nav-link-icon d-md-none d-lg-inline-block">{item.icon}</span>
<span className="nav-link-title">{item.label}</span>
</NavLink>
</li>
))}
</ul>
<div className="navbar-footer px-3">
<div className="text-secondary small mb-2">
Signed in as {user.name ?? user.email ?? user.sub} · {user.role}
</div>
<a className="btn btn-outline-secondary btn-sm w-100" href="/auth/logout">
Sign out
</a>
</div>
</div>
</div>
</aside>
<div className="page-wrapper">
<div className="page-body">
<div className="container-xl">{children}</div>
</div>
</div>
</div>
);
}