Files
app/frontend/src/components/Sidebar/AppSidebar.tsx
T
stroblme 49b8985e71 Put resources where somebody would look for them
`resources` was reachable from the SDK and the API and nowhere else, and
`node_queued` was published to nothing at all — so a node waiting for a machine
looked exactly like a node that had hung, which is the failure the event was
added for.

The node panel gets a Resources section: a size by name, the numbers for a node
that wants its own, and how long it is expected to take. Picking a flavor drops
the numbers, because saying both is two answers and the engine refuses it.

A queued node draws a neutral dot rather than one of the three status colours —
it is not running, it did not go well, and it did not go wrong; it is idle with
a reason, which the tooltip gives.

And a Workers screen, which is the first UI for any of this: every machine the
engine can reach with what is free of each, what is attached, what a cluster
has been asked for, and the sizes, editable. `fluksio status` grew a line of
the same.

The demo's training node already preferred a GPU worker, which was exactly the
declaration that used to be dropped, so it now says how much of that machine it
takes as well.
2026-08-27 09:32:08 +02:00

104 lines
3.6 KiB
TypeScript

import {
ArrowLeft,
Bell,
FlaskConical,
Home,
KeyRound,
LayoutDashboard,
LogOut,
Package,
Server,
Settings,
Users,
Workflow,
} from "lucide-react"
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 { type Item, Main } from "./Main"
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 withAdmin = currentUser?.is_superuser
? [...baseItems, { icon: Users, title: "Admin", path: "/admin" }]
: baseItems
// Reached through a portal, the way out is back to the installations list —
// and the session belongs to the portal, so logging out is its business.
const items: Item[] = portal
? [
{
icon: ArrowLeft,
title: "All installations",
onClick: () => window.location.assign(portal.portalUrl),
},
...withAdmin,
]
: withAdmin
const footerItems: Item[] = portal
? [{ icon: Settings, title: "Settings", path: "/settings" }]
: [
{ 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.
<Sidebar
collapsible="icon"
variant="floating"
className="[&>[data-sidebar=sidebar]]:bg-card/80 [&>[data-sidebar=sidebar]]:backdrop-blur-md [&>[data-sidebar=sidebar]]:shadow-e2"
>
<SidebarHeader className="px-4 py-6 group-data-[collapsible=icon]:px-0 group-data-[collapsible=icon]:items-center">
<div className="flex w-full items-center justify-between gap-2 group-data-[collapsible=icon]:justify-center">
{/* Collapsed, the rail has room for one thing, and that is the way
back out. */}
<span className="group-data-[collapsible=icon]:hidden">
<Logo variant="responsive" />
</span>
{/* On a phone the sidebar is a sheet with its own way in and out. */}
<SidebarTrigger className="hidden shrink-0 text-muted-foreground md:inline-flex" />
</div>
</SidebarHeader>
<SidebarContent>
<Main items={items} />
</SidebarContent>
{/* Main already pads horizontally; the footer only adds the bottom gap. */}
<SidebarFooter className="px-0">
<Main items={footerItems} />
</SidebarFooter>
</Sidebar>
)
}
export default AppSidebar