fix: sidebar active state, breadcrumbs, collapse, username fallback, lucide icons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
import { Outlet, useNavigate } from 'react-router';
|
import { useState, useMemo } from 'react';
|
||||||
|
import { Outlet, useNavigate, useLocation } from 'react-router';
|
||||||
|
import { LayoutDashboard, ShieldCheck, Building, Activity } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
AppShell,
|
AppShell,
|
||||||
Sidebar,
|
Sidebar,
|
||||||
@@ -21,54 +23,23 @@ function CameleerLogo() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 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 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Layout() {
|
export function Layout() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const { logout } = useAuth();
|
const { logout } = useAuth();
|
||||||
const scopes = useScopes();
|
const scopes = useScopes();
|
||||||
const { username } = useOrgStore();
|
const { username } = useOrgStore();
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
const sidebar = (
|
const sidebar = (
|
||||||
<Sidebar collapsed={false} onCollapseToggle={() => {}}>
|
<Sidebar collapsed={collapsed} onCollapseToggle={() => setCollapsed(c => !c)}>
|
||||||
<Sidebar.Header
|
<Sidebar.Header
|
||||||
logo={<CameleerLogo />}
|
logo={<CameleerLogo />}
|
||||||
title="Cameleer SaaS"
|
title="Cameleer SaaS"
|
||||||
@@ -77,9 +48,10 @@ export function Layout() {
|
|||||||
|
|
||||||
{/* Dashboard */}
|
{/* Dashboard */}
|
||||||
<Sidebar.Section
|
<Sidebar.Section
|
||||||
icon={<DashboardIcon />}
|
icon={<LayoutDashboard size={18} />}
|
||||||
label="Dashboard"
|
label="Dashboard"
|
||||||
open={false}
|
open={false}
|
||||||
|
active={location.pathname === '/' || location.pathname === ''}
|
||||||
onToggle={() => navigate('/')}
|
onToggle={() => navigate('/')}
|
||||||
>
|
>
|
||||||
{null}
|
{null}
|
||||||
@@ -87,9 +59,10 @@ export function Layout() {
|
|||||||
|
|
||||||
{/* License */}
|
{/* License */}
|
||||||
<Sidebar.Section
|
<Sidebar.Section
|
||||||
icon={<LicenseIcon />}
|
icon={<ShieldCheck size={18} />}
|
||||||
label="License"
|
label="License"
|
||||||
open={false}
|
open={false}
|
||||||
|
active={location.pathname.startsWith('/license')}
|
||||||
onToggle={() => navigate('/license')}
|
onToggle={() => navigate('/license')}
|
||||||
>
|
>
|
||||||
{null}
|
{null}
|
||||||
@@ -98,9 +71,10 @@ export function Layout() {
|
|||||||
{/* Platform Admin section */}
|
{/* Platform Admin section */}
|
||||||
{scopes.has('platform:admin') && (
|
{scopes.has('platform:admin') && (
|
||||||
<Sidebar.Section
|
<Sidebar.Section
|
||||||
icon={<PlatformIcon />}
|
icon={<Building size={18} />}
|
||||||
label="Platform"
|
label="Platform"
|
||||||
open={false}
|
open={false}
|
||||||
|
active={location.pathname.startsWith('/admin')}
|
||||||
onToggle={() => navigate('/admin/tenants')}
|
onToggle={() => navigate('/admin/tenants')}
|
||||||
>
|
>
|
||||||
{null}
|
{null}
|
||||||
@@ -110,7 +84,7 @@ export function Layout() {
|
|||||||
<Sidebar.Footer>
|
<Sidebar.Footer>
|
||||||
{/* Link to the server observability dashboard */}
|
{/* Link to the server observability dashboard */}
|
||||||
<Sidebar.FooterLink
|
<Sidebar.FooterLink
|
||||||
icon={<ObsIcon />}
|
icon={<Activity size={18} />}
|
||||||
label="Open Server Dashboard"
|
label="Open Server Dashboard"
|
||||||
onClick={() => window.open('/server/', '_blank', 'noopener')}
|
onClick={() => window.open('/server/', '_blank', 'noopener')}
|
||||||
/>
|
/>
|
||||||
@@ -120,9 +94,17 @@ export function Layout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell sidebar={sidebar}>
|
<AppShell sidebar={sidebar}>
|
||||||
|
{/*
|
||||||
|
* 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.
|
||||||
|
*/}
|
||||||
<TopBar
|
<TopBar
|
||||||
breadcrumb={[]}
|
breadcrumb={breadcrumb}
|
||||||
user={username ? { name: username } : undefined}
|
user={{ name: username || 'User' }}
|
||||||
onLogout={logout}
|
onLogout={logout}
|
||||||
/>
|
/>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
Reference in New Issue
Block a user