139 lines
5.0 KiB
TypeScript
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>
|
|
);
|
|
}
|