1717import { Icon } from '@iconify/react' ;
1818import AppBar from '@mui/material/AppBar' ;
1919import Box from '@mui/material/Box' ;
20+ import Divider from '@mui/material/Divider' ;
2021import IconButton from '@mui/material/IconButton' ;
2122import ListItemIcon from '@mui/material/ListItemIcon' ;
2223import ListItemText from '@mui/material/ListItemText' ;
2324import Menu from '@mui/material/Menu' ;
2425import MenuItem from '@mui/material/MenuItem' ;
2526import { useTheme } from '@mui/material/styles' ;
2627import Toolbar from '@mui/material/Toolbar' ;
28+ import Typography from '@mui/material/Typography' ;
2729import useMediaQuery from '@mui/material/useMediaQuery' ;
28- import { useQuery , useQueryClient } from '@tanstack/react-query' ;
30+ import { useQueries , useQueryClient } from '@tanstack/react-query' ;
2931import { has } from 'lodash' ;
3032import React , { memo , useCallback } from 'react' ;
3133import { useTranslation } from 'react-i18next' ;
3234import { useDispatch } from 'react-redux' ;
3335import { useHistory } from 'react-router-dom' ;
3436import { getProductName , getVersion } from '../../helpers/getProductInfo' ;
3537import { logout } from '../../lib/auth' ;
36- import { useCluster , useClustersConf } from '../../lib/k8s' ;
37- import { clusterRequest } from '../../lib/k8s/api/v1/clusterRequests ' ;
38+ import { useCluster , useClustersConf , useSelectedClusters } from '../../lib/k8s' ;
39+ import { ClusterUserInfo , getClusterUserInfo } from '../../lib/k8s/api/v1/clusterApi ' ;
3840import { createRouteURL } from '../../lib/router/createRouteURL' ;
3941import {
4042 AppBarAction ,
@@ -86,70 +88,48 @@ export default function TopBar({}: TopBarProps) {
8688
8789 const clustersConfig = useClustersConf ( ) ;
8890 const cluster = useCluster ( ) ;
91+ const selectedClusters = useSelectedClusters ( ) ;
8992 const history = useHistory ( ) ;
9093 const { appBarActions, appBarActionsProcessors } = useAppBarActionsProcessed ( ) ;
9194 const queryClient = useQueryClient ( ) ;
92- const clusterName = cluster ?? undefined ;
93- const { data : me } = useQuery < { username ?: string ; email ?: string } | null > ( {
94- queryKey : [ 'clusterMe' , clusterName ] ,
95- queryFn : async ( ) => {
96- if ( ! clusterName ) {
97- return null ;
98- }
99-
100- try {
101- const res = await clusterRequest ( '/me' , {
102- cluster : clusterName ,
103- autoLogoutOnAuthError : false ,
104- } ) ;
105-
106- if ( ! res ) {
107- return null ;
108- }
10995
110- if ( ! ( typeof res . userInfoURL === 'string' && res . userInfoURL . length > 0 ) ) {
111- return { username : res . username , email : res . email } ;
112- }
113-
114- const ui : {
115- preferredUsername ?: string ;
116- email ?: string ;
117- } = await fetch ( res . userInfoURL , {
118- credentials : 'include' ,
119- headers : {
120- 'Content-Type' : 'application/json' ,
121- } ,
122- } ) . then ( r => {
123- if ( ! r . ok ) {
124- throw new Error ( `Could not fetch user info from ${ res . userInfoURL } ` ) ;
96+ const logoutCallback = useCallback (
97+ async ( clusterToLogout ?: string ) => {
98+ if ( clusterToLogout ) {
99+ await logout ( clusterToLogout ) ;
100+ queryClient . removeQueries ( { queryKey : [ 'clusterMe' , clusterToLogout ] , exact : true } ) ;
101+
102+ if ( selectedClusters . length > 1 ) {
103+ const remainingClusters = selectedClusters . filter ( c => c !== clusterToLogout ) ;
104+ if ( remainingClusters . length > 0 ) {
105+ const currentPath = history . location . pathname ;
106+ const clusterParam = selectedClusters . join ( '+' ) ;
107+ const newClusterParam = remainingClusters . join ( '+' ) ;
108+ const newPath = currentPath . replace ( `/c/${ clusterParam } ` , `/c/${ newClusterParam } ` ) ;
109+ history . push ( newPath ) ;
110+ } else {
111+ history . push ( '/' ) ;
125112 }
126- return r . json ( ) ;
127- } ) ;
128-
129- if ( ! ui || ( ! ui . preferredUsername && ! ui . email ) ) {
130- return null ;
113+ } else {
114+ history . push ( '/' ) ;
131115 }
132-
133- return {
134- username : ui . preferredUsername ,
135- email : ui . email ,
136- } ;
137- } catch {
138- return null ;
116+ } else {
117+ if ( selectedClusters . length > 0 ) {
118+ await Promise . all (
119+ selectedClusters . map ( async c => {
120+ await logout ( c ) ;
121+ queryClient . removeQueries ( { queryKey : [ 'clusterMe' , c ] , exact : true } ) ;
122+ } )
123+ ) ;
124+ } else if ( cluster ) {
125+ await logout ( cluster ) ;
126+ queryClient . removeQueries ( { queryKey : [ 'clusterMe' , cluster ] , exact : true } ) ;
127+ }
128+ history . push ( '/' ) ;
139129 }
140130 } ,
141- enabled : Boolean ( clusterName ) ,
142- staleTime : 0 ,
143- refetchOnMount : 'always' ,
144- } ) ;
145-
146- const logoutCallback = useCallback ( async ( ) => {
147- if ( ! ! cluster ) {
148- await logout ( cluster ) ;
149- queryClient . removeQueries ( { queryKey : [ 'clusterMe' , cluster ] , exact : true } ) ;
150- }
151- history . push ( '/' ) ;
152- } , [ cluster , history , queryClient ] ) ;
131+ [ cluster , selectedClusters , history , queryClient ]
132+ ) ;
153133
154134 const handletoggleOpen = useCallback ( ( ) => {
155135 // For medium view we default to closed if they have not made a selection.
@@ -174,8 +154,8 @@ export default function TopBar({}: TopBarProps) {
174154 isSidebarOpenUserSelected = { isSidebarOpenUserSelected }
175155 onToggleOpen = { handletoggleOpen }
176156 cluster = { cluster || undefined }
157+ selectedClusters = { selectedClusters }
177158 clusters = { clustersConfig || undefined }
178- userInfo = { me || undefined }
179159 />
180160 ) ;
181161}
@@ -185,14 +165,14 @@ export interface PureTopBarProps {
185165 appBarActions : AppBarAction [ ] ;
186166 /** functions which filter the app bar action buttons */
187167 appBarActionsProcessors ?: AppBarActionsProcessor [ ] ;
188- logout : ( ) => Promise < any > | void ;
168+ logout : ( cluster ?: string ) => Promise < any > | void ;
189169 clusters ?: {
190170 [ clusterName : string ] : any ;
191171 } ;
192172 cluster ?: string ;
173+ selectedClusters ?: string [ ] ;
193174 isSidebarOpen ?: boolean ;
194175 isSidebarOpenUserSelected ?: boolean ;
195- userInfo ?: { username ?: string ; email ?: string } ;
196176
197177 /** Called when sidebar toggles between open and closed. */
198178 onToggleOpen : ( ) => void ;
@@ -265,11 +245,11 @@ export const PureTopBar = memo(
265245 appBarActionsProcessors = [ ] ,
266246 logout,
267247 cluster,
248+ selectedClusters,
268249 clusters,
269250 isSidebarOpen,
270251 isSidebarOpenUserSelected,
271252 onToggleOpen,
272- userInfo,
273253 } : PureTopBarProps ) => {
274254 const { t } = useTranslation ( ) ;
275255 const theme = useTheme ( ) ;
@@ -303,13 +283,37 @@ export const PureTopBar = memo(
303283 setMobileMoreAnchorEl ( event . currentTarget ) ;
304284 } ;
305285 const userMenuId = 'primary-user-menu' ;
306- const userDisplayName = userInfo ?. username || userInfo ?. email || '' ;
307- const userSecondaryInfo =
308- userInfo ?. username && userInfo ?. email && userInfo . username !== userInfo . email
309- ? userInfo . email
310- : undefined ;
311286
312- const renderUserMenu = ! ! isClusterContext && (
287+ const clustersToQuery =
288+ selectedClusters && selectedClusters . length > 0 ? selectedClusters : cluster ? [ cluster ] : [ ] ;
289+
290+ const userInfoQueries = useQueries ( {
291+ queries : clustersToQuery . map ( clusterName => ( {
292+ queryKey : [ 'clusterMe' , clusterName ] ,
293+ queryFn : ( ) => getClusterUserInfo ( clusterName ) ,
294+ staleTime : 5 * 60 * 1000 ,
295+ retry : 1 ,
296+ } ) ) ,
297+ } ) ;
298+
299+ const clusterUserInfoMap : Record < string , ClusterUserInfo | undefined > = { } ;
300+ clustersToQuery . forEach ( ( clusterName , index ) => {
301+ if ( userInfoQueries [ index ] ?. data ) {
302+ clusterUserInfoMap [ clusterName ] = userInfoQueries [ index ] . data ;
303+ }
304+ } ) ;
305+
306+ const getUserDisplayName = ( clusterName : string ) : string => {
307+ const userInfo = clusterUserInfoMap [ clusterName ] ;
308+ if ( userInfo ?. username ) {
309+ return userInfo . username ;
310+ }
311+ return clusterName ;
312+ } ;
313+
314+ const showUserMenu = ( ! ! selectedClusters && selectedClusters . length > 0 ) || isClusterContext ;
315+
316+ const renderUserMenu = showUserMenu && (
313317 < Menu
314318 anchorEl = { anchorEl }
315319 anchorOrigin = { { vertical : 'top' , horizontal : 'right' } }
@@ -327,38 +331,83 @@ export const PureTopBar = memo(
327331 } ,
328332 } }
329333 >
330- { ! ! userDisplayName && (
334+ { selectedClusters && selectedClusters . length > 1 && (
331335 < MenuItem
332- disableRipple
333- sx = { {
334- pointerEvents : 'none' ,
335- cursor : 'default' ,
336- '&:hover' : { backgroundColor : 'inherit' } ,
336+ component = "a"
337+ onClick = { async ( ) => {
338+ await logout ( ) ;
339+ handleMenuClose ( ) ;
337340 } }
338341 >
339342 < ListItemIcon >
340- < Icon icon = "mdi:account" />
343+ < Icon icon = "mdi:logout" />
344+ </ ListItemIcon >
345+ < ListItemText primary = { t ( 'Log out from all' ) } />
346+ </ MenuItem >
347+ ) }
348+
349+ { selectedClusters && selectedClusters . length > 1 ? (
350+ selectedClusters . map ( clusterName => {
351+ const userName = getUserDisplayName ( clusterName ) ;
352+ return (
353+ < MenuItem
354+ key = { `logout-${ clusterName } ` }
355+ component = "a"
356+ onClick = { async ( ) => {
357+ await logout ( clusterName ) ;
358+ handleMenuClose ( ) ;
359+ } }
360+ >
361+ < ListItemIcon >
362+ < Icon icon = "mdi:logout" />
363+ </ ListItemIcon >
364+ < ListItemText
365+ primary = {
366+ < Box display = "flex" flexDirection = "column" >
367+ < Typography variant = "body2" component = "span" >
368+ { t ( 'Log out' ) } : { userName }
369+ </ Typography >
370+ < Typography variant = "caption" color = "text.secondary" component = "span" >
371+ { clusterName }
372+ </ Typography >
373+ </ Box >
374+ }
375+ />
376+ </ MenuItem >
377+ ) ;
378+ } )
379+ ) : (
380+ < MenuItem
381+ component = "a"
382+ onClick = { async ( ) => {
383+ await logout ( ) ;
384+ handleMenuClose ( ) ;
385+ } }
386+ >
387+ < ListItemIcon >
388+ < Icon icon = "mdi:logout" />
341389 </ ListItemIcon >
342390 < ListItemText
343- primaryTypographyProps = { { variant : 'subtitle2' } }
344- secondaryTypographyProps = { { variant : 'body2' } }
345- primary = { userDisplayName }
346- secondary = { userSecondaryInfo }
391+ primary = {
392+ cluster ? (
393+ < Box display = "flex" flexDirection = "column" >
394+ < Typography variant = "body2" component = "span" >
395+ { t ( 'Log out' ) } : { getUserDisplayName ( cluster ) }
396+ </ Typography >
397+ < Typography variant = "caption" color = "text.secondary" component = "span" >
398+ { cluster }
399+ </ Typography >
400+ </ Box >
401+ ) : (
402+ t ( 'Log out' )
403+ )
404+ }
347405 />
348406 </ MenuItem >
349407 ) }
350- < MenuItem
351- component = "a"
352- onClick = { async ( ) => {
353- await logout ( ) ;
354- handleMenuClose ( ) ;
355- } }
356- >
357- < ListItemIcon >
358- < Icon icon = "mdi:logout" />
359- </ ListItemIcon >
360- < ListItemText primary = { t ( 'Log out' ) } />
361- </ MenuItem >
408+
409+ < Divider />
410+
362411 < MenuItem
363412 component = "a"
364413 onClick = { ( ) => {
@@ -411,7 +460,7 @@ export const PureTopBar = memo(
411460 } ,
412461 {
413462 id : DefaultAppBarAction . USER ,
414- action : ! ! isClusterContext && (
463+ action : showUserMenu && (
415464 < MenuItem >
416465 < IconButton
417466 aria-label = { t ( 'Account of current user' ) }
@@ -470,7 +519,7 @@ export const PureTopBar = memo(
470519 } ,
471520 {
472521 id : DefaultAppBarAction . USER ,
473- action : ! ! isClusterContext && (
522+ action : showUserMenu && (
474523 < IconButton
475524 aria-label = { t ( 'Account of current user' ) }
476525 aria-controls = { userMenuId }
0 commit comments