488bf90abc
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>
295 lines
8.5 KiB
TypeScript
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}
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
}
|