Vendor persona doesn't need "Open Server Dashboard" in sidebar footer. Removed inline Fingerprint icon from Identity (Logto) menu item. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
183 lines
5.9 KiB
TypeScript
183 lines
5.9 KiB
TypeScript
import { Outlet, useNavigate, useLocation } from 'react-router';
|
|
import {
|
|
AppShell,
|
|
Sidebar,
|
|
TopBar,
|
|
} from '@cameleer/design-system';
|
|
import { LayoutDashboard, ShieldCheck, Server, Users, Settings, Shield, Building, ScrollText } from 'lucide-react';
|
|
import { useAuth } from '../auth/useAuth';
|
|
import { useScopes } from '../auth/useScopes';
|
|
import { useOrgStore } from '../auth/useOrganization';
|
|
import cameleerLogo from '@cameleer/design-system/assets/cameleer3-logo.svg';
|
|
|
|
function CameleerLogo() {
|
|
return (
|
|
<img
|
|
src={cameleerLogo}
|
|
alt=""
|
|
width="24"
|
|
height="24"
|
|
aria-hidden="true"
|
|
/>
|
|
);
|
|
}
|
|
|
|
function isActive(location: ReturnType<typeof useLocation>, path: string) {
|
|
return location.pathname === path || location.pathname.startsWith(path + '/');
|
|
}
|
|
|
|
export function Layout() {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const { logout } = useAuth();
|
|
const scopes = useScopes();
|
|
const { username, organizations, currentOrgId } = useOrgStore();
|
|
|
|
const isVendor = scopes.has('platform:admin');
|
|
const isTenantAdmin = scopes.has('tenant:manage');
|
|
const onVendorRoute = location.pathname.startsWith('/vendor');
|
|
// Vendor on vendor routes: show only TENANTS. On tenant routes: show tenant portal too (for debugging).
|
|
const showTenantPortal = isTenantAdmin && (!isVendor || !onVendorRoute);
|
|
|
|
// Determine current org slug for server dashboard link
|
|
const currentOrg = organizations.find((o) => o.id === currentOrgId);
|
|
const serverDashboardHref = currentOrg?.slug ? `/t/${currentOrg.slug}/` : '/server/';
|
|
|
|
// Build breadcrumbs from path
|
|
const segments = location.pathname.replace(/^\//, '').split('/').filter(Boolean);
|
|
const breadcrumb = segments.map((seg) => {
|
|
const label = seg.charAt(0).toUpperCase() + seg.slice(1).replace(/-/g, ' ');
|
|
return { label };
|
|
});
|
|
|
|
const sidebar = (
|
|
<Sidebar collapsed={false} onCollapseToggle={() => {}}>
|
|
<Sidebar.Header
|
|
logo={<CameleerLogo />}
|
|
title="Cameleer SaaS"
|
|
onClick={() => navigate(isVendor ? '/vendor/tenants' : '/tenant')}
|
|
/>
|
|
|
|
{/* Vendor console — only visible to platform:admin */}
|
|
{isVendor && (
|
|
<Sidebar.Section
|
|
icon={<Building size={16} />}
|
|
label="Vendor"
|
|
open={onVendorRoute}
|
|
active={isActive(location, '/vendor')}
|
|
onToggle={() => navigate('/vendor/tenants')}
|
|
>
|
|
<div
|
|
style={{ padding: '6px 12px 6px 36px', fontSize: 13, cursor: 'pointer',
|
|
fontWeight: isActive(location, '/vendor/tenants') ? 600 : 400,
|
|
color: isActive(location, '/vendor/tenants') ? 'var(--amber)' : 'var(--text-muted)' }}
|
|
onClick={() => navigate('/vendor/tenants')}
|
|
>
|
|
Tenants
|
|
</div>
|
|
<div
|
|
style={{ padding: '6px 12px 6px 36px', fontSize: 13, cursor: 'pointer',
|
|
fontWeight: isActive(location, '/vendor/audit') ? 600 : 400,
|
|
color: isActive(location, '/vendor/audit') ? 'var(--amber)' : 'var(--text-muted)' }}
|
|
onClick={() => navigate('/vendor/audit')}
|
|
>
|
|
Audit Log
|
|
</div>
|
|
<div
|
|
style={{ padding: '6px 12px 6px 36px', fontSize: 13, cursor: 'pointer', color: 'var(--text-muted)' }}
|
|
onClick={() => window.open(`${window.location.protocol}//${window.location.hostname}:3002`, '_blank', 'noopener')}
|
|
>
|
|
Identity (Logto)
|
|
</div>
|
|
</Sidebar.Section>
|
|
)}
|
|
|
|
{/* Tenant portal — visible to tenant admins; hidden for vendor on vendor routes */}
|
|
{showTenantPortal && (
|
|
<>
|
|
<Sidebar.Section
|
|
icon={<LayoutDashboard size={16} />}
|
|
label="Dashboard"
|
|
open={false}
|
|
active={location.pathname === '/tenant'}
|
|
onToggle={() => navigate('/tenant')}
|
|
>
|
|
{null}
|
|
</Sidebar.Section>
|
|
|
|
<Sidebar.Section
|
|
icon={<ShieldCheck size={16} />}
|
|
label="License"
|
|
open={false}
|
|
active={isActive(location, '/tenant/license')}
|
|
onToggle={() => navigate('/tenant/license')}
|
|
>
|
|
{null}
|
|
</Sidebar.Section>
|
|
|
|
<Sidebar.Section
|
|
icon={<Shield size={16} />}
|
|
label="SSO"
|
|
open={false}
|
|
active={isActive(location, '/tenant/sso')}
|
|
onToggle={() => navigate('/tenant/sso')}
|
|
>
|
|
{null}
|
|
</Sidebar.Section>
|
|
|
|
<Sidebar.Section
|
|
icon={<Users size={16} />}
|
|
label="Team"
|
|
open={false}
|
|
active={isActive(location, '/tenant/team')}
|
|
onToggle={() => navigate('/tenant/team')}
|
|
>
|
|
{null}
|
|
</Sidebar.Section>
|
|
|
|
<Sidebar.Section
|
|
icon={<ScrollText size={16} />}
|
|
label="Audit Log"
|
|
open={false}
|
|
active={isActive(location, '/tenant/audit')}
|
|
onToggle={() => navigate('/tenant/audit')}
|
|
>
|
|
{null}
|
|
</Sidebar.Section>
|
|
|
|
<Sidebar.Section
|
|
icon={<Settings size={16} />}
|
|
label="Settings"
|
|
open={false}
|
|
active={isActive(location, '/tenant/settings')}
|
|
onToggle={() => navigate('/tenant/settings')}
|
|
>
|
|
{null}
|
|
</Sidebar.Section>
|
|
</>
|
|
)}
|
|
|
|
{showTenantPortal && (
|
|
<Sidebar.Footer>
|
|
<Sidebar.FooterLink
|
|
icon={<Server size={16} />}
|
|
label="Open Server Dashboard"
|
|
onClick={() => window.open(serverDashboardHref, '_blank', 'noopener')}
|
|
/>
|
|
</Sidebar.Footer>
|
|
)}
|
|
</Sidebar>
|
|
);
|
|
|
|
return (
|
|
<AppShell sidebar={sidebar}>
|
|
<TopBar
|
|
breadcrumb={breadcrumb}
|
|
user={username ? { name: username } : undefined}
|
|
onLogout={logout}
|
|
/>
|
|
<Outlet />
|
|
</AppShell>
|
|
);
|
|
}
|