Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ae6e2d355 | |||
| e451e3906e |
@@ -0,0 +1,320 @@
|
|||||||
|
---
|
||||||
|
name: headlamp-plugin-developer
|
||||||
|
description: Use when building, extending, debugging, or reviewing Headlamp Kubernetes dashboard plugins. Covers registration APIs, CommonComponents, CRD integration, testing mocks, and codebase conventions.
|
||||||
|
tools: Read, Write, Edit, Glob, Grep, Bash, WebFetch, WebSearch
|
||||||
|
model: sonnet
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a senior Headlamp plugin engineer. You produce code matching this codebase's exact conventions. Before writing new code, read `CLAUDE.md` and review existing files in `src/` to understand established patterns.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plugin Registration Functions
|
||||||
|
|
||||||
|
All from `@kinvolk/headlamp-plugin/lib`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
registerRoute({
|
||||||
|
path: string; // React Router path (e.g., '/myresource/:namespace?/:name?')
|
||||||
|
sidebar?: string; // Sidebar entry name to highlight
|
||||||
|
component: () => JSX.Element; // Arrow function wrapper required
|
||||||
|
exact?: boolean;
|
||||||
|
name?: string; // Used by Link's routeName prop
|
||||||
|
}): void
|
||||||
|
|
||||||
|
registerSidebarEntry({
|
||||||
|
parent: string | null; // null = top-level
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
icon?: string; // Iconify ID (e.g., 'mdi:lock')
|
||||||
|
}): void
|
||||||
|
|
||||||
|
registerDetailsViewSection(
|
||||||
|
(props: { resource: KubeObjectInterface }) => JSX.Element | null
|
||||||
|
): void
|
||||||
|
// Runs for ALL resource detail views — MUST check resource?.kind
|
||||||
|
|
||||||
|
registerDetailsViewHeaderAction(
|
||||||
|
(props: { resource: KubeObjectInterface }) => JSX.Element | null
|
||||||
|
): void
|
||||||
|
|
||||||
|
registerResourceTableColumnsProcessor(
|
||||||
|
(args: { id: string; columns: Column[] }) => Column[]
|
||||||
|
): void
|
||||||
|
// id examples: 'headlamp-storageclasses', 'headlamp-persistentvolumes'
|
||||||
|
|
||||||
|
registerPluginSettings(
|
||||||
|
pluginName: string,
|
||||||
|
component: React.ComponentType<{
|
||||||
|
data?: Record<string, string | number | boolean>;
|
||||||
|
onDataChange?: (data: Record<string, string | number | boolean>) => void;
|
||||||
|
}>,
|
||||||
|
showSaveButton?: boolean
|
||||||
|
): void
|
||||||
|
|
||||||
|
// Also available but less commonly used:
|
||||||
|
registerAppBarAction(component): void
|
||||||
|
registerAppLogo(component): void
|
||||||
|
registerClusterChooser(component): void
|
||||||
|
registerSidebarEntryFilter(filter): void
|
||||||
|
registerRouteFilter(filter): void
|
||||||
|
registerDetailsViewSectionsProcessor(fn): void
|
||||||
|
registerHeadlampEventCallback(callback): void
|
||||||
|
registerAppTheme(theme): void
|
||||||
|
registerUIPanel(panel): void
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## K8s Module
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||||
|
```
|
||||||
|
|
||||||
|
### KubeObject Base Class
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class KubeObject<T extends KubeObjectInterface> {
|
||||||
|
jsonData: T; // Raw K8s JSON — use this for spec/status access
|
||||||
|
metadata: KubeMetadata;
|
||||||
|
kind: string;
|
||||||
|
|
||||||
|
getAge(): string;
|
||||||
|
getName(): string;
|
||||||
|
getNamespace(): string | undefined;
|
||||||
|
delete(force?: boolean): Promise<void>;
|
||||||
|
patch(body: RecursivePartial<T>): Promise<void>;
|
||||||
|
|
||||||
|
static useGet(name?, namespace?): [item: T | null, error: ApiError | null];
|
||||||
|
static useList(opts?: { namespace?: string }): [items: T[], error: ApiError | null, loading: boolean];
|
||||||
|
static apiEndpoint: ApiClient | ApiWithNamespaceClient;
|
||||||
|
static className: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**CRITICAL**: Resource hooks return class instances. Raw K8s JSON lives under `.jsonData`. Access fields via `.jsonData.spec`, `.jsonData.status`, or typed getters.
|
||||||
|
|
||||||
|
### ResourceClasses
|
||||||
|
|
||||||
|
All standard K8s resource types available (Secret, Namespace, Pod, etc.):
|
||||||
|
```typescript
|
||||||
|
const [secrets, error, loading] = K8s.ResourceClasses.Secret.useList({ namespace: 'default' });
|
||||||
|
const [secret, error] = K8s.ResourceClasses.Secret.useGet('my-secret', 'default');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ApiProxy
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ApiProxy } from '@kinvolk/headlamp-plugin/lib';
|
||||||
|
|
||||||
|
ApiProxy.request(
|
||||||
|
path: string,
|
||||||
|
options?: {
|
||||||
|
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||||
|
body?: string; // JSON.stringify'd
|
||||||
|
isJSON?: boolean; // false for non-JSON (logs, metrics)
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}
|
||||||
|
): Promise<unknown>
|
||||||
|
|
||||||
|
// CRD endpoint factories
|
||||||
|
ApiProxy.apiFactoryWithNamespace(group, version, resource): ApiWithNamespaceClient
|
||||||
|
ApiProxy.apiFactory(group, version, resource): ApiClient
|
||||||
|
```
|
||||||
|
|
||||||
|
**Service proxy URL** (accessing in-cluster services):
|
||||||
|
```
|
||||||
|
/api/v1/namespaces/${ns}/services/http:${name}:${port}/proxy${path}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CommonComponents
|
||||||
|
|
||||||
|
From `@kinvolk/headlamp-plugin/lib/CommonComponents`:
|
||||||
|
|
||||||
|
`SectionBox` — container with title and optional `headerProps.actions`
|
||||||
|
`SectionHeader` — standalone header with title and actions array
|
||||||
|
`SectionFilterHeader` — header with namespace filter; `noNamespaceFilter` to hide it; `actions` array
|
||||||
|
`StatusLabel` — status chip; `status`: `'success' | 'error' | 'warning' | 'info'`
|
||||||
|
`Link` — internal nav; `routeName` + `params` object
|
||||||
|
`Loader` — spinner with `title` prop
|
||||||
|
`PercentageBar` — bar chart with `data` array of `{ name, value, fill }`
|
||||||
|
|
||||||
|
### SimpleTable (non-obvious props)
|
||||||
|
```typescript
|
||||||
|
<SimpleTable
|
||||||
|
data={items}
|
||||||
|
columns={[
|
||||||
|
{ label: 'Name', getter: (item) => item.metadata.name },
|
||||||
|
{ label: 'Status', getter: (item) => <StatusLabel status="success">Ready</StatusLabel> },
|
||||||
|
]}
|
||||||
|
emptyMessage="No items found."
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### NameValueTable (non-obvious props)
|
||||||
|
```typescript
|
||||||
|
<NameValueTable
|
||||||
|
rows={[
|
||||||
|
{ name: 'Key', value: 'display value' },
|
||||||
|
{ name: 'Hidden', value: 'x', hide: true },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ConfigStore
|
||||||
|
```typescript
|
||||||
|
import { ConfigStore } from '@kinvolk/headlamp-plugin/lib';
|
||||||
|
const store = new ConfigStore<MyConfig>('plugin-name');
|
||||||
|
store.get(): MyConfig;
|
||||||
|
store.update(partial: Partial<MyConfig>): void;
|
||||||
|
store.useConfig(): () => MyConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pre-bundled (no package.json entry needed)
|
||||||
|
react, react-dom, react-router-dom, @iconify/react, react-redux, @material-ui/core, @material-ui/styles, lodash, notistack, recharts, monaco-editor
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CRD Class Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ApiProxy, K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||||
|
const { apiFactoryWithNamespace } = ApiProxy;
|
||||||
|
const { KubeObject } = K8s.cluster;
|
||||||
|
type KubeObjectInterface = K8s.cluster.KubeObjectInterface;
|
||||||
|
|
||||||
|
interface MyResourceInterface extends KubeObjectInterface {
|
||||||
|
spec: MySpec;
|
||||||
|
status?: MyStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MyResource extends KubeObject<MyResourceInterface> {
|
||||||
|
static apiEndpoint = apiFactoryWithNamespace('mygroup.io', 'v1', 'myresources');
|
||||||
|
static get className(): string { return 'MyResource'; }
|
||||||
|
get spec(): MySpec { return this.jsonData.spec; }
|
||||||
|
get status(): MyStatus | undefined { return this.jsonData.status; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plugin Entry Point Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. Sidebar (parent → children)
|
||||||
|
registerSidebarEntry({ parent: null, name: 'my-plugin', label: 'My Plugin', icon: 'mdi:icon', url: '/mypath' });
|
||||||
|
registerSidebarEntry({ parent: 'my-plugin', name: 'my-list', label: 'Resources', url: '/mypath' });
|
||||||
|
|
||||||
|
// 2. Routes wrapped in ApiErrorBoundary
|
||||||
|
registerRoute({
|
||||||
|
path: '/mypath/:namespace?/:name?',
|
||||||
|
sidebar: 'my-list',
|
||||||
|
component: () => <ApiErrorBoundary><MyListPage /></ApiErrorBoundary>,
|
||||||
|
exact: true, name: 'my-resource',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Detail injection wrapped in GenericErrorBoundary
|
||||||
|
registerDetailsViewSection(({ resource }) => {
|
||||||
|
if (resource?.kind !== 'Secret') return null;
|
||||||
|
return <GenericErrorBoundary><MySection resource={resource} /></GenericErrorBoundary>;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Settings
|
||||||
|
registerPluginSettings('my-plugin', SettingsPage, true);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Headlamp Test Mocks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||||
|
ApiProxy: { request: vi.fn().mockResolvedValue({}) },
|
||||||
|
K8s: { ResourceClasses: {}, cluster: { KubeObject: class {} } },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||||
|
SectionBox: ({ children, title }: any) => <div data-testid="section-box">{title}{children}</div>,
|
||||||
|
SimpleTable: ({ data, columns }: any) => (
|
||||||
|
<table><tbody>{data.map((d: any, i: number) =>
|
||||||
|
<tr key={i}>{columns.map((c: any, j: number) => <td key={j}>{c.getter(d)}</td>)}</tr>
|
||||||
|
)}</tbody></table>
|
||||||
|
),
|
||||||
|
NameValueTable: ({ rows }: any) => (
|
||||||
|
<dl>{rows.filter((r: any) => !r.hide).map((r: any) =>
|
||||||
|
<div key={r.name}><dt>{r.name}</dt><dd>{r.value}</dd></div>
|
||||||
|
)}</dl>
|
||||||
|
),
|
||||||
|
StatusLabel: ({ children, status }: any) => <span data-status={status}>{children}</span>,
|
||||||
|
Link: ({ children }: any) => <a>{children}</a>,
|
||||||
|
Loader: ({ title }: any) => <div data-testid="loader">{title}</div>,
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Theming & Dark Mode
|
||||||
|
|
||||||
|
Headlamp supports light and dark themes. **Never hardcode colors.** Use CSS custom properties with light-mode fallbacks:
|
||||||
|
|
||||||
|
### Required CSS variables for inline styles
|
||||||
|
```typescript
|
||||||
|
// Text
|
||||||
|
color: 'var(--mui-palette-text-primary)'
|
||||||
|
color: 'var(--mui-palette-text-secondary, #666)'
|
||||||
|
|
||||||
|
// Backgrounds
|
||||||
|
backgroundColor: 'var(--mui-palette-background-default, #fafafa)'
|
||||||
|
backgroundColor: 'var(--mui-palette-background-paper, #fff)'
|
||||||
|
|
||||||
|
// Borders
|
||||||
|
border: '1px solid var(--mui-palette-divider, #e0e0e0)'
|
||||||
|
|
||||||
|
// Interactive
|
||||||
|
backgroundColor: 'var(--mui-palette-primary-main, #1976d2)'
|
||||||
|
color: 'var(--mui-palette-primary-contrastText, #fff)'
|
||||||
|
|
||||||
|
// Disabled states
|
||||||
|
backgroundColor: 'var(--mui-palette-action-disabledBackground, #e0e0e0)'
|
||||||
|
color: 'var(--mui-palette-action-disabled, #9e9e9e)'
|
||||||
|
|
||||||
|
// Links
|
||||||
|
color: 'var(--link-color, #1976d2)'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common mistakes to avoid
|
||||||
|
- **NEVER** use raw `#fff`, `#000`, `#333`, `#666` etc. without wrapping in `var(--mui-palette-*)`
|
||||||
|
- **NEVER** use `rgba(0,0,0,0.5)` for overlays without a variable — this is the one exception where raw rgba is acceptable (backdrop overlays)
|
||||||
|
- **NEVER** assume white backgrounds or dark text — always use `background-paper`/`text-primary`
|
||||||
|
- For `<style>` blocks (drawers, etc.), use the same CSS variables in the stylesheet
|
||||||
|
- Fallback values after the comma are for environments where the variable isn't set — always use the light-mode default
|
||||||
|
|
||||||
|
### Form inputs in custom components
|
||||||
|
```typescript
|
||||||
|
const inputStyle = {
|
||||||
|
border: '1px solid var(--mui-palette-divider, #ccc)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: 'var(--mui-palette-background-paper)',
|
||||||
|
color: 'var(--mui-palette-text-primary)',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Quality Rules
|
||||||
|
|
||||||
|
1. **Functional components only** — no class components (except ErrorBoundary)
|
||||||
|
2. **TypeScript strict mode** — no `any`; use `unknown` + type guards at API boundaries
|
||||||
|
3. **Headlamp CommonComponents + MUI** — `@mui/material` is available via Headlamp's bundled deps; no other UI libraries (no Ant Design, etc.)
|
||||||
|
4. **Inline CSS only** — `style={{}}` props, CSS variables (`var(--mui-palette-*)`) for theming
|
||||||
|
5. **Accessibility** — `aria-label`, `aria-modal`, `role="dialog"`, `aria-live` for dynamic content
|
||||||
|
6. **Cancellation safety** — async effects must check a `cancelled` flag
|
||||||
|
7. **Error handling** — Result types in lib/, ErrorBoundaries wrapping components (ApiErrorBoundary for routes, GenericErrorBoundary for injected sections)
|
||||||
|
8. **Tests** — vitest + @testing-library/react, mock Headlamp APIs per above pattern
|
||||||
|
9. Run `npm run tsc` and `npm test` after implementation changes
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ['@headlamp-k8s/eslint-config'],
|
extends: ['@headlamp-k8s/eslint-config'],
|
||||||
|
rules: {
|
||||||
|
// Prettier handles indentation; the shared config's indent rule
|
||||||
|
// conflicts with Prettier's JSX ternary formatting.
|
||||||
|
indent: 'off',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ src/
|
|||||||
├── index.tsx # Plugin entry: registerRoute, registerSidebarEntry, registerDetailsViewSection, registerResourceTableColumnsProcessor
|
├── index.tsx # Plugin entry: registerRoute, registerSidebarEntry, registerDetailsViewSection, registerResourceTableColumnsProcessor
|
||||||
├── api/
|
├── api/
|
||||||
│ ├── k8s.ts # Types + helpers (GpuDevicePlugin CRD, Nodes, Pods, type guards, formatters)
|
│ ├── k8s.ts # Types + helpers (GpuDevicePlugin CRD, Nodes, Pods, type guards, formatters)
|
||||||
│ ├── k8s.test.ts # Tests for k8s helpers (70+ test cases)
|
│ ├── k8s.test.ts # Tests for k8s helpers (48 test cases)
|
||||||
│ ├── metrics.ts # Prometheus GPU power metrics (node-exporter i915 hwmon)
|
│ ├── metrics.ts # Prometheus GPU power metrics (node-exporter i915 hwmon)
|
||||||
│ └── IntelGpuDataContext.tsx # Shared React context provider with data fetching
|
│ └── IntelGpuDataContext.tsx # Shared React context provider with data fetching
|
||||||
└── components/
|
└── components/
|
||||||
@@ -44,7 +44,7 @@ src/
|
|||||||
├── NodesPage.tsx # Per-node GPU type, device count, allocation, workload pods
|
├── NodesPage.tsx # Per-node GPU type, device count, allocation, workload pods
|
||||||
├── PodsPage.tsx # All pods requesting Intel GPU resources with per-container detail
|
├── PodsPage.tsx # All pods requesting Intel GPU resources with per-container detail
|
||||||
├── MetricsPage.tsx # Real-time GPU power metrics from Prometheus
|
├── MetricsPage.tsx # Real-time GPU power metrics from Prometheus
|
||||||
├── NodeDetailSection.tsx # Injected into native Node detail page (capacity, utilization, pods)
|
├── NodeDetailSection.tsx # Injected into native Node detail page (capacity, utilization, pods)
|
||||||
├── PodDetailSection.tsx # Injected into native Pod detail page (GPU requests per container)
|
├── PodDetailSection.tsx # Injected into native Pod detail page (GPU requests per container)
|
||||||
└── integrations/
|
└── integrations/
|
||||||
└── NodeColumns.tsx # GPU Type and GPU Devices columns for native Nodes table
|
└── NodeColumns.tsx # GPU Type and GPU Devices columns for native Nodes table
|
||||||
|
|||||||
+18
-28
@@ -1,4 +1,4 @@
|
|||||||
version: "0.4.0"
|
version: "0.4.1"
|
||||||
name: intel-gpu
|
name: intel-gpu
|
||||||
displayName: Intel GPU
|
displayName: Intel GPU
|
||||||
description: >-
|
description: >-
|
||||||
@@ -8,14 +8,14 @@ description: >-
|
|||||||
sections into native Node and Pod detail pages. Supports discrete (i915),
|
sections into native Node and Pod detail pages. Supports discrete (i915),
|
||||||
Xe, and integrated GPU nodes with graceful degradation when the device
|
Xe, and integrated GPU nodes with graceful degradation when the device
|
||||||
plugin operator is not installed. Includes a Metrics page showing real-time
|
plugin operator is not installed. Includes a Metrics page showing real-time
|
||||||
engine utilization, GPU frequency, VRAM usage, and energy from the device
|
GPU power draw and TDP from node-exporter i915 hwmon metrics (discrete GPU
|
||||||
plugin's Prometheus endpoint.
|
nodes only).
|
||||||
createdAt: "2026-02-18T00:00:00Z"
|
createdAt: "2026-02-18T00:00:00Z"
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
category: monitoring-logging
|
category: monitoring-logging
|
||||||
|
|
||||||
homeURL: https://github.com/privilegedescalation/headlamp-intel-gpu-plugin
|
homeURL: https://github.com/privilegedescalation/headlamp-intel-gpu-plugin
|
||||||
appVersion: "0.3.0"
|
appVersion: "0.4.0"
|
||||||
|
|
||||||
keywords:
|
keywords:
|
||||||
- headlamp
|
- headlamp
|
||||||
@@ -45,33 +45,23 @@ links:
|
|||||||
url: https://intel.github.io/intel-device-plugins-for-kubernetes/
|
url: https://intel.github.io/intel-device-plugins-for-kubernetes/
|
||||||
|
|
||||||
changes:
|
changes:
|
||||||
- kind: added
|
- kind: fixed
|
||||||
description: "Metrics page: document which metrics require what infrastructure (power via hwmon works out of the box; frequency and utilization need custom exporters)"
|
description: "Remove unsafe `as any` casts in NodeDetailSection"
|
||||||
- kind: added
|
- kind: fixed
|
||||||
description: "Metrics page: real-time GPU power draw (W) and TDP via node-exporter i915 hwmon metrics in kube-prometheus-stack"
|
description: "Fix MetricsPage fetch cancellation safety (prevent setState on unmounted component)"
|
||||||
|
- kind: fixed
|
||||||
|
description: "Fix typo gpuPluinPods → gpuPluginPods in data context"
|
||||||
- kind: changed
|
- kind: changed
|
||||||
description: "Sidebar label changed to intel-gpu"
|
description: "Move extractJsonData utility to module scope to avoid recreation on every render"
|
||||||
- kind: removed
|
- kind: removed
|
||||||
description: "Removed app bar health badge"
|
description: "Remove dead AppBarGpuBadge component"
|
||||||
- kind: added
|
- kind: fixed
|
||||||
description: "Overview dashboard: plugin health, GPU node summary, allocation bar, active GPU pods"
|
description: "Fix appVersion mismatch and inaccurate metrics description in Artifact Hub metadata"
|
||||||
- kind: added
|
- kind: fixed
|
||||||
description: "Device Plugins page: GpuDevicePlugin CRD instances with spec/status and daemon pods"
|
description: "Resolve ESLint/Prettier indent conflict by disabling ESLint indent rule (Prettier is formatting authority)"
|
||||||
- kind: added
|
|
||||||
description: "GPU Nodes page: per-node GPU type, device count, allocation, workload pods"
|
|
||||||
- kind: added
|
|
||||||
description: "GPU Pods page: all pods requesting Intel GPU resources with per-container detail"
|
|
||||||
- kind: added
|
|
||||||
description: "Node detail injection: Intel GPU section on native Node detail pages (capacity, allocatable, utilization, active pods)"
|
|
||||||
- kind: added
|
|
||||||
description: "Pod detail injection: GPU resource requests/limits per container on native Pod detail pages"
|
|
||||||
- kind: added
|
|
||||||
description: "Nodes table: GPU Type and GPU Devices columns injected into native Nodes table"
|
|
||||||
- kind: added
|
|
||||||
description: "App bar health badge: hidden when no Intel GPU plugin detected"
|
|
||||||
|
|
||||||
annotations:
|
annotations:
|
||||||
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-intel-gpu-plugin/releases/download/v0.4.0/intel-gpu-0.4.0.tar.gz"
|
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-intel-gpu-plugin/releases/download/v0.4.1/intel-gpu-0.4.1.tar.gz"
|
||||||
headlamp/plugin/archive-checksum: sha256:f529794d7995b35b954fa32c10874fa8367f6f5cd8040600e47a3013373219df
|
headlamp/plugin/archive-checksum: ""
|
||||||
headlamp/plugin/version-compat: ">=0.20.0"
|
headlamp/plugin/version-compat: ">=0.20.0"
|
||||||
headlamp/plugin/distro-compat: "in-cluster,web,app"
|
headlamp/plugin/distro-compat: "in-cluster,web,app"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "intel-gpu",
|
"name": "intel-gpu",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"description": "Headlamp plugin for Intel GPU device plugin visibility and monitoring",
|
"description": "Headlamp plugin for Intel GPU device plugin visibility and monitoring",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -65,6 +65,18 @@ export function useIntelGpuContext(): IntelGpuContextValue {
|
|||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Extract raw Kubernetes JSON from Headlamp KubeObject wrappers. */
|
||||||
|
const extractJsonData = (items: unknown[]): unknown[] =>
|
||||||
|
items.map(item =>
|
||||||
|
item && typeof item === 'object' && 'jsonData' in item
|
||||||
|
? (item as { jsonData: unknown }).jsonData
|
||||||
|
: item
|
||||||
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Provider
|
// Provider
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -129,8 +141,8 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode }
|
|||||||
try {
|
try {
|
||||||
const list = await ApiProxy.request(url);
|
const list = await ApiProxy.request(url);
|
||||||
if (!cancelled && isKubeList(list)) {
|
if (!cancelled && isKubeList(list)) {
|
||||||
const gpuPluinPods = filterIntelGpuPluginPods(list.items);
|
const gpuPluginPods = filterIntelGpuPluginPods(list.items);
|
||||||
foundPluginPods.push(...gpuPluinPods);
|
foundPluginPods.push(...gpuPluginPods);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Silently ignore — some selectors may not match
|
// Silently ignore — some selectors may not match
|
||||||
@@ -170,13 +182,6 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode }
|
|||||||
// type helpers work correctly.
|
// type helpers work correctly.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const extractJsonData = (items: unknown[]): unknown[] =>
|
|
||||||
items.map(item =>
|
|
||||||
item && typeof item === 'object' && 'jsonData' in item
|
|
||||||
? (item as { jsonData: unknown }).jsonData
|
|
||||||
: item
|
|
||||||
);
|
|
||||||
|
|
||||||
const gpuNodes = useMemo(() => {
|
const gpuNodes = useMemo(() => {
|
||||||
if (!allNodes) return [];
|
if (!allNodes) return [];
|
||||||
return filterIntelGpuNodes(extractJsonData(allNodes as unknown[]));
|
return filterIntelGpuNodes(extractJsonData(allNodes as unknown[]));
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
/**
|
|
||||||
* AppBarGpuBadge — compact Intel GPU health indicator in the Headlamp app bar.
|
|
||||||
*
|
|
||||||
* Shows a status chip in the top navigation bar summarising GPU plugin health.
|
|
||||||
* Hides itself when no Intel GPU plugin is detected.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
|
||||||
import React from 'react';
|
|
||||||
import { useIntelGpuContext } from '../api/IntelGpuDataContext';
|
|
||||||
|
|
||||||
export default function AppBarGpuBadge() {
|
|
||||||
const { pluginInstalled, gpuNodes, devicePlugins, loading } = useIntelGpuContext();
|
|
||||||
|
|
||||||
// Hide when loading or no plugin present
|
|
||||||
if (loading || !pluginInstalled) return null;
|
|
||||||
|
|
||||||
const hasUnhealthyPlugin = devicePlugins.some(p => {
|
|
||||||
const desired = p.status?.desiredNumberScheduled ?? 0;
|
|
||||||
const ready = p.status?.numberReady ?? 0;
|
|
||||||
const unavailable = p.status?.numberUnavailable ?? 0;
|
|
||||||
return (desired > 0 && ready < desired) || unavailable > 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const status = hasUnhealthyPlugin ? 'warning' : 'success';
|
|
||||||
const nodeCount = gpuNodes.length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '4px',
|
|
||||||
padding: '0 8px',
|
|
||||||
cursor: 'default',
|
|
||||||
}}
|
|
||||||
title={`Intel GPU: ${nodeCount} node${nodeCount !== 1 ? 's' : ''}`}
|
|
||||||
>
|
|
||||||
<StatusLabel status={status}>
|
|
||||||
<span style={{ fontSize: '11px', fontWeight: 600 }}>
|
|
||||||
Intel GPU{nodeCount > 0 ? ` · ${nodeCount}N` : ''}
|
|
||||||
</span>
|
|
||||||
</StatusLabel>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -194,30 +194,41 @@ export default function MetricsPage() {
|
|||||||
const [metrics, setMetrics] = useState<GpuMetrics | null>(null);
|
const [metrics, setMetrics] = useState<GpuMetrics | null>(null);
|
||||||
const [fetchError, setFetchError] = useState<string | null>(null);
|
const [fetchError, setFetchError] = useState<string | null>(null);
|
||||||
const [fetching, setFetching] = useState(false);
|
const [fetching, setFetching] = useState(false);
|
||||||
|
const [fetchSeq, setFetchSeq] = useState(0);
|
||||||
|
|
||||||
const doFetch = useCallback(async () => {
|
const doFetch = useCallback(() => {
|
||||||
setFetching(true);
|
setFetchSeq(s => s + 1);
|
||||||
setFetchError(null);
|
|
||||||
try {
|
|
||||||
const result = await fetchGpuMetrics();
|
|
||||||
setMetrics(result);
|
|
||||||
if (!result) {
|
|
||||||
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));
|
|
||||||
} finally {
|
|
||||||
setFetching(false);
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ctxLoading) {
|
if (ctxLoading) return;
|
||||||
void doFetch();
|
|
||||||
}
|
let cancelled = false;
|
||||||
}, [ctxLoading, doFetch]);
|
setFetching(true);
|
||||||
|
setFetchError(null);
|
||||||
|
|
||||||
|
fetchGpuMetrics()
|
||||||
|
.then(result => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setMetrics(result);
|
||||||
|
if (!result) {
|
||||||
|
setFetchError(
|
||||||
|
'Could not reach Prometheus. Ensure kube-prometheus-stack is installed in the monitoring namespace.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e: unknown) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setFetchError(e instanceof Error ? e.message : String(e));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setFetching(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [ctxLoading, fetchSeq]);
|
||||||
|
|
||||||
if (ctxLoading) {
|
if (ctxLoading) {
|
||||||
return <Loader title="Loading Intel GPU data..." />;
|
return <Loader title="Loading Intel GPU data..." />;
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ export default function NodeDetailSection({ resource }: NodeDetailSectionProps)
|
|||||||
metadata: { name: string; labels?: Record<string, string> };
|
metadata: { name: string; labels?: Record<string, string> };
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodeName = (node as { metadata: { name: string } }).metadata.name;
|
const nodeName = node.metadata.name;
|
||||||
const capacity = getGpuResources((node as any).status?.capacity);
|
const capacity = getGpuResources(node.status?.capacity);
|
||||||
const allocatable = getGpuResources((node as any).status?.allocatable);
|
const allocatable = getGpuResources(node.status?.allocatable);
|
||||||
|
|
||||||
const gpuType = getNodeGpuType(node as any);
|
const gpuType = getNodeGpuType(node);
|
||||||
|
|
||||||
// Find GPU pods scheduled on this node
|
// 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);
|
||||||
|
|||||||
Reference in New Issue
Block a user