Files
autoglue/ui/src/layouts/app-shell.tsx
allanice001 7985b310c5 feat: Complete AG Loadbalancer & Cluster API
Refactor routing logic (Chi can be a pain when you're managing large sets of routes, but its one of the better options when considering a potential gRPC future)
       Upgrade API Generation to fully support OAS3.1
      Update swagger interface to RapiDoc - the old swagger interface doesnt support OAS3.1 yet
      Docs are now embedded as part of the UI - once logged in they pick up the cookies and org id from what gets set by the UI, but you can override it
      Other updates include better portability of the db-studio

Signed-off-by: allanice001 <allanice001@gmail.com>
2025-11-17 04:59:39 +00:00

189 lines
5.9 KiB
TypeScript

import { useEffect, useState } from "react"
import { meApi } from "@/api/me.ts"
import { orgStore } from "@/auth/org.ts"
import { Footer } from "@/layouts/footer.tsx"
import { adminNav, mainNav, orgNav, userNav } from "@/layouts/nav-config.ts"
import { OrgSwitcher } from "@/layouts/org-switcher.tsx"
import { ThemePillSwitcher } from "@/layouts/theme-switcher"
import { Topbar } from "@/layouts/topbar.tsx"
import { NavLink, Outlet } from "react-router-dom"
import { cn } from "@/lib/utils.ts"
import { useAuthActions } from "@/hooks/use-auth-actions.ts"
import { Button } from "@/components/ui/button.tsx"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
} from "@/components/ui/sidebar.tsx"
type Org = {
id: string
name: string
}
export const AppShell = () => {
const [orgs, setOrgs] = useState<Org[]>([])
const { logout } = useAuthActions()
useEffect(() => {
let alive = true
;(async () => {
try {
const me = await meApi.getMe() // HandlersMeResponse
const list = (me.organizations ?? []).map((o) => ({
id: o.id,
name: o.name ?? o.id,
}))
if (!alive) return
setOrgs(list as Org[])
// default selection if none
if (!orgStore.get() && list.length > 0) {
orgStore.set(list[0].id!)
}
} catch {
// ignore; ProtectedRoute will handle auth
}
})()
return () => {
alive = false
}
}, [])
return (
<SidebarProvider defaultOpen>
<Sidebar collapsible="icon" variant="floating">
<SidebarHeader>
<div className="px-2 py-2">
<OrgSwitcher orgs={orgs} />
</div>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{mainNav.map((n) => (
<SidebarMenuItem key={n.to}>
<SidebarMenuButton asChild tooltip={n.label}>
<NavLink
to={n.to}
className={({ isActive }) =>
cn("flex items-center gap-2", isActive && "text-primary")
}
>
<n.icon className="h-4 w-4" />
<span>{n.label}</span>
</NavLink>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Organization</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{orgNav.map((n) => (
<SidebarMenuItem key={n.to}>
<SidebarMenuButton asChild tooltip={n.label}>
<NavLink
to={n.to}
className={({ isActive }) =>
cn("flex items-center gap-2", isActive && "text-primary")
}
>
<n.icon className="h-4 w-4" />
<span>{n.label}</span>
</NavLink>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>User</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{userNav.map((n) => (
<SidebarMenuItem key={n.to}>
<SidebarMenuButton asChild tooltip={n.label}>
<NavLink
to={n.to}
className={({ isActive }) =>
cn("flex items-center gap-2", isActive && "text-primary")
}
>
<n.icon className="h-4 w-4" />
<span>{n.label}</span>
</NavLink>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Admin</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{adminNav.map((n) => (
<SidebarMenuItem key={n.to}>
<SidebarMenuButton asChild tooltip={n.label}>
<NavLink
to={n.to}
target={n.target ? n.target : "_self"}
className={({ isActive }) =>
cn("flex items-center gap-2", isActive && "text-primary")
}
>
<n.icon className="h-4 w-4" />
<span>{n.label}</span>
</NavLink>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<div className="mt-auto flex items-center justify-center p-3">
<ThemePillSwitcher />
</div>
</SidebarContent>
<SidebarFooter>
<div className="px-2 py-2">
<Button variant="ghost" size="sm" className="w-full" onClick={() => void logout()}>
Sign out
</Button>
</div>
</SidebarFooter>
</Sidebar>
<SidebarInset className="flex min-h-screen flex-col">
<Topbar />
<main className="p-4">
<Outlet />
</main>
<Footer />
</SidebarInset>
</SidebarProvider>
)
}