UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

391 lines (390 loc) 20 kB
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 }); }