feat(frontend): top-right profile dropdown for OIDC sessions

Replace the OIDC user chip in the sidebar with an account avatar in the
top-right action bar (the existing UserButton placeholder). The avatar — a
brand-purple circle with the user's initial, or the IdP picture — opens a
popover showing name, email, and a sign-out action, reusing the existing
Avatar/Popover primitives with a hover ring. Renders only under
AUTH_TYPE=oidc.
This commit is contained in:
Alex committed 2026-06-10 15:17:42 +01:00
1 parent 321d88d5be
commit 78c3fc1593
4 files changed
+88 -54

No files matched your search

+1 -52
View File
@@ -4,7 +4,6 @@ import { useDispatch, useSelector } from 'react-redux';
import { NavLink, useNavigate } from 'react-router-dom';
import {
LayoutGrid,
LogOut,
Menu,
PanelLeftClose,
PanelLeftOpen,
@@ -79,15 +78,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const selectedAgent = useSelector(selectSelectedAgent);
const { isMobile, isTablet } = useMediaQuery();
const {
showTokenModal,
handleTokenSubmit,
authType,
logout,
userEmail,
userName,
userPicture,
} = useTokenAuth();
const { showTokenModal, handleTokenSubmit } = useTokenAuth();
const [isDeletingConversation, setIsDeletingConversation] = useState(false);
const [uploadModalState, setUploadModalState] =
@@ -656,48 +647,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
{t('settings.label')}
</p>
</NavLink>
{authType === 'oidc' && (
<div className="mx-4 my-auto flex items-center gap-2.5 py-0.5 pr-1 pl-3">
{userPicture ? (
<Avatar
src={userPicture}
alt={userName || userEmail || 'User avatar'}
className="size-6"
imgClassName="size-6 rounded-full object-cover"
/>
) : (
<Avatar className="size-6">
<span className="bg-sidebar-accent text-foreground flex size-6 items-center justify-center rounded-full text-xs font-medium dark:text-white">
{(userName || userEmail || '?').charAt(0).toUpperCase()}
</span>
</Avatar>
)}
<span className="flex min-w-0 flex-1 flex-col items-start">
{userName && (
<p className="text-foreground max-w-[160px] truncate text-sm dark:text-white">
{userName}
</p>
)}
{userEmail && (
<p className="text-muted-foreground max-w-[160px] truncate text-xs">
{userEmail}
</p>
)}
</span>
<Button
type="button"
variant="ghost"
size="icon"
onClick={logout}
data-testid="oidc-signout"
aria-label={t('auth.signOut')}
title={t('auth.signOut')}
className="text-muted-foreground hover:text-foreground shrink-0 rounded-full"
>
<LogOut className="size-5" strokeWidth={1.75} />
</Button>
</div>
)}
</div>
<div className="text-foreground flex flex-col justify-end dark:text-white">
<div className="flex items-center justify-between py-1">
+2 -1
View File
@@ -11,6 +11,7 @@ import {
updateConversationId,
} from '../conversation/conversationSlice';
import { Button } from './ui/button';
import ProfileButton from './ProfileButton';
interface ActionButtonsProps {
className?: string;
@@ -86,7 +87,7 @@ export default function ActionButtons({
)}
</>
)}
<div>{/* <UserButton /> */}</div>
<ProfileButton />
</div>
</div>
);
+83
View File
@@ -0,0 +1,83 @@
import { LogOut } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import useTokenAuth from '../hooks/useTokenAuth';
import { Avatar } from './ui/avatar';
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
/**
* Top-right account menu for OIDC sessions: an avatar that opens a popover
* showing the signed-in name/email and a sign-out action. Renders nothing for
* other auth modes, which carry no user identity.
*/
export default function ProfileButton() {
const { t } = useTranslation();
const { authType, userName, userEmail, userPicture, logout } = useTokenAuth();
if (authType !== 'oidc' || (!userName && !userEmail)) return null;
const initial = (userName || userEmail || '?').charAt(0).toUpperCase();
const renderAvatar = (size: string, text: string) =>
userPicture ? (
<Avatar
src={userPicture}
alt={userName || userEmail || 'User avatar'}
className={size}
imgClassName={`${size} rounded-full object-cover`}
/>
) : (
<Avatar className={size}>
<span
className={`bg-primary text-primary-foreground flex ${size} items-center justify-center rounded-full ${text} font-medium`}
>
{initial}
</span>
</Avatar>
);
return (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
aria-label={t('auth.account')}
className="ring-offset-background focus-visible:ring-ring hover:ring-primary/40 rounded-full transition outline-none hover:ring-2 hover:ring-offset-2 focus-visible:ring-2 focus-visible:ring-offset-2"
>
{renderAvatar('size-8', 'text-sm')}
</button>
</PopoverTrigger>
<PopoverContent align="end" sideOffset={8} className="w-64 p-0">
<div className="flex items-center gap-3 p-4">
{renderAvatar('size-10', 'text-base')}
<span className="flex min-w-0 flex-col">
{userName && (
<p className="text-foreground truncate text-sm font-medium dark:text-white">
{userName}
</p>
)}
{userEmail && (
<p className="text-muted-foreground truncate text-xs">
{userEmail}
</p>
)}
</span>
</div>
<div className="dark:border-sidebar-border border-t" />
<div className="p-1">
<button
type="button"
onClick={logout}
data-testid="oidc-signout"
className="text-foreground hover:bg-sidebar-accent flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-sm dark:text-white"
>
<LogOut
className="text-muted-foreground size-4 shrink-0"
strokeWidth={1.75}
/>
{t('auth.signOut')}
</button>
</div>
</PopoverContent>
</Popover>
);
}
+2 -1
View File
@@ -36,7 +36,8 @@
"signInWith": "Sign in with {{provider}}",
"notAuthorized": "Your account isn't authorized to access this workspace. Sign in with a different account or contact your administrator.",
"accountDisabled": "Your account has been disabled. Contact your administrator.",
"signOut": "Sign out"
"signOut": "Sign out",
"account": "Account"
},
"settings": {
"label": "Settings",