109 lines
4.0 KiB
TypeScript
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>
|
|
);
|
|
}
|