UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

306 lines (305 loc) 14.2 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 Grid from '@mui/material/Grid'; import Paper from '@mui/material/Paper'; 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, drainNode, drainNodeStatus } from '../../lib/k8s/apiProxy'; import Node from '../../lib/k8s/node'; import { getCluster, timeAgo } from '../../lib/util'; import { DefaultHeaderAction } from '../../redux/actionButtonsSlice'; import { clusterAction } from '../../redux/clusterActionSlice'; import { CpuCircularChart, MemoryCircularChart } from '../cluster/Charts'; import { ActionButton, ConfirmDialog } from '../common'; 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 [isupdatingNodeScheduleProperty, setisUpdatingNodeScheduleProperty] = React.useState(false); const [isNodeDrainInProgress, setisNodeDrainInProgress] = React.useState(false); const [nodeFromAPI, nodeError] = Node.useGet(name); 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, metrics: nodeMetrics, noMetrics: noMetrics })), 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-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, metrics, 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(Grid, { container: true, style: { marginBottom: '2rem', }, alignItems: "stretch", spacing: 2, children: [_jsx(Grid, { item: true, xs: 4, 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(Grid, { item: true, xs: 4, children: _jsx(CpuCircularChart, { items: node && [node], itemsMetrics: metrics, noMetrics: noMetrics }) }), _jsx(Grid, { item: true, xs: 4, children: _jsx(MemoryCircularChart, { items: node && [node], itemsMetrics: metrics, noMetrics: noMetrics }) })] }) })); } 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 }); }