mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 12:37:20 +00:00
241 lines
7.4 KiB
TypeScript
241 lines
7.4 KiB
TypeScript
import { SegmentedControl } from "@mantine/core";
|
|
import { IconApi, IconBook, IconKeyOff, IconSettings, IconUser } from "@tabler/icons-react";
|
|
import {
|
|
TbDatabase,
|
|
TbFingerprint,
|
|
TbHierarchy2,
|
|
TbMenu2,
|
|
TbPhoto,
|
|
TbSelector,
|
|
TbUser,
|
|
TbX,
|
|
} from "react-icons/tb";
|
|
import { useAuth, useBkndWindowContext } from "bknd/client";
|
|
import { useBknd } from "ui/client/bknd";
|
|
import { useTheme } from "ui/client/use-theme";
|
|
import { Button } from "ui/components/buttons/Button";
|
|
import { IconButton } from "ui/components/buttons/IconButton";
|
|
import { Logo } from "ui/components/display/Logo";
|
|
import { Dropdown, type DropdownProps } from "ui/components/overlay/Dropdown";
|
|
import { Link } from "ui/components/wouter/Link";
|
|
import { useEvent } from "ui/hooks/use-event";
|
|
import { useNavigate } from "ui/lib/routes";
|
|
import { useLocation } from "wouter";
|
|
import { NavLink } from "./AppShell";
|
|
import { autoFormatString } from "core/utils";
|
|
import { appShellStore } from "ui/store";
|
|
import { getVersion, isDebug } from "core/env";
|
|
import { McpIcon } from "ui/routes/tools/mcp/components/mcp-icon";
|
|
import { useAppShellAdminOptions } from "ui/options";
|
|
|
|
export function HeaderNavigation() {
|
|
const [location, navigate] = useLocation();
|
|
const { config } = useBknd();
|
|
|
|
const items: {
|
|
label: string;
|
|
href: string;
|
|
Icon?: any;
|
|
exact?: boolean;
|
|
tooltip?: string;
|
|
disabled?: boolean;
|
|
}[] = [
|
|
{ label: "Data", href: "/data", Icon: TbDatabase },
|
|
{ label: "Auth", href: "/auth", Icon: TbFingerprint },
|
|
{ label: "Media", href: "/media", Icon: TbPhoto },
|
|
];
|
|
|
|
if (isDebug() || Object.keys(config.flows?.flows ?? {}).length > 0) {
|
|
items.push({ label: "Flows", href: "/flows", Icon: TbHierarchy2 });
|
|
}
|
|
|
|
if (config.server.mcp.enabled) {
|
|
items.push({ label: "MCP", href: "/tools/mcp", Icon: McpIcon });
|
|
}
|
|
|
|
const activeItem = items.find((item) =>
|
|
item.exact ? location === item.href : location.startsWith(item.href),
|
|
);
|
|
|
|
const handleItemClick = useEvent((item) => {
|
|
navigate(item.href);
|
|
});
|
|
|
|
const renderDropdownItem = (item, { key, onClick }) => (
|
|
<NavLink key={key} onClick={onClick} as="button" className="rounded-md">
|
|
<div
|
|
data-active={activeItem?.label === item.label}
|
|
className="flex flex-row items-center gap-2.5 data-[active=true]:opacity-50"
|
|
>
|
|
<item.Icon size={18} />
|
|
<span className="text-lg">{item.label}</span>
|
|
</div>
|
|
</NavLink>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<nav className="hidden md:flex flex-row gap-2.5 pl-0 p-2.5 items-center">
|
|
{items.map((item) => (
|
|
<NavLink
|
|
key={item.href}
|
|
as={Link}
|
|
href={item.href}
|
|
Icon={item.Icon}
|
|
disabled={item.disabled}
|
|
>
|
|
{item.label}
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
<nav className="flex md:hidden flex-row items-center">
|
|
{activeItem && (
|
|
<Dropdown
|
|
items={items}
|
|
onClickItem={handleItemClick}
|
|
renderItem={renderDropdownItem}
|
|
>
|
|
<NavLink as="button" Icon={activeItem.Icon} className="active pl-6 pr-3.5">
|
|
<div className="flex flex-row gap-2 items-center">
|
|
<span className="text-lg">{activeItem.label}</span>
|
|
<TbSelector size={18} className="opacity-70" />
|
|
</div>
|
|
</NavLink>
|
|
</Dropdown>
|
|
)}
|
|
</nav>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function SidebarToggler({ name = "default" }: { name?: string }) {
|
|
const toggle = appShellStore((store) => store.toggleSidebar(name));
|
|
const open = appShellStore((store) => store.sidebars[name]?.open);
|
|
return <IconButton id="toggle-sidebar" size="lg" Icon={open ? TbX : TbMenu2} onClick={toggle} />;
|
|
}
|
|
|
|
export function Header({ hasSidebar = true }) {
|
|
const { app } = useBknd();
|
|
const { theme } = useTheme();
|
|
const { logo_return_path = "/" } = app.options;
|
|
|
|
return (
|
|
<header
|
|
id="header"
|
|
data-shell="header"
|
|
className="flex flex-row w-full h-16 gap-2.5 border-muted border-b justify-start bg-muted/10"
|
|
>
|
|
<Link
|
|
href={logo_return_path}
|
|
native={logo_return_path !== "/"}
|
|
className="max-h-full flex hover:bg-primary/5 link p-2.5 w-[134px] outline-none"
|
|
>
|
|
<Logo theme={theme} />
|
|
</Link>
|
|
<HeaderNavigation />
|
|
<div className="flex flex-grow" />
|
|
<div className="flex md:hidden flex-row items-center pr-2 gap-2">
|
|
<SidebarToggler name="default" />
|
|
<UserMenu />
|
|
</div>
|
|
<div className="hidden md:flex flex-row items-center px-4 gap-2">
|
|
<UserMenu />
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|
|
|
|
function UserMenu() {
|
|
const { config } = useBknd();
|
|
const uiOptions = useAppShellAdminOptions();
|
|
|
|
const auth = useAuth();
|
|
const [navigate] = useNavigate();
|
|
const { logout_route } = useBkndWindowContext();
|
|
|
|
async function handleLogout() {
|
|
await auth.logout();
|
|
|
|
if (!auth.local) {
|
|
navigate(logout_route, { reload: true });
|
|
}
|
|
}
|
|
|
|
async function handleLogin() {
|
|
navigate("/auth/login");
|
|
}
|
|
|
|
const items: DropdownProps["items"] = [
|
|
...(uiOptions.userMenu ?? []),
|
|
{ label: "Settings", onClick: () => navigate("/settings"), icon: IconSettings },
|
|
{
|
|
label: "OpenAPI",
|
|
onClick: () => window.open("/api/system/swagger", "_blank"),
|
|
icon: IconApi,
|
|
},
|
|
{
|
|
label: "Docs",
|
|
onClick: () => window.open("https://docs.bknd.io", "_blank"),
|
|
icon: IconBook,
|
|
},
|
|
];
|
|
|
|
if (config.server.mcp.enabled) {
|
|
items.push({
|
|
label: "MCP",
|
|
onClick: () => navigate("/tools/mcp"),
|
|
icon: McpIcon,
|
|
});
|
|
}
|
|
|
|
if (config.auth.enabled) {
|
|
if (!auth.user) {
|
|
items.push({ label: "Login", onClick: handleLogin, icon: IconUser });
|
|
} else {
|
|
items.push({
|
|
label: "Logout",
|
|
title: `Logout ${auth.user.email}`,
|
|
onClick: handleLogout,
|
|
icon: IconKeyOff,
|
|
});
|
|
}
|
|
}
|
|
|
|
items.push(() => <UserMenuThemeToggler />);
|
|
items.push(() => (
|
|
<div className="font-mono leading-none text-xs text-primary/50 text-center pb-1 pt-2 mt-1 border-t border-primary/5">
|
|
{getVersion()}
|
|
</div>
|
|
));
|
|
|
|
return (
|
|
<>
|
|
<Dropdown items={items} position="bottom-end">
|
|
{auth.user ? (
|
|
<Button className="rounded-full w-12 h-12 justify-center p-0 text-lg">
|
|
{auth.user.email[0]?.toUpperCase()}
|
|
</Button>
|
|
) : (
|
|
<Button className="rounded-full w-12 h-12 justify-center p-0" IconLeft={TbUser} />
|
|
)}
|
|
</Dropdown>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function UserMenuThemeToggler() {
|
|
const { value, themes, setTheme } = useTheme();
|
|
return (
|
|
<div className="flex flex-col items-center mt-1 pt-1 border-t border-primary/5">
|
|
<SegmentedControl
|
|
withItemsBorders={false}
|
|
className="w-full"
|
|
data={themes.map((t) => ({ value: t, label: autoFormatString(t) }))}
|
|
value={value}
|
|
onChange={setTheme}
|
|
size="xs"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|