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>
This commit is contained in:
DevContainer User
2026-03-04 11:50:29 +00:00
parent 034e0b9db8
commit 488bf90abc
14 changed files with 288 additions and 210 deletions
+8 -1
View File
@@ -3,7 +3,14 @@
"allow": [
"Bash(done)",
"Bash(npm install:*)",
"Bash(git add:*)"
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git push:*)",
"Bash(gh workflow:*)",
"Bash(gh run:*)",
"Bash(npm run:*)",
"Bash(npm ci:*)",
"Bash(npm test:*)"
]
},
"enabledMcpjsonServers": [
+7 -3
View File
@@ -116,9 +116,11 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode }
// Intel device plugins operator deployment
`/api/v1/pods?labelSelector=${encodeURIComponent('app=intel-gpu-plugin')}`,
// Alternative: by component label
`/api/v1/pods?labelSelector=${encodeURIComponent('app.kubernetes.io/name=intel-gpu-plugin')}`,
`/api/v1/pods?labelSelector=${encodeURIComponent(
'app.kubernetes.io/name=intel-gpu-plugin'
)}`,
// Intel device plugins from inteldeviceplugins-system namespace
`/api/v1/namespaces/inteldeviceplugins-system/pods`,
'/api/v1/namespaces/inteldeviceplugins-system/pods',
];
const foundPluginPods: IntelGpuPod[] = [];
@@ -155,7 +157,9 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode }
}
void fetchAsync();
return () => { cancelled = true; };
return () => {
cancelled = true;
};
}, [refreshKey]);
// ---------------------------------------------------------------------------
+4 -8
View File
@@ -12,18 +12,18 @@ import {
getNodeGpuCount,
getNodeGpuType,
getPodGpuRequests,
type GpuDevicePlugin,
INTEL_GPU_NODE_LABEL,
INTEL_GPU_RESOURCE,
INTEL_GPU_XE_RESOURCE,
type IntelGpuNode,
type IntelGpuPod,
isGpuRequestingPod,
isIntelGpuNode,
isKubeList,
isNodeReady,
pluginStatusText,
pluginStatusToStatus,
type GpuDevicePlugin,
type IntelGpuNode,
type IntelGpuPod,
} from './k8s';
// ---------------------------------------------------------------------------
@@ -413,11 +413,7 @@ describe('formatGpuType', () => {
// ---------------------------------------------------------------------------
describe('pluginStatusToStatus', () => {
function makePlugin(
desired: number,
ready: number,
unavailable = 0
): GpuDevicePlugin {
function makePlugin(desired: number, ready: number, unavailable = 0): GpuDevicePlugin {
return {
apiVersion: 'deviceplugin.intel.com/v1',
kind: 'GpuDevicePlugin',
+21 -28
View File
@@ -28,8 +28,7 @@ export const INTEL_DISCRETE_GPU_NODE_ROLE = 'node-role.kubernetes.io/gpu';
export const INTEL_INTEGRATED_GPU_NODE_ROLE = 'node-role.kubernetes.io/igpu';
/** Label selector for Intel GPU device plugin DaemonSet pods */
export const INTEL_GPU_PLUGIN_LABEL_SELECTOR =
'app=intel-gpu-plugin';
export const INTEL_GPU_PLUGIN_LABEL_SELECTOR = 'app=intel-gpu-plugin';
// ---------------------------------------------------------------------------
// Generic Kubernetes object base shapes
@@ -194,9 +193,12 @@ export function getNodeGpuType(node: IntelGpuNode): GpuType {
export function formatGpuType(type: GpuType): string {
switch (type) {
case 'discrete': return 'Discrete';
case 'integrated': return 'Integrated';
default: return 'Unknown';
case 'discrete':
return 'Discrete';
case 'integrated':
return 'Integrated';
default:
return 'Unknown';
}
}
@@ -272,9 +274,11 @@ export function isIntelGpuPluginPod(pod: unknown): pod is IntelGpuPod {
const meta = obj['metadata'] as Record<string, unknown> | undefined;
const labels = meta?.['labels'] as Record<string, string> | undefined;
if (!labels) return false;
return labels['app'] === 'intel-gpu-plugin' ||
(labels['app.kubernetes.io/name'] === 'intel-gpu-plugin') ||
(labels['component'] === 'intel-gpu-plugin');
return (
labels['app'] === 'intel-gpu-plugin' ||
labels['app.kubernetes.io/name'] === 'intel-gpu-plugin' ||
labels['component'] === 'intel-gpu-plugin'
);
}
export function filterIntelGpuPluginPods(items: unknown[]): IntelGpuPod[] {
@@ -284,10 +288,7 @@ export function filterIntelGpuPluginPods(items: unknown[]): IntelGpuPod[] {
/** Get total GPU requests from a pod's containers */
export function getPodGpuRequests(pod: IntelGpuPod): Record<string, string> {
const totals: Record<string, number> = {};
const allContainers = [
...(pod.spec?.containers ?? []),
...(pod.spec?.initContainers ?? []),
];
const allContainers = [...(pod.spec?.containers ?? []), ...(pod.spec?.initContainers ?? [])];
for (const c of allContainers) {
const requests = c.resources?.requests ?? {};
for (const [key, value] of Object.entries(requests)) {
@@ -300,15 +301,11 @@ export function getPodGpuRequests(pod: IntelGpuPod): Record<string, string> {
}
export function isPodReady(pod: IntelGpuPod): boolean {
return (
pod.status?.conditions?.some(c => c.type === 'Ready' && c.status === 'True') ?? false
);
return pod.status?.conditions?.some(c => c.type === 'Ready' && c.status === 'True') ?? false;
}
export function getPodRestarts(pod: IntelGpuPod): number {
return (
pod.status?.containerStatuses?.reduce((sum, c) => sum + c.restartCount, 0) ?? 0
);
return pod.status?.containerStatuses?.reduce((sum, c) => sum + c.restartCount, 0) ?? 0;
}
// ---------------------------------------------------------------------------
@@ -330,9 +327,7 @@ export function isKubeList(value: unknown): value is KubeList<unknown> {
// ---------------------------------------------------------------------------
export function isNodeReady(node: IntelGpuNode): boolean {
return (
node.status?.conditions?.some(c => c.type === 'Ready' && c.status === 'True') ?? false
);
return node.status?.conditions?.some(c => c.type === 'Ready' && c.status === 'True') ?? false;
}
// ---------------------------------------------------------------------------
@@ -359,11 +354,11 @@ export function formatAge(timestamp: string | undefined): string {
export function formatGpuResourceName(resourceKey: string): string {
const name = resourceKey.replace(INTEL_GPU_RESOURCE_PREFIX, '');
const map: Record<string, string> = {
'i915': 'GPU (i915)',
'xe': 'GPU (Xe)',
'millicores': 'GPU Millicores',
i915: 'GPU (i915)',
xe: 'GPU (Xe)',
millicores: 'GPU Millicores',
'memory.max': 'GPU Memory (max)',
'tiles': 'GPU Tiles',
tiles: 'GPU Tiles',
};
return map[name] ?? name;
}
@@ -372,9 +367,7 @@ export function formatGpuResourceName(resourceKey: string): string {
// Status helpers
// ---------------------------------------------------------------------------
export function pluginStatusToStatus(
plugin: GpuDevicePlugin
): 'success' | 'warning' | 'error' {
export function pluginStatusToStatus(plugin: GpuDevicePlugin): 'success' | 'warning' | 'error' {
const desired = plugin.status?.desiredNumberScheduled ?? 0;
const ready = plugin.status?.numberReady ?? 0;
const unavailable = plugin.status?.numberUnavailable ?? 0;
+5 -6
View File
@@ -64,14 +64,11 @@ const PROMETHEUS_SERVICES = [
{ namespace: 'monitoring', service: 'prometheus', port: '9090' },
];
async function queryPrometheus(
query: string,
prometheusPath: string
): Promise<PrometheusResult[]> {
async function queryPrometheus(query: string, prometheusPath: string): Promise<PrometheusResult[]> {
const encoded = encodeURIComponent(query);
const path = `${prometheusPath}/api/v1/query?query=${encoded}`;
const raw = await ApiProxy.request(path, { method: 'GET' }) as PrometheusResponse;
const raw = (await ApiProxy.request(path, { method: 'GET' })) as PrometheusResponse;
if (raw?.status !== 'success') return [];
return raw.data?.result ?? [];
@@ -81,7 +78,9 @@ async function findPrometheusPath(): Promise<string | null> {
for (const { namespace, service, port } of PROMETHEUS_SERVICES) {
const basePath = `/api/v1/namespaces/${namespace}/services/${service}:${port}/proxy`;
try {
const raw = await ApiProxy.request(`${basePath}/api/v1/query?query=1`, { method: 'GET' }) as PrometheusResponse;
const raw = (await ApiProxy.request(`${basePath}/api/v1/query?query=1`, {
method: 'GET',
})) as PrometheusResponse;
if (raw?.status === 'success') return basePath;
} catch {
// try next
+29 -21
View File
@@ -18,8 +18,7 @@ import { useIntelGpuContext } from '../api/IntelGpuDataContext';
import { formatAge, isPodReady, pluginStatusText, pluginStatusToStatus } from '../api/k8s';
export default function DevicePluginsPage() {
const { devicePlugins, pluginPods, crdAvailable, loading, error, refresh } =
useIntelGpuContext();
const { devicePlugins, pluginPods, crdAvailable, loading, error, refresh } = useIntelGpuContext();
if (loading) {
return <Loader title="Loading device plugin data..." />;
@@ -27,7 +26,14 @@ export default function DevicePluginsPage() {
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px',
}}
>
<SectionHeader title="Intel GPU — Device Plugins" />
<button
onClick={refresh}
@@ -102,7 +108,10 @@ export default function DevicePluginsPage() {
)}
{devicePlugins.map(plugin => (
<SectionBox key={plugin.metadata.uid ?? plugin.metadata.name} title={`GpuDevicePlugin: ${plugin.metadata.name}`}>
<SectionBox
key={plugin.metadata.uid ?? plugin.metadata.name}
title={`GpuDevicePlugin: ${plugin.metadata.name}`}
>
<NameValueTable
rows={[
{
@@ -146,14 +155,14 @@ export default function DevicePluginsPage() {
value: String(plugin.status?.numberReady ?? '—'),
},
...(plugin.status?.numberUnavailable
? [{
name: 'Unavailable Nodes',
value: (
<StatusLabel status="error">
{plugin.status.numberUnavailable}
</StatusLabel>
),
}]
? [
{
name: 'Unavailable Nodes',
value: (
<StatusLabel status="error">{plugin.status.numberUnavailable}</StatusLabel>
),
},
]
: []),
{
name: 'Node Selector',
@@ -177,12 +186,12 @@ export default function DevicePluginsPage() {
<SectionBox title="Plugin Daemon Pods">
<SimpleTable
columns={[
{ label: 'Name', getter: (p) => p.metadata.name },
{ label: 'Namespace', getter: (p) => p.metadata.namespace ?? '—' },
{ label: 'Node', getter: (p) => p.spec?.nodeName ?? '—' },
{ label: 'Name', getter: p => p.metadata.name },
{ label: 'Namespace', getter: p => p.metadata.namespace ?? '—' },
{ label: 'Node', getter: p => p.spec?.nodeName ?? '—' },
{
label: 'Ready',
getter: (p) => (
getter: p => (
<StatusLabel status={isPodReady(p) ? 'success' : 'warning'}>
{isPodReady(p) ? 'Ready' : p.status?.phase ?? 'Unknown'}
</StatusLabel>
@@ -190,10 +199,9 @@ export default function DevicePluginsPage() {
},
{
label: 'Restarts',
getter: (p) => {
const restarts = p.status?.containerStatuses?.reduce(
(sum, c) => sum + c.restartCount, 0
) ?? 0;
getter: p => {
const restarts =
p.status?.containerStatuses?.reduce((sum, c) => sum + c.restartCount, 0) ?? 0;
return restarts > 0 ? (
<StatusLabel status="warning">{restarts}</StatusLabel>
) : (
@@ -201,7 +209,7 @@ export default function DevicePluginsPage() {
);
},
},
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
]}
data={pluginPods}
/>
+48 -20
View File
@@ -35,7 +35,13 @@ import {
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React, { useCallback, useEffect, useState } from 'react';
import { useIntelGpuContext } from '../api/IntelGpuDataContext';
import { fetchGpuMetrics, formatPercent, formatWatts, GpuChipMetrics, GpuMetrics } from '../api/metrics';
import {
fetchGpuMetrics,
formatPercent,
formatWatts,
GpuChipMetrics,
GpuMetrics,
} from '../api/metrics';
// ---------------------------------------------------------------------------
// Power bar
@@ -43,7 +49,8 @@ import { fetchGpuMetrics, formatPercent, formatWatts, GpuChipMetrics, GpuMetrics
function PowerBar({ watts, maxWatts }: { watts: number; maxWatts: number | null }) {
const pct = maxWatts && maxWatts > 0 ? Math.min(100, Math.round((watts / maxWatts) * 100)) : null;
const color = pct === null ? '#0071c5' : pct >= 90 ? '#d32f2f' : pct >= 70 ? '#f57c00' : '#0071c5';
const color =
pct === null ? '#0071c5' : pct >= 90 ? '#d32f2f' : pct >= 70 ? '#f57c00' : '#0071c5';
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
@@ -91,9 +98,12 @@ function GpuChipCard({ chip }: { chip: GpuChipMetrics }) {
{ name: 'GPU (PCI)', value: chip.chip },
{
name: 'Current Power',
value: chip.powerWatts !== null
? <PowerBar watts={chip.powerWatts} maxWatts={chip.powerMaxWatts} />
: <StatusLabel status="warning">No data needs 5m of scrape history</StatusLabel>,
value:
chip.powerWatts !== null ? (
<PowerBar watts={chip.powerWatts} maxWatts={chip.powerMaxWatts} />
) : (
<StatusLabel status="warning">No data needs 5m of scrape history</StatusLabel>
),
},
];
@@ -123,8 +133,9 @@ function MetricRequirements() {
<>
<StatusLabel status="success">Available discrete GPU nodes</StatusLabel>
<div style={{ marginTop: '4px', fontSize: '12px', color: '#666' }}>
Source: <code>node_hwmon_energy_joule_total</code> via node-exporter hwmon collector (enabled by default).
Requires the i915 kernel driver on the node. iGPU nodes do not expose hwmon sensors.
Source: <code>node_hwmon_energy_joule_total</code> via node-exporter hwmon
collector (enabled by default). Requires the i915 kernel driver on the node. iGPU
nodes do not expose hwmon sensors.
</div>
</>
),
@@ -136,8 +147,9 @@ function MetricRequirements() {
<StatusLabel status="error">Not available</StatusLabel>
<div style={{ marginTop: '4px', fontSize: '12px', color: '#666' }}>
i915 exposes <code>gt_*_freq_mhz</code> via DRM sysfs but node-exporter&apos;s{' '}
<code>--collector.drm</code> flag is AMD-only and does not read these files.
A custom exporter or textfile-collector sidecar writing these values would be required.
<code>--collector.drm</code> flag is AMD-only and does not read these files. A
custom exporter or textfile-collector sidecar writing these values would be
required.
</div>
</>
),
@@ -148,8 +160,8 @@ function MetricRequirements() {
<>
<StatusLabel status="error">Not available</StatusLabel>
<div style={{ marginTop: '4px', fontSize: '12px', color: '#666' }}>
No standard Prometheus collector exposes i915 engine busy percentage.
Would require intel-gpu-top, XPU Manager, or a custom DRM-based exporter.
No standard Prometheus collector exposes i915 engine busy percentage. Would
require intel-gpu-top, XPU Manager, or a custom DRM-based exporter.
</div>
</>
),
@@ -160,8 +172,8 @@ function MetricRequirements() {
<>
<StatusLabel status="error">No metrics available</StatusLabel>
<div style={{ marginTop: '4px', fontSize: '12px', color: '#666' }}>
The integrated GPU driver does not expose hwmon sensors. No Prometheus metrics
are available for iGPU nodes regardless of configuration.
The integrated GPU driver does not expose hwmon sensors. No Prometheus metrics are
available for iGPU nodes regardless of configuration.
</div>
</>
),
@@ -190,7 +202,9 @@ export default function MetricsPage() {
const result = await fetchGpuMetrics();
setMetrics(result);
if (!result) {
setFetchError('Could not reach Prometheus. Ensure kube-prometheus-stack is installed in the monitoring namespace.');
setFetchError(
'Could not reach Prometheus. Ensure kube-prometheus-stack is installed in the monitoring namespace.'
);
}
} catch (e: unknown) {
setFetchError(e instanceof Error ? e.message : String(e));
@@ -211,7 +225,14 @@ export default function MetricsPage() {
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px',
}}
>
<SectionHeader title="Intel GPU — Metrics" />
<button
onClick={() => void doFetch()}
@@ -246,7 +267,8 @@ export default function MetricsPage() {
},
{
name: 'Checked services',
value: 'kube-prometheus-stack-prometheus:9090, prometheus-operated:9090, prometheus:9090 (monitoring namespace)',
value:
'kube-prometheus-stack-prometheus:9090, prometheus-operated:9090, prometheus:9090 (monitoring namespace)',
},
]}
/>
@@ -261,17 +283,22 @@ export default function MetricsPage() {
name: 'Status',
value: (
<StatusLabel status="warning">
Prometheus reachable no node_hwmon_chip_names&#123;chip_name=&quot;i915&quot;&#125; found
Prometheus reachable no
node_hwmon_chip_names&#123;chip_name=&quot;i915&quot;&#125; found
</StatusLabel>
),
},
{
name: 'GPU Nodes',
value: gpuNodes.length > 0 ? gpuNodes.map(n => n.metadata.name).join(', ') : 'None detected',
value:
gpuNodes.length > 0
? gpuNodes.map(n => n.metadata.name).join(', ')
: 'None detected',
},
{
name: 'Likely cause',
value: 'node-exporter is not running on the GPU nodes, or the hwmon collector is disabled.',
value:
'node-exporter is not running on the GPU nodes, or the hwmon collector is disabled.',
},
]}
/>
@@ -301,7 +328,8 @@ export default function MetricsPage() {
},
{
name: 'Query',
value: 'rate(node_hwmon_energy_joule_total[5m]) joined with node_hwmon_chip_names{chip_name="i915"}',
value:
'rate(node_hwmon_energy_joule_total[5m]) joined with node_hwmon_chip_names{chip_name="i915"}',
},
]}
/>
+9 -15
View File
@@ -19,10 +19,8 @@ import {
getGpuResources,
getNodeGpuType,
INTEL_GPU_RESOURCE,
INTEL_GPU_RESOURCE_PREFIX,
INTEL_GPU_XE_RESOURCE,
isIntelGpuNode,
isNodeReady,
} from '../api/k8s';
interface NodeDetailSectionProps {
@@ -40,9 +38,7 @@ export default function NodeDetailSection({ resource }: NodeDetailSectionProps)
// Extract the raw Kubernetes JSON — Headlamp KubeObject wraps it in jsonData
const rawNode =
resource.jsonData && typeof resource.jsonData === 'object'
? resource.jsonData
: resource;
resource.jsonData && typeof resource.jsonData === 'object' ? resource.jsonData : resource;
// Only render for Node resources that have Intel GPU
if (!isIntelGpuNode(rawNode)) return null;
@@ -63,9 +59,7 @@ export default function NodeDetailSection({ resource }: NodeDetailSectionProps)
const gpuType = getNodeGpuType(node as any);
// Find GPU pods scheduled on this node
const podsOnNode = loading
? []
: gpuPods.filter(p => p.spec?.nodeName === nodeName);
const podsOnNode = loading ? [] : gpuPods.filter(p => p.spec?.nodeName === nodeName);
if (Object.keys(capacity).length === 0 && Object.keys(allocatable).length === 0) {
return null;
@@ -81,18 +75,18 @@ export default function NodeDetailSection({ resource }: NodeDetailSectionProps)
}
}
for (const pod of podsOnNode.filter(p => p.status?.phase === 'Running')) {
const reqs = pod.spec?.containers?.flatMap(c =>
Object.entries(c.resources?.requests ?? {}).filter(([k]) =>
k === INTEL_GPU_RESOURCE || k === INTEL_GPU_XE_RESOURCE
)
) ?? [];
const reqs =
pod.spec?.containers?.flatMap(c =>
Object.entries(c.resources?.requests ?? {}).filter(
([k]) => k === INTEL_GPU_RESOURCE || k === INTEL_GPU_XE_RESOURCE
)
) ?? [];
for (const [, val] of reqs) {
gpuInUse += parseInt(val, 10) || 0;
}
}
const utilizationPct =
gpuAllocatable > 0 ? Math.round((gpuInUse / gpuAllocatable) * 100) : 0;
const utilizationPct = gpuAllocatable > 0 ? Math.round((gpuInUse / gpuAllocatable) * 100) : 0;
const utilizationStatus: 'success' | 'warning' | 'error' =
utilizationPct >= 90 ? 'error' : utilizationPct >= 70 ? 'warning' : 'success';
+19 -22
View File
@@ -23,7 +23,6 @@ import {
getNodeGpuCount,
getNodeGpuType,
INTEL_GPU_RESOURCE,
INTEL_GPU_RESOURCE_PREFIX,
INTEL_GPU_XE_RESOURCE,
IntelGpuNode,
isNodeReady,
@@ -33,13 +32,7 @@ import {
// GPU allocation bar component
// ---------------------------------------------------------------------------
function GpuAllocationBar({
used,
allocatable,
}: {
used: number;
allocatable: number;
}) {
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';
@@ -105,21 +98,18 @@ function NodeDetailCard({
name: 'GPU Type',
value: formatGpuType(gpuType),
},
...(gpuCount > 0
? [{ name: 'GPU Devices (i915/xe)', value: String(gpuCount) }]
: []),
...(gpuCount > 0 ? [{ name: 'GPU Devices (i915/xe)', value: String(gpuCount) }] : []),
...Object.entries(capacityResources).map(([key, cap]) => {
const alloc = parseInt(allocatableResources[key] ?? '0', 10);
const total = parseInt(cap, 10);
return {
name: `${formatGpuResourceName(key)} (capacity)`,
value: String(total),
};
}),
...Object.entries(allocatableResources).map(([key, alloc]) => {
...Object.entries(allocatableResources).map(([key, value]) => {
return {
name: `${formatGpuResourceName(key)} (allocatable)`,
value: alloc ?? '0',
value: value ?? '0',
};
}),
{
@@ -200,7 +190,14 @@ export default function NodesPage() {
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px',
}}
>
<SectionHeader title="Intel GPU — Nodes" />
<button
onClick={refresh}
@@ -256,28 +253,28 @@ export default function NodesPage() {
<SectionBox title="GPU Node Summary">
<SimpleTable
columns={[
{ label: 'Node', getter: (d) => d.node.metadata.name },
{ label: 'Node', getter: d => d.node.metadata.name },
{
label: 'Ready',
getter: (d) => (
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: 'GPU Type', getter: d => formatGpuType(d.gpuType) },
{ label: 'GPU Devices', getter: d => String(d.gpuCount || '—') },
{
label: 'Allocation',
getter: (d) => (
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) },
{ label: 'GPU Pods', getter: d => String(d.podsOnNode.length) },
{ label: 'Age', getter: d => formatAge(d.node.metadata.creationTimestamp) },
]}
data={tableData}
/>
+79 -38
View File
@@ -18,7 +18,6 @@ import React from 'react';
import { useIntelGpuContext } from '../api/IntelGpuDataContext';
import {
formatAge,
formatGpuType,
getNodeGpuCount,
getNodeGpuType,
getPodGpuRequests,
@@ -42,7 +41,8 @@ function gpuTypeChartData(
): Array<{ name: string; value: number; fill: string }> {
const data = [];
if (discreteCount > 0) data.push({ name: 'Discrete', value: discreteCount, fill: '#0071c5' });
if (integratedCount > 0) data.push({ name: 'Integrated', value: integratedCount, fill: '#60a4dc' });
if (integratedCount > 0)
data.push({ name: 'Integrated', value: integratedCount, fill: '#60a4dc' });
if (unknownCount > 0) data.push({ name: 'Unknown', value: unknownCount, fill: '#9e9e9e' });
return data;
}
@@ -113,9 +113,7 @@ export default function OverviewPage() {
}
const gpuUtilizationPct =
totalCapacityGpus > 0
? Math.round((totalAllocatedGpus / totalCapacityGpus) * 100)
: 0;
totalCapacityGpus > 0 ? Math.round((totalAllocatedGpus / totalCapacityGpus) * 100) : 0;
const chartData = gpuTypeChartData(discreteCount, integratedCount, unknownCount);
const totalGpuNodes = gpuNodes.length;
@@ -133,7 +131,14 @@ export default function OverviewPage() {
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px',
}}
>
<SectionHeader title="Intel GPU — Overview" />
<button
onClick={refresh}
@@ -218,26 +223,25 @@ export default function OverviewPage() {
<SectionBox title="Device Plugin Status">
<SimpleTable
columns={[
{ label: 'Name', getter: (p) => p.metadata.name },
{ label: 'Name', getter: p => p.metadata.name },
{
label: 'Status',
getter: (p) => (
<StatusLabel status={pluginStatusToStatus(p)}>
{pluginStatusText(p)}
</StatusLabel>
getter: p => (
<StatusLabel status={pluginStatusToStatus(p)}>{pluginStatusText(p)}</StatusLabel>
),
},
{
label: 'Monitoring',
getter: (p) => p.spec.enableMonitoring ? (
<StatusLabel status="success">Enabled</StatusLabel>
) : (
<StatusLabel status="warning">Disabled</StatusLabel>
),
getter: p =>
p.spec.enableMonitoring ? (
<StatusLabel status="success">Enabled</StatusLabel>
) : (
<StatusLabel status="warning">Disabled</StatusLabel>
),
},
{ label: 'Shared/Node', getter: (p) => String(p.spec.sharedDevNum ?? 1) },
{ label: 'Policy', getter: (p) => p.spec.preferredAllocationPolicy ?? '—' },
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
{ label: 'Shared/Node', getter: p => String(p.spec.sharedDevNum ?? 1) },
{ label: 'Policy', getter: p => p.spec.preferredAllocationPolicy ?? '—' },
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
]}
data={devicePlugins}
/>
@@ -249,18 +253,18 @@ export default function OverviewPage() {
<SectionBox title="Plugin Daemon Pods">
<SimpleTable
columns={[
{ label: 'Name', getter: (p) => p.metadata.name },
{ label: 'Namespace', getter: (p) => p.metadata.namespace ?? '—' },
{ label: 'Node', getter: (p) => p.spec?.nodeName ?? '—' },
{ label: 'Name', getter: p => p.metadata.name },
{ label: 'Namespace', getter: p => p.metadata.namespace ?? '—' },
{ label: 'Node', getter: p => p.spec?.nodeName ?? '—' },
{
label: 'Status',
getter: (p) => (
getter: p => (
<StatusLabel status={isPodReady(p) ? 'success' : 'warning'}>
{isPodReady(p) ? 'Ready' : p.status?.phase ?? 'Unknown'}
</StatusLabel>
),
},
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
]}
data={pluginPods}
/>
@@ -271,7 +275,13 @@ export default function OverviewPage() {
<SectionBox title="GPU Nodes">
{totalGpuNodes > 0 && chartData.length > 0 && (
<div style={{ marginBottom: '16px' }}>
<div style={{ marginBottom: '8px', fontSize: '14px', color: 'var(--mui-palette-text-secondary)' }}>
<div
style={{
marginBottom: '8px',
fontSize: '14px',
color: 'var(--mui-palette-text-secondary)',
}}
>
GPU Type Distribution
</div>
<PercentageBar data={chartData} total={totalGpuNodes} />
@@ -288,9 +298,15 @@ export default function OverviewPage() {
),
},
{ name: 'Ready Nodes', value: String(readyNodeCount) },
...(discreteCount > 0 ? [{ name: 'Discrete GPU Nodes', value: String(discreteCount) }] : []),
...(integratedCount > 0 ? [{ name: 'Integrated GPU Nodes', value: String(integratedCount) }] : []),
...(totalGpuCount > 0 ? [{ name: 'Total GPU Devices', value: String(totalGpuCount) }] : []),
...(discreteCount > 0
? [{ name: 'Discrete GPU Nodes', value: String(discreteCount) }]
: []),
...(integratedCount > 0
? [{ name: 'Integrated GPU Nodes', value: String(integratedCount) }]
: []),
...(totalGpuCount > 0
? [{ name: 'Total GPU Devices', value: String(totalGpuCount) }]
: []),
]}
/>
</SectionBox>
@@ -299,13 +315,23 @@ export default function OverviewPage() {
{totalCapacityGpus > 0 && (
<SectionBox title="GPU Allocation">
<div style={{ marginBottom: '16px' }}>
<div style={{ marginBottom: '8px', fontSize: '14px', color: 'var(--mui-palette-text-secondary)' }}>
<div
style={{
marginBottom: '8px',
fontSize: '14px',
color: 'var(--mui-palette-text-secondary)',
}}
>
GPU Utilization ({gpuUtilizationPct}%)
</div>
<PercentageBar
data={[
{ name: 'In Use', value: totalAllocatedGpus, fill: '#0071c5' },
{ name: 'Available', value: totalAllocatableGpus - totalAllocatedGpus, fill: '#e0e0e0' },
{
name: 'Available',
value: totalAllocatableGpus - totalAllocatedGpus,
fill: '#e0e0e0',
},
]}
total={totalAllocatableGpus}
/>
@@ -336,13 +362,28 @@ export default function OverviewPage() {
rows={[
{ name: 'Total GPU Pods', value: String(gpuPods.length) },
...(podPhaseCounts.Running > 0
? [{ name: 'Running', value: <StatusLabel status="success">{podPhaseCounts.Running}</StatusLabel> }]
? [
{
name: 'Running',
value: <StatusLabel status="success">{podPhaseCounts.Running}</StatusLabel>,
},
]
: []),
...(podPhaseCounts.Pending > 0
? [{ name: 'Pending', value: <StatusLabel status="warning">{podPhaseCounts.Pending}</StatusLabel> }]
? [
{
name: 'Pending',
value: <StatusLabel status="warning">{podPhaseCounts.Pending}</StatusLabel>,
},
]
: []),
...(podPhaseCounts.Failed > 0
? [{ name: 'Failed', value: <StatusLabel status="error">{podPhaseCounts.Failed}</StatusLabel> }]
? [
{
name: 'Failed',
value: <StatusLabel status="error">{podPhaseCounts.Failed}</StatusLabel>,
},
]
: []),
]}
/>
@@ -353,12 +394,12 @@ export default function OverviewPage() {
<SectionBox title="Active GPU Pods">
<SimpleTable
columns={[
{ label: 'Name', getter: (p) => p.metadata.name },
{ label: 'Namespace', getter: (p) => p.metadata.namespace ?? '—' },
{ label: 'Node', getter: (p) => p.spec?.nodeName ?? '—' },
{ label: 'Name', getter: p => p.metadata.name },
{ label: 'Namespace', getter: p => p.metadata.namespace ?? '—' },
{ label: 'Node', getter: p => p.spec?.nodeName ?? '—' },
{
label: 'GPU Request',
getter: (p) => {
getter: p => {
const reqs = getPodGpuRequests(p);
const parts: string[] = [];
for (const [key, val] of Object.entries(reqs)) {
@@ -368,7 +409,7 @@ export default function OverviewPage() {
return parts.join(', ') || '—';
},
},
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
]}
data={gpuPods.filter(p => p.status?.phase === 'Running').slice(0, 10)}
/>
+2 -6
View File
@@ -25,9 +25,7 @@ interface PodDetailSectionProps {
export default function PodDetailSection({ resource }: PodDetailSectionProps) {
// Extract raw Kubernetes JSON
const rawPod =
resource.jsonData && typeof resource.jsonData === 'object'
? resource.jsonData
: resource;
resource.jsonData && typeof resource.jsonData === 'object' ? resource.jsonData : resource;
// Only render for pods that request Intel GPU resources
if (!isGpuRequestingPod(rawPod)) return null;
@@ -98,9 +96,7 @@ export default function PodDetailSection({ resource }: PodDetailSectionProps) {
rows={[
{
name: 'Phase',
value: (
<StatusLabel status={phaseStatus}>{phase ?? 'Unknown'}</StatusLabel>
),
value: <StatusLabel status={phaseStatus}>{phase ?? 'Unknown'}</StatusLabel>,
},
{
name: 'Scheduled Node',
+55 -30
View File
@@ -17,11 +17,10 @@ import { useIntelGpuContext } from '../api/IntelGpuDataContext';
import {
formatAge,
formatGpuResourceName,
IntelGpuPod,
INTEL_GPU_RESOURCE_PREFIX,
isPodReady,
getPodGpuRequests,
getPodRestarts,
INTEL_GPU_RESOURCE_PREFIX,
IntelGpuPod,
} from '../api/k8s';
// ---------------------------------------------------------------------------
@@ -30,11 +29,16 @@ import {
function phaseToStatus(phase: string | undefined): 'success' | 'warning' | 'error' {
switch (phase) {
case 'Running': return 'success';
case 'Succeeded': return 'success';
case 'Pending': return 'warning';
case 'Failed': return 'error';
default: return 'warning';
case 'Running':
return 'success';
case 'Succeeded':
return 'success';
case 'Pending':
return 'warning';
case 'Failed':
return 'error';
default:
return 'warning';
}
}
@@ -98,13 +102,17 @@ export default function PodsPage() {
const running = gpuPods.filter(p => p.status?.phase === 'Running');
const pending = gpuPods.filter(p => p.status?.phase === 'Pending');
const failed = gpuPods.filter(p => p.status?.phase === 'Failed');
const other = gpuPods.filter(
p => !['Running', 'Pending', 'Failed'].includes(p.status?.phase ?? '')
);
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px',
}}
>
<SectionHeader title="Intel GPU — Pods" />
<button
onClick={refresh}
@@ -161,13 +169,28 @@ export default function PodsPage() {
rows={[
{ name: 'Total GPU Pods', value: String(gpuPods.length) },
...(running.length > 0
? [{ name: 'Running', value: <StatusLabel status="success">{running.length}</StatusLabel> }]
? [
{
name: 'Running',
value: <StatusLabel status="success">{running.length}</StatusLabel>,
},
]
: []),
...(pending.length > 0
? [{ name: 'Pending', value: <StatusLabel status="warning">{pending.length}</StatusLabel> }]
? [
{
name: 'Pending',
value: <StatusLabel status="warning">{pending.length}</StatusLabel>,
},
]
: []),
...(failed.length > 0
? [{ name: 'Failed', value: <StatusLabel status="error">{failed.length}</StatusLabel> }]
? [
{
name: 'Failed',
value: <StatusLabel status="error">{failed.length}</StatusLabel>,
},
]
: []),
]}
/>
@@ -179,12 +202,12 @@ export default function PodsPage() {
<SectionBox title="All GPU Pods">
<SimpleTable
columns={[
{ label: 'Name', getter: (p) => p.metadata.name },
{ label: 'Namespace', getter: (p) => p.metadata.namespace ?? '—' },
{ label: 'Node', getter: (p) => p.spec?.nodeName ?? '—' },
{ label: 'Name', getter: p => p.metadata.name },
{ label: 'Namespace', getter: p => p.metadata.namespace ?? '—' },
{ label: 'Node', getter: p => p.spec?.nodeName ?? '—' },
{
label: 'Phase',
getter: (p) => (
getter: p => (
<StatusLabel status={phaseToStatus(p.status?.phase)}>
{p.status?.phase ?? 'Unknown'}
</StatusLabel>
@@ -192,11 +215,11 @@ export default function PodsPage() {
},
{
label: 'GPU Resources',
getter: (p) => <GpuContainerList pod={p} />,
getter: p => <GpuContainerList pod={p} />,
},
{
label: 'Restarts',
getter: (p) => {
getter: p => {
const restarts = getPodRestarts(p);
return restarts > 0 ? (
<StatusLabel status="warning">{restarts}</StatusLabel>
@@ -205,7 +228,7 @@ export default function PodsPage() {
);
},
},
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
]}
data={gpuPods}
/>
@@ -217,25 +240,27 @@ export default function PodsPage() {
<SectionBox title="Attention: Pending GPU Pods">
<SimpleTable
columns={[
{ label: 'Name', getter: (p) => p.metadata.name },
{ label: 'Namespace', getter: (p) => p.metadata.namespace ?? '—' },
{ label: 'Name', getter: p => p.metadata.name },
{ label: 'Namespace', getter: p => p.metadata.namespace ?? '—' },
{
label: 'GPU Resources',
getter: (p) => {
getter: p => {
const reqs = getPodGpuRequests(p);
return Object.entries(reqs)
.map(([k, v]) => `${formatGpuResourceName(k)}: ${v}`)
.join(', ') || '—';
return (
Object.entries(reqs)
.map(([k, v]) => `${formatGpuResourceName(k)}: ${v}`)
.join(', ') || '—'
);
},
},
{
label: 'Waiting Reason',
getter: (p) => {
getter: p => {
const reason = p.status?.containerStatuses?.[0]?.state?.waiting?.reason;
return reason ?? '—';
},
},
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
]}
data={pending}
/>
+2 -11
View File
@@ -11,12 +11,7 @@
import { StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import {
formatGpuType,
getNodeGpuCount,
getNodeGpuType,
isIntelGpuNode,
} from '../../api/k8s';
import { formatGpuType, getNodeGpuCount, getNodeGpuType, isIntelGpuNode } from '../../api/k8s';
/** Build GPU columns to append to the native Nodes table. */
export function buildNodeGpuColumns() {
@@ -33,11 +28,7 @@ export function buildNodeGpuColumns() {
if (!isIntelGpuNode(raw)) return '—';
const node = raw as Parameters<typeof getNodeGpuType>[0];
const type = getNodeGpuType(node);
return (
<StatusLabel status="success">
{formatGpuType(type)}
</StatusLabel>
);
return <StatusLabel status="success">{formatGpuType(type)}</StatusLabel>;
},
},
{
-1
View File
@@ -180,4 +180,3 @@ registerResourceTableColumnsProcessor(({ id, columns }) => {
}
return columns;
});