UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

344 lines (343 loc) 18 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 Box from '@mui/material/Box'; import Fade from '@mui/material/Fade'; import React from 'react'; import { useTranslation } from 'react-i18next'; import Pod from '../../lib/k8s/pod'; import { METRIC_REFETCH_INTERVAL_MS, PodMetrics } from '../../lib/k8s/PodMetrics'; import { parseCpu, parseRam, unparseCpu, unparseRam } from '../../lib/units'; import { timeAgo } from '../../lib/util'; import { useNamespaces } from '../../redux/filterSlice'; import { HeadlampEventType, useEventCallback } from '../../redux/headlampEventSlice'; import { CreateResourceButton } from '../common'; import { StatusLabel } from '../common/Label'; import Link from '../common/Link'; import ResourceListView from '../common/Resource/ResourceListView'; import { TooltipIcon } from '../common/Tooltip'; import LightTooltip from '../common/Tooltip/TooltipLight'; function getPodStatus(pod) { const phase = pod.status.phase; let status = ''; if (phase === 'Failed') { status = 'error'; } else if (phase === 'Succeeded' || phase === 'Running') { const readyCondition = pod.status.conditions.find(condition => condition.type === 'Ready'); if (readyCondition?.status === 'True' || phase === 'Succeeded') { status = 'success'; } else { status = 'warning'; } } return status; } export function makePodStatusLabel(pod, showContainerStatus = true) { const status = getPodStatus(pod); const { reason, message: tooltip } = pod.getDetailedStatus(); const containerStatuses = pod.status?.containerStatuses || []; const containerIndicators = containerStatuses.map((cs, index) => { const { color, tooltip } = getContainerDisplayStatus(cs); return (_jsx(LightTooltip, { title: tooltip, TransitionComponent: Fade, TransitionProps: { timeout: 0 }, slotProps: { popper: { modifiers: [{ name: 'computeStyles', options: { gpuAcceleration: false } }], }, tooltip: { sx: { maxWidth: 'none', willChange: 'opacity' } }, }, children: _jsx(Icon, { icon: "mdi:circle", style: { color }, width: "1rem", height: "1rem" }) }, index)); }); return (_jsxs(Box, { display: "flex", alignItems: "center", gap: 1, children: [_jsx(LightTooltip, { title: tooltip, interactive: true, TransitionComponent: Fade, TransitionProps: { timeout: 0 }, slotProps: { popper: { modifiers: [{ name: 'computeStyles', options: { gpuAcceleration: false } }], }, tooltip: { sx: { maxWidth: 'none', willChange: 'opacity' } }, }, children: _jsx(Box, { display: "inline", children: _jsxs(StatusLabel, { status: status, children: [(status === 'warning' || status === 'error') && (_jsx(Icon, { "aria-label": "hidden", icon: "mdi:alert-outline", width: "1.2rem", height: "1.2rem" })), reason] }) }) }), showContainerStatus && containerIndicators.length > 0 && (_jsx(Box, { display: "flex", gap: 0.5, children: containerIndicators }))] })); } function getReadinessGatesStatus(pods) { const readinessGates = pods?.spec?.readinessGates?.map(gate => gate.conditionType) || []; const readinessGatesMap = {}; if (readinessGates.length === 0) { return readinessGatesMap; } pods?.status?.conditions?.forEach(condition => { if (readinessGates.includes(condition.type)) { readinessGatesMap[condition.type] = condition.status; } }); return readinessGatesMap; } function getContainerDisplayStatus(container) { const state = container.state || {}; let color = 'grey'; let label = ''; const tooltipLines = [`Name: ${container.name}`]; if (state.waiting) { color = 'orange'; label = 'Waiting'; if (state.waiting.reason) { tooltipLines.push(`Reason: ${state.waiting.reason}`); } } else if (state.terminated) { color = 'green'; label = 'Terminated'; if (state.terminated.reason === 'Error') { color = 'red'; } if (state.terminated.reason) { tooltipLines.push(`Reason: ${state.terminated.reason}`); } if (state.terminated.exitCode !== undefined) { tooltipLines.push(`Exit Code: ${state.terminated.exitCode}`); } if (state.terminated.startedAt) { tooltipLines.push(`Started: ${new Date(state.terminated.startedAt).toLocaleString()}`); } if (state.terminated.finishedAt) { tooltipLines.push(`Finished: ${new Date(state.terminated.finishedAt).toLocaleString()}`); } if (container.restartCount > 0) { tooltipLines.push(`Restarts: ${container.restartCount}`); } } else if (state.running) { color = 'green'; label = 'Running'; if (state.running.startedAt) { tooltipLines.push(`Started: ${new Date(state.running.startedAt).toLocaleString()}`); } if (container.restartCount > 0) { tooltipLines.push(`Restarts: ${container.restartCount}`); } } tooltipLines.splice(1, 0, `Status: ${label}`); return { color, label, tooltip: _jsx("span", { style: { whiteSpace: 'pre-line' }, children: tooltipLines.join('\n') }), }; } export function PodListRenderer(props) { const { pods, metrics, hideColumns = [], reflectTableInURL = 'pods', noNamespaceFilter, errors, hideCreateButton, enableRowActions, enableRowSelection, } = props; const { t } = useTranslation(['glossary', 'translation']); const getCpuUsage = (pod) => { const metric = metrics?.find(it => it.getName() === pod.getName() && it.getNamespace() === pod.getNamespace()); if (!metric) return; return (metric?.jsonData.containers.map(it => parseCpu(it.usage.cpu)).reduce((a, b) => a + b, 0) ?? 0); }; const getMemoryUsage = (pod) => { const metric = metrics?.find(it => it.getName() === pod.getName() && it.getNamespace() === pod.getNamespace()); if (!metric) return; return (metric?.jsonData.containers.map(it => parseRam(it.usage.memory)).reduce((a, b) => a + b, 0) ?? 0); }; return (_jsx(ResourceListView, { title: t('Pods'), headerProps: { noNamespaceFilter, titleSideActions: hideCreateButton ? [] : [_jsx(CreateResourceButton, { resourceClass: Pod }, "create-pod-button")], }, hideColumns: hideColumns, errors: errors, columns: [ 'name', 'namespace', 'cluster', { label: t('Restarts'), gridTemplate: 'min-content', disableFiltering: true, getValue: pod => { const { restarts, lastRestartDate } = pod.getDetailedStatus(); return lastRestartDate.getTime() !== 0 ? t('{{ restarts }} ({{ abbrevTime }} ago)', { restarts: restarts, abbrevTime: timeAgo(lastRestartDate, { format: 'mini' }), }) : restarts; }, }, { id: 'ready', gridTemplate: 'min-content', label: t('translation|Ready'), disableFiltering: true, getValue: pod => { const podRow = pod.getDetailedStatus(); return `${podRow.readyContainers}/${podRow.totalContainers}`; }, }, { id: 'status', gridTemplate: 'min-content', filterVariant: 'multi-select', label: t('translation|Status'), // include ready condition status so the cell re-renders when icon state changes getValue: pod => { const status = pod.getDetailedStatus(); const readyCondition = pod.status?.conditions?.find(c => c.type === 'Ready'); return `${status.reason}:${readyCondition?.status ?? ''}`; }, render: makePodStatusLabel, }, ...(metrics?.length ? [ { id: 'cpu', label: t('CPU'), gridTemplate: 'min-content', disableFiltering: true, render: (pod) => { const cpu = getCpuUsage(pod); if (cpu === undefined) return; const { value: aValue, unit: aUnit } = unparseCpu(String(cpu)); const request = pod.spec.containers .map(c => parseCpu(c.resources?.requests?.cpu || '0')) .reduce((a, b) => a + b, 0); const limit = pod.spec.containers .map(c => parseCpu(c.resources?.limits?.cpu || '0')) .reduce((a, b) => a + b, 0); const tooltipLines = []; if (request > 0) { const { value: rValue, unit: rUnit } = unparseCpu(String(request)); const percentOfRequest = ((cpu / request) * 100).toFixed(1); tooltipLines.push(t('Request') + `: ${percentOfRequest}% (${aValue} ${aUnit}/${rValue} ${rUnit})`); } if (limit > 0) { const { value: lValue, unit: lUnit } = unparseCpu(String(limit)); const percentOfLimit = ((cpu / limit) * 100).toFixed(1); tooltipLines.push(t('Limit') + `: ${percentOfLimit}% (${aValue} ${aUnit}/${lValue} ${lUnit})`); } return (_jsxs(Box, { display: "flex", alignItems: "center", width: "100%", children: [_jsx("span", { style: { whiteSpace: 'nowrap' }, children: `${aValue} ${aUnit}` }), tooltipLines.length > 0 && (_jsx(Box, { component: "span", sx: { display: 'inline-flex', ml: 'auto' }, children: _jsx(TooltipIcon, { children: _jsx("span", { style: { whiteSpace: 'pre-line' }, children: tooltipLines.join('\n') }) }) }))] })); }, getValue: (pod) => getCpuUsage(pod) ?? 0, }, { id: 'memory', label: t('Memory'), gridTemplate: 'min-content', disableFiltering: true, render: (pod) => { const memory = getMemoryUsage(pod); if (memory === undefined) return; const { value: aValue, unit: aUnit } = unparseRam(memory); const request = pod.spec.containers .map(c => parseRam(c.resources?.requests?.memory || '0')) .reduce((a, b) => a + b, 0); const limit = pod.spec.containers .map(c => parseRam(c.resources?.limits?.memory || '0')) .reduce((a, b) => a + b, 0); const tooltipLines = []; if (request > 0) { const { value: rValue, unit: rUnit } = unparseRam(request); const percentOfRequest = ((memory / request) * 100).toFixed(1); tooltipLines.push(t('Request') + `: ${percentOfRequest}% (${aValue} ${aUnit}/${rValue} ${rUnit})`); } if (limit > 0) { const { value: lValue, unit: lUnit } = unparseRam(limit); const percentOfLimit = ((memory / limit) * 100).toFixed(1); tooltipLines.push(t('Limit') + `: ${percentOfLimit}% (${aValue} ${aUnit}/${lValue} ${lUnit})`); } return (_jsxs(Box, { display: "flex", alignItems: "center", width: "100%", children: [_jsx("span", { style: { whiteSpace: 'nowrap' }, children: `${aValue} ${aUnit}` }), tooltipLines.length > 0 && (_jsx(Box, { component: "span", sx: { display: 'inline-flex', ml: 'auto' }, children: _jsx(TooltipIcon, { children: _jsx("span", { style: { whiteSpace: 'pre-line' }, children: tooltipLines.join('\n') }) }) }))] })); }, getValue: (pod) => getMemoryUsage(pod) ?? 0, }, ] : []), { id: 'ip', gridTemplate: 'min-content', label: t('glossary|IP'), getValue: pod => pod.status?.podIP ?? '', }, { id: 'node', label: t('glossary|Node'), gridTemplate: 'auto', filterVariant: 'multi-select', getValue: pod => pod?.spec?.nodeName, render: pod => pod?.spec?.nodeName && (_jsx(Link, { routeName: "node", params: { name: pod.spec.nodeName }, activeCluster: pod.cluster, tooltip: true, children: pod.spec.nodeName })), }, { id: 'nominatedNode', label: t('glossary|Nominated Node'), getValue: pod => pod?.status?.nominatedNodeName, render: pod => !!pod?.status?.nominatedNodeName && (_jsx(Link, { routeName: "node", params: { name: pod?.status?.nominatedNodeName }, activeCluster: pod.cluster, tooltip: true, children: pod?.status?.nominatedNodeName })), show: false, }, { id: 'readinessGates', label: t('glossary|Readiness Gates'), getValue: pod => { const readinessGatesStatus = getReadinessGatesStatus(pod); const total = Object.keys(readinessGatesStatus).length; if (total === 0) { return ''; } const statusTrueCount = Object.values(readinessGatesStatus).filter(status => status === 'True').length; return statusTrueCount; }, render: pod => { const readinessGatesStatus = getReadinessGatesStatus(pod); const total = Object.keys(readinessGatesStatus).length; if (total === 0) { return null; } const statusTrueCount = Object.values(readinessGatesStatus).filter(status => status === 'True').length; return (_jsx(LightTooltip, { title: Object.keys(readinessGatesStatus) .map(conditionType => `${conditionType}: ${readinessGatesStatus[conditionType]}`) .join('\n'), interactive: true, children: _jsx("span", { children: `${statusTrueCount}/${total}` }) })); }, sort: (p1, p2) => { const readinessGatesStatus1 = getReadinessGatesStatus(p1); const readinessGatesStatus2 = getReadinessGatesStatus(p2); const total1 = Object.keys(readinessGatesStatus1).length; const total2 = Object.keys(readinessGatesStatus2).length; if (total1 !== total2) { return total1 - total2; } const statusTrueCount1 = Object.values(readinessGatesStatus1).filter(status => status === 'True').length; const statusTrueCount2 = Object.values(readinessGatesStatus2).filter(status => status === 'True').length; return statusTrueCount1 - statusTrueCount2; }, show: false, }, 'labels', 'age', ], data: pods, reflectInURL: reflectTableInURL, id: "headlamp-pods", enableRowActions: enableRowActions, enableRowSelection: enableRowSelection })); } export default function PodList() { const { items, errors } = Pod.useList({ namespace: useNamespaces() }); const { items: podMetrics } = PodMetrics.useList({ namespace: useNamespaces(), refetchInterval: METRIC_REFETCH_INTERVAL_MS, }); const dispatchHeadlampEvent = useEventCallback(HeadlampEventType.LIST_VIEW); React.useEffect(() => { dispatchHeadlampEvent({ resources: items ?? [], resourceKind: 'Pod', error: errors?.[0] || undefined, }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [items, errors]); return _jsx(PodListRenderer, { pods: items, errors: errors, metrics: podMetrics, reflectTableInURL: true }); }