@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
391 lines (390 loc) • 20 kB
JavaScript
import { jsx as _jsx, Fragment as _Fragment, 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 { InlineIcon } from '@iconify/react';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import _ from 'lodash';
import { useSnackbar } from 'notistack';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';
import { apply } from '../../lib/k8s/api/v1/apply';
import { drainNode, drainNodeStatus } from '../../lib/k8s/api/v1/drainNode';
import Node from '../../lib/k8s/node';
import Pod from '../../lib/k8s/pod';
import * as units from '../../lib/units';
import { getCluster, timeAgo } from '../../lib/util';
import { DefaultHeaderAction } from '../../redux/actionButtonsSlice';
import { clusterAction } from '../../redux/clusterActionSlice';
import { CpuCircularChart, EphemeralStorageCircularChart, MemoryCircularChart, PodCapacityCircularChart, } from '../cluster/Charts';
import ActionButton from '../common/ActionButton';
import ConfirmDialog from '../common/ConfirmDialog';
import { HeaderLabel, StatusLabel, ValueLabel } from '../common/Label';
import { ConditionsSection, DetailsGrid, OwnedPodsSection } from '../common/Resource';
import AuthVisible from '../common/Resource/AuthVisible';
import { SectionBox } from '../common/SectionBox';
import { NameValueTable } from '../common/SimpleTable';
import { NodeShellAction } from './NodeShellAction';
import { NodeTaintsLabel } from './utils';
function NodeConditionsLabel(props) {
const { node } = props;
const unschedulable = node?.jsonData?.spec?.unschedulable;
const { t } = useTranslation();
return unschedulable ? (_jsx(StatusLabel, { status: "warning", children: t('translation|Scheduling Disabled') })) : (_jsx(StatusLabel, { status: "success", children: t('translation|Scheduling Enabled') }));
}
export default function NodeDetails(props) {
const params = useParams();
const { name = params.name, cluster } = props;
const { t } = useTranslation(['glossary']);
const dispatch = useDispatch();
const { enqueueSnackbar } = useSnackbar();
const [nodeMetrics, metricsError] = Node.useMetrics();
const [nodeSummaryStats, nodeSummaryError] = Node.useNodeSummaryStats(name, cluster);
const [isupdatingNodeScheduleProperty, setisUpdatingNodeScheduleProperty] = React.useState(false);
const [isNodeDrainInProgress, setisNodeDrainInProgress] = React.useState(false);
const [nodeFromAPI, nodeError] = Node.useGet(name);
const { items: nodePods } = Pod.useList({
fieldSelector: name
? `spec.nodeName=${name},status.phase!=Succeeded,status.phase!=Failed`
: undefined,
cluster,
});
const [node, setNode] = useState(nodeFromAPI);
const noMetrics = metricsError?.status === 404;
const [drainDialogOpen, setDrainDialogOpen] = useState(false);
useEffect(() => {
setNode(nodeFromAPI);
}, [nodeFromAPI]);
function getAddresses(item) {
return (item.status.addresses?.map(({ type, address }) => {
return {
name: type,
value: address,
};
}) || []);
}
function handleNodeScheduleState(node, cordon) {
setisUpdatingNodeScheduleProperty(true);
const cloneNode = _.cloneDeep(node);
cloneNode.spec.unschedulable = !cordon;
dispatch(clusterAction(() => apply(cloneNode.jsonData)
.then(() => {
setNode(cloneNode);
})
.finally(() => {
setisUpdatingNodeScheduleProperty(false);
}), {
startMessage: cordon
? t('Uncordoning node {{name}}…', {
name: node.metadata.name,
})
: t('Cordoning node {{name}}…', { name: node.metadata.name }),
successMessage: cordon
? t('Uncordoned node {{name}}.', { name: node.metadata.name })
: t('Cordoned node {{name}}.', { name: node.metadata.name }),
errorMessage: cordon
? t('Failed to uncordon node {{name}}.', { name: node.metadata.name })
: t('Failed to cordon node {{name}}.', { name: node.metadata.name }),
cancelledMessage: cordon
? t('Uncordon node {{name}} cancelled.', { name: node.metadata.name })
: t('Cordon node {{name}} cancelled.', { name: node.metadata.name }),
cancelCallback: () => {
setisUpdatingNodeScheduleProperty(false);
},
}));
}
function getDrainNodeStatus(cluster, nodeName) {
setTimeout(() => {
drainNodeStatus(cluster, nodeName)
.then(data => {
if (data && data.id.startsWith('error')) {
enqueueSnackbar(data.id, { variant: 'error' });
return;
}
if (data && data.id !== 'success') {
getDrainNodeStatus(cluster, nodeName);
return;
}
const cloneNode = _.cloneDeep(node);
cloneNode.spec.unschedulable = !node.spec.unschedulable;
setNode(cloneNode);
})
.catch(error => {
enqueueSnackbar(error.message, { variant: 'error' });
});
}, 1000);
}
function toggleDrainDialogVisibility() {
setDrainDialogOpen(drainDialogOpen => !drainDialogOpen);
}
function handleNodeDrain(node) {
const cluster = getCluster();
if (!cluster)
return;
setisNodeDrainInProgress(true);
dispatch(clusterAction(() => drainNode(cluster, node.metadata.name)
.then(() => {
getDrainNodeStatus(cluster, node.metadata.name);
})
.catch(error => {
enqueueSnackbar(error.message, { variant: 'error' });
})
.finally(() => {
setisNodeDrainInProgress(false);
}), {
startMessage: t('Draining node {{name}}…', { name: node.metadata.name }),
successMessage: t('Drained node {{name}}.', { name: node.metadata.name }),
errorMessage: t('Failed to drain node {{name}}.', { name: node.metadata.name }),
cancelledMessage: t('Draining node {{name}} cancelled.', { name: node.metadata.name }),
cancelCallback: () => {
setisNodeDrainInProgress(false);
},
}));
}
function DrainDialog() {
return (_jsx(_Fragment, { children: _jsx(ConfirmDialog, { title: t('Drain Node'), description: t('Are you sure you want to drain the node {{name}}?', {
name: node?.metadata.name,
}), onConfirm: () => {
setDrainDialogOpen(false);
handleNodeDrain(node);
}, handleClose: () => setDrainDialogOpen(false), open: drainDialogOpen }) }));
}
return (_jsxs(_Fragment, { children: [_jsx(DrainDialog, {}), _jsx(DetailsGrid, { resourceType: Node, name: name, cluster: cluster, error: nodeError, headerSection: item => (_jsx(ChartsSection, { node: item, pods: nodePods, metrics: nodeMetrics, noMetrics: noMetrics, summaryStats: nodeSummaryStats, summaryError: nodeSummaryError })), withEvents: true, actions: item => {
const cordon = item?.jsonData?.spec?.unschedulable;
const cordonOrUncordon = cordon ? t('Uncordon') : t('Cordon');
return [
{
id: DefaultHeaderAction.NODE_TOGGLE_CORDON,
action: (_jsx(AuthVisible, { item: item, authVerb: "update", children: _jsx(ActionButton, { description: cordonOrUncordon, icon: cordon ? 'mdi:check-circle-outline' : 'mdi:cancel', onClick: () => handleNodeScheduleState(item, cordon), iconButtonProps: {
disabled: isupdatingNodeScheduleProperty,
} }) })),
},
{
id: DefaultHeaderAction.NODE_DRAIN,
action: (_jsx(AuthVisible, { item: item, authVerb: "delete", children: _jsx(ActionButton, { description: t('Drain'), icon: "mdi:delete-variant", onClick: () => toggleDrainDialogVisibility(), iconButtonProps: {
disabled: isNodeDrainInProgress,
} }) })),
},
{
id: DefaultHeaderAction.NODE_SHELL,
action: _jsx(NodeShellAction, { item: item }),
},
];
}, extraInfo: item => item && [
{
name: t('translation|Taints'),
value: _jsx(NodeTaintsLabel, { node: item }),
},
{
name: t('translation|Ready'),
value: _jsx(NodeReadyLabel, { node: item }),
},
{
name: t('translation|Conditions'),
value: _jsx(NodeConditionsLabel, { node: item }),
},
{
name: t('Pod CIDR'),
value: item.spec.podCIDR,
},
...getAddresses(item),
], extraSections: item => item && [
{
id: 'headlamp.node-resource-allocation',
section: _jsx(AllocatedResourcesSection, { node: item, pods: nodePods }),
},
{
id: 'headlamp.node-system-info',
section: _jsx(SystemInfoSection, { node: item }),
},
{
id: 'headlamp.node-conditions',
section: _jsx(ConditionsSection, { resource: item }),
},
{
id: 'headlamp.node-owned-pods',
section: _jsx(OwnedPodsSection, { resource: item }),
},
] })] }));
}
function ChartsSection(props) {
const { node, pods, metrics, summaryStats, summaryError, noMetrics } = props;
const { t } = useTranslation('glossary');
function getUptime() {
if (!node) {
return '…';
}
const readyInfo = node.status.conditions?.find(({ type }) => type === 'Ready');
if (readyInfo) {
return timeAgo(readyInfo.lastTransitionTime);
}
return t('translation|Not ready yet!');
}
return (_jsx(Box, { py: 2, children: _jsxs(Box, { sx: {
display: 'grid',
gap: 2,
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 240px), 1fr))',
marginBottom: '2rem',
}, children: [_jsx(Box, { children: _jsx(Paper, { variant: "outlined", sx: theme => ({
background: theme.palette.background.muted,
padding: theme.spacing(2),
height: '100%',
maxWidth: '300px',
margin: '0 auto',
}), children: _jsx(HeaderLabel, { value: getUptime(), label: t('Uptime') }) }) }), _jsx(Box, { children: _jsx(CpuCircularChart, { items: node && [node], itemsMetrics: metrics, noMetrics: noMetrics }) }), _jsx(Box, { children: _jsx(MemoryCircularChart, { items: node && [node], itemsMetrics: metrics, noMetrics: noMetrics }) }), _jsx(Box, { children: _jsx(PodCapacityCircularChart, { node: node, pods: pods }) }), _jsx(Box, { children: _jsx(EphemeralStorageCircularChart, { node: node, summaryStats: summaryStats, summaryError: summaryError }) })] }) }));
}
const getPercentage = (value, capacity) => {
if (capacity === 0)
return '0';
return ((value / capacity) * 100).toFixed(1);
};
function AllocatedResourcesSection(props) {
const { node, pods } = props;
const { t } = useTranslation('glossary');
const cpuCapacity = units.parseCpu(node?.status.allocatable?.cpu || node?.status.capacity?.cpu || '0');
const memoryCapacity = units.parseRam(node?.status.allocatable?.memory || node?.status.capacity?.memory || '0');
const { cpuRequests, cpuLimits, memoryRequests, memoryLimits } = React.useMemo(() => {
let reqCpu = 0;
let limCpu = 0;
let reqMem = 0;
let limMem = 0;
pods?.forEach((pod) => {
let podCpuRequests = 0;
let podCpuLimits = 0;
let podMemoryRequests = 0;
let podMemoryLimits = 0;
pod.spec.containers.forEach((container) => {
podCpuRequests += units.parseCpu(container.resources?.requests?.cpu || '0');
podCpuLimits += units.parseCpu(container.resources?.limits?.cpu || '0');
podMemoryRequests += units.parseRam(container.resources?.requests?.memory || '0');
podMemoryLimits += units.parseRam(container.resources?.limits?.memory || '0');
});
pod.spec.initContainers?.forEach((container) => {
const initCpuReq = units.parseCpu(container.resources?.requests?.cpu || '0');
const initCpuLimit = units.parseCpu(container.resources?.limits?.cpu || '0');
const initMemReq = units.parseRam(container.resources?.requests?.memory || '0');
const initMemLimit = units.parseRam(container.resources?.limits?.memory || '0');
podCpuRequests = Math.max(podCpuRequests, initCpuReq);
podCpuLimits = Math.max(podCpuLimits, initCpuLimit);
podMemoryRequests = Math.max(podMemoryRequests, initMemReq);
podMemoryLimits = Math.max(podMemoryLimits, initMemLimit);
});
reqCpu += podCpuRequests;
limCpu += podCpuLimits;
reqMem += podMemoryRequests;
limMem += podMemoryLimits;
});
return {
cpuRequests: reqCpu,
cpuLimits: limCpu,
memoryRequests: reqMem,
memoryLimits: limMem,
};
}, [pods]);
return (_jsxs(SectionBox, { title: t('Resource Allocation'), children: [_jsx(Box, { mb: 2, children: _jsx(Typography, { color: "textSecondary", variant: "body2", children: t('Total limits may be over 100 percent, i.e., overcommitted.') }) }), _jsx(NameValueTable, { rows: [
{
name: t('CPU Requests'),
value: (_jsxs(Box, { display: "flex", alignItems: "center", children: [_jsx(ValueLabel, { children: `${units.unparseCpu(cpuRequests.toString()).value} ${units.unparseCpu(cpuRequests.toString()).unit}` }), _jsx(Box, { ml: 2, children: _jsxs(StatusLabel, { status: cpuRequests > cpuCapacity ? 'error' : 'success', children: [getPercentage(cpuRequests, cpuCapacity), " %"] }) })] })),
},
{
name: t('CPU Limits'),
value: (_jsxs(Box, { display: "flex", alignItems: "center", children: [_jsx(ValueLabel, { children: `${units.unparseCpu(cpuLimits.toString()).value} ${units.unparseCpu(cpuLimits.toString()).unit}` }), _jsx(Box, { ml: 2, children: _jsxs(StatusLabel, { status: cpuLimits > cpuCapacity ? 'warning' : 'success', children: [getPercentage(cpuLimits, cpuCapacity), " %"] }) })] })),
},
{
name: t('Memory Requests'),
value: (_jsxs(Box, { display: "flex", alignItems: "center", children: [_jsx(ValueLabel, { children: `${units.unparseRam(memoryRequests).value} ${units.unparseRam(memoryRequests).unit}` }), _jsx(Box, { ml: 2, children: _jsxs(StatusLabel, { status: memoryRequests > memoryCapacity ? 'error' : 'success', children: [getPercentage(memoryRequests, memoryCapacity), " %"] }) })] })),
},
{
name: t('Memory Limits'),
value: (_jsxs(Box, { display: "flex", alignItems: "center", children: [_jsx(ValueLabel, { children: `${units.unparseRam(memoryLimits).value} ${units.unparseRam(memoryLimits).unit}` }), _jsx(Box, { ml: 2, children: _jsxs(StatusLabel, { status: memoryLimits > memoryCapacity ? 'warning' : 'success', children: [getPercentage(memoryLimits, memoryCapacity), " %"] }) })] })),
},
] })] }));
}
function SystemInfoSection(props) {
const { node } = props;
const { t } = useTranslation('glossary');
function getOSComponent(osName) {
let icon = null;
if (osName.toLowerCase() === 'linux') {
icon = _jsx(InlineIcon, { icon: "mdi:penguin" });
}
return (_jsxs(React.Fragment, { children: [icon, _jsx(ValueLabel, { children: osName })] }));
}
if (!node || !node.status.nodeInfo) {
return null;
}
return (_jsx(SectionBox, { title: t('System Info'), children: _jsx(NameValueTable, { rows: [
{
name: t('Architecture'),
value: node.status.nodeInfo.architecture,
},
{
name: t('Boot ID'),
value: node.status.nodeInfo.bootID,
},
{
name: t('System UUID'),
value: node.status.nodeInfo.systemUUID,
},
{
name: t('OS'),
value: getOSComponent(node.status.nodeInfo.operatingSystem),
},
{
name: t('Image'),
value: node.status.nodeInfo.osImage,
},
{
name: t('Kernel Version'),
value: node.status.nodeInfo.kernelVersion,
},
{
name: t('Machine ID'),
value: node.status.nodeInfo.machineID,
},
{
name: t('Kube Proxy Version'),
value: node.status.nodeInfo.kubeProxyVersion,
},
{
name: t('Kubelet Version'),
value: node.status.nodeInfo.kubeletVersion,
},
{
name: t('Container Runtime Version'),
value: node.status.nodeInfo.containerRuntimeVersion,
},
] }) }));
}
export function NodeReadyLabel(props) {
const { node } = props;
const isReady = !!node.status.conditions?.find(condition => condition.type === 'Ready' && condition.status === 'True');
const { t } = useTranslation();
let status = '';
let label = null;
if (isReady) {
status = 'success';
label = t('translation|Yes');
}
else {
status = 'error';
label = t('translation|No');
}
return _jsx(StatusLabel, { status: status, children: label });
}