196 lines
7.4 KiB
TypeScript
196 lines
7.4 KiB
TypeScript
import { Link, useLocation } from "wouter";
|
|
import { LayoutDashboard, Wrench, BarChart3, LogIn, LogOut, ShieldCheck, AlertTriangle, Search } from "lucide-react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useAuth } from "@/hooks/use-auth";
|
|
import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ThemeToggle } from "@/components/theme-toggle";
|
|
import { CommandPalette, requestOpenCommandPalette } from "@/components/command-palette";
|
|
import { LanguageSwitcher } from "@/components/language-switcher";
|
|
import { UserMenu } from "@/components/user-menu";
|
|
import { Breadcrumbs } from "@/components/breadcrumbs";
|
|
import { HeaderSearch } from "@/components/header-search";
|
|
import {
|
|
SidebarProvider,
|
|
Sidebar,
|
|
SidebarContent,
|
|
SidebarFooter,
|
|
SidebarGroup,
|
|
SidebarGroupContent,
|
|
SidebarGroupLabel,
|
|
SidebarHeader,
|
|
SidebarInset,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
SidebarRail,
|
|
SidebarTrigger,
|
|
} from "@/components/ui/sidebar";
|
|
|
|
export function Layout({ children }: { children: React.ReactNode }) {
|
|
const [location] = useLocation();
|
|
const { t } = useTranslation();
|
|
const { isAuthenticated, isLoading, isAdmin, login, logout } = useAuth();
|
|
const { data: version } = useGetVersion({
|
|
query: { queryKey: getGetVersionQueryKey(), staleTime: Infinity, retry: false },
|
|
});
|
|
|
|
const mainLinks = [
|
|
{ href: "/", label: t("nav.home"), icon: LayoutDashboard },
|
|
{ href: "/tools", label: t("nav.browseTools"), icon: Wrench },
|
|
{ href: "/analytics", label: t("nav.analytics"), icon: BarChart3 },
|
|
];
|
|
|
|
const adminLinks = [
|
|
{ href: "/admin", label: t("nav.admin"), icon: ShieldCheck },
|
|
{ href: "/admin/redundancy", label: t("nav.redundancy"), icon: AlertTriangle },
|
|
];
|
|
|
|
function isActive(href: string) {
|
|
return location === href || (href !== "/" && location.startsWith(href));
|
|
}
|
|
|
|
return (
|
|
<SidebarProvider>
|
|
<CommandPalette />
|
|
<Sidebar collapsible="icon">
|
|
<SidebarHeader className="flex h-14 items-center justify-between border-b px-4">
|
|
<Link href="/" className="flex items-center gap-2 text-primary font-bold text-xl">
|
|
<Wrench className="w-6 h-6 shrink-0" />
|
|
<span className="group-data-[collapsible=icon]:hidden">toolr</span>
|
|
</Link>
|
|
</SidebarHeader>
|
|
<SidebarContent>
|
|
<SidebarGroup>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
{mainLinks.map((link) => {
|
|
const Icon = link.icon;
|
|
return (
|
|
<SidebarMenuItem key={link.href}>
|
|
<SidebarMenuButton asChild isActive={isActive(link.href)} tooltip={link.label}>
|
|
<Link href={link.href}>
|
|
<Icon />
|
|
<span>{link.label}</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
);
|
|
})}
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
{isAdmin && (
|
|
<SidebarGroup>
|
|
<SidebarGroupLabel className="group-data-[collapsible=icon]:hidden">
|
|
{t("nav.admin")}
|
|
</SidebarGroupLabel>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
{adminLinks.map((link) => {
|
|
const Icon = link.icon;
|
|
return (
|
|
<SidebarMenuItem key={link.href}>
|
|
<SidebarMenuButton asChild isActive={isActive(link.href)} tooltip={link.label}>
|
|
<Link href={link.href}>
|
|
<Icon />
|
|
<span>{link.label}</span>
|
|
</Link>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
);
|
|
})}
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
)}
|
|
</SidebarContent>
|
|
<SidebarFooter className="gap-2">
|
|
<UserMenu />
|
|
<div className="border-t pt-2 flex items-center justify-between text-[11px] text-muted-foreground px-2 group-data-[collapsible=icon]:justify-center">
|
|
<span className="font-medium group-data-[collapsible=icon]:hidden">toolr</span>
|
|
{version?.commitSha ? (
|
|
<a
|
|
href={`https://git.kubebase.de/admin/tool-evaluator/commit/${version.commitSha}`}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="font-mono hover:underline truncate group-data-[collapsible=icon]:hidden"
|
|
title={version.commitSha}
|
|
>
|
|
{version.version && version.version !== "dev"
|
|
? version.version
|
|
: `sha-${version.commitSha.slice(0, 7)}`}
|
|
</a>
|
|
) : version?.version && version.version !== "dev" ? (
|
|
<span className="font-mono group-data-[collapsible=icon]:hidden">{version.version}</span>
|
|
) : null}
|
|
</div>
|
|
</SidebarFooter>
|
|
<SidebarRail />
|
|
</Sidebar>
|
|
|
|
<SidebarInset>
|
|
<header className="flex items-center justify-between gap-2 border-b bg-card px-3 md:px-5 h-14 shrink-0">
|
|
<div className="flex items-center gap-1 min-w-0">
|
|
<SidebarTrigger className="h-8 w-8 shrink-0" data-testid="button-sidebar-toggle" />
|
|
<Link
|
|
href="/"
|
|
className="md:hidden flex items-center gap-2 text-primary font-bold text-lg shrink-0 pl-1"
|
|
>
|
|
<Wrench className="w-5 h-5" />
|
|
<span>toolr</span>
|
|
</Link>
|
|
<div className="hidden md:block min-w-0 pl-1">
|
|
<Breadcrumbs />
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 shrink-0">
|
|
<HeaderSearch />
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-9 w-9 md:hidden"
|
|
onClick={requestOpenCommandPalette}
|
|
aria-label={t("nav.search")}
|
|
data-testid="button-search-mobile"
|
|
>
|
|
<Search className="w-4 h-4" />
|
|
</Button>
|
|
<LanguageSwitcher />
|
|
<ThemeToggle />
|
|
{!isLoading && (
|
|
<div className="md:hidden">
|
|
{isAuthenticated ? (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-9 w-9"
|
|
onClick={logout}
|
|
aria-label={t("auth.signOut")}
|
|
data-testid="button-logout-mobile"
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => login(location)}
|
|
data-testid="button-login-mobile"
|
|
>
|
|
<LogIn className="w-4 h-4 mr-1" />
|
|
{t("auth.signIn")}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</header>
|
|
<div className="flex-1 overflow-auto p-4 md:p-6 lg:p-8">
|
|
{children}
|
|
</div>
|
|
</SidebarInset>
|
|
</SidebarProvider>
|
|
);
|
|
}
|