UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

243 lines (242 loc) 12.3 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 { 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'; import { useTypedSelector } from '../../redux/reducers/reducers'; import { ActionButton } from '../common'; 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: t('translation|Collapse 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)); }, [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; /** * 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, ...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'), children: _jsx(Drawer, { variant: isTemporaryDrawer ? 'temporary' : 'permanent', PaperProps: { sx: { 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) { 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; } } const dispatch = useDispatch(); React.useEffect(() => { dispatch(setSidebarSelected({ item: itemName, sidebar: sidebar })); }, // eslint-disable-next-line react-hooks/exhaustive-deps [itemName]); } export { NavigationTabs };