167 lines
4.4 KiB
TypeScript
167 lines
4.4 KiB
TypeScript
|
|
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>
|
||
|
|
);
|
||
|
|
}
|