import { ArrowUpRight, BookUser, Boxes, BoxIcon, BugIcon, Building, Building2, ChartColumnBig, ChevronsLeftRightEllipsis, Construction, DatabaseZap, FlaskConical, FolderGit, Globe, KeyRound, Landmark, LayoutGrid, LogOut, Logs, Network, PanelLeftClose, Plug, Puzzle, ScrollText, Search, SearchCheck, Settings, Settings2Icon, ShieldCheck, ShieldUser, Shuffle, SlidersHorizontal, Telescope, ToolCase, TrendingUp, User, UserRoundCheck, Users, Wallet, WalletCards, } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Separator } from "@/components/ui/separator"; import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, useSidebar, } from "@/components/ui/sidebar"; import { useWebSocket } from "@/hooks/useWebSocket"; import { IS_ENTERPRISE, TRIAL_EXPIRY } from "@/lib/constants/config"; import { useGetCoreConfigQuery, useGetLatestReleaseQuery, useGetVersionQuery, useLogoutMutation } from "@/lib/store"; import { cn } from "@/lib/utils"; import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib"; import type { UserInfo } from "@enterprise/lib/store/utils/tokenManager"; import { getUserInfo } from "@enterprise/lib/store/utils/tokenManager"; import { BooksIcon, DiscordLogoIcon, GithubLogoIcon } from "@phosphor-icons/react"; import { Link, useLocation, useNavigate } from "@tanstack/react-router"; import { differenceInDays } from "date-fns"; import { ChevronRight } from "lucide-react"; import { useTheme } from "next-themes"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCookies } from "react-cookie"; import { ThemeToggle } from "./themeToggle"; import { Badge } from "./ui/badge"; import { PromoCardStack } from "./ui/promoCardStack"; // Cookie name for dismissing production setup card const PRODUCTION_SETUP_DISMISSED_COOKIE = "bifrost_production_setup_dismissed"; // Custom MCP Icon Component const MCPIcon = ({ className }: { className?: string }) => ( MCP clients icon ); // Main navigation items // External links const externalLinks = [ { title: "Discord Server", url: "https://discord.gg/exN5KAydbU", icon: DiscordLogoIcon, }, { title: "GitHub Repository", url: "https://github.com/maximhq/bifrost", icon: GithubLogoIcon, }, { title: "Report a bug", url: "https://github.com/maximhq/bifrost/issues/new?title=[Bug Report]&labels=bug&type=bug&projects=maximhq/1", icon: BugIcon, strokeWidth: 1.5, }, { title: "Full Documentation", url: "https://docs.getbifrost.ai", icon: BooksIcon, strokeWidth: 1, }, ]; // Base promotional card (memoized outside component to prevent recreation) const productionSetupHelpCard = { id: "production-setup", title: "Need help with production setup?", description: ( <> We offer help with production setup including custom integrations and dedicated support. Book a demo with our team{" "} here . > ), dismissible: true, }; // Sidebar item interface interface SidebarItem { title: string; url: string; icon: React.ComponentType<{ className?: string }>; description: string; isAllowed?: boolean; hasAccess: boolean; subItems?: SidebarItem[]; tag?: string; isExternal?: boolean; queryParam?: string; // Optional: for tab-based subitems (e.g., "client-settings") } const getSidebarItemHref = (item: Pick) => { return item.queryParam ? `${item.url}?tab=${item.queryParam}` : item.url; }; const TIME_FILTER_PAGES = new Set(["/workspace/dashboard", "/workspace/logs", "/workspace/mcp-logs"]); const SidebarItemView = ({ item, isActive, isExternal, isWebSocketConnected, isExpanded, onToggle, pathname, search, isSidebarCollapsed, expandSidebar, highlightedUrl, }: { item: SidebarItem; isActive: boolean; isExternal?: boolean; isWebSocketConnected: boolean; isExpanded?: boolean; onToggle?: () => void; pathname: string; search: string; isSidebarCollapsed: boolean; expandSidebar: () => void; highlightedUrl?: string; }) => { const hasSubItems = "subItems" in item && item.subItems && item.subItems.length > 0; const isRouteMatch = (url: string) => { if (url === "/workspace/custom-pricing") return pathname === url; return pathname.startsWith(url); }; const isAnySubItemActive = hasSubItems && item.subItems?.some((subItem) => { return isRouteMatch(subItem.url); }); const handleClick = (e: React.MouseEvent) => { if (hasSubItems && item.hasAccess) { e.preventDefault(); // If sidebar is collapsed, expand it first then toggle the submenu if (isSidebarCollapsed) { expandSidebar(); // Small delay to allow sidebar to expand before toggling submenu setTimeout(() => { if (onToggle) onToggle(); }, 100); } else if (onToggle) { onToggle(); } } }; const isHighlighted = !hasSubItems && highlightedUrl === item.url; const buttonClassName = `relative h-7.5 cursor-pointer rounded-sm border px-3 transition-all duration-200 ${ isHighlighted ? "bg-sidebar-accent text-accent-foreground border-primary/20" : isActive || isAnySubItemActive ? "bg-sidebar-accent text-primary border-primary/20" : item.hasAccess ? "hover:bg-sidebar-accent hover:text-accent-foreground border-transparent text-slate-500 dark:text-zinc-400" : "hover:bg-destructive/5 hover:text-muted-foreground text-muted-foreground cursor-not-allowed border-transparent" } `; const innerContent = ( {item.title} {item.tag && ( {item.tag} )} {hasSubItems && ( )} {!hasSubItems && item.url === "/logs" && isWebSocketConnected && ( )} {isExternal && } ); // Render strategy: // - Items with sub-items: (toggle, not navigation) // - Leaf items, no access: (disabled-style, non-clickable) // - Leaf items, external: // - Leaf items, internal: TanStack with preload-on-hover let menuButton: React.ReactNode; if (hasSubItems) { menuButton = ( {innerContent} ); } else if (!item.hasAccess) { menuButton = ( {innerContent} ); } else if (isExternal) { menuButton = ( {innerContent} ); } else { menuButton = ( {innerContent} ); } return ( {menuButton} {hasSubItems && isExpanded && ( {item.subItems?.map((subItem: SidebarItem) => { const baseHref = getSidebarItemHref(subItem); const subItemHref = (() => { if (TIME_FILTER_PAGES.has(subItem.url) && TIME_FILTER_PAGES.has(pathname)) { const currentParams = new URLSearchParams(search); const startTime = currentParams.get("start_time"); const endTime = currentParams.get("end_time"); const period = currentParams.get("period"); if ((startTime && endTime) || period) { const params = new URLSearchParams(); if (startTime) params.set("start_time", startTime); if (endTime) params.set("end_time", endTime); if (period) params.set("period", period); const sep = baseHref.includes("?") ? "&" : "?"; return `${baseHref}${sep}${params.toString()}`; } } return baseHref; })(); // For query param based subitems, check if tab matches const isSubItemActive = subItem.queryParam ? pathname === subItem.url : isRouteMatch(subItem.url); const isSubItemHighlighted = highlightedUrl ? subItemHref.startsWith(highlightedUrl) : false; const SubItemIcon = subItem.icon; const subItemClassName = `h-7 cursor-pointer rounded-sm px-2 transition-all duration-200 ${ isSubItemHighlighted ? "bg-sidebar-accent text-accent-foreground" : isSubItemActive ? "bg-sidebar-accent text-primary font-medium" : subItem.hasAccess === false ? "hover:bg-destructive/5 hover:text-muted-foreground text-muted-foreground cursor-not-allowed border-transparent" : "hover:bg-sidebar-accent hover:text-accent-foreground text-slate-500 dark:text-zinc-400" }`; const subInner = ( {SubItemIcon && } {subItem.title} {subItem.tag && ( {subItem.tag} )} ); return ( {subItem.hasAccess === false ? ( {subInner} ) : ( {subInner} )} ); })} )} ); }; // Helper function to compare semantic versions const compareVersions = (v1: string, v2: string): number => { // Remove 'v' prefix if present const cleanV1 = v1.startsWith("v") ? v1.slice(1) : v1; const cleanV2 = v2.startsWith("v") ? v2.slice(1) : v2; // Split into main version and prerelease const [mainV1, prereleaseV1] = cleanV1.split("-"); const [mainV2, prereleaseV2] = cleanV2.split("-"); // Compare main version numbers (major.minor.patch) const partsV1 = mainV1.split(".").map(Number); const partsV2 = mainV2.split(".").map(Number); for (let i = 0; i < Math.max(partsV1.length, partsV2.length); i++) { const num1 = partsV1[i] || 0; const num2 = partsV2[i] || 0; if (num1 > num2) return 1; if (num1 < num2) return -1; } // If main versions are equal, check prerelease // Version without prerelease is higher than version with prerelease if (!prereleaseV1 && prereleaseV2) return 1; if (prereleaseV1 && !prereleaseV2) return -1; // Both have prereleases, compare them if (prereleaseV1 && prereleaseV2) { // Extract prerelease number (e.g., "prerelease1" -> 1) const prereleaseNum1 = parseInt(prereleaseV1.replace(/\D/g, "")) || 0; const prereleaseNum2 = parseInt(prereleaseV2.replace(/\D/g, "")) || 0; if (prereleaseNum1 > prereleaseNum2) return 1; if (prereleaseNum1 < prereleaseNum2) return -1; } return 0; }; export default function AppSidebar() { const pathname = useLocation({ select: (l) => l.pathname }); const search = useLocation({ select: (l) => l.searchStr ?? "" }); const tsNavigate = useNavigate(); // Wrapper that accepts arbitrary string URLs (TanStack Router's `to` is // strictly typed, but our sidebar items come from a runtime config). const navigate = useCallback((url: string) => tsNavigate({ to: url as any }), [tsNavigate]); const [mounted, setMounted] = useState(false); const [expandedItems, setExpandedItems] = useState>(new Set()); const [areCardsEmpty, setAreCardsEmpty] = useState(false); const [userPopoverOpen, setUserPopoverOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [focusedIndex, setFocusedIndex] = useState(-1); const searchInputRef = useRef(null); const [cookies, setCookie] = useCookies([PRODUCTION_SETUP_DISMISSED_COOKIE]); const isProductionSetupDismissed = !!cookies[PRODUCTION_SETUP_DISMISSED_COOKIE]; const { data: latestRelease } = useGetLatestReleaseQuery(undefined, { skip: !mounted, // Only fetch after component is mounted }); const hasLogsAccess = useRbac(RbacResource.Logs, RbacOperation.View); const hasObservabilityAccess = useRbac(RbacResource.Observability, RbacOperation.View); const hasModelProvidersAccess = useRbac(RbacResource.ModelProvider, RbacOperation.View); const hasMCPGatewayAccess = useRbac(RbacResource.MCPGateway, RbacOperation.View); const hasPluginsAccess = useRbac(RbacResource.Plugins, RbacOperation.View); const hasUsersAccess = useRbac(RbacResource.Users, RbacOperation.View); const hasUserProvisioningAccess = useRbac(RbacResource.UserProvisioning, RbacOperation.View); const hasAuditLogsAccess = useRbac(RbacResource.AuditLogs, RbacOperation.View); const hasCustomersAccess = useRbac(RbacResource.Customers, RbacOperation.View); const hasTeamsAccess = useRbac(RbacResource.Teams, RbacOperation.View); const hasBusinessUnitsAccess = useRbac(RbacResource.Governance, RbacOperation.View); const hasRbacAccess = useRbac(RbacResource.RBAC, RbacOperation.View); const hasVirtualKeysAccess = useRbac(RbacResource.VirtualKeys, RbacOperation.View); const hasGovernanceAccess = useRbac(RbacResource.Governance, RbacOperation.View); const hasRoutingRulesAccess = useRbac(RbacResource.RoutingRules, RbacOperation.View); const hasGuardrailsProvidersAccess = useRbac(RbacResource.GuardrailsProviders, RbacOperation.View); const hasGuardrailsConfigAccess = useRbac(RbacResource.GuardrailsConfig, RbacOperation.View); const hasClusterConfigAccess = useRbac(RbacResource.Cluster, RbacOperation.View); const isAdaptiveRoutingAllowed = useRbac(RbacResource.AdaptiveRouter, RbacOperation.View); const hasSettingsAccess = useRbac(RbacResource.Settings, RbacOperation.View); const hasPromptRepositoryAccess = useRbac(RbacResource.PromptRepository, RbacOperation.View); const hasPromptDeploymentStrategyAccess = useRbac(RbacResource.PromptDeploymentStrategy, RbacOperation.View); const hasAccessProfilesAccess = useRbac(RbacResource.AccessProfiles, RbacOperation.View); const { data: coreConfig } = useGetCoreConfigQuery({}); const isDbConnected = coreConfig?.is_db_connected ?? false; const items = useMemo( () => [ { title: "Observability", url: "/workspace/logs", icon: Telescope, description: "Request logs & monitoring", hasAccess: hasLogsAccess, subItems: [ { title: "Dashboard", url: "/workspace/dashboard", icon: ChartColumnBig, description: "Dashboard", hasAccess: hasObservabilityAccess, }, { title: "LLM Logs", url: "/workspace/logs", icon: Logs, description: "LLM request logs & monitoring", hasAccess: hasLogsAccess, }, { title: "MCP Logs", url: "/workspace/mcp-logs", icon: MCPIcon, description: "MCP tool execution logs", hasAccess: hasLogsAccess, }, { title: "Connectors", url: "/workspace/observability", icon: ChevronsLeftRightEllipsis, description: "Log connectors", hasAccess: hasObservabilityAccess, }, { title: "Logs Settings", url: "/workspace/config/logging", icon: Settings, description: "Logs configuration", hasAccess: hasSettingsAccess, }, ], }, { title: "Models", url: "/workspace/providers", icon: BoxIcon, description: "Configure models", hasAccess: true, subItems: [ { title: "Model Catalog", url: "/workspace/model-catalog", icon: LayoutGrid, description: "Overview of providers, keys, and usage", hasAccess: hasModelProvidersAccess, }, { title: "Model Providers", url: "/workspace/providers", icon: Boxes, description: "Configure models", hasAccess: hasModelProvidersAccess, }, { title: "Budgets & Limits", url: "/workspace/model-limits", icon: Wallet, description: "Model limits", hasAccess: hasGovernanceAccess, }, { title: "Routing Rules", url: "/workspace/routing-rules", icon: Network, description: "Intelligent routing rules", hasAccess: hasRoutingRulesAccess, }, { title: "Pricing Overrides", url: "/workspace/custom-pricing/overrides", icon: SlidersHorizontal, description: "Scoped pricing overrides", hasAccess: hasSettingsAccess, }, { title: "Model Settings", url: "/workspace/custom-pricing", icon: Settings, description: "Model and routing configuration", hasAccess: hasSettingsAccess, }, ], }, { title: "MCP Gateway", icon: MCPIcon, description: "MCP configuration", url: "/workspace/mcp-gateway", hasAccess: hasMCPGatewayAccess, subItems: [ { title: "MCP Catalog", url: "/workspace/mcp-registry", icon: LayoutGrid, description: "MCP tool catalog", hasAccess: hasMCPGatewayAccess, }, { title: "Tool Groups", url: "/workspace/mcp-tool-groups", icon: ToolCase, description: "Tool Groups", hasAccess: hasMCPGatewayAccess, }, { title: "Auth Config", url: "/workspace/mcp-auth-config", icon: ShieldUser, description: "MCP auth config", hasAccess: hasMCPGatewayAccess, }, { title: "MCP Settings", url: "/workspace/mcp-settings", icon: Settings, description: "MCP configuration", hasAccess: hasMCPGatewayAccess, }, ], }, { title: "Plugins", url: "/workspace/plugins", icon: Puzzle, description: "Manage custom plugins", hasAccess: hasPluginsAccess, }, { title: "Governance", url: "/workspace/governance", icon: Landmark, description: "Virtual keys, users, teams, customers & roles", hasAccess: hasGovernanceAccess, subItems: [ { title: "Virtual Keys", url: "/workspace/governance/virtual-keys", icon: KeyRound, description: "Manage virtual keys & access", hasAccess: hasVirtualKeysAccess, }, { title: "Users", url: "/workspace/governance/users", icon: Users, description: "Manage users", hasAccess: hasUsersAccess, }, { title: "Teams", url: "/workspace/governance/teams", icon: Building, description: "Manage teams", hasAccess: hasTeamsAccess, }, { title: "Business Units", url: "/workspace/governance/business-units", icon: Building2, description: "Manage business units", hasAccess: hasBusinessUnitsAccess, }, { title: "Customers", url: "/workspace/governance/customers", icon: WalletCards, description: "Manage customers", hasAccess: hasCustomersAccess, }, { title: "User Provisioning", url: "/workspace/scim", icon: BookUser, description: "User management and provisioning", hasAccess: hasUserProvisioningAccess, }, { title: "Roles & Permissions", url: "/workspace/governance/rbac", icon: UserRoundCheck, description: "User roles and permissions", hasAccess: hasRbacAccess, }, { title: "Access Profiles", url: "/workspace/governance/access-profiles", icon: ShieldCheck, description: "Manage access profiles for roles", hasAccess: hasAccessProfilesAccess, }, { title: "Audit Logs", url: "/workspace/audit-logs", icon: ScrollText, description: "Audit logs and compliance", hasAccess: hasAuditLogsAccess, }, ], }, { title: "Guardrails", url: "/workspace/guardrails", icon: Construction, description: "Guardrails configuration", hasAccess: hasGuardrailsConfigAccess || hasGuardrailsProvidersAccess, subItems: [ { title: "Rules", url: "/workspace/guardrails/configuration", icon: SearchCheck, description: "Guardrail rules", hasAccess: hasGuardrailsConfigAccess, }, { title: "Providers", url: "/workspace/guardrails/providers", icon: Boxes, description: "Guardrail providers configuration", hasAccess: hasGuardrailsProvidersAccess, }, ], }, { title: "Cluster Config", url: "/workspace/cluster", icon: Network, description: "Manage Bifrost cluster", hasAccess: hasClusterConfigAccess, }, { title: "Adaptive Routing", url: "/workspace/adaptive-routing", icon: Shuffle, description: "Manage adaptive load balancer", hasAccess: isAdaptiveRoutingAllowed, }, ...(isDbConnected ? [ { title: "Prompt Repository", url: "/workspace/prompt-repo", icon: FolderGit, description: "Prompt repository", hasAccess: hasPromptRepositoryAccess, }, ] : []), { title: "Evals", url: "https://www.getmaxim.ai", icon: FlaskConical, isExternal: true, description: "Evaluations", hasAccess: true, }, { title: "Settings", url: "/workspace/config", icon: Settings2Icon, description: "Bifrost settings", hasAccess: hasSettingsAccess || hasAuditLogsAccess || hasUserProvisioningAccess, subItems: [ { title: "Client Settings", url: "/workspace/config/client-settings", icon: Settings, description: "Client configuration settings", hasAccess: hasSettingsAccess, }, { title: "Compatibility", url: "/workspace/config/compatibility", icon: Plug, description: "Compatibility conversion settings", hasAccess: hasSettingsAccess, }, { title: "Caching", url: "/workspace/config/caching", icon: DatabaseZap, description: "Caching configuration", hasAccess: hasSettingsAccess, }, { title: "Security", url: "/workspace/config/security", icon: ShieldCheck, description: "Security settings", hasAccess: hasSettingsAccess, }, ...(IS_ENTERPRISE ? [ { title: "Proxy", url: "/workspace/config/proxy", icon: Globe, description: "Proxy configuration", hasAccess: hasSettingsAccess, }, ] : []), { title: "API Keys", url: "/workspace/config/api-keys", icon: KeyRound, description: "API keys management", hasAccess: hasSettingsAccess, }, { title: "Performance Tuning", url: "/workspace/config/performance-tuning", icon: TrendingUp, description: "Performance tuning settings", hasAccess: hasSettingsAccess, }, ], }, ], [ hasLogsAccess, hasObservabilityAccess, hasModelProvidersAccess, hasMCPGatewayAccess, hasPluginsAccess, hasUsersAccess, hasUserProvisioningAccess, hasAuditLogsAccess, hasCustomersAccess, hasTeamsAccess, hasBusinessUnitsAccess, hasRbacAccess, hasVirtualKeysAccess, hasGovernanceAccess, hasRoutingRulesAccess, hasGuardrailsProvidersAccess, hasGuardrailsConfigAccess, hasClusterConfigAccess, isAdaptiveRoutingAllowed, hasSettingsAccess, hasPromptRepositoryAccess, hasPromptDeploymentStrategyAccess, hasAccessProfilesAccess, isDbConnected, ], ); const filteredItems: SidebarItem[] = useMemo(() => { const query = searchQuery.trim().toLowerCase(); if (!query) return items; return items .map((item) => { const parentMatches = item.title.toLowerCase().includes(query); if (parentMatches) return item; if (item.subItems) { const matchingSubItems = item.subItems.filter((sub) => sub.title.toLowerCase().includes(query)); if (matchingSubItems.length > 0) { return { ...item, subItems: matchingSubItems }; } } return null; }) .filter(Boolean) as SidebarItem[]; }, [items, searchQuery]); const { data: version } = useGetVersionQuery(); const { resolvedTheme } = useTheme(); const [logout] = useLogoutMutation(); // Get user info from localStorage (for enterprise SCIM OAuth) const [userInfo, setUserInfo] = useState(null); useEffect(() => { if (IS_ENTERPRISE) { const info = getUserInfo(); setUserInfo(info); } }, []); const showNewReleaseBanner = useMemo(() => { if (IS_ENTERPRISE) return false; if (latestRelease && version) { return compareVersions(latestRelease.name, version) > 0; } return false; }, [latestRelease, version]); const isAuthEnabled = coreConfig?.auth_config?.is_enabled || false; useEffect(() => { setMounted(true); }, []); // Auto-expand items when their subitems are active useEffect(() => { const newExpandedItems = new Set(); const isRouteMatch = (url: string) => { if (url === "/workspace/custom-pricing") return pathname === url; return pathname.startsWith(url); }; items.forEach((item) => { if (item.subItems?.some((subItem) => isRouteMatch(subItem.url))) { newExpandedItems.add(item.title); } }); if (newExpandedItems.size > 0) { setExpandedItems((prev) => new Set([...prev, ...newExpandedItems])); } }, [pathname, items]); // Auto-expand parents when search matches their subItems useEffect(() => { const query = searchQuery.trim().toLowerCase(); if (!query) return; const toExpand = new Set(); items.forEach((item) => { if (!item.subItems?.length) return; const parentMatches = item.title.toLowerCase().includes(query); if (parentMatches) return; const hasMatchingChild = item.subItems.some((sub) => sub.title.toLowerCase().includes(query)); if (hasMatchingChild) { toExpand.add(item.title); } }); if (toExpand.size > 0) { setExpandedItems((prev) => { const hasAll = [...toExpand].every((t) => prev.has(t)); if (hasAll) return prev; return new Set([...prev, ...toExpand]); }); } }, [searchQuery, items]); // Cmd+K to focus search input useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "k" && (event.metaKey || event.ctrlKey)) { event.preventDefault(); searchInputRef.current?.focus(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, []); // Flat list of navigable items for keyboard navigation const navigableItems = useMemo(() => { const result: { title: string; url: string; queryParam?: string; isExternal?: boolean; }[] = []; for (const item of filteredItems) { if (item.isExternal) { if (item.hasAccess) result.push({ title: item.title, url: item.url, isExternal: true }); continue; } const hasSubItems = item.subItems && item.subItems.length > 0; if (hasSubItems) { // When search is active or parent is expanded, include visible subItems if (searchQuery.trim() || expandedItems.has(item.title)) { for (const sub of item.subItems!) { if (sub.hasAccess === false) continue; result.push({ title: sub.title, url: getSidebarItemHref(sub), queryParam: sub.queryParam, }); } } else { // Parent is collapsed - include parent as a toggle target if (item.hasAccess) result.push({ title: item.title, url: item.url }); } } else { if (item.hasAccess) result.push({ title: item.title, url: item.url }); } } return result; }, [filteredItems, expandedItems, searchQuery]); const handleSearchKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "ArrowDown") { e.preventDefault(); setFocusedIndex((prev) => Math.min(prev + 1, navigableItems.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setFocusedIndex((prev) => Math.max(prev - 1, 0)); } else if (e.key === "Enter") { e.preventDefault(); const target = navigableItems[focusedIndex]; if (target) { const url = target.url; if (target.isExternal || e.metaKey || e.ctrlKey) { window.open(url, "_blank", "noopener,noreferrer"); } else { navigate(url); } setSearchQuery(""); setFocusedIndex(-1); searchInputRef.current?.blur(); } } else if (e.key === "Escape") { setSearchQuery(""); setFocusedIndex(-1); searchInputRef.current?.blur(); } }, [navigableItems, focusedIndex, navigate], ); // Auto-scroll focused item into view useEffect(() => { if (focusedIndex < 0) return; const url = navigableItems[focusedIndex]?.url; if (!url) return; const el = document.querySelector(`[data-nav-url="${url}"]`); el?.scrollIntoView({ block: "nearest" }); }, [focusedIndex, navigableItems]); const toggleItem = (title: string) => { setExpandedItems((prev) => { const next = new Set(prev); if (next.has(title)) { next.delete(title); } else { next.add(title); } return next; }); }; const configExceptions = ["/workspace/config/logging"]; const isActiveRoute = (url: string) => { if (url === "/" && pathname === "/") return true; // Avoid double-highlighting with "/workspace/custom-pricing/overrides" if (url === "/workspace/custom-pricing") return pathname === url; if (url !== "/" && pathname.startsWith(url)) { if (url === "/workspace/config" && configExceptions.some((e) => pathname.startsWith(e))) { return false; } return true; } return false; }; // Always render the light theme version for SSR to avoid hydration mismatch const logoSrc = mounted && resolvedTheme === "dark" ? "/bifrost-logo-dark.webp" : "/bifrost-logo.webp"; const iconSrc = mounted && resolvedTheme === "dark" ? "/bifrost-icon-dark.webp" : "/bifrost-icon.webp"; const { isConnected: isWebSocketConnected } = useWebSocket(); // New release image - based on theme const newReleaseImage = mounted && resolvedTheme === "dark" ? "/images/new-release-image-dark.webp" : "/images/new-release-image.webp"; // Memoize promo cards array to prevent duplicates and unnecessary re-renders const promoCards = useMemo(() => { const cards = []; // Restart required card - non-dismissible, shown first if (coreConfig?.restart_required?.required) { cards.push({ id: "restart-required", title: "Restart Required", description: ( {coreConfig.restart_required.reason || "Configuration changes require a server restart to take effect."} ), dismissible: false, variant: "warning" as const, }); } if (showNewReleaseBanner && latestRelease) { cards.push({ id: "new-release", title: `${latestRelease.name} is now available.`, description: ( View release notes ), dismissible: true, }); } // Only show after mounted to ensure cookie is properly hydrated and avoid flash if (!IS_ENTERPRISE && mounted && !isProductionSetupDismissed) { cards.push(productionSetupHelpCard); } return cards; }, [coreConfig?.restart_required, showNewReleaseBanner, latestRelease, newReleaseImage, isProductionSetupDismissed, mounted]); // Reset areCardsEmpty when promoCards changes useEffect(() => { if (promoCards.length > 0) { setAreCardsEmpty(false); } }, [promoCards]); const hasPromoCards = promoCards.length > 0 && !areCardsEmpty; // When cards are present: 13rem (header 3rem + bottom section ~10rem) // When no cards: 8rem (header 3rem + bottom section without cards ~5rem) const sidebarGroupHeight = hasPromoCards ? "h-[calc(100vh-13rem)]" : "h-[calc(100vh-8rem)]"; const handleCardsEmpty = () => { setAreCardsEmpty(true); }; const handlePromoDismiss = useCallback( (cardId: string) => { if (cardId === "production-setup") { const expiryDate = new Date(); expiryDate.setDate(expiryDate.getDate() + 7); setCookie(PRODUCTION_SETUP_DISMISSED_COOKIE, "true", { path: "/", expires: expiryDate, }); } }, [setCookie], ); const handleLogout = async () => { try { setUserPopoverOpen(false); await logout().unwrap(); navigate("/login"); } catch { // Even if logout fails on server, redirect to login navigate("/login"); } }; const trialDaysRemaining = useMemo(() => { if (IS_ENTERPRISE && TRIAL_EXPIRY) { const daysRemaining = differenceInDays(new Date(TRIAL_EXPIRY), new Date()); return daysRemaining > 0 ? daysRemaining : 0; } return null; }, []); const { state: sidebarState, toggleSidebar } = useSidebar(); return ( {/* Expanded state: horizontal layout */} {/* Collapsed state: vertical layout */} { setSearchQuery(e.target.value); setFocusedIndex(-1); }} onKeyDown={handleSearchKeyDown} className="border-input text-foreground placeholder:text-shadow-muted-foreground focus:ring-ring h-8 w-full rounded-sm border bg-transparent pr-14 pl-8 text-sm outline-none focus:bg-transparent" /> ⌘ K {filteredItems.map((item) => { const isActive = isActiveRoute(item.url); const highlightedUrl = focusedIndex >= 0 ? navigableItems[focusedIndex]?.url : undefined; return ( toggleItem(item.title)} pathname={pathname} search={search} isSidebarCollapsed={sidebarState === "collapsed"} expandSidebar={() => toggleSidebar()} highlightedUrl={highlightedUrl} /> ); })} {externalLinks.map((item, index) => ( ))} {IS_ENTERPRISE && userInfo && (userInfo.name || userInfo.email) ? ( {userInfo.name || userInfo.email || "User"} Logout ) : isAuthEnabled && !IS_ENTERPRISE ? ( ) : null} {version ?? ""} {trialDaysRemaining !== null && ( {trialDaysRemaining} {trialDaysRemaining === 1 ? "day" : "days"} remaining )} ); }
{userInfo.name || userInfo.email || "User"}