UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

196 lines (195 loc) 8.28 kB
import { jsx as _jsx, jsxs as _jsxs } 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 { useTheme } from '@mui/material/styles'; import { styled } from '@mui/material/styles'; import { alpha } from '@mui/system/colorManipulator'; import { Handle, Position } from '@xyflow/react'; import { memo, useEffect, useState } from 'react'; import { Activity } from '../../activity/Activity'; import { GraphNodeDetails } from '../details/GraphNodeDetails'; import { getMainNode } from '../graph/graphGrouping'; import { useGraphView, useNode } from '../GraphView'; import { KubeIcon } from '../kubeIcon/KubeIcon'; import { NodeGlance } from '../KubeObjectGlance/NodeGlance'; import { GroupNodeComponent } from './GroupNode'; import { getGraphNodeStatus } from './KubeObjectStatus'; const Container = styled('div')(({ theme, isSelected, isExpanded, childrenCount }) => ({ display: 'flex', flexDirection: 'column', width: isExpanded ? 'auto' : '100%!important', minWidth: '100%', height: isExpanded ? 'auto' : '100%', minHeight: '100%', boxSizing: 'border-box', position: 'absolute', background: theme.palette.background.paper, borderRadius: '10px', border: '1px solid', borderColor: isSelected ? theme.palette.action.active : theme.palette.divider, padding: '10px', willChange: 'width, height, border-color, box-shadow', ':hover': { borderColor: isSelected ? undefined : alpha(theme.palette.action.active, 0.2), boxShadow: '4px 4px 6px rgba(0,0,0,0.06)', }, '::before': childrenCount > 1 ? { content: `''`, width: '100%', height: '100%', position: 'absolute', left: '5px', top: '5px', background: theme.palette.background.paper, border: '1px solid', borderColor: theme.palette.divider, borderRadius: '10px', zIndex: '-1', } : undefined, '::after': childrenCount > 2 ? { content: `''`, width: '100%', height: '100%', position: 'absolute', left: '9px', top: '9px', background: theme.palette.background.paper, border: '1px solid', borderColor: theme.palette.divider, borderRadius: '10px', zIndex: '-2', } : undefined, })); const CircleBadge = styled('div')(({ theme }) => ({ position: 'absolute', right: 0, top: 0, width: '32px', height: '32px', borderRadius: '50%', display: 'flex', alignItems: 'center', margin: theme.spacing(-1), justifyContent: 'center', background: theme.palette.background.paper, boxShadow: '1px 1px 5px rgba(0,0,0,0.08)', transition: 'top 0.05s', transitionTimingFunction: 'linear', border: '1px solid #e1e1e1', borderColor: theme.palette.divider, color: theme.typography.caption.color, })); const TextContainer = styled('div')({ display: 'flex', alignItems: 'center', gap: '8px', height: '48px', }); const LabelContainer = styled('div')(({ isCollapsed }) => ({ display: 'flex', flexDirection: isCollapsed ? 'column' : 'row', overflow: 'hidden', justifyContent: 'center', alignItems: isCollapsed ? undefined : 'center', })); const Subtitle = styled('div')({ opacity: 0.8, fontSize: 14, }); const Title = styled('div')({ textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap', }); const EXPAND_DELAY = 450; export const KubeObjectNodeComponent = memo(({ id }) => { const node = useNode(id); const [isHovered, setHovered] = useState(false); const [isExpanded, setIsExpanded] = useState(false); const theme = useTheme(); const graph = useGraphView(); const mainNode = node?.nodes ? getMainNode(node.nodes) : undefined; const kubeObject = node?.kubeObject ?? mainNode?.kubeObject; const apiGroup = kubeObject?.jsonData?.apiVersion && kubeObject.jsonData.apiVersion.includes('/') ? kubeObject.jsonData.apiVersion.split('/')[0] : 'core'; const isSelected = id === graph.nodeSelection; const isCollapsed = node?.nodes?.length ? node?.collapsed : true; let status = node ? getGraphNodeStatus(node) : 'success'; if (node?.nodes) { const errors = node.nodes.filter(it => getGraphNodeStatus(it) === 'error').length; const warnings = node.nodes.filter(it => getGraphNodeStatus(it) === 'warning').length; if (warnings) { status = 'warning'; } if (errors) { status = 'error'; } } useEffect(() => { if (!isHovered) { // eslint-disable-next-line react-hooks/set-state-in-effect setIsExpanded(false); return; } const id = setTimeout(() => setIsExpanded(true), EXPAND_DELAY); return () => clearInterval(id); }, [isHovered]); const icon = kubeObject ? (_jsx(KubeIcon, { width: "42px", height: "42px", kind: kubeObject.kind, apiGroup: apiGroup })) : (node?.icon ?? null); const openDetails = () => { graph.setNodeSelection(id); setHovered(false); if (!node || node?.nodes) return; const hasContent = node.detailsComponent || node.kubeObject; if (!hasContent) return; Activity.launch({ id: node.id, location: 'split-right', temporary: true, cluster: node.kubeObject?.cluster, hideTitleInHeader: true, icon: node.kubeObject ? (_jsx(KubeIcon, { kind: node.kubeObject.kind, apiGroup: apiGroup, width: "100%", height: "100%" })) : null, title: node.label ?? node.kubeObject?.metadata?.name, content: _jsx(GraphNodeDetails, { node: node }), }); }; const label = node?.label ?? kubeObject?.metadata?.name; const subtitle = node?.subtitle ?? kubeObject?.kind; if (!node) { return null; } if (node?.nodes?.length && !node.collapsed) { return _jsx(GroupNodeComponent, { id: id }); } return (_jsxs(Container, { tabIndex: 0, role: "button", isFaded: false, childrenCount: node.nodes?.length ?? 0, isSelected: isSelected, isExpanded: isExpanded, onClick: openDetails, onFocus: () => setHovered(true), onBlur: () => setHovered(false), onPointerEnter: () => setHovered(true), onPointerLeave: () => { setHovered(false); }, onKeyDown: e => { if (e.key === 'Enter' || e.key === 'Space') { openDetails(); } }, children: [_jsx(Handle, { type: "target", position: Position.Top, style: { opacity: 0 } }), _jsx(Handle, { type: "source", position: Position.Bottom, style: { opacity: 0 } }), isCollapsed && status !== 'success' && (_jsx(CircleBadge, { style: { right: node.nodes?.length ? '36px' : undefined }, children: _jsx(Icon, { color: theme.palette[status].main, icon: status === 'error' ? 'mdi:exclamation' : 'mdi:information', width: "22px", height: "22px" }) })), node.collapsed && (node.nodes?.length ?? 0) > 0 && (_jsx(CircleBadge, { children: node.nodes?.length ?? 0 })), _jsxs(TextContainer, { children: [icon, _jsxs(LabelContainer, { isCollapsed: isCollapsed, children: [_jsx(Subtitle, { children: subtitle }), _jsx(Title, { sx: { textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap', }, children: label })] })] }), isExpanded && _jsx(NodeGlance, { node: node })] })); });