feat: add sidebar layout, environment tree, and router
Wires up AppShell + Sidebar compound component, a per-environment SidebarTree that lazy-fetches apps, React Router nested routes, and provider-wrapped main.tsx with ThemeProvider/ToastProvider/BreadcrumbProvider. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
166
ui/src/components/Layout.tsx
Normal file
166
ui/src/components/Layout.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { useState } from 'react';
|
||||
import { Outlet, useNavigate } from 'react-router';
|
||||
import {
|
||||
AppShell,
|
||||
Sidebar,
|
||||
TopBar,
|
||||
} from '@cameleer/design-system';
|
||||
import { useAuthStore } from '../auth/auth-store';
|
||||
import { EnvironmentTree } from './EnvironmentTree';
|
||||
|
||||
// Simple SVG logo mark for the sidebar header
|
||||
function CameleerLogo() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" fill="currentColor" opacity="0.15" />
|
||||
<path
|
||||
d="M7 14c0-2.5 2-4.5 4.5-4.5S16 11.5 16 14"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle cx="12" cy="8" r="2" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// Nav icon helpers
|
||||
function DashboardIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<rect x="1" y="1" width="6" height="6" rx="1" fill="currentColor" />
|
||||
<rect x="9" y="1" width="6" height="6" rx="1" fill="currentColor" />
|
||||
<rect x="1" y="9" width="6" height="6" rx="1" fill="currentColor" />
|
||||
<rect x="9" y="9" width="6" height="6" rx="1" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function EnvIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M2 4h12M2 8h12M2 12h12"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function LicenseIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<rect x="2" y="2" width="12" height="12" rx="2" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path d="M5 8h6M5 5h6M5 11h4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ObsIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path d="M4 8h8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
<path d="M8 4v8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function UserIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<circle cx="8" cy="5" r="3" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path
|
||||
d="M2 13c0-3 2.7-5 6-5s6 2 6 5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Layout() {
|
||||
const navigate = useNavigate();
|
||||
const username = useAuthStore((s) => s.username);
|
||||
const logout = useAuthStore((s) => s.logout);
|
||||
|
||||
const [envSectionOpen, setEnvSectionOpen] = useState(true);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const sidebar = (
|
||||
<Sidebar collapsed={collapsed} onCollapseToggle={() => setCollapsed((c) => !c)}>
|
||||
<Sidebar.Header
|
||||
logo={<CameleerLogo />}
|
||||
title="Cameleer SaaS"
|
||||
onClick={() => navigate('/')}
|
||||
/>
|
||||
|
||||
{/* Dashboard */}
|
||||
<Sidebar.Section
|
||||
icon={<DashboardIcon />}
|
||||
label="Dashboard"
|
||||
open={false}
|
||||
onToggle={() => navigate('/')}
|
||||
>
|
||||
{null}
|
||||
</Sidebar.Section>
|
||||
|
||||
{/* Environments — expandable tree */}
|
||||
<Sidebar.Section
|
||||
icon={<EnvIcon />}
|
||||
label="Environments"
|
||||
open={envSectionOpen}
|
||||
onToggle={() => setEnvSectionOpen((o) => !o)}
|
||||
>
|
||||
<EnvironmentTree />
|
||||
</Sidebar.Section>
|
||||
|
||||
{/* License */}
|
||||
<Sidebar.Section
|
||||
icon={<LicenseIcon />}
|
||||
label="License"
|
||||
open={false}
|
||||
onToggle={() => navigate('/license')}
|
||||
>
|
||||
{null}
|
||||
</Sidebar.Section>
|
||||
|
||||
<Sidebar.Footer>
|
||||
{/* Link to the observability SPA (external) */}
|
||||
<Sidebar.FooterLink
|
||||
icon={<ObsIcon />}
|
||||
label="View Dashboard"
|
||||
onClick={() => window.open('/dashboard', '_blank', 'noopener')}
|
||||
/>
|
||||
|
||||
{/* User info + logout */}
|
||||
<Sidebar.FooterLink
|
||||
icon={<UserIcon />}
|
||||
label={username ?? 'Account'}
|
||||
onClick={logout}
|
||||
/>
|
||||
</Sidebar.Footer>
|
||||
</Sidebar>
|
||||
);
|
||||
|
||||
return (
|
||||
<AppShell sidebar={sidebar}>
|
||||
<TopBar
|
||||
breadcrumb={[]}
|
||||
user={username ? { name: username } : undefined}
|
||||
onLogout={logout}
|
||||
/>
|
||||
<Outlet />
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user