UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

209 lines (208 loc) 10 kB
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 { Icon } from '@iconify/react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Divider from '@mui/material/Divider'; import ListItemIcon from '@mui/material/ListItemIcon'; 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 { isElectron } from '../../helpers/isElectron'; import { getRecentClusters, setRecentCluster } from '../../helpers/recentClusters'; import { useClustersConf, useSelectedClusters } from '../../lib/k8s'; import { createRouteURL } from '../../lib/router'; import { getCluster, getClusterPrefixedPath } from '../../lib/util'; function ClusterListItem(props) { const { cluster, selected, onClick } = props; const { t } = useTranslation(); const theme = useTheme(); return (_jsxs(MenuItem, { selected: selected, onClick: onClick, id: cluster.name, sx: theme => ({ borderRadius: theme.shape.borderRadius + 'px', }), children: [_jsx(ListItemIcon, { children: _jsx(Icon, { icon: "mdi:kubernetes", width: 26, color: theme.palette.text.primary }) }), _jsx(ListItemText, { primary: cluster.name, secondary: !!cluster.isCurrent ? t('Current', { context: 'cluster' }) : '' })] }, `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]; }, [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, }; }, [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", 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, 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, {}), 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;