Files
multi-seal-mail-webui/src/layout/Titlebar.tsx

139 lines
5.0 KiB
TypeScript

import { useRef, useState, useEffect } from "react";
import { Check, LogOut, Settings, UserCircle } from "lucide-react";
import type { ApiSettings, AuthInfo, AuthTenantMembership, LoginResponse } from "../types";
import HelpMenu from "./HelpMenu";
import LoginModal from "../features/auth/LoginModal";
import { logout } from "../api/auth";
type Props = {
settings: ApiSettings;
auth: AuthInfo | null;
onSettingsChange: (settings: ApiSettings) => void;
onAuthChange: (auth: AuthInfo | null, accessToken?: string) => void;
};
export default function Titlebar({ settings, auth, onAuthChange }: Props) {
const [accountOpen, setAccountOpen] = useState(false);
const [tenantOpen, setTenantOpen] = useState(false);
const [loginOpen, setLoginOpen] = useState(false);
const accountRef = useRef<HTMLDivElement>(null);
const tenantRef = useRef<HTMLDivElement>(null);
const activeTenant = auth?.active_tenant ?? auth?.tenant ?? null;
const tenants = auth?.tenants ?? (activeTenant ? [activeTenant] : []);
const canSwitchTenant = tenants.length > 1;
useEffect(() => {
function onPointerDown(event: MouseEvent) {
const target = event.target as Node;
if (accountRef.current && !accountRef.current.contains(target)) {
setAccountOpen(false);
}
if (tenantRef.current && !tenantRef.current.contains(target)) {
setTenantOpen(false);
}
}
window.addEventListener("mousedown", onPointerDown);
return () => window.removeEventListener("mousedown", onPointerDown);
}, []);
function handleLogin(response: LoginResponse) {
const active = response.active_tenant ?? response.tenant;
onAuthChange(
{
user: response.user,
tenant: active,
active_tenant: active,
tenants: response.tenants ?? [active],
scopes: response.scopes,
roles: response.roles,
groups: response.groups
},
response.access_token
);
}
async function handleLogout() {
try {
await logout(settings);
} catch {
// Logout is best effort; clear local session either way.
}
onAuthChange(null, "");
setAccountOpen(false);
}
function handleTenantSelect(tenant: AuthTenantMembership) {
// Backend-side active tenant switching will be wired later. For now this is a UI placeholder.
onAuthChange(auth ? { ...auth, tenant, active_tenant: tenant } : null);
setTenantOpen(false);
}
return (
<header className="titlebar">
{auth && activeTenant && (
<div className="tenant-selector" ref={tenantRef}>
<span className="tenant-label">tenant:</span>
{canSwitchTenant ? (
<>
<button className="tenant-name-button" onClick={() => setTenantOpen(!tenantOpen)}>
<strong>{activeTenant.name}</strong>
<span className="tenant-caret"></span>
</button>
{tenantOpen && (
<div className="dropdown-menu tenant-menu">
{tenants.map((tenant) => {
const active = tenant.id === activeTenant.id;
return (
<button
key={tenant.id}
className={`dropdown-item ${active ? "active" : ""}`}
onClick={() => handleTenantSelect(tenant)}
>
<span>{tenant.name}</span>
{active && <Check size={16} />}
</button>
);
})}
</div>
)}
</>
) : (
<strong>{activeTenant.name}</strong>
)}
</div>
)}
<div className="titlebar-spacer" />
<HelpMenu />
<div className="context-menu-wrap" ref={accountRef}>
<button className="account-pill" onClick={() => setAccountOpen(!accountOpen)}>
<UserCircle size={22} />
<span>{auth?.user.display_name || auth?.user.email || "Sign in"}</span>
<span className="tenant-caret"></span>
</button>
{accountOpen && (
<div className="dropdown-menu account-menu">
{auth ? (
<>
<div className="account-menu-header">
<strong>{auth.user.display_name || auth.user.email}</strong>
<span>{auth.user.email}</span>
</div>
<button className="dropdown-item" onClick={() => setAccountOpen(false)}><Settings size={16} /> Account settings</button>
<button className="dropdown-item" onClick={handleLogout}><LogOut size={16} /> Sign out</button>
</>
) : (
<button className="dropdown-item" onClick={() => { setLoginOpen(true); setAccountOpen(false); }}><UserCircle size={16} /> Sign in</button>
)}
</div>
)}
</div>
{loginOpen && <LoginModal settings={settings} onClose={() => setLoginOpen(false)} onLogin={handleLogin} />}
</header>
);
}