@@ -87,6 +87,59 @@ export default function TopBar({}: TopBarProps) {
8787 const history = useHistory ( ) ;
8888 const { appBarActions, appBarActionsProcessors } = useAppBarActionsProcessed ( ) ;
8989
90+ // Fetch current user info from backend
91+ const [ me , setMe ] = React . useState < { username ?: string ; email ?: string } | null > ( null ) ;
92+ React . useEffect ( ( ) => {
93+ let aborted = false ;
94+ async function loadMe ( ) {
95+ if ( ! cluster ) {
96+ setMe ( null ) ;
97+ return ;
98+ }
99+
100+ const headlampBaseUrl = ( window as any ) . headlampBaseUrl || '' ;
101+ const isFile = typeof window !== 'undefined' && window . location ?. protocol === 'file:' ;
102+ const base = isFile ? 'http://localhost:4466/' : headlampBaseUrl || '' ;
103+ const apiBase = base === '' ? '/' : base . replace ( / \/ ? $ / , '/' ) ;
104+
105+ try {
106+ // Use per-cluster endpoint so cookies stay scoped with cluster name in the path
107+ const url = `${ apiBase } cluster/${ cluster } /me` ;
108+ const res = await fetch ( url , {
109+ credentials : 'include' ,
110+ cache : 'no-store' ,
111+ headers : {
112+ 'Cache-Control' : 'no-cache' ,
113+ } ,
114+ } ) ;
115+
116+ if ( res . status === 304 ) {
117+ // Keep previous user info when the backend signals "not modified".
118+ return ;
119+ }
120+
121+ if ( ! res . ok ) {
122+ if ( ! aborted ) {
123+ setMe ( null ) ;
124+ }
125+ return ;
126+ }
127+
128+ const data = await res . json ( ) ;
129+
130+ if ( ! aborted ) {
131+ setMe ( { username : data . username , email : data . email } ) ;
132+ }
133+ } catch {
134+ if ( ! aborted ) setMe ( null ) ;
135+ }
136+ }
137+ loadMe ( ) ;
138+ return ( ) => {
139+ aborted = true ;
140+ } ;
141+ } , [ cluster ] ) ;
142+
90143 const logoutCallback = useCallback ( async ( ) => {
91144 if ( ! ! cluster ) {
92145 await logout ( cluster ) ;
@@ -118,6 +171,7 @@ export default function TopBar({}: TopBarProps) {
118171 onToggleOpen = { handletoggleOpen }
119172 cluster = { cluster || undefined }
120173 clusters = { clustersConfig || undefined }
174+ userInfo = { me || undefined }
121175 />
122176 ) ;
123177}
@@ -134,6 +188,7 @@ export interface PureTopBarProps {
134188 cluster ?: string ;
135189 isSidebarOpen ?: boolean ;
136190 isSidebarOpenUserSelected ?: boolean ;
191+ userInfo ?: { username ?: string ; email ?: string } ;
137192
138193 /** Called when sidebar toggles between open and closed. */
139194 onToggleOpen : ( ) => void ;
@@ -210,6 +265,7 @@ export const PureTopBar = memo(
210265 isSidebarOpen,
211266 isSidebarOpenUserSelected,
212267 onToggleOpen,
268+ userInfo,
213269 } : PureTopBarProps ) => {
214270 const { t } = useTranslation ( ) ;
215271 const theme = useTheme ( ) ;
@@ -243,6 +299,11 @@ export const PureTopBar = memo(
243299 setMobileMoreAnchorEl ( event . currentTarget ) ;
244300 } ;
245301 const userMenuId = 'primary-user-menu' ;
302+ const userDisplayName = userInfo ?. username || userInfo ?. email || '' ;
303+ const userSecondaryInfo =
304+ userInfo ?. username && userInfo ?. email && userInfo . username !== userInfo . email
305+ ? userInfo . email
306+ : undefined ;
246307
247308 const renderUserMenu = ! ! isClusterContext && (
248309 < Menu
@@ -262,6 +323,26 @@ export const PureTopBar = memo(
262323 } ,
263324 } }
264325 >
326+ { userInfo && (
327+ < MenuItem
328+ disableRipple
329+ sx = { {
330+ pointerEvents : 'none' ,
331+ cursor : 'default' ,
332+ '&:hover' : { backgroundColor : 'inherit' } ,
333+ } }
334+ >
335+ < ListItemIcon >
336+ < Icon icon = "mdi:account" />
337+ </ ListItemIcon >
338+ < ListItemText
339+ primaryTypographyProps = { { variant : 'subtitle2' } }
340+ secondaryTypographyProps = { { variant : 'body2' } }
341+ primary = { userDisplayName }
342+ secondary = { userSecondaryInfo }
343+ />
344+ </ MenuItem >
345+ ) }
265346 < MenuItem
266347 component = "a"
267348 onClick = { async ( ) => {
0 commit comments