2026-04-04 21:55:21 +02:00
|
|
|
import { useState } from 'react';
|
|
|
|
|
import { Outlet, useNavigate } from 'react-router';
|
|
|
|
|
import {
|
|
|
|
|
AppShell,
|
|
|
|
|
Sidebar,
|
|
|
|
|
TopBar,
|
|
|
|
|
} from '@cameleer/design-system';
|
2026-04-05 01:17:47 +02:00
|
|
|
import { useAuth } from '../auth/useAuth';
|
2026-04-05 14:04:06 +02:00
|
|
|
import { useScopes } from '../auth/useScopes';
|
2026-04-07 12:20:40 +02:00
|
|
|
import { useOrgStore } from '../auth/useOrganization';
|
2026-04-04 21:55:21 +02:00
|
|
|
import { EnvironmentTree } from './EnvironmentTree';
|
2026-04-06 23:03:18 +02:00
|
|
|
import cameleerLogo from '@cameleer/design-system/assets/cameleer3-logo.svg';
|
2026-04-04 21:55:21 +02:00
|
|
|
|
|
|
|
|
function CameleerLogo() {
|
|
|
|
|
return (
|
2026-04-06 22:09:13 +02:00
|
|
|
<img
|
2026-04-06 22:39:29 +02:00
|
|
|
src={cameleerLogo}
|
2026-04-06 22:09:13 +02:00
|
|
|
alt=""
|
2026-04-04 21:55:21 +02:00
|
|
|
width="24"
|
|
|
|
|
height="24"
|
|
|
|
|
aria-hidden="true"
|
2026-04-06 22:09:13 +02:00
|
|
|
/>
|
2026-04-04 21:55:21 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 02:50:51 +02:00
|
|
|
function PlatformIcon() {
|
|
|
|
|
return (
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
|
|
|
<path d="M8 1l6 3.5v7L8 15l-6-3.5v-7L8 1z" stroke="currentColor" strokeWidth="1.5" />
|
|
|
|
|
<path d="M8 1v14M2 4.5L14 4.5M2 11.5L14 11.5" stroke="currentColor" strokeWidth="1" opacity="0.4" />
|
|
|
|
|
</svg>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 21:55:21 +02:00
|
|
|
export function Layout() {
|
|
|
|
|
const navigate = useNavigate();
|
2026-04-05 14:04:06 +02:00
|
|
|
const { logout } = useAuth();
|
|
|
|
|
const scopes = useScopes();
|
2026-04-07 12:20:40 +02:00
|
|
|
const { username } = useOrgStore();
|
2026-04-04 21:55:21 +02:00
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
2026-04-05 02:50:51 +02:00
|
|
|
{/* Platform Admin section */}
|
2026-04-05 14:04:06 +02:00
|
|
|
{scopes.has('platform:admin') && (
|
2026-04-05 02:50:51 +02:00
|
|
|
<Sidebar.Section
|
|
|
|
|
icon={<PlatformIcon />}
|
|
|
|
|
label="Platform"
|
|
|
|
|
open={false}
|
|
|
|
|
onToggle={() => navigate('/admin/tenants')}
|
|
|
|
|
>
|
|
|
|
|
{null}
|
|
|
|
|
</Sidebar.Section>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-04-04 21:55:21 +02:00
|
|
|
<Sidebar.Footer>
|
2026-04-05 02:50:51 +02:00
|
|
|
{/* Link to the observability SPA (direct port, not via Traefik prefix) */}
|
2026-04-04 21:55:21 +02:00
|
|
|
<Sidebar.FooterLink
|
|
|
|
|
icon={<ObsIcon />}
|
|
|
|
|
label="View Dashboard"
|
2026-04-05 21:10:03 +02:00
|
|
|
onClick={() => window.open('/server/', '_blank', 'noopener')}
|
2026-04-04 21:55:21 +02:00
|
|
|
/>
|
|
|
|
|
</Sidebar.Footer>
|
|
|
|
|
</Sidebar>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<AppShell sidebar={sidebar}>
|
|
|
|
|
<TopBar
|
|
|
|
|
breadcrumb={[]}
|
2026-04-07 12:20:40 +02:00
|
|
|
user={username ? { name: username } : undefined}
|
2026-04-04 21:55:21 +02:00
|
|
|
onLogout={logout}
|
|
|
|
|
/>
|
|
|
|
|
<Outlet />
|
|
|
|
|
</AppShell>
|
|
|
|
|
);
|
|
|
|
|
}
|