@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
196 lines (195 loc) • 8.28 kB
JavaScript
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 })] }));
});