@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
250 lines (249 loc) • 12.9 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 { InlineIcon } from '@iconify/react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Drawer from '@mui/material/Drawer';
import Grid from '@mui/material/Grid';
import List from '@mui/material/List';
import useMediaQuery from '@mui/material/useMediaQuery';
import React, { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { isElectron } from '../../helpers/isElectron';
import { createRouteURL } from '../../lib/router/createRouteURL';
import { useTypedSelector } from '../../redux/hooks';
import ActionButton from '../common/ActionButton';
import CreateButton from '../common/Resource/CreateButton';
import NavigationTabs from './NavigationTabs';
import SidebarItem from './SidebarItem';
import { DefaultSidebars, setSidebarSelected, setWhetherSidebarOpen } from './sidebarSlice';
import { useSidebarItems } from './useSidebarItems';
import VersionButton from './VersionButton';
export const drawerWidth = 240;
export const mobileDrawerWidth = 320;
export const drawerWidthClosed = 64;
// exported for backwards compatibility for plugins
export { DefaultSidebars };
export function useSidebarInfo() {
const isSidebarOpen = useTypedSelector(state => state.sidebar.isSidebarOpen);
const isSidebarOpenUserSelected = useTypedSelector(state => state.sidebar.isSidebarOpenUserSelected);
const isTemporary = useMediaQuery('(max-width:599px)');
const isNarrowOnly = useMediaQuery('(max-width:960px) and (min-width:600px)');
const temporarySideBarOpen = isSidebarOpen === true && isTemporary && isSidebarOpenUserSelected === true;
// The large sidebar does not open in medium view (600-960px).
const isOpen = (isSidebarOpen === true && !isNarrowOnly) || (isSidebarOpen === true && temporarySideBarOpen);
return {
isCollapsed: !temporarySideBarOpen && !isNarrowOnly,
isOpen,
isNarrow: !isSidebarOpen || isNarrowOnly,
canExpand: !isNarrowOnly,
isTemporary,
isUserOpened: isSidebarOpenUserSelected,
width: isOpen ? `${drawerWidth}px` : isTemporary ? '0px' : `${drawerWidthClosed}px`,
};
}
function AddClusterButton() {
const history = useHistory();
const { t } = useTranslation(['translation']);
const { isOpen } = useSidebarInfo();
return (_jsx(Box, { pb: 2, children: isOpen ? (_jsx(Button, { onClick: () => history.push(createRouteURL('addCluster')), startIcon: _jsx(InlineIcon, { icon: "mdi:plus-box-outline" }), sx: { color: theme => theme.palette.sidebar.color }, children: t('translation|Add Cluster') })) : (_jsx(ActionButton, { onClick: () => history.push(createRouteURL('addCluster')), icon: "mdi:plus-box-outline", description: t('translation|Add Cluster'), color: "#adadad", width: 38 })) }));
}
function SidebarToggleButton() {
const dispatch = useDispatch();
const { isOpen, isNarrow, canExpand, isTemporary } = useSidebarInfo();
const { t } = useTranslation();
const isNarrowOnly = isNarrow && !canExpand;
if (isTemporary || isNarrowOnly) {
return null;
}
return (_jsx(Box, { textAlign: isOpen ? 'right' : 'center', children: _jsx(ActionButton, { iconButtonProps: {
size: 'small',
sx: theme => ({
color: theme.palette.sidebar.color,
}),
}, onClick: () => {
dispatch(setWhetherSidebarOpen(!isOpen));
}, icon: isOpen ? 'mdi:chevron-left-box-outline' : 'mdi:chevron-right-box-outline', description: isOpen ? t('translation|Shrink sidebar') : t('translation|Expand sidebar') }) }));
}
const DefaultLinkArea = memo((props) => {
const { sidebarName, isOpen } = props;
if (sidebarName === DefaultSidebars.HOME) {
return (_jsxs(Box, { display: "flex", justifyContent: "space-between", alignItems: "center", flexDirection: isOpen ? 'row' : 'column', p: 1, children: [_jsx(Box, { children: isElectron() && _jsx(AddClusterButton, {}) }), _jsx(Box, { children: _jsx(SidebarToggleButton, {}) })] }));
}
return (_jsxs(Box, { textAlign: "center", children: [_jsx(CreateButton, { isNarrow: !isOpen }), _jsxs(Box, { display: "flex", justifyContent: "space-between", alignItems: "center", flexDirection: isOpen ? 'row' : 'column', p: 1, children: [_jsx(Box, { children: _jsx(VersionButton, {}) }), _jsx(Box, { children: _jsx(SidebarToggleButton, {}) })] })] }));
});
/**
* Checks if item or any sub items are selected
*/
function getIsSelected(item, selectedName) {
if (!selectedName)
return false;
return (item.name === selectedName || Boolean(item.subList?.find(it => getIsSelected(it, selectedName))));
}
/**
* Updates the isSelected field of an item
*/
function updateItemSelected(item, selectedName) {
const isSelected = getIsSelected(item, selectedName);
if (isSelected === false)
return item;
return {
...item,
isSelected: isSelected,
subList: item.subList
? item.subList.map(it => updateItemSelected(it, selectedName))
: item.subList,
};
}
export default function Sidebar() {
const sidebar = useTypedSelector(state => state.sidebar);
const { isOpen, isUserOpened, isNarrow, canExpand, isTemporary: isTemporaryDrawer, } = useSidebarInfo();
const isNarrowOnly = isNarrow && !canExpand;
const namespaces = useTypedSelector(state => state.filter.namespaces);
const dispatch = useDispatch();
const items = useSidebarItems(sidebar?.selected?.sidebar ?? undefined);
const search = namespaces.size !== 0 ? `?namespace=${[...namespaces].join('+')}` : '';
const handleToggleOpen = useCallback(() => {
dispatch(setWhetherSidebarOpen(!sidebar.isSidebarOpen));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sidebar.isSidebarOpen]);
const linkArea = useMemo(() => _jsx(DefaultLinkArea, { sidebarName: sidebar.selected.sidebar || '', isOpen: isOpen }), [sidebar.selected.sidebar, isOpen]);
const processedItems = useMemo(() => items.map(item => updateItemSelected(item, sidebar.selected.item)), [items, sidebar.selected.item]);
if (sidebar.selected.sidebar === null || !sidebar?.isVisible) {
return null;
}
return (_jsx(PureSidebar, { items: processedItems, open: isOpen, openUserSelected: isUserOpened, isNarrowOnly: isNarrowOnly, isTemporaryDrawer: isTemporaryDrawer, selectedName: sidebar?.selected.item, search: search, onToggleOpen: handleToggleOpen, linkArea: linkArea }));
}
export const PureSidebar = memo(({ open, openUserSelected, items, isTemporaryDrawer = false, isNarrowOnly = false, onToggleOpen, search, linkArea, }) => {
const { t } = useTranslation();
const listContainerRef = React.useRef(null);
const [isOverflowing, setIsOverflowing] = React.useState(false);
const [scrollbarWidth, setScrollbarWidth] = React.useState(0);
const closedWidth = 72 + (isOverflowing ? scrollbarWidth : 0);
const temporarySideBarOpen = open === true && isTemporaryDrawer && openUserSelected === true;
// The large sidebar does not open in medium view (600-960px).
const largeSideBarOpen = (open === true && !isNarrowOnly) || (open === true && temporarySideBarOpen);
const adjustedDrawerWidth = largeSideBarOpen ? drawerWidth : closedWidth;
// Remove items from tab order when the sidebar is collapsed.
const tabIndex = largeSideBarOpen ? undefined : -1;
/**
* For closing the sidebar if temporaryDrawer on mobile.
*/
const toggleDrawer = (event) => {
if (event.type === 'keydown' &&
(event.key === 'Tab' ||
event.key === 'Shift')) {
return;
}
onToggleOpen();
};
const contents = (_jsx(_Fragment, { children: _jsxs(Grid, { ref: listContainerRef, sx: {
height: '100%',
overflowY: 'auto',
}, container: true, direction: "column", justifyContent: "space-between", wrap: "nowrap", children: [_jsx(Grid, { item: true, children: _jsx(List, { onClick: isTemporaryDrawer ? toggleDrawer : undefined, onKeyDown: isTemporaryDrawer ? toggleDrawer : undefined, children: items.map(item => (_jsx(SidebarItem, { isSelected: item.isSelected, fullWidth: largeSideBarOpen, search: search, tabIndex: tabIndex, ...item }, item.name))) }) }), _jsx(Grid, { item: true, children: _jsx(Box, { textAlign: "center", children: linkArea }) })] }) }));
const conditionalProps = isTemporaryDrawer
? {
open: temporarySideBarOpen,
onClose: onToggleOpen,
}
: {};
React.useEffect(() => {
const el = listContainerRef.current;
if (!el) {
return;
}
const update = () => {
setIsOverflowing(el.scrollHeight > el.clientHeight);
setScrollbarWidth(Math.max(0, el.offsetWidth - el.clientWidth));
};
const observer = new ResizeObserver(update);
observer.observe(el);
window.addEventListener('resize', update);
update();
return () => {
observer.disconnect();
window.removeEventListener('resize', update);
};
}, [items]);
return (_jsx(Box, { component: "nav", "aria-label": t('translation|Navigation'), sx: { minHeight: 0, gridColumn: '1 / 2', gridRow: '1 / 3' }, children: _jsx(Drawer, { variant: isTemporaryDrawer ? 'temporary' : 'permanent', PaperProps: {
sx: {
borderTop: 'none',
position: 'initial',
},
}, sx: theme => {
const drawer = {
width: drawerWidth,
flexShrink: 0,
height: '100%',
background: theme.palette.sidebar.background,
color: theme.palette.sidebar.color,
};
const drawerOpen = {
zIndex: 1300,
width: drawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
background: theme.palette.sidebar.background,
};
const drawerClose = {
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: 'hidden',
width: adjustedDrawerWidth,
background: theme.palette.sidebar.background,
};
if ((isTemporaryDrawer && temporarySideBarOpen) ||
(!isTemporaryDrawer && largeSideBarOpen)) {
return { ...drawer, ...drawerOpen, '& .MuiPaper-root': { ...drawerOpen } };
}
else {
return { ...drawer, ...drawerClose, '& .MuiPaper-root': { ...drawerClose } };
}
}, ...conditionalProps, children: contents }) }));
});
export function useSidebarItem(sidebarDesc, computedMatch) {
let itemName = null;
let sidebar = DefaultSidebars.IN_CLUSTER;
if (typeof sidebarDesc === 'string') {
itemName = sidebarDesc;
}
else if (sidebarDesc === null) {
sidebar = null;
}
else if (!!sidebarDesc) {
itemName = sidebarDesc.item;
if (!!sidebarDesc.sidebar) {
sidebar = sidebarDesc.sidebar || DefaultSidebars.IN_CLUSTER;
}
}
if (itemName === 'customresources' && sidebar === DefaultSidebars.IN_CLUSTER) {
itemName = computedMatch?.params?.crd ?? itemName;
}
const dispatch = useDispatch();
React.useEffect(() => {
dispatch(setSidebarSelected({ item: itemName, sidebar: sidebar }));
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[itemName]);
}
export { NavigationTabs };