mirror of
https://github.com/tale/headplane.git
synced 2026-08-06 12:17:41 +00:00
166 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
}
|