Files
Homelab-manager/web/src/layout/AppShell.tsx
T
bobbanandClaude Sonnet 5 8fef68c27a Move Tailscale, Semaphore, and Gitea to their own top-level pages
Same move as Docker: each was buried inside the generic Integrations
browsing view, sharing one "pick any integration" dropdown across six
unrelated types. Give them dedicated pages instead.

New web/src/pages/{Tailscale,Semaphore,Gitea}.tsx each reuse the
existing, unchanged API routes and adapters (no server changes) with
their own integration picker scoped to just that type. Added matching
nav items (Tailscale, Semaphore, Gitea, right after Docker) and routes.

Removed the corresponding state/handlers/tables from Integrations.tsx
entirely (374 lines). Adding, editing, enabling/disabling, and
deleting each credential itself still happens under Integrations ->
Manage integrations, same as Proxmox and Synology, which stay on the
Integrations page. Selecting one of these three types in Integrations'
generic browsing dropdown now points to its new page instead of
rendering a table there too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 00:09:02 +02:00

103 lines
3.7 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,
IconUsers,
IconHistory,
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 & Tasks", icon: <IconServer2 size={20} /> },
{ to: "/dns", label: "DNS", icon: <IconWorld size={20} /> },
{ to: "/ipam", label: "IP Addresses", icon: <IconNetwork size={20} /> },
{ to: "/secrets", label: "Secrets", icon: <IconKey size={20} /> },
{ to: "/integrations", label: "Integrations", icon: <IconPlugConnected size={20} /> },
{ to: "/docker", label: "Docker", icon: <IconBrandDocker 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: "/users", label: "Users", icon: <IconUsers size={20} />, minRole: "admin" },
{ to: "/audit-log", label: "Audit Log", icon: <IconHistory size={20} />, minRole: "operator" },
{ 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>
);
}