@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
211 lines (210 loc) • 10.4 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
/*
* Copyright 2025 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Divider from '@mui/material/Divider';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import Popover from '@mui/material/Popover';
import { useTheme } from '@mui/material/styles';
import TextField from '@mui/material/TextField';
import useMediaQuery from '@mui/material/useMediaQuery';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router';
import { useHistory } from 'react-router-dom';
import { getClusterAppearanceFromMeta } from '../../helpers/clusterAppearance';
import { isElectron } from '../../helpers/isElectron';
import { getRecentClusters, setRecentCluster } from '../../helpers/recentClusters';
import { useClustersConf, useSelectedClusters } from '../../lib/k8s';
import { createRouteURL } from '../../lib/router/createRouteURL';
import { getCluster, getClusterPrefixedPath } from '../../lib/util';
import ClusterBadge from '../Sidebar/ClusterBadge';
function ClusterListItem(props) {
const { cluster, selected, onClick } = props;
const { t } = useTranslation();
const appearance = getClusterAppearanceFromMeta(cluster?.name || '');
return (_jsxs(MenuItem, { selected: selected, "aria-selected": !!cluster.isCurrent, onClick: onClick, id: cluster.name, role: "option", sx: theme => ({
borderRadius: theme.shape.borderRadius + 'px',
}), children: [_jsx(ClusterBadge, { name: cluster.name, icon: appearance.icon, accentColor: appearance.accentColor }), !!cluster.isCurrent && (_jsx(ListItemText, { secondary: t('Current', { context: 'cluster' }), sx: { marginLeft: 'auto', paddingLeft: 2 } }))] }, `recent_cluster_${cluster.name}`));
}
/** A popup that allows the user to choose a cluster.
* @param anchor The element to which the popup will be anchored.
* @param onClose Callback to be called when the popup is closed.
*/
function ClusterChooserPopup(props) {
const { t } = useTranslation(['translation']);
const { anchor, onClose, ...otherProps } = props;
const [filter, setFilter] = React.useState('');
const clusters = useClustersConf();
const history = useHistory();
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
const [activeDescendantIndex, setActiveDescendantIndex] = React.useState(-1);
const focusedRef = React.useCallback((node) => {
if (node !== null) {
node.focus();
}
}, []);
const selectedClusters = useSelectedClusters();
function handleClose() {
setFilter('');
if (!!onClose) {
onClose();
}
}
const [recentClusters, clustersToShow] = React.useMemo(() => {
let allClusters = Object.values(clusters || {});
if (filter !== '') {
allClusters = allClusters.filter(cluster => cluster.name.includes(filter));
}
const recentClustersNames = !!filter ? [] : getRecentClusters();
const clustersToShow = [];
const recentClusters = [];
allClusters.forEach(c => {
const cluster = { ...c };
if (selectedClusters.includes(c.name)) {
cluster.isCurrent = true;
}
if (recentClustersNames.includes(c.name)) {
recentClusters.push(cluster);
}
else {
clustersToShow.push(cluster);
}
});
recentClusters.sort((a, b) => {
if (a.isCurrent) {
return -1;
}
else if (b.isCurrent) {
return 1;
}
return 0;
});
clustersToShow.sort((a, b) => {
if (a.isCurrent) {
return -1;
}
else if (b.isCurrent) {
return 1;
}
return a.name.localeCompare(b.name);
});
return [recentClusters, clustersToShow];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [clusters, selectedClusters.join(','), filter]);
React.useEffect(() => {
setActiveDescendantIndex(-1);
}, [filter]);
function selectCluster(cluster) {
handleClose();
if (cluster.name !== getCluster()) {
setRecentCluster(cluster);
history.push({
pathname: generatePath(getClusterPrefixedPath(), {
cluster: cluster.name,
}),
});
}
}
const activeDescendantProp = React.useMemo(() => {
const cluster = getActiveDescendantCluster();
if (!cluster) {
return {};
}
return {
'aria-activedescendant': cluster.name,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeDescendantIndex]);
function getActiveDescendantCluster() {
const allClusters = [...recentClusters, ...clustersToShow];
if (activeDescendantIndex === -1 || activeDescendantIndex >= allClusters.length) {
return null;
}
return allClusters[activeDescendantIndex];
}
function onKeyDown(e) {
switch (e.key) {
case 'ArrowUp': {
setActiveDescendantIndex(idx => (idx - 1) % (recentClusters.length + clustersToShow.length));
break;
}
case 'ArrowDown': {
setActiveDescendantIndex(idx => (idx + 1) % (recentClusters.length + clustersToShow.length));
break;
}
case 'Enter': {
const cluster = getActiveDescendantCluster();
if (!!cluster) {
selectCluster(cluster);
}
break;
}
}
}
if (!anchor) {
return null;
}
return (_jsxs(Popover, { open: !!anchor, anchorEl: isSmallScreen ? null : anchor, onClose: handleClose, anchorOrigin: {
vertical: 'top',
horizontal: isSmallScreen ? 'center' : 'left',
}, transformOrigin: {
vertical: isSmallScreen ? 'center' : 'top',
horizontal: isSmallScreen ? 'center' : 'left',
}, "aria-labelledby": "chooser-dialog-title", "aria-busy": clusters === null, sx: {
marginTop: '-5px',
}, ...otherProps, children: [_jsxs(Box, { p: 2, pb: 1, children: [_jsx(TextField, { label: t('Choose cluster'), id: "filled-size-small", placeholder: t('translation|Name'), variant: "outlined", size: "small", fullWidth: true, InputLabelProps: { shrink: true }, onChange: e => setFilter(e.target.value), inputRef: focusedRef, onKeyDown: onKeyDown, InputProps: {
'aria-owns': 'cluster-chooser-list',
'aria-haspopup': 'listbox',
}, ...activeDescendantProp }), _jsxs(MenuList, { id: "cluster-chooser-list", role: "listbox", tabIndex: 0, sx: {
width: '280px',
minWidth: '280px',
minHeight: '200px',
maxHeight: '50vh',
overflowY: 'auto',
position: 'relative',
'& .MuiListItemIcon-root': {
minWidth: 0,
paddingRight: theme.spacing(2),
},
'& .MuiListItem-gutters': {
paddingLeft: 0,
},
}, dense: true, children: [recentClusters.length > 0 && (_jsxs(_Fragment, { children: [
// We only show the subheader if the recent clusters list is not all the clusters we have.
clustersToShow.length > 0 && (_jsx(ListSubheader, { disableSticky: true, role: "presentation", sx: {
paddingLeft: 0,
lineHeight: theme.typography.pxToRem(24),
}, children: t('Recent clusters') })), recentClusters.map(cluster => (_jsx(ClusterListItem, { cluster: cluster, onClick: () => selectCluster(cluster), selected: cluster.name === getActiveDescendantCluster()?.name }, `recent_cluster_${cluster.name}`)))] })), clustersToShow.length > 0 && recentClusters.length > 0 && _jsx(Divider, { role: "separator" }), clustersToShow.map(cluster => (_jsx(ClusterListItem, { cluster: cluster, onClick: () => selectCluster(cluster), selected: cluster.name === getActiveDescendantCluster()?.name }, `cluster_button_${cluster.name}`)))] })] }), isElectron() && (_jsx(_Fragment, { children: _jsx(Button, { sx: theme => ({
backgroundColor: theme.palette.sidebarBg,
color: theme.palette.mode === 'dark'
? theme.palette.text.primary
: theme.palette.primary.contrastText,
'&:hover': {
color: theme.palette.text.secondary,
},
width: '100%',
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
textTransform: 'none',
}), onClick: () => history.push(createRouteURL('loadKubeConfig')), children: t('translation|Add Cluster') }) }))] }));
}
export default ClusterChooserPopup;