Skip to content

Commit 622c74a

Browse files
committed
frontend: Add cluster chooser action for the cluster sidebar item
And disable the cluster chooser button. The logic for the cluster chooser button however remains, in order to support the plugin-registered chooser components. Signed-off-by: Joaquim Rocha <[email protected]>
1 parent dbfd9c9 commit 622c74a

File tree

4 files changed

+108
-45
lines changed

4 files changed

+108
-45
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2025 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Icon } from '@iconify/react';
18+
import Button from '@mui/material/Button';
19+
import React from 'react';
20+
import { useHotkeys } from 'react-hotkeys-hook';
21+
import { useTypedSelector } from '../../redux/hooks';
22+
import ClusterChooserPopup from '../cluster/ClusterChooserPopup';
23+
24+
/**
25+
* Action component for cluster sidebar items.
26+
* This shows an ellipsis icon that opens the cluster chooser popup.
27+
* Only displays when no plugin has registered a custom cluster chooser button,
28+
* whose default button component used to be displayed at the top bar.
29+
*/
30+
export const ClusterChooserAction: React.FC = () => {
31+
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
32+
const ChooserButton = useTypedSelector(state => state.ui.clusterChooserButtonComponent);
33+
const buttonRef = React.useRef<HTMLButtonElement>(null);
34+
35+
useHotkeys(
36+
'ctrl+shift+l',
37+
() => {
38+
// Only open popup if no plugin has registered a custom cluster chooser button
39+
if (!ChooserButton) {
40+
setAnchorEl(buttonRef.current);
41+
}
42+
},
43+
{ preventDefault: true }
44+
);
45+
46+
// Don't render if a plugin has registered a custom cluster chooser button
47+
if (ChooserButton) {
48+
return null;
49+
}
50+
51+
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
52+
// Prevent both default action and event propagation to avoid sidebar navigation
53+
event.stopPropagation();
54+
event.preventDefault();
55+
setAnchorEl(event.currentTarget);
56+
};
57+
58+
const handleClose = () => {
59+
setAnchorEl(null);
60+
};
61+
62+
return (
63+
<>
64+
<Button
65+
ref={buttonRef}
66+
variant="outlined"
67+
sx={{
68+
minWidth: 'unset',
69+
px: 0.5,
70+
}}
71+
onClick={handleClick}
72+
>
73+
<Icon icon="mdi:dots-horizontal" width={16} height={16} />
74+
</Button>
75+
<ClusterChooserPopup anchor={anchorEl} onClose={handleClose} />
76+
</>
77+
);
78+
};

frontend/src/components/Sidebar/useSidebarItems.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { useSelectedClusters } from '../../lib/k8s';
2222
import { createRouteURL } from '../../lib/router';
2323
import { useTypedSelector } from '../../redux/hooks';
2424
import { DefaultSidebars, SidebarItemProps } from '.';
25+
import { ClusterChooserAction } from './ClusterChooserAction';
2526

