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:
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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'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{chip_name="i915"} found
|
||||
Prometheus reachable — no
|
||||
node_hwmon_chip_names{chip_name="i915"} 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"}',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -180,4 +180,3 @@ registerResourceTableColumnsProcessor(({ id, columns }) => {
|
||||
}
|
||||
return columns;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user