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