Files
headlamp-intel-gpu-plugin/src/components/NodesPage.tsx
T
DevContainer User 488bf90abc fix: resolve eslint errors and apply formatting to match shared config
Auto-fix import ordering, quote style, and indentation via eslint --fix
and prettier --write. Remove unused variable in NodesPage and PodsPage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 11:50:29 +00:00

295 lines
8.5 KiB
TypeScript

/**
* NodesPage — lists all nodes with Intel GPU capabilities.
*
* Shows GPU type, device count, resource allocation, and pod assignments
* for each GPU-capable node in the cluster.
*/
import {
Loader,
NameValueTable,
SectionBox,
SectionHeader,
SimpleTable,
StatusLabel,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import { useIntelGpuContext } from '../api/IntelGpuDataContext';
import {
formatAge,
formatGpuResourceName,
formatGpuType,
getGpuResources,
getNodeGpuCount,
getNodeGpuType,
INTEL_GPU_RESOURCE,
INTEL_GPU_XE_RESOURCE,
IntelGpuNode,
isNodeReady,
} from '../api/k8s';
// ---------------------------------------------------------------------------
// GPU allocation bar component
// ---------------------------------------------------------------------------
function GpuAllocationBar({ used, allocatable }: { used: number; allocatable: number }) {
if (allocatable === 0) return <span></span>;
const pct = Math.min(100, Math.round((used / allocatable) * 100));
const color = pct >= 90 ? '#d32f2f' : pct >= 70 ? '#f57c00' : '#0071c5';
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div
style={{
width: '80px',
height: '8px',
backgroundColor: '#e0e0e0',
borderRadius: '4px',
overflow: 'hidden',
}}
>
<div
style={{
width: `${pct}%`,
height: '100%',
backgroundColor: color,
borderRadius: '4px',
}}
/>
</div>
<span style={{ fontSize: '12px' }}>{`${used}/${allocatable} (${pct}%)`}</span>
</div>
);
}
// ---------------------------------------------------------------------------
// Node detail card
// ---------------------------------------------------------------------------
function NodeDetailCard({
node,
podsByNode,
}: {
node: IntelGpuNode;
podsByNode: Map<string, string[]>;
}) {
const gpuType = getNodeGpuType(node);
const gpuCount = getNodeGpuCount(node);
const ready = isNodeReady(node);
const capacityResources = getGpuResources(node.status?.capacity);
const allocatableResources = getGpuResources(node.status?.allocatable);
const podsOnNode = podsByNode.get(node.metadata.name) ?? [];
return (
<SectionBox title={node.metadata.name}>
<NameValueTable
rows={[
{
name: 'Status',
value: (
<StatusLabel status={ready ? 'success' : 'error'}>
{ready ? 'Ready' : 'Not Ready'}
</StatusLabel>
),
},
{
name: 'GPU Type',
value: formatGpuType(gpuType),
},
...(gpuCount > 0 ? [{ name: 'GPU Devices (i915/xe)', value: String(gpuCount) }] : []),
...Object.entries(capacityResources).map(([key, cap]) => {
const total = parseInt(cap, 10);
return {
name: `${formatGpuResourceName(key)} (capacity)`,
value: String(total),
};
}),
...Object.entries(allocatableResources).map(([key, value]) => {
return {
name: `${formatGpuResourceName(key)} (allocatable)`,
value: value ?? '0',
};
}),
{
name: 'GPU Workload Pods',
value: podsOnNode.length > 0 ? podsOnNode.join(', ') : '—',
},
{
name: 'OS Image',
value: node.status?.nodeInfo?.osImage ?? '—',
},
{
name: 'Kernel',
value: node.status?.nodeInfo?.kernelVersion ?? '—',
},
{
name: 'Kubelet',
value: node.status?.nodeInfo?.kubeletVersion ?? '—',
},
{
name: 'Age',
value: formatAge(node.metadata.creationTimestamp),
},
]}
/>
</SectionBox>
);
}
// ---------------------------------------------------------------------------
// Main component
// ---------------------------------------------------------------------------
export default function NodesPage() {
const { gpuNodes, gpuPods, loading, error, refresh } = useIntelGpuContext();
if (loading) {
return <Loader title="Loading GPU node data..." />;
}
// Build map: nodeName → list of GPU pod names
const podsByNode = new Map<string, string[]>();
for (const pod of gpuPods) {
if (!pod.spec?.nodeName) continue;
const existing = podsByNode.get(pod.spec.nodeName) ?? [];
existing.push(pod.metadata.name);
podsByNode.set(pod.spec.nodeName, existing);
}
// Build table data for summary
const tableData = gpuNodes.map(node => {
const gpuType = getNodeGpuType(node);
const gpuCount = getNodeGpuCount(node);
const ready = isNodeReady(node);
const capacity = node.status?.capacity ?? {};
const allocatable = node.status?.allocatable ?? {};
let totalCapacity = 0;
let totalAllocatable = 0;
for (const key of Object.keys(capacity)) {
if (key === INTEL_GPU_RESOURCE || key === INTEL_GPU_XE_RESOURCE) {
totalCapacity += parseInt(capacity[key] ?? '0', 10);
totalAllocatable += parseInt(allocatable[key] ?? '0', 10);
}
}
const podsOnNode = podsByNode.get(node.metadata.name) ?? [];
return {
node,
gpuType,
gpuCount,
ready,
totalCapacity,
totalAllocatable,
podsOnNode,
};
});
return (
<>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px',
}}
>
<SectionHeader title="Intel GPU — Nodes" />
<button
onClick={refresh}
aria-label="Refresh node data"
style={{
padding: '6px 16px',
backgroundColor: 'transparent',
color: 'var(--mui-palette-primary-main, #0071c5)',
border: '1px solid var(--mui-palette-primary-main, #0071c5)',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '13px',
fontWeight: 500,
}}
>
Refresh
</button>
</div>
{error && (
<SectionBox title="Error">
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]}
/>
</SectionBox>
)}
{gpuNodes.length === 0 && (
<SectionBox title="No GPU Nodes Found">
<NameValueTable
rows={[
{
name: 'Status',
value: (
<StatusLabel status="warning">
No nodes with Intel GPU resources or labels were found
</StatusLabel>
),
},
{
name: 'Note',
value:
'Nodes appear here when they have gpu.intel.com/* resources or Intel GPU node labels. ' +
'Ensure the Intel GPU device plugin and Node Feature Discovery are installed.',
},
]}
/>
</SectionBox>
)}
{/* Summary table */}
{gpuNodes.length > 0 && (
<SectionBox title="GPU Node Summary">
<SimpleTable
columns={[
{ label: 'Node', getter: d => d.node.metadata.name },
{
label: 'Ready',
getter: d => (
<StatusLabel status={d.ready ? 'success' : 'error'}>
{d.ready ? 'Ready' : 'Not Ready'}
</StatusLabel>
),
},
{ label: 'GPU Type', getter: d => formatGpuType(d.gpuType) },
{ label: 'GPU Devices', getter: d => String(d.gpuCount || '—') },
{
label: 'Allocation',
getter: d => (
<GpuAllocationBar
used={d.podsOnNode.length}
allocatable={d.totalAllocatable || d.gpuCount}
/>
),
},
{ label: 'GPU Pods', getter: d => String(d.podsOnNode.length) },
{ label: 'Age', getter: d => formatAge(d.node.metadata.creationTimestamp) },
]}
data={tableData}
/>
</SectionBox>
)}
{/* Per-node detail cards */}
{gpuNodes.map(node => (
<NodeDetailCard
key={node.metadata.uid ?? node.metadata.name}
node={node}
podsByNode={podsByNode}
/>
))}
</>
);
}