2026-04-09 19:49:42 +02:00
|
|
|
import { useState, useMemo } from 'react';
|
|
|
|
|
import { Outlet, useNavigate, useLocation } from 'react-router';
|
|
|
|
|
import { LayoutDashboard, ShieldCheck, Building, Activity } from 'lucide-react';
|
2026-04-04 21:55:21 +02:00
|
|
|
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-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
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function Layout() {
|
|
|
|
|
const navigate = useNavigate();
|
2026-04-09 19:49:42 +02:00
|
|
|
const location = useLocation();
|
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
|
|
|
|
2026-04-09 19:49:42 +02:00
|
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
|
|
|
|
|
|
|
|
const breadcrumb = useMemo(() => {
|
|
|
|
|
if (location.pathname.startsWith('/admin')) return [{ label: 'Admin' }, { label: 'Tenants' }];
|
|
|
|
|
if (location.pathname.startsWith('/license')) return [{ label: 'License' }];
|
|
|
|
|
return [{ label: 'Dashboard' }];
|
|
|
|
|
}, [location.pathname]);
|
|
|
|
|
|
2026-04-04 21:55:21 +02:00
|
|
|
const sidebar = (
|
2026-04-09 19:49:42 +02:00
|
|
|
<Sidebar collapsed={collapsed} onCollapseToggle={() => setCollapsed(c => !c)}>
|
2026-04-04 21:55:21 +02:00
|
|
|
<Sidebar.Header
|
|
|
|
|
logo={<CameleerLogo />}
|
|
|
|
|
title="Cameleer SaaS"
|
|
|
|
|
onClick={() => navigate('/')}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Dashboard */}
|
|
|
|
|
<Sidebar.Section
|
2026-04-09 19:49:42 +02:00
|
|
|
icon={<LayoutDashboard size={18} />}
|
2026-04-04 21:55:21 +02:00
|
|
|
label="Dashboard"
|
|
|
|
|
open={false}
|
2026-04-09 19:49:42 +02:00
|
|
|
active={location.pathname === '/' || location.pathname === ''}
|
2026-04-04 21:55:21 +02:00
|
|
|
onToggle={() => navigate('/')}
|
|
|
|
|
>
|
|
|
|
|
{null}
|
|
|
|
|
</Sidebar.Section>
|
|
|
|
|
|
|
|
|
|
{/* License */}
|
|
|
|
|
<Sidebar.Section
|
2026-04-09 19:49:42 +02:00
|
|
|
icon={<ShieldCheck size={18} />}
|
2026-04-04 21:55:21 +02:00
|
|
|
label="License"
|
|
|
|
|
open={false}
|
2026-04-09 19:49:42 +02:00
|
|
|
active={location.pathname.startsWith('/license')}
|
2026-04-04 21:55:21 +02:00
|
|
|
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
|
2026-04-09 19:49:42 +02:00
|
|
|
icon={<Building size={18} />}
|
2026-04-05 02:50:51 +02:00
|
|
|
label="Platform"
|
|
|
|
|
open={false}
|
2026-04-09 19:49:42 +02:00
|
|
|
active={location.pathname.startsWith('/admin')}
|
2026-04-05 02:50:51 +02:00
|
|
|
onToggle={() => navigate('/admin/tenants')}
|
|
|
|
|
>
|
|
|
|
|
{null}
|
|
|
|
|
</Sidebar.Section>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-04-04 21:55:21 +02:00
|
|
|
<Sidebar.Footer>
|
2026-04-08 00:03:01 +02:00
|
|
|
{/* Link to the server observability dashboard */}
|
2026-04-04 21:55:21 +02:00
|
|
|
<Sidebar.FooterLink
|
2026-04-09 19:49:42 +02:00
|
|
|
icon={<Activity size={18} />}
|
2026-04-08 00:03:01 +02:00
|
|
|
label="Open Server 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}>
|
2026-04-09 19:49:42 +02:00
|
|
|
{/*
|
|
|
|
|
* TopBar always renders status filters, time range pills, auto-refresh, and
|
|
|
|
|
* command palette search via useGlobalFilters() / useCommandPalette(). Both
|
|
|
|
|
* hooks throw if their providers are absent, so GlobalFilterProvider and
|
|
|
|
|
* CommandPaletteProvider cannot be removed from main.tsx without crashing the
|
|
|
|
|
* app. The TopBar API has no props to suppress these server-oriented controls.
|
|
|
|
|
* Hiding them on platform pages would require a DS change.
|
|
|
|
|
*/}
|
2026-04-04 21:55:21 +02:00
|
|
|
<TopBar
|
2026-04-09 19:49:42 +02:00
|
|
|
breadcrumb={breadcrumb}
|
|
|
|
|
user={{ name: username || 'User' }}
|
2026-04-04 21:55:21 +02:00
|
|
|
onLogout={logout}
|
|
|
|
|
/>
|
|
|
|
|
<Outlet />
|
|
|
|
|
</AppShell>
|
|
|
|
|
);
|
|
|
|
|
}
|