2627
/** Iterates over every entry in the list, including children */
2728
const forEachEntry = (items: SidebarItemProps[], cb: (item: SidebarItemProps) => void) => {
@@ -67,6 +68,8 @@ export const useSidebarItems = (sidebarName: string = DefaultSidebars.IN_CLUSTER
6768
? '/'
6869
: createRouteURL('cluster', { cluster: Object.keys(clusters)[0] }),
6970
divider: !shouldShowHomeItem,
71+
// Add cluster chooser action to cluster items (when not showing home)
72+
endAction: !shouldShowHomeItem ? ClusterChooserAction : undefined,
7073
},
7174
{
7275
name: 'notifications',
@@ -112,6 +115,7 @@ export const useSidebarItems = (sidebarName: string = DefaultSidebars.IN_CLUSTER
112115
label: selectedClusters.length ? t('Clusters') : t('glossary|Cluster'),
113116
subtitle: selectedClusters.join('\n') || undefined,
114117
icon: 'mdi:hexagon-multiple-outline',
118+
endAction: ClusterChooserAction,
115119
subList: [
116120
{
117121
name: 'namespaces',

frontend/src/components/cluster/Chooser.tsx

Lines changed: 13 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import Typography from '@mui/material/Typography';
3333
import useMediaQuery from '@mui/material/useMediaQuery';
3434
import _ from 'lodash';
3535
import React, { isValidElement, PropsWithChildren } from 'react';
36-
import { useHotkeys } from 'react-hotkeys-hook';
3736
import { useTranslation } from 'react-i18next';
3837
import { useDispatch } from 'react-redux';
3938
import { generatePath } from 'react-router';
@@ -51,32 +50,30 @@ import ActionButton from '../common/ActionButton';
5150
import { DialogTitle } from '../common/Dialog';
5251
import ErrorBoundary from '../common/ErrorBoundary';
5352
import Loader from '../common/Loader';
54-
import ClusterChooser from './ClusterChooser';
55-
import ClusterChooserPopup from './ClusterChooserPopup';
5653

5754
export interface ClusterTitleProps {
55+
/** @deprecated This prop is not used in the current implementation. */
5856
clusters?: {
5957
[clusterName: string]: Cluster;
6058
};
6159
cluster?: string;
60+
/** @deprecated This prop is not used in the current implementation. */
6261
onClick?: (event?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
6362
}
6463

64+
/**
65+
* This component renders the cluster chooser button if it's registered by
66+
* plugins. Otherwise, it doesn't show anything. This component still exists to
67+
* provide compatibility with the plugins that expect a cluster chooser to exist.
68+
*
69+
* @param props
70+
* @returns
71+
*/
6572
export function ClusterTitle(props: ClusterTitleProps) {
66-
const { cluster, clusters, onClick } = props;
67-
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
68-
const buttonRef = React.useRef<HTMLButtonElement>(null);
73+
const { cluster } = props;
6974
const arePluginsLoaded = useTypedSelector(state => state.plugins.loaded);
7075
const ChooserButton = useTypedSelector(state => state.ui.clusterChooserButtonComponent);
7176

72-
useHotkeys(
73-
'ctrl+shift+l',
74-
() => {
75-
setAnchorEl(buttonRef.current);
76-
},
77-
{ preventDefault: true }
78-
);
79-
8077
if (!cluster) {
8178
return null;
8279
}
@@ -85,37 +82,11 @@ export function ClusterTitle(props: ClusterTitleProps) {
8582
return null;
8683
}
8784

88-
if (!ChooserButton && Object.keys(clusters || {}).length <= 1) {
85+
if (!ChooserButton || !isValidElement(ChooserButton)) {
8986
return null;
9087
}
9188

92-
return (
93-
<ErrorBoundary>
94-
{ChooserButton ? (
95-
isValidElement(ChooserButton) ? (
96-
ChooserButton
97-
) : (
98-
<ChooserButton
99-
clickHandler={e => {
100-
onClick && onClick(e);
101-
e?.currentTarget && setAnchorEl(e.currentTarget);
102-
}}
103-
cluster={cluster}
104-
/>
105-
)
106-
) : (
107-
<ClusterChooser
108-
ref={buttonRef}
109-
clickHandler={e => {
110-
onClick && onClick(e);
111-
e?.currentTarget && setAnchorEl(e.currentTarget);
112-
}}
113-
cluster={cluster}
114-
/>
115-
)}
116-
<ClusterChooserPopup anchor={anchorEl} onClose={() => setAnchorEl(null)} />
117-
</ErrorBoundary>
118-
);
89+
return <ErrorBoundary>{ChooserButton}</ErrorBoundary>;
11990
}
12091

12192
interface ClusterButtonProps extends PropsWithChildren<{}> {

frontend/src/components/cluster/ClusterChooserPopup.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ import { Cluster } from '../../lib/k8s/cluster';
3838
import { createRouteURL } from '../../lib/router';
3939
import { getCluster, getClusterPrefixedPath } from '../../lib/util';
4040

41-
function ClusterListItem(props: { cluster: Cluster; onClick: () => void; selected?: boolean }) {
41+
function ClusterListItem(props: {
42+
cluster: Cluster;
43+
onClick: (event: React.MouseEvent<HTMLElement>) => void;
44+
selected?: boolean;
45+
}) {
4246
const { cluster, selected, onClick } = props;
4347
const { t } = useTranslation();
4448
const theme = useTheme();
@@ -209,6 +213,12 @@ function ClusterChooserPopup(props: ChooserPopupPros) {
209213
}
210214
}
211215

216+
function clusterClicked(event: React.MouseEvent<HTMLElement>, cluster: Cluster) {
217+
event.stopPropagation();
218+
event.preventDefault();
219+
selectCluster(cluster);
220+
}
221+
212222
if (!anchor) {
213223
return null;
214224
}
@@ -290,7 +300,7 @@ function ClusterChooserPopup(props: ChooserPopupPros) {
290300
<ClusterListItem
291301
key={`recent_cluster_${cluster.name}`}
292302
cluster={cluster}
293-
onClick={() => selectCluster(cluster)}
303+
onClick={(event: React.MouseEvent<HTMLElement>) => clusterClicked(event, cluster)}
294304
selected={cluster.name === getActiveDescendantCluster()?.name}
295305
/>
296306
))}
@@ -301,7 +311,7 @@ function ClusterChooserPopup(props: ChooserPopupPros) {
301311
<ClusterListItem
302312
key={`cluster_button_${cluster.name}`}
303313
cluster={cluster}
304-
onClick={() => selectCluster(cluster)}
314+
onClick={(event: React.MouseEvent<HTMLElement>) => clusterClicked(event, cluster)}
305315
selected={cluster.name === getActiveDescendantCluster()?.name}
306316
/>
307317
))}

0 commit comments

Comments
 (0)