import { useQuery } from "@tanstack/react-query" import { ArrowLeft, Bell, BookOpen, Bot, FlaskConical, Home, KeyRound, LayoutDashboard, LogOut, Package, Search, Server, Settings, Users, Workflow, } from "lucide-react" import { useState } from "react" import { aiStatusQueryOptions, ChatPanel } from "@/components/Chat/ChatPanel" import { chatStore } from "@/components/Chat/chatStore" import { GlobalSearch } from "@/components/Common/GlobalSearch" import { Logo } from "@/components/Common/Logo" import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarTrigger, } from "@/components/ui/sidebar" import useAuth from "@/hooks/useAuth" import { portalConfig } from "@/lib/portal" import { useShortcuts } from "@/lib/shortcuts" import { type Item, Main } from "./Main" /** * Where the documentation is published. * * A constant rather than something derived: an instance reached over a LAN * address, or at localhost, has no domain to build this from, and the docs are * one site for all of them. */ const DOCS_URL = "https://docs.fluksio.com" const baseItems: Item[] = [ // "Home" rather than "Dashboard": dashboards are their own thing now, and // the brain and the health screens are sections of Home rather than routes. { icon: Home, title: "Home", path: "/" }, { icon: Workflow, title: "Flows", path: "/flows" }, { icon: LayoutDashboard, title: "Dashboards", path: "/dashboards" }, // What a batch flow leaves behind. Beside the flows rather than under Home: // an experiment log is browsed, not glanced at. { icon: FlaskConical, title: "Runs", path: "/runs" }, // Both are engine-wide operator settings rather than personal ones, so they // sit here and not among the per-user tabs under Settings. { icon: KeyRound, title: "Secrets", path: "/secrets" }, { icon: Package, title: "Modules", path: "/modules" }, { icon: Server, title: "Workers", path: "/workers" }, { icon: Bell, title: "Alerts", path: "/alerts" }, ] export function AppSidebar() { const { user: currentUser, logout } = useAuth() const portal = portalConfig() const [searchOpen, setSearchOpen] = useState(false) // Mounted by both shells, so this one binding covers every screen. Listed as // firing inside text entry too, because reaching anything at all should not // depend on where the caret happens to be. useShortcuts({ "mod+k": () => setSearchOpen((open) => !open) }, ["mod+k"]) const withAdmin = currentUser?.is_superuser ? [...baseItems, { icon: Users, title: "Admin", path: "/admin" }] : baseItems // Reached through a portal, the way out is back to the instances list — // and the session belongs to the portal, so logging out is its business. const items: Item[] = portal ? [ { icon: ArrowLeft, title: "All instances", onClick: () => window.location.assign(portal.portalUrl), }, ...withAdmin, ] : withAdmin // A new tab rather than a navigation: the docs are read beside the work, // not instead of it. const docs: Item = { icon: BookOpen, title: "Documentation", onClick: () => window.open(DOCS_URL, "_blank", "noopener"), } const search: Item = { icon: Search, title: "Search", onClick: () => setSearchOpen(true), } // Only once an operator has set a provider up: with none there is nothing // to open, and nothing on screen says the feature exists. const assistant = useQuery(aiStatusQueryOptions()) const tools: Item[] = assistant.data?.available ? [{ icon: Bot, title: "Assistant", onClick: chatStore.toggle }, search] : [search] const footerItems: Item[] = portal ? [...tools, docs, { icon: Settings, title: "Settings", path: "/settings" }] : [ ...tools, docs, { icon: Settings, title: "Settings", path: "/settings" }, { icon: LogOut, title: "Log Out", onClick: logout }, ] return ( <> {/* Floating frosted chrome over whatever surface the shell paints; see the root DESIGN-GUIDELINES.md → Shells and → Overlay surfaces & content chips. */}
{/* Collapsed, the rail has room for one thing, and that is the way back out. `shrink-0` because the rail's width animates while the logo is already back: a flex item short of room is squeezed, and a wordmark would rather be clipped than squashed. */} {/* On a phone the sidebar is a sheet with its own way in and out. */}
{/* Main already pads horizontally; the footer only adds the bottom gap. */}
{/* Outside the sidebar: on a phone that is a sheet, and a dialog is not one of its children. */} {/* Mounted once, here, for the same reason: both shells carry the sidebar, and the chat follows whoever opened it from screen to screen. */} ) } export default AppSidebar