Files
headplane/app/components/Header.tsx
T
2026-03-09 00:17:39 -04:00

166 lines
5.1 KiB
TypeScript

import { CircleUser, Globe2, Lock, Server, Settings, Users } from "lucide-react";
import type { ReactNode } from "react";
import { NavLink, useSubmit } from "react-router";
import Logo from "~/components/Logo";
import Menu from "~/components/Menu";
import { AuthSession } from "~/server/web/sessions";
import cn from "~/utils/cn";
interface Props {
configAvailable: boolean;
onboarding: boolean;
user?: AuthSession["user"];
access: {
ui: boolean;
machines: boolean;
dns: boolean;
users: boolean;
policy: boolean;
settings: boolean;
};
}
interface LinkProps {
href: string;
text: string;
}
interface TabLinkProps {
name: string;
to: string;
icon: ReactNode;
}
function TabLink({ name, to, icon }: TabLinkProps) {
return (
<div className="relative py-2">
<NavLink
className={({ isActive }) =>
cn(
"px-3 py-2 flex items-center rounded-md text-nowrap gap-x-2.5",
"after:absolute after:bottom-0 after:left-3 after:right-3",
"after:h-0.5 after:bg-mist-900 dark:after:bg-mist-200",
"hover:bg-mist-200 dark:hover:bg-mist-900",
"focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1",
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
isActive ? "after:visible" : "after:invisible",
)
}
prefetch="intent"
to={to}
>
{icon} {name}
</NavLink>
</div>
);
}
function Link({ href, text }: LinkProps) {
return (
<a
className={cn(
"hidden sm:block hover:underline text-sm",
"focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1 rounded-md",
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
)}
href={href}
rel="noreferrer"
target="_blank"
>
{text}
</a>
);
}
export default function Header(data: Props) {
const submit = useSubmit();
return (
<header
className={cn(
"bg-mist-100 dark:bg-mist-950",
"text-mist-800 dark:text-mist-200",
"dark:border-b dark:border-mist-800",
"shadow-inner",
)}
>
<div className="container flex items-center justify-between py-4">
<div className="flex items-center gap-x-2">
<Logo />
<h1 className="text-2xl font-semibold">headplane</h1>
</div>
<div className="flex items-center gap-x-4">
<Link href="https://tailscale.com/download" text="Download" />
<Link href="https://github.com/tale/headplane" text="GitHub" />
<Link href="https://github.com/juanfont/headscale" text="Headscale" />
{data.user ? (
<Menu>
<Menu.IconButton className={cn(data.user.picture ? "p-0" : "")} label="User">
{data.user.picture ? (
<img
alt={data.user.name}
className="h-8 w-8 rounded-full"
src={data.user.picture}
/>
) : (
<CircleUser />
)}
</Menu.IconButton>
<Menu.Panel
disabledKeys={["profile"]}
onAction={(key) => {
if (key === "logout") {
submit(
{},
{
method: "POST",
action: "/logout",
},
);
}
}}
>
<Menu.Section>
<Menu.Item key="profile" textValue="Profile">
<div className="text-black dark:text-mist-50">
<p className="font-bold">{data.user.name}</p>
<p>{data.user.email}</p>
</div>
</Menu.Item>
<Menu.Item key="logout" textValue="Logout">
<p className="text-red-500 dark:text-red-400">Logout</p>
</Menu.Item>
</Menu.Section>
</Menu.Panel>
</Menu>
) : undefined}
</div>
</div>
{data.access.ui && !data.onboarding ? (
<nav className="container flex items-center gap-x-4 overflow-x-auto font-semibold">
{data.access.machines ? (
<TabLink icon={<Server className="w-5" />} name="Machines" to="/machines" />
) : undefined}
{data.access.users ? (
<TabLink icon={<Users className="w-5" />} name="Users" to="/users" />
) : undefined}
{data.access.policy ? (
<TabLink icon={<Lock className="w-5" />} name="Access Control" to="/acls" />
) : undefined}
{data.configAvailable ? (
<>
{data.access.dns ? (
<TabLink icon={<Globe2 className="w-5" />} name="DNS" to="/dns" />
) : undefined}
{data.access.settings ? (
<TabLink icon={<Settings className="w-5" />} name="Settings" to="/settings" />
) : undefined}
</>
) : undefined}
</nav>
) : undefined}
</header>
);
}