Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b3234bddc7 | |||
| fb639f4736 | |||
| b99be4f461 | |||
| 7508058f84 | |||
| c65d792a01 | |||
| aff63c4541 | |||
| 2c117eff9f | |||
| 32d825e441 | |||
| c7920b5b8e | |||
| c99e235caa | |||
| 85c839bc19 | |||
| 00c29e36dd | |||
| 823e590513 | |||
| 3cc0094842 | |||
| 161d817e6c | |||
| 375f43265d | |||
| b81f25ad74 | |||
| ca430b8b03 | |||
| e139999f20 | |||
| d4ac2b2f23 | |||
| 15320dbcba | |||
| 82ad1faa33 | |||
| 547f743016 | |||
| aceb06f2e5 | |||
| fcb72d344c | |||
| 673949f361 | |||
| eed5724d5f | |||
| 0c7e096231 | |||
| 796ec48ad1 | |||
| fc592e9e38 | |||
| 6057c81402 | |||
| f547348ef7 | |||
| cd55d1bbba | |||
| 4cace284a4 | |||
| 46821c747c | |||
| e3c17c9380 | |||
| fbd8e27a56 | |||
| e0ebd38653 | |||
| 6d889494c4 | |||
| 6cd159b5a4 | |||
| 8ec38cb247 | |||
| e77f075521 | |||
| 60d76f1cb2 | |||
| 0d72d07048 | |||
| daad91880c | |||
| b9137958f0 | |||
| 37a2232178 | |||
| 56eb0761dd | |||
| 18c6a03c0c | |||
| cbd86f696d | |||
| 510affbe1a | |||
| fcb2e5f9fd | |||
| a34802b477 | |||
| e5e681b415 |
+202
-4
@@ -2,12 +2,210 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: ['**']
|
||||
pull_request:
|
||||
branches: [main]
|
||||
branches: [main, dev, uat]
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
uses: privilegedescalation/.github/.github/workflows/plugin-ci.yaml@main
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
container: node:22-slim
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Python
|
||||
run: apt-get update && apt-get install -y --no-install-recommends python3 python3-yaml
|
||||
|
||||
- name: Validate artifacthub-pkg.yml
|
||||
run: |
|
||||
python3 - <<'EOF'
|
||||
import sys, re
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print("::warning::PyYAML not available, skipping artifacthub-pkg.yml validation")
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
with open("artifacthub-pkg.yml") as f:
|
||||
pkg = yaml.safe_load(f)
|
||||
except FileNotFoundError:
|
||||
print("::error::artifacthub-pkg.yml not found")
|
||||
sys.exit(1)
|
||||
except yaml.YAMLError as e:
|
||||
print(f"::error::artifacthub-pkg.yml is invalid YAML: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
errors = []
|
||||
|
||||
for field in ["version", "name", "description", "homeURL"]:
|
||||
if not pkg.get(field):
|
||||
errors.append(f"Missing required field: {field}")
|
||||
|
||||
version = pkg.get("version", "")
|
||||
if version and not re.match(r'^\d+\.\d+\.\d+$', str(version)):
|
||||
errors.append(f"version '{version}' is not SemVer (expected X.Y.Z)")
|
||||
|
||||
annotations = pkg.get("annotations", {}) or {}
|
||||
archive_url = annotations.get("headlamp/plugin/archive-url", "")
|
||||
archive_checksum = annotations.get("headlamp/plugin/archive-checksum", "")
|
||||
|
||||
if not archive_url:
|
||||
errors.append("Missing annotation: headlamp/plugin/archive-url")
|
||||
if not archive_checksum:
|
||||
errors.append("Missing annotation: headlamp/plugin/archive-checksum")
|
||||
elif not re.match(r'^sha256:[0-9a-f]{64}$', str(archive_checksum)):
|
||||
errors.append(f"archive-checksum has unexpected format: '{archive_checksum}' (expected sha256:<64 hex chars>)")
|
||||
|
||||
if errors:
|
||||
for e in errors:
|
||||
print(f"::error::{e}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"artifacthub-pkg.yml valid: name={pkg['name']} version={pkg['version']}")
|
||||
EOF
|
||||
|
||||
- name: Detect package manager
|
||||
id: pkg-manager
|
||||
run: |
|
||||
if [ -f "pnpm-lock.yaml" ]; then
|
||||
echo "manager=pnpm" >> $GITHUB_OUTPUT
|
||||
PM=$(python3 -c "import json,sys; d=json.load(open('package.json')); print('true' if d.get('packageManager','').startswith('pnpm@') else 'false')" 2>/dev/null || echo "false")
|
||||
echo "has_package_manager=$PM" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "manager=npm" >> $GITHUB_OUTPUT
|
||||
echo "has_package_manager=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: ${{ steps.pkg-manager.outputs.manager == 'npm' && 'npm' || '' }}
|
||||
|
||||
- name: Setup pnpm (via Corepack, reads version from packageManager field)
|
||||
if: steps.pkg-manager.outputs.manager == 'pnpm' && steps.pkg-manager.outputs.has_package_manager == 'true'
|
||||
run: |
|
||||
npm install -g corepack
|
||||
corepack enable pnpm
|
||||
corepack install
|
||||
|
||||
- name: Setup pnpm (version latest)
|
||||
if: steps.pkg-manager.outputs.manager == 'pnpm' && steps.pkg-manager.outputs.has_package_manager == 'false'
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
run_install: false
|
||||
version: latest
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-store
|
||||
if: steps.pkg-manager.outputs.manager == 'pnpm'
|
||||
run: echo "dir=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache pnpm store
|
||||
if: steps.pkg-manager.outputs.manager == 'pnpm'
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.dir }}
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
|
||||
- name: Validate pnpm lockfile freshness
|
||||
if: steps.pkg-manager.outputs.manager == 'pnpm'
|
||||
run: |
|
||||
if [ ! -f "pnpm-lock.yaml" ]; then
|
||||
echo "No pnpm-lock.yaml found, skipping lockfile freshness check"
|
||||
exit 0
|
||||
fi
|
||||
if ! grep -q 'overrides:' pnpm-lock.yaml 2>/dev/null; then
|
||||
echo "No overrides section in pnpm-lock.yaml, skipping lockfile freshness check"
|
||||
exit 0
|
||||
fi
|
||||
echo "Detected pnpm-lock.yaml with overrides section. Checking lockfile freshness..."
|
||||
ERR_FILE=$(mktemp)
|
||||
if pnpm install --frozen-lockfile 2>&1 | tee "$ERR_FILE"; then
|
||||
echo "Lockfile is fresh."
|
||||
else
|
||||
if grep -q "CONFIG_MISMATCH\|EBADLOCKFILE\|ERR_PNPM_LOCKFILE" "$ERR_FILE"; then
|
||||
echo ""
|
||||
echo "::error::pnpm-lock.yaml is out of sync with package.json overrides."
|
||||
echo "::error::Run 'pnpm install' to regenerate the lockfile and commit the updated pnpm-lock.yaml."
|
||||
rm -f "$ERR_FILE"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$ERR_FILE"
|
||||
echo "::warning::Install failed with a different error. Will retry in the Install dependencies step."
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
max_attempts=3
|
||||
attempt=1
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
echo "Attempt $attempt of $max_attempts"
|
||||
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
|
||||
pnpm install --frozen-lockfile && break
|
||||
else
|
||||
npm ci && break
|
||||
fi
|
||||
if [ $attempt -lt $max_attempts ]; then
|
||||
echo "::warning::Install step failed on attempt $attempt. Retrying in 5 seconds..."
|
||||
sleep 5
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
if [ $attempt -gt $max_attempts ]; then
|
||||
echo "::error::Install step failed after $max_attempts attempts."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build plugin
|
||||
run: npx @kinvolk/headlamp-plugin build
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
|
||||
pnpm run lint
|
||||
else
|
||||
npm run lint
|
||||
fi
|
||||
|
||||
- name: Type-check
|
||||
run: |
|
||||
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
|
||||
pnpm run tsc
|
||||
else
|
||||
npm run tsc
|
||||
fi
|
||||
|
||||
- name: Format check
|
||||
run: |
|
||||
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
|
||||
pnpm run format:check
|
||||
else
|
||||
npm run format:check
|
||||
fi
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
|
||||
pnpm test
|
||||
else
|
||||
npm test
|
||||
fi
|
||||
|
||||
- name: Security audit
|
||||
run: |
|
||||
if [ "${{ steps.pkg-manager.outputs.manager }}" = "pnpm" ]; then
|
||||
npx audit-ci --pnpm --audit-level=high --config ./audit-ci.jsonc
|
||||
else
|
||||
npx audit-ci --npm --audit-level=high --config ./audit-ci.jsonc
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Promotion Gate
|
||||
|
||||
# Calls the shared promotion gate workflow.
|
||||
# dev PRs: no gate (engineer self-merges).
|
||||
# uat PRs: QA approval required.
|
||||
# main PRs: UAT approval required (uat→main promotions).
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted, dismissed]
|
||||
pull_request:
|
||||
branches: [uat, main]
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
promotion-gate:
|
||||
uses: privilegedescalation/.github/.github/workflows/dual-approval-check.yaml@main
|
||||
secrets: inherit
|
||||
with:
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
|
||||
@@ -10,10 +10,14 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: privilegedescalation/.github/.github/workflows/plugin-release.yaml@main
|
||||
secrets:
|
||||
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
|
||||
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
||||
with:
|
||||
version: ${{ inputs.version }}
|
||||
upstream-repo: 'intel/intel-device-plugins-for-kubernetes'
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.tar.gz
|
||||
.playwright-mcp/
|
||||
|
||||
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
Headlamp plugin for Intel GPU device plugin visibility and monitoring. Read-only — monitors GpuDevicePlugin CRDs, GPU-capable nodes, pods requesting Intel GPU resources, and real-time power metrics via Prometheus. No cluster write operations.
|
||||
|
||||
- **Plugin name**: `headlamp-intel-gpu`
|
||||
- **Plugin name**: `intel-gpu`
|
||||
- **Target**: Headlamp >= v0.20.0
|
||||
- **Data sources**: GpuDevicePlugin CRDs (`deviceplugin.intel.com/v1`), Nodes, Pods (all namespaces), Prometheus (node-exporter i915 hwmon)
|
||||
- **Reference plugin**: `../headlamp-kube-vip-plugin`
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Installation Policy
|
||||
|
||||
## Approved Installation Method
|
||||
|
||||
**The ONLY approved method for installing this plugin is via [Artifact Hub](https://artifacthub.io/) using the Headlamp plugin installer.**
|
||||
|
||||
No other installation method is acceptable. This includes but is not limited to:
|
||||
|
||||
- Direct installation from GitHub release assets
|
||||
- Manual npm pack / tarball extraction
|
||||
- initContainer workarounds that bypass Artifact Hub
|
||||
- Direct file copy or sidecar injection
|
||||
|
||||
## Enforcement
|
||||
|
||||
All deployment configurations, CI/CD pipelines, and documentation MUST reference Artifact Hub as the sole plugin distribution channel. Any pull request that introduces an alternative installation method will be rejected.
|
||||
|
||||
## Rationale
|
||||
|
||||
Artifact Hub provides verified checksums, consistent versioning, and a standard discovery mechanism for the CNCF ecosystem. Bypassing it introduces security and integrity risks.
|
||||
|
||||
---
|
||||
|
||||
*This policy is set by the CTO and approved by the CEO of Privileged Escalation.*
|
||||
@@ -18,29 +18,7 @@ A [Headlamp](https://headlamp.dev/) plugin providing visibility into [Intel GPU
|
||||
|
||||
## Installation
|
||||
|
||||
### Plugin Manager (Headlamp UI)
|
||||
|
||||
Search for `headlamp-intel-gpu` in the Headlamp Plugin Manager.
|
||||
|
||||
### Manual
|
||||
|
||||
```bash
|
||||
# Download the latest release tarball
|
||||
curl -LO https://github.com/privilegedescalation/headlamp-intel-gpu-plugin/releases/latest/download/headlamp-intel-gpu-*.tar.gz
|
||||
|
||||
# Extract to Headlamp plugins directory
|
||||
mkdir -p ~/.config/Headlamp/plugins
|
||||
tar -xzf headlamp-intel-gpu-*.tar.gz -C ~/.config/Headlamp/plugins/
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/privilegedescalation/headlamp-intel-gpu-plugin.git
|
||||
cd headlamp-intel-gpu-plugin
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
Search for `headlamp-intel-gpu` in the Headlamp Plugin Manager (Settings → Plugins → Catalog).
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
+41
-3
@@ -1,4 +1,4 @@
|
||||
version: "0.4.2"
|
||||
version: "1.1.0"
|
||||
name: headlamp-intel-gpu
|
||||
displayName: Intel GPU
|
||||
description: >-
|
||||
@@ -17,6 +17,36 @@ category: monitoring-logging
|
||||
homeURL: https://github.com/privilegedescalation/headlamp-intel-gpu-plugin
|
||||
appVersion: "0.35.0"
|
||||
|
||||
install: |
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. [Headlamp](https://headlamp.dev) v0.20.0 or later
|
||||
2. [Intel Device Plugins for Kubernetes](https://intel.github.io/intel-device-plugins-for-kubernetes/) operator installed in your cluster (required for GPU node discovery and CRD visibility)
|
||||
|
||||
### Install via Headlamp Plugin Catalog
|
||||
|
||||
1. Open Headlamp and navigate to **Settings → Plugin Catalog**
|
||||
2. Search for **"Intel GPU"**
|
||||
3. Click **Install** and restart Headlamp when prompted
|
||||
|
||||
The plugin is sourced directly from [ArtifactHub](https://artifacthub.io/packages/headlamp/headlamp/headlamp-intel-gpu).
|
||||
|
||||
## Usage
|
||||
|
||||
After installation, the Intel GPU plugin adds:
|
||||
- An **Overview** page showing cluster-level GPU counts, type distribution (discrete/integrated/Xe/unknown), and pod allocation summary
|
||||
- A **Nodes** page with per-node GPU capacity, allocatable counts, and allocation bars
|
||||
- A **Pods** page listing GPU-requesting pods grouped by phase (Running/Pending/Failed)
|
||||
- A **Device Plugins** page showing GpuDevicePlugin CRD status
|
||||
- A **Metrics** page with real-time power draw and TDP from i915 hwmon metrics (discrete GPU nodes only)
|
||||
- Injected GPU sections on native **Node** and **Pod** detail pages
|
||||
|
||||
The plugin degrades gracefully when the Intel Device Plugins operator is not installed.
|
||||
|
||||
For more information, see the [README](https://github.com/privilegedescalation/headlamp-intel-gpu-plugin/blob/main/README.md).
|
||||
|
||||
keywords:
|
||||
- headlamp
|
||||
- kubernetes
|
||||
@@ -60,8 +90,16 @@ changes:
|
||||
- kind: fixed
|
||||
description: "Resolve ESLint/Prettier indent conflict by disabling ESLint indent rule (Prettier is formatting authority)"
|
||||
|
||||
screenshots:
|
||||
- title: Overview — cluster GPU summary, operator status, and active workloads
|
||||
url: https://raw.githubusercontent.com/privilegedescalation/headlamp-intel-gpu-plugin/main/docs/screenshots/01-overview.svg
|
||||
- title: GPU Nodes — per-node GPU type, capacity, and allocation bars
|
||||
url: https://raw.githubusercontent.com/privilegedescalation/headlamp-intel-gpu-plugin/main/docs/screenshots/02-nodes.svg
|
||||
- title: Metrics — real-time GPU power draw and TDP utilization (discrete GPUs)
|
||||
url: https://raw.githubusercontent.com/privilegedescalation/headlamp-intel-gpu-plugin/main/docs/screenshots/03-metrics.svg
|
||||
|
||||
annotations:
|
||||
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-intel-gpu-plugin/releases/download/v0.4.2/headlamp-intel-gpu-0.4.2.tar.gz"
|
||||
headlamp/plugin/archive-checksum: sha256:0713b099a79ed63ea30675fee96f2a70e37471507d4135b529df158a09960492
|
||||
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-intel-gpu-plugin/releases/download/v1.1.0/intel-gpu-1.1.0.tar.gz"
|
||||
headlamp/plugin/archive-checksum: sha256:e212381f38c331383604b06f6552997fcba5c8b42a3bd828e3b43ed3e5028448
|
||||
headlamp/plugin/version-compat: ">=0.20.0"
|
||||
headlamp/plugin/distro-compat: "in-cluster,web,app"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
// Allowlist for inherited dev-dependency CVEs from @kinvolk/headlamp-plugin
|
||||
// CTO decision (PRI-854): these high-severity vulns are dev/build-time only,
|
||||
// trace to @kinvolk/headlamp-plugin transitive deps (Picomatch, Vite, lodash),
|
||||
// and do NOT ship in production plugin artifacts.
|
||||
"allowlist": [
|
||||
{
|
||||
"id": "GHSA-hhpm-516h-p3p6",
|
||||
"reason": "Picomatch ReDoS: devDependency only, does not ship in production plugin bundle"
|
||||
},
|
||||
{
|
||||
"id": "GHSA-36xf-7xpp-53w5",
|
||||
"reason": "Vite arbitrary file read: devDependency only, does not ship in production plugin bundle"
|
||||
},
|
||||
{
|
||||
"id": "GHSA-jf8v-p3pp-93qh",
|
||||
"reason": "lodash code injection via _.template: devDependency only, does not ship in production plugin bundle"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="750" viewBox="0 0 1200 750" font-family="Inter, Segoe UI, Arial, sans-serif">
|
||||
<!-- Background -->
|
||||
<rect width="1200" height="750" fill="#0f1117"/>
|
||||
|
||||
<!-- Top nav bar -->
|
||||
<rect width="1200" height="48" fill="#1a1d27"/>
|
||||
<text x="16" y="30" font-size="18" font-weight="700" fill="#ffffff">⚡ Headlamp</text>
|
||||
<text x="120" y="30" font-size="14" fill="#8b8fa8">Intel GPU</text>
|
||||
<text x="200" y="30" font-size="14" fill="#8b8fa8">/</text>
|
||||
<text x="214" y="30" font-size="14" fill="#a78bfa">Overview</text>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<rect x="0" y="48" width="200" height="702" fill="#13151f"/>
|
||||
<rect x="0" y="90" width="200" height="36" fill="#a78bfa22"/>
|
||||
<text x="16" y="113" font-size="13" fill="#a78bfa" font-weight="600">Overview</text>
|
||||
<text x="16" y="153" font-size="13" fill="#8b8fa8">Device Plugins</text>
|
||||
<text x="16" y="193" font-size="13" fill="#8b8fa8">GPU Nodes</text>
|
||||
<text x="16" y="233" font-size="13" fill="#8b8fa8">GPU Pods</text>
|
||||
<text x="16" y="273" font-size="13" fill="#8b8fa8">Metrics</text>
|
||||
|
||||
<!-- Page title -->
|
||||
<text x="220" y="90" font-size="22" font-weight="700" fill="#ffffff">Intel GPU Overview</text>
|
||||
<text x="220" y="112" font-size="13" fill="#8b8fa8">Cluster-wide GPU device plugin status and workload summary</text>
|
||||
|
||||
<!-- Summary cards row -->
|
||||
<!-- Card 1: GPU Nodes -->
|
||||
<rect x="220" y="130" width="200" height="100" rx="8" fill="#1e2130"/>
|
||||
<rect x="220" y="130" width="200" height="4" rx="2" fill="#a78bfa"/>
|
||||
<text x="240" y="165" font-size="12" fill="#8b8fa8">GPU Nodes</text>
|
||||
<text x="240" y="200" font-size="36" font-weight="700" fill="#a78bfa">6</text>
|
||||
<text x="290" y="200" font-size="13" fill="#8b8fa8">/ 12 total</text>
|
||||
|
||||
<!-- Card 2: Active GPU Pods -->
|
||||
<rect x="436" y="130" width="200" height="100" rx="8" fill="#1e2130"/>
|
||||
<rect x="436" y="130" width="200" height="4" rx="2" fill="#34d399"/>
|
||||
<text x="456" y="165" font-size="12" fill="#8b8fa8">Active GPU Pods</text>
|
||||
<text x="456" y="200" font-size="36" font-weight="700" fill="#34d399">14</text>
|
||||
<text x="506" y="200" font-size="13" fill="#8b8fa8">running</text>
|
||||
|
||||
<!-- Card 3: Device Plugins -->
|
||||
<rect x="652" y="130" width="200" height="100" rx="8" fill="#1e2130"/>
|
||||
<rect x="652" y="130" width="200" height="4" rx="2" fill="#60a5fa"/>
|
||||
<text x="672" y="165" font-size="12" fill="#8b8fa8">Device Plugins</text>
|
||||
<text x="672" y="200" font-size="36" font-weight="700" fill="#60a5fa">2</text>
|
||||
<text x="722" y="200" font-size="13" fill="#8b8fa8">healthy</text>
|
||||
|
||||
<!-- Card 4: GPU Allocation -->
|
||||
<rect x="868" y="130" width="300" height="100" rx="8" fill="#1e2130"/>
|
||||
<rect x="868" y="130" width="300" height="4" rx="2" fill="#fb923c"/>
|
||||
<text x="888" y="165" font-size="12" fill="#8b8fa8">Cluster GPU Allocation</text>
|
||||
<text x="888" y="195" font-size="28" font-weight="700" fill="#fb923c">58%</text>
|
||||
<!-- Allocation bar -->
|
||||
<rect x="888" y="208" width="260" height="8" rx="4" fill="#2d3148"/>
|
||||
<rect x="888" y="208" width="151" height="8" rx="4" fill="#fb923c"/>
|
||||
|
||||
<!-- GPU Type Distribution -->
|
||||
<rect x="220" y="248" width="440" height="220" rx="8" fill="#1e2130"/>
|
||||
<text x="240" y="278" font-size="15" font-weight="600" fill="#ffffff">GPU Type Distribution</text>
|
||||
<!-- Pie chart mockup -->
|
||||
<circle cx="340" cy="370" r="70" fill="none" stroke="#2d3148" stroke-width="30"/>
|
||||
<circle cx="340" cy="370" r="70" fill="none" stroke="#a78bfa" stroke-width="30" stroke-dasharray="176 264" stroke-dashoffset="0"/>
|
||||
<circle cx="340" cy="370" r="70" fill="none" stroke="#60a5fa" stroke-width="30" stroke-dasharray="88 352" stroke-dashoffset="-176"/>
|
||||
<!-- Legend -->
|
||||
<rect x="430" y="340" width="12" height="12" rx="2" fill="#a78bfa"/>
|
||||
<text x="448" y="352" font-size="13" fill="#e2e4f0">Discrete (i915/Xe)</text>
|
||||
<text x="448" y="370" font-size="12" fill="#8b8fa8">4 nodes · 67%</text>
|
||||
<rect x="430" y="390" width="12" height="12" rx="2" fill="#60a5fa"/>
|
||||
<text x="448" y="402" font-size="13" fill="#e2e4f0">Integrated</text>
|
||||
<text x="448" y="420" font-size="12" fill="#8b8fa8">2 nodes · 33%</text>
|
||||
|
||||
<!-- Operator Status -->
|
||||
<rect x="676" y="248" width="492" height="220" rx="8" fill="#1e2130"/>
|
||||
<text x="696" y="278" font-size="15" font-weight="600" fill="#ffffff">Operator Status</text>
|
||||
<!-- Status rows -->
|
||||
<rect x="696" y="295" width="452" height="40" rx="4" fill="#13151f"/>
|
||||
<circle cx="720" cy="315" r="6" fill="#34d399"/>
|
||||
<text x="736" y="320" font-size="13" fill="#e2e4f0">gpu-device-plugin</text>
|
||||
<text x="1020" y="320" font-size="12" fill="#34d399">Running</text>
|
||||
<text x="1060" y="320" font-size="12" fill="#8b8fa8">6/6</text>
|
||||
<rect x="696" y="343" width="452" height="40" rx="4" fill="#13151f"/>
|
||||
<circle cx="720" cy="363" r="6" fill="#34d399"/>
|
||||
<text x="736" y="368" font-size="13" fill="#e2e4f0">node-feature-discovery</text>
|
||||
<text x="1020" y="368" font-size="12" fill="#34d399">Running</text>
|
||||
<text x="1060" y="368" font-size="12" fill="#8b8fa8">1/1</text>
|
||||
<rect x="696" y="391" width="452" height="40" rx="4" fill="#13151f"/>
|
||||
<circle cx="720" cy="411" r="6" fill="#60a5fa"/>
|
||||
<text x="736" y="416" font-size="13" fill="#e2e4f0">prometheus / node-exporter</text>
|
||||
<text x="1020" y="416" font-size="12" fill="#60a5fa">Available</text>
|
||||
<text x="1060" y="416" font-size="12" fill="#8b8fa8">6/6</text>
|
||||
|
||||
<!-- Recent GPU Pods table -->
|
||||
<rect x="220" y="486" width="948" height="220" rx="8" fill="#1e2130"/>
|
||||
<text x="240" y="516" font-size="15" font-weight="600" fill="#ffffff">Active GPU Pods</text>
|
||||
<!-- Table header -->
|
||||
<rect x="220" y="526" width="948" height="30" fill="#13151f"/>
|
||||
<text x="240" y="546" font-size="12" fill="#8b8fa8" font-weight="600">NAME</text>
|
||||
<text x="480" y="546" font-size="12" fill="#8b8fa8" font-weight="600">NAMESPACE</text>
|
||||
<text x="660" y="546" font-size="12" fill="#8b8fa8" font-weight="600">NODE</text>
|
||||
<text x="860" y="546" font-size="12" fill="#8b8fa8" font-weight="600">GPU REQUEST</text>
|
||||
<text x="1000" y="546" font-size="12" fill="#8b8fa8" font-weight="600">STATUS</text>
|
||||
<!-- Rows -->
|
||||
<rect x="220" y="556" width="948" height="34" fill="#1e2130"/>
|
||||
<text x="240" y="578" font-size="13" fill="#e2e4f0">gpu-inference-7d9c4f</text>
|
||||
<text x="480" y="578" font-size="13" fill="#8b8fa8">ml-workloads</text>
|
||||
<text x="660" y="578" font-size="13" fill="#8b8fa8">gpu-node-01</text>
|
||||
<text x="860" y="578" font-size="13" fill="#a78bfa">2</text>
|
||||
<rect x="998" y="563" width="60" height="20" rx="10" fill="#34d39922"/>
|
||||
<text x="1028" y="578" font-size="12" fill="#34d399" text-anchor="middle">Running</text>
|
||||
<rect x="220" y="590" width="948" height="34" fill="#13151f"/>
|
||||
<text x="240" y="612" font-size="13" fill="#e2e4f0">training-job-abc12</text>
|
||||
<text x="480" y="612" font-size="13" fill="#8b8fa8">training</text>
|
||||
<text x="660" y="612" font-size="13" fill="#8b8fa8">gpu-node-03</text>
|
||||
<text x="860" y="612" font-size="13" fill="#a78bfa">4</text>
|
||||
<rect x="998" y="597" width="60" height="20" rx="10" fill="#34d39922"/>
|
||||
<text x="1028" y="612" font-size="12" fill="#34d399" text-anchor="middle">Running</text>
|
||||
<rect x="220" y="624" width="948" height="34" fill="#1e2130"/>
|
||||
<text x="240" y="646" font-size="13" fill="#e2e4f0">render-worker-9xk2p</text>
|
||||
<text x="480" y="646" font-size="13" fill="#8b8fa8">rendering</text>
|
||||
<text x="660" y="646" font-size="13" fill="#8b8fa8">gpu-node-02</text>
|
||||
<text x="860" y="646" font-size="13" fill="#a78bfa">1</text>
|
||||
<rect x="998" y="631" width="60" height="20" rx="10" fill="#fbbf2422"/>
|
||||
<text x="1028" y="646" font-size="12" fill="#fbbf24" text-anchor="middle">Pending</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1,142 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="750" viewBox="0 0 1200 750" font-family="Inter, Segoe UI, Arial, sans-serif">
|
||||
<!-- Background -->
|
||||
<rect width="1200" height="750" fill="#0f1117"/>
|
||||
|
||||
<!-- Top nav bar -->
|
||||
<rect width="1200" height="48" fill="#1a1d27"/>
|
||||
<text x="16" y="30" font-size="18" font-weight="700" fill="#ffffff">⚡ Headlamp</text>
|
||||
<text x="120" y="30" font-size="14" fill="#8b8fa8">Intel GPU</text>
|
||||
<text x="200" y="30" font-size="14" fill="#8b8fa8">/</text>
|
||||
<text x="214" y="30" font-size="14" fill="#a78bfa">GPU Nodes</text>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<rect x="0" y="48" width="200" height="702" fill="#13151f"/>
|
||||
<text x="16" y="113" font-size="13" fill="#8b8fa8">Overview</text>
|
||||
<text x="16" y="153" font-size="13" fill="#8b8fa8">Device Plugins</text>
|
||||
<rect x="0" y="170" width="200" height="36" fill="#a78bfa22"/>
|
||||
<text x="16" y="193" font-size="13" fill="#a78bfa" font-weight="600">GPU Nodes</text>
|
||||
<text x="16" y="233" font-size="13" fill="#8b8fa8">GPU Pods</text>
|
||||
<text x="16" y="273" font-size="13" fill="#8b8fa8">Metrics</text>
|
||||
|
||||
<!-- Page title -->
|
||||
<text x="220" y="90" font-size="22" font-weight="700" fill="#ffffff">GPU Nodes</text>
|
||||
<text x="220" y="112" font-size="13" fill="#8b8fa8">Per-node GPU capacity, allocatable, allocation, and active workloads</text>
|
||||
|
||||
<!-- Node Card 1 -->
|
||||
<rect x="220" y="130" width="460" height="170" rx="8" fill="#1e2130"/>
|
||||
<rect x="220" y="130" width="460" height="4" rx="2" fill="#a78bfa"/>
|
||||
<text x="240" y="160" font-size="15" font-weight="600" fill="#ffffff">gpu-node-01</text>
|
||||
<rect x="550" y="142" width="80" height="22" rx="11" fill="#34d39922"/>
|
||||
<text x="590" y="158" font-size="12" fill="#34d399" text-anchor="middle">Ready</text>
|
||||
<text x="240" y="183" font-size="12" fill="#8b8fa8">Type</text>
|
||||
<text x="320" y="183" font-size="12" fill="#e2e4f0">Discrete (i915)</text>
|
||||
<text x="240" y="205" font-size="12" fill="#8b8fa8">Capacity</text>
|
||||
<text x="320" y="205" font-size="12" fill="#e2e4f0">4 GPUs</text>
|
||||
<text x="240" y="227" font-size="12" fill="#8b8fa8">Allocatable</text>
|
||||
<text x="320" y="227" font-size="12" fill="#e2e4f0">4 GPUs</text>
|
||||
<!-- Allocation bar -->
|
||||
<text x="240" y="255" font-size="12" fill="#8b8fa8">Allocation</text>
|
||||
<text x="640" y="255" font-size="12" fill="#fb923c" text-anchor="end">75% (3/4)</text>
|
||||
<rect x="240" y="262" width="380" height="12" rx="6" fill="#2d3148"/>
|
||||
<rect x="240" y="262" width="285" height="12" rx="6" fill="#fb923c"/>
|
||||
<text x="240" y="293" font-size="12" fill="#8b8fa8">Active Pods: </text>
|
||||
<text x="316" y="293" font-size="12" fill="#e2e4f0">gpu-inference-7d9c4f, render-worker-9xk2p</text>
|
||||
|
||||
<!-- Node Card 2 -->
|
||||
<rect x="700" y="130" width="460" height="170" rx="8" fill="#1e2130"/>
|
||||
<rect x="700" y="130" width="460" height="4" rx="2" fill="#a78bfa"/>
|
||||
<text x="720" y="160" font-size="15" font-weight="600" fill="#ffffff">gpu-node-02</text>
|
||||
<rect x="1030" y="142" width="80" height="22" rx="11" fill="#34d39922"/>
|
||||
<text x="1070" y="158" font-size="12" fill="#34d399" text-anchor="middle">Ready</text>
|
||||
<text x="720" y="183" font-size="12" fill="#8b8fa8">Type</text>
|
||||
<text x="800" y="183" font-size="12" fill="#e2e4f0">Discrete (Xe)</text>
|
||||
<text x="720" y="205" font-size="12" fill="#8b8fa8">Capacity</text>
|
||||
<text x="800" y="205" font-size="12" fill="#e2e4f0">2 GPUs</text>
|
||||
<text x="720" y="227" font-size="12" fill="#8b8fa8">Allocatable</text>
|
||||
<text x="800" y="227" font-size="12" fill="#e2e4f0">2 GPUs</text>
|
||||
<!-- Allocation bar -->
|
||||
<text x="720" y="255" font-size="12" fill="#8b8fa8">Allocation</text>
|
||||
<text x="1120" y="255" font-size="12" fill="#34d399" text-anchor="end">50% (1/2)</text>
|
||||
<rect x="720" y="262" width="380" height="12" rx="6" fill="#2d3148"/>
|
||||
<rect x="720" y="262" width="190" height="12" rx="6" fill="#34d399"/>
|
||||
<text x="720" y="293" font-size="12" fill="#8b8fa8">Active Pods: </text>
|
||||
<text x="796" y="293" font-size="12" fill="#e2e4f0">render-worker-9xk2p</text>
|
||||
|
||||
<!-- Node Card 3 -->
|
||||
<rect x="220" y="318" width="460" height="170" rx="8" fill="#1e2130"/>
|
||||
<rect x="220" y="318" width="460" height="4" rx="2" fill="#a78bfa"/>
|
||||
<text x="240" y="348" font-size="15" font-weight="600" fill="#ffffff">gpu-node-03</text>
|
||||
<rect x="550" y="330" width="80" height="22" rx="11" fill="#34d39922"/>
|
||||
<text x="590" y="346" font-size="12" fill="#34d399" text-anchor="middle">Ready</text>
|
||||
<text x="240" y="371" font-size="12" fill="#8b8fa8">Type</text>
|
||||
<text x="320" y="371" font-size="12" fill="#e2e4f0">Discrete (i915)</text>
|
||||
<text x="240" y="393" font-size="12" fill="#8b8fa8">Capacity</text>
|
||||
<text x="320" y="393" font-size="12" fill="#e2e4f0">8 GPUs</text>
|
||||
<text x="240" y="415" font-size="12" fill="#8b8fa8">Allocatable</text>
|
||||
<text x="320" y="415" font-size="12" fill="#e2e4f0">8 GPUs</text>
|
||||
<!-- Allocation bar -->
|
||||
<text x="240" y="443" font-size="12" fill="#8b8fa8">Allocation</text>
|
||||
<text x="640" y="443" font-size="12" fill="#fb923c" text-anchor="end">100% (8/8)</text>
|
||||
<rect x="240" y="450" width="380" height="12" rx="6" fill="#2d3148"/>
|
||||
<rect x="240" y="450" width="380" height="12" rx="6" fill="#f87171"/>
|
||||
<text x="240" y="481" font-size="12" fill="#8b8fa8">Active Pods: </text>
|
||||
<text x="316" y="481" font-size="12" fill="#e2e4f0">training-job-abc12 (+3 more)</text>
|
||||
|
||||
<!-- Node Card 4 (integrated) -->
|
||||
<rect x="700" y="318" width="460" height="170" rx="8" fill="#1e2130"/>
|
||||
<rect x="700" y="318" width="460" height="4" rx="2" fill="#60a5fa"/>
|
||||
<text x="720" y="348" font-size="15" font-weight="600" fill="#ffffff">worker-node-05</text>
|
||||
<rect x="1030" y="330" width="80" height="22" rx="11" fill="#34d39922"/>
|
||||
<text x="1070" y="346" font-size="12" fill="#34d399" text-anchor="middle">Ready</text>
|
||||
<text x="720" y="371" font-size="12" fill="#8b8fa8">Type</text>
|
||||
<text x="800" y="371" font-size="12" fill="#e2e4f0">Integrated</text>
|
||||
<text x="720" y="393" font-size="12" fill="#8b8fa8">Capacity</text>
|
||||
<text x="800" y="393" font-size="12" fill="#e2e4f0">1 GPU</text>
|
||||
<text x="720" y="415" font-size="12" fill="#8b8fa8">Allocatable</text>
|
||||
<text x="800" y="415" font-size="12" fill="#e2e4f0">1 GPU</text>
|
||||
<!-- Allocation bar -->
|
||||
<text x="720" y="443" font-size="12" fill="#8b8fa8">Allocation</text>
|
||||
<text x="1120" y="443" font-size="12" fill="#8b8fa8" text-anchor="end">0% (0/1)</text>
|
||||
<rect x="720" y="450" width="380" height="12" rx="6" fill="#2d3148"/>
|
||||
<text x="720" y="481" font-size="12" fill="#8b8fa8">Active Pods: </text>
|
||||
<text x="796" y="481" font-size="12" fill="#6b7280">none</text>
|
||||
|
||||
<!-- Node Card 5 -->
|
||||
<rect x="220" y="506" width="460" height="170" rx="8" fill="#1e2130"/>
|
||||
<rect x="220" y="506" width="460" height="4" rx="2" fill="#a78bfa"/>
|
||||
<text x="240" y="536" font-size="15" font-weight="600" fill="#ffffff">gpu-node-04</text>
|
||||
<rect x="550" y="518" width="80" height="22" rx="11" fill="#34d39922"/>
|
||||
<text x="590" y="534" font-size="12" fill="#34d399" text-anchor="middle">Ready</text>
|
||||
<text x="240" y="559" font-size="12" fill="#8b8fa8">Type</text>
|
||||
<text x="320" y="559" font-size="12" fill="#e2e4f0">Discrete (i915)</text>
|
||||
<text x="240" y="581" font-size="12" fill="#8b8fa8">Capacity</text>
|
||||
<text x="320" y="581" font-size="12" fill="#e2e4f0">2 GPUs</text>
|
||||
<text x="240" y="603" font-size="12" fill="#8b8fa8">Allocatable</text>
|
||||
<text x="320" y="603" font-size="12" fill="#e2e4f0">2 GPUs</text>
|
||||
<!-- Allocation bar -->
|
||||
<text x="240" y="631" font-size="12" fill="#8b8fa8">Allocation</text>
|
||||
<text x="640" y="631" font-size="12" fill="#34d399" text-anchor="end">25% (0.5/2)</text>
|
||||
<rect x="240" y="638" width="380" height="12" rx="6" fill="#2d3148"/>
|
||||
<rect x="240" y="638" width="95" height="12" rx="6" fill="#34d399"/>
|
||||
<text x="240" y="669" font-size="12" fill="#8b8fa8">Active Pods: </text>
|
||||
<text x="316" y="669" font-size="12" fill="#e2e4f0">light-inference-pod</text>
|
||||
|
||||
<!-- Node Card 6 (integrated) -->
|
||||
<rect x="700" y="506" width="460" height="170" rx="8" fill="#1e2130"/>
|
||||
<rect x="700" y="506" width="460" height="4" rx="2" fill="#60a5fa"/>
|
||||
<text x="720" y="536" font-size="15" font-weight="600" fill="#ffffff">worker-node-07</text>
|
||||
<rect x="1030" y="518" width="80" height="22" rx="11" fill="#fbbf2422"/>
|
||||
<text x="1070" y="534" font-size="12" fill="#fbbf24" text-anchor="middle">NotReady</text>
|
||||
<text x="720" y="559" font-size="12" fill="#8b8fa8">Type</text>
|
||||
<text x="800" y="559" font-size="12" fill="#e2e4f0">Integrated</text>
|
||||
<text x="720" y="581" font-size="12" fill="#8b8fa8">Capacity</text>
|
||||
<text x="800" y="581" font-size="12" fill="#e2e4f0">1 GPU</text>
|
||||
<text x="720" y="603" font-size="12" fill="#8b8fa8">Allocatable</text>
|
||||
<text x="800" y="603" font-size="12" fill="#8b8fa8">—</text>
|
||||
<!-- Allocation bar -->
|
||||
<text x="720" y="631" font-size="12" fill="#8b8fa8">Allocation</text>
|
||||
<text x="1120" y="631" font-size="12" fill="#6b7280" text-anchor="end">—</text>
|
||||
<rect x="720" y="638" width="380" height="12" rx="6" fill="#2d3148"/>
|
||||
<text x="720" y="669" font-size="12" fill="#8b8fa8">Active Pods: </text>
|
||||
<text x="796" y="669" font-size="12" fill="#6b7280">node unavailable</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.0 KiB |
@@ -0,0 +1,117 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="750" viewBox="0 0 1200 750" font-family="Inter, Segoe UI, Arial, sans-serif">
|
||||
<!-- Background -->
|
||||
<rect width="1200" height="750" fill="#0f1117"/>
|
||||
|
||||
<!-- Top nav bar -->
|
||||
<rect width="1200" height="48" fill="#1a1d27"/>
|
||||
<text x="16" y="30" font-size="18" font-weight="700" fill="#ffffff">⚡ Headlamp</text>
|
||||
<text x="120" y="30" font-size="14" fill="#8b8fa8">Intel GPU</text>
|
||||
<text x="200" y="30" font-size="14" fill="#8b8fa8">/</text>
|
||||
<text x="214" y="30" font-size="14" fill="#a78bfa">Metrics</text>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<rect x="0" y="48" width="200" height="702" fill="#13151f"/>
|
||||
<text x="16" y="113" font-size="13" fill="#8b8fa8">Overview</text>
|
||||
<text x="16" y="153" font-size="13" fill="#8b8fa8">Device Plugins</text>
|
||||
<text x="16" y="193" font-size="13" fill="#8b8fa8">GPU Nodes</text>
|
||||
<text x="16" y="233" font-size="13" fill="#8b8fa8">GPU Pods</text>
|
||||
<rect x="0" y="250" width="200" height="36" fill="#a78bfa22"/>
|
||||
<text x="16" y="273" font-size="13" fill="#a78bfa" font-weight="600">Metrics</text>
|
||||
|
||||
<!-- Page title -->
|
||||
<text x="220" y="90" font-size="22" font-weight="700" fill="#ffffff">GPU Metrics</text>
|
||||
<text x="220" y="112" font-size="13" fill="#8b8fa8">Real-time GPU power draw from node-exporter i915 hwmon (discrete GPU nodes only)</text>
|
||||
|
||||
<!-- Time range selector -->
|
||||
<rect x="980" y="72" width="200" height="30" rx="6" fill="#1e2130"/>
|
||||
<text x="1000" y="92" font-size="13" fill="#8b8fa8">Last 30 min ▾</text>
|
||||
|
||||
<!-- Summary stat cards -->
|
||||
<rect x="220" y="130" width="230" height="80" rx="8" fill="#1e2130"/>
|
||||
<text x="240" y="158" font-size="12" fill="#8b8fa8">Peak Power Draw</text>
|
||||
<text x="240" y="195" font-size="28" font-weight="700" fill="#f87171">186 W</text>
|
||||
<text x="360" y="195" font-size="12" fill="#8b8fa8">gpu-node-03</text>
|
||||
|
||||
<rect x="466" y="130" width="230" height="80" rx="8" fill="#1e2130"/>
|
||||
<text x="486" y="158" font-size="12" fill="#8b8fa8">Avg Power (cluster)</text>
|
||||
<text x="486" y="195" font-size="28" font-weight="700" fill="#fb923c">124 W</text>
|
||||
<text x="606" y="195" font-size="12" fill="#8b8fa8">3 nodes</text>
|
||||
|
||||
<rect x="712" y="130" width="230" height="80" rx="8" fill="#1e2130"/>
|
||||
<text x="732" y="158" font-size="12" fill="#8b8fa8">Avg TDP Utilization</text>
|
||||
<text x="732" y="195" font-size="28" font-weight="700" fill="#a78bfa">68%</text>
|
||||
<text x="820" y="195" font-size="12" fill="#8b8fa8">of 250W TDP</text>
|
||||
|
||||
<rect x="958" y="130" width="210" height="80" rx="8" fill="#1e2130"/>
|
||||
<text x="978" y="158" font-size="12" fill="#8b8fa8">Nodes Reporting</text>
|
||||
<text x="978" y="195" font-size="28" font-weight="700" fill="#34d399">3</text>
|
||||
<text x="1030" y="195" font-size="12" fill="#8b8fa8">/ 4 discrete</text>
|
||||
|
||||
<!-- Main chart: GPU Power Draw over time -->
|
||||
<rect x="220" y="226" width="948" height="300" rx="8" fill="#1e2130"/>
|
||||
<text x="240" y="256" font-size="15" font-weight="600" fill="#ffffff">GPU Power Draw (W) — Last 30 Minutes</text>
|
||||
<!-- Chart area -->
|
||||
<rect x="260" y="270" width="880" height="230" fill="#13151f" rx="4"/>
|
||||
<!-- Y axis labels -->
|
||||
<text x="250" y="498" font-size="11" fill="#6b7280" text-anchor="end">0</text>
|
||||
<text x="250" y="440" font-size="11" fill="#6b7280" text-anchor="end">50</text>
|
||||
<text x="250" y="382" font-size="11" fill="#6b7280" text-anchor="end">100</text>
|
||||
<text x="250" y="325" font-size="11" fill="#6b7280" text-anchor="end">150</text>
|
||||
<text x="250" y="278" font-size="11" fill="#6b7280" text-anchor="end">200</text>
|
||||
<!-- Y grid lines -->
|
||||
<line x1="260" y1="497" x2="1140" y2="497" stroke="#2d3148" stroke-width="1"/>
|
||||
<line x1="260" y1="439" x2="1140" y2="439" stroke="#2d3148" stroke-width="1"/>
|
||||
<line x1="260" y1="381" x2="1140" y2="381" stroke="#2d3148" stroke-width="1"/>
|
||||
<line x1="260" y1="323" x2="1140" y2="323" stroke="#2d3148" stroke-width="1"/>
|
||||
<line x1="260" y1="275" x2="1140" y2="275" stroke="#2d3148" stroke-width="1"/>
|
||||
<!-- X axis labels -->
|
||||
<text x="260" y="515" font-size="11" fill="#6b7280">-30m</text>
|
||||
<text x="480" y="515" font-size="11" fill="#6b7280">-22m</text>
|
||||
<text x="700" y="515" font-size="11" fill="#6b7280">-15m</text>
|
||||
<text x="920" y="515" font-size="11" fill="#6b7280">-7m</text>
|
||||
<text x="1120" y="515" font-size="11" fill="#6b7280">now</text>
|
||||
|
||||
<!-- Line for gpu-node-01 (purple) — ~186W with some variation -->
|
||||
<polyline points="260,323 315,318 370,310 425,315 480,325 535,320 590,312 645,308 700,315 755,322 810,318 865,310 920,305 975,312 1030,320 1085,315 1140,318" fill="none" stroke="#a78bfa" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
|
||||
<!-- Line for gpu-node-03 (orange) — ~124W workload burst then taper -->
|
||||
<polyline points="260,381 315,370 370,355 425,340 480,330 535,328 590,335 645,340 700,345 755,350 810,355 865,345 920,338 975,340 1030,348 1085,355 1140,350" fill="none" stroke="#fb923c" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
|
||||
<!-- Line for gpu-node-02 (blue) — ~80W relatively steady -->
|
||||
<polyline points="260,429 315,427 370,425 425,430 480,432 535,428 590,426 645,422 700,425 755,428 810,430 865,427 920,423 975,428 1030,430 1085,425 1140,427" fill="none" stroke="#60a5fa" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="240" y1="553" x2="268" y2="553" stroke="#a78bfa" stroke-width="2.5"/>
|
||||
<text x="276" y="558" font-size="12" fill="#e2e4f0">gpu-node-01 (i915, 4x GPU, avg 186W)</text>
|
||||
<line x1="540" y1="553" x2="568" y2="553" stroke="#fb923c" stroke-width="2.5"/>
|
||||
<text x="576" y="558" font-size="12" fill="#e2e4f0">gpu-node-03 (i915, 8x GPU, avg 124W)</text>
|
||||
<line x1="860" y1="553" x2="888" y2="553" stroke="#60a5fa" stroke-width="2.5"/>
|
||||
<text x="896" y="558" font-size="12" fill="#e2e4f0">gpu-node-02 (Xe, 2x GPU, avg 80W)</text>
|
||||
|
||||
<!-- TDP bars section -->
|
||||
<rect x="220" y="540" width="948" height="180" rx="8" fill="#1e2130"/>
|
||||
<text x="240" y="568" font-size="15" font-weight="600" fill="#ffffff">TDP Utilization — Current</text>
|
||||
<!-- Node rows -->
|
||||
<!-- gpu-node-01 -->
|
||||
<text x="240" y="600" font-size="13" fill="#e2e4f0">gpu-node-01</text>
|
||||
<text x="440" y="600" font-size="12" fill="#8b8fa8">186W / 250W TDP</text>
|
||||
<rect x="600" y="588" width="500" height="16" rx="8" fill="#2d3148"/>
|
||||
<rect x="600" y="588" width="372" height="16" rx="8" fill="#a78bfa"/>
|
||||
<text x="1110" y="600" font-size="12" fill="#a78bfa">74%</text>
|
||||
<!-- gpu-node-03 -->
|
||||
<text x="240" y="634" font-size="13" fill="#e2e4f0">gpu-node-03</text>
|
||||
<text x="440" y="634" font-size="12" fill="#8b8fa8">124W / 250W TDP</text>
|
||||
<rect x="600" y="622" width="500" height="16" rx="8" fill="#2d3148"/>
|
||||
<rect x="600" y="622" width="248" height="16" rx="8" fill="#fb923c"/>
|
||||
<text x="1110" y="634" font-size="12" fill="#fb923c">50%</text>
|
||||
<!-- gpu-node-02 -->
|
||||
<text x="240" y="668" font-size="13" fill="#e2e4f0">gpu-node-02</text>
|
||||
<text x="440" y="668" font-size="12" fill="#8b8fa8">80W / 150W TDP</text>
|
||||
<rect x="600" y="656" width="500" height="16" rx="8" fill="#2d3148"/>
|
||||
<rect x="600" y="656" width="267" height="16" rx="8" fill="#60a5fa"/>
|
||||
<text x="1110" y="668" font-size="12" fill="#60a5fa">53%</text>
|
||||
<!-- No data node -->
|
||||
<text x="240" y="702" font-size="13" fill="#6b7280">gpu-node-04</text>
|
||||
<text x="440" y="702" font-size="12" fill="#6b7280">No hwmon data — integrated GPU</text>
|
||||
<text x="1110" y="702" font-size="12" fill="#6b7280">n/a</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
Generated
+869
-529
File diff suppressed because it is too large
Load Diff
+21
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "headlamp-intel-gpu",
|
||||
"version": "0.4.2",
|
||||
"name": "intel-gpu",
|
||||
"version": "1.1.0",
|
||||
"description": "Headlamp plugin for Intel GPU device plugin visibility and monitoring",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -24,7 +24,25 @@
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kinvolk/headlamp-plugin": "^0.13.0"
|
||||
"@kinvolk/headlamp-plugin": "^0.13.0",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"jsdom": "^24.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"vitest": "^3.0.5"
|
||||
},
|
||||
"overrides": {
|
||||
"tar": "^7.5.11",
|
||||
"undici": "^7.24.3",
|
||||
"lodash": ">=4.18.0",
|
||||
"elliptic": ">=6.6.1"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-16
@@ -1,19 +1,5 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"baseBranches": ["main"],
|
||||
"schedule": ["every weekend"],
|
||||
"prConcurrentLimit": 10,
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["npm"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"groupName": "npm minor and patch"
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"groupName": "github-actions minor and patch"
|
||||
}
|
||||
]
|
||||
"extends": ["github>privilegedescalation/.github:renovate-config"]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { ApiProxy, K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||
import { act, render, renderHook, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { IntelGpuDataProvider, useIntelGpuContext } from './IntelGpuDataContext';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
|
||||
K8s: {
|
||||
ResourceClasses: {
|
||||
Node: { useList: vi.fn() },
|
||||
Pod: { useList: vi.fn() },
|
||||
},
|
||||
},
|
||||
ApiProxy: { request: vi.fn() },
|
||||
}));
|
||||
|
||||
// Minimal GPU node fixture
|
||||
const gpuNodeRaw = {
|
||||
metadata: {
|
||||
name: 'gpu-node-1',
|
||||
uid: 'uid-001',
|
||||
labels: { 'intel.feature.node.kubernetes.io/gpu': 'true' },
|
||||
},
|
||||
status: {
|
||||
capacity: { 'gpu.intel.com/i915': '1' },
|
||||
allocatable: { 'gpu.intel.com/i915': '1' },
|
||||
},
|
||||
};
|
||||
|
||||
// Minimal GPU plugin CRD fixture
|
||||
const gpuDevicePluginRaw = {
|
||||
kind: 'GpuDevicePlugin',
|
||||
metadata: { name: 'gpu-plugin-default', uid: 'uid-dp-001' },
|
||||
spec: {},
|
||||
};
|
||||
|
||||
function makeNodeWrapper(raw: unknown) {
|
||||
return { jsonData: raw };
|
||||
}
|
||||
|
||||
function Wrapper({ children }: { children: React.ReactNode }) {
|
||||
return <IntelGpuDataProvider>{children}</IntelGpuDataProvider>;
|
||||
}
|
||||
|
||||
describe('useIntelGpuContext', () => {
|
||||
it('throws when used outside provider', () => {
|
||||
// Suppress React error boundary output
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
expect(() => renderHook(() => useIntelGpuContext())).toThrow(
|
||||
'useIntelGpuContext must be used within an IntelGpuDataProvider'
|
||||
);
|
||||
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('IntelGpuDataProvider', () => {
|
||||
it('renders children', async () => {
|
||||
vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([[], null] as any);
|
||||
vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[], null] as any);
|
||||
vi.mocked(ApiProxy.request).mockResolvedValue({ items: [] });
|
||||
|
||||
render(
|
||||
<IntelGpuDataProvider>
|
||||
<div data-testid="child">hello</div>
|
||||
</IntelGpuDataProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('child')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('exposes loading=true while nodes/pods are null', async () => {
|
||||
vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([null, null] as any);
|
||||
vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([null, null] as any);
|
||||
// Keep async request pending forever
|
||||
vi.mocked(ApiProxy.request).mockReturnValue(new Promise(() => {}));
|
||||
|
||||
const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper });
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
});
|
||||
|
||||
it('exposes loaded state with GPU nodes once data arrives', async () => {
|
||||
vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([
|
||||
[makeNodeWrapper(gpuNodeRaw)] as any,
|
||||
null,
|
||||
] as any);
|
||||
vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[], null] as any);
|
||||
vi.mocked(ApiProxy.request).mockResolvedValue({ items: [] });
|
||||
|
||||
const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
expect(result.current.gpuNodes).toHaveLength(1);
|
||||
expect(result.current.gpuNodes[0].metadata.name).toBe('gpu-node-1');
|
||||
});
|
||||
|
||||
it('sets crdAvailable=true and populates devicePlugins when ApiProxy returns plugin list', async () => {
|
||||
vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([[], null] as any);
|
||||
vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[], null] as any);
|
||||
|
||||
// First call = CRD list, subsequent calls = plugin pod selectors (empty)
|
||||
vi.mocked(ApiProxy.request)
|
||||
.mockResolvedValueOnce({ items: [gpuDevicePluginRaw] })
|
||||
.mockResolvedValue({ items: [] });
|
||||
|
||||
const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
expect(result.current.crdAvailable).toBe(true);
|
||||
expect(result.current.devicePlugins).toHaveLength(1);
|
||||
expect(result.current.devicePlugins[0].metadata.name).toBe('gpu-plugin-default');
|
||||
});
|
||||
|
||||
it('sets crdAvailable=false and does not surface error when ApiProxy throws on CRD request', async () => {
|
||||
vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([[], null] as any);
|
||||
vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[], null] as any);
|
||||
|
||||
// First call (CRD endpoint) throws, plugin pod selectors resolve empty
|
||||
vi.mocked(ApiProxy.request)
|
||||
.mockRejectedValueOnce(new Error('CRD not found'))
|
||||
.mockResolvedValue({ items: [] });
|
||||
|
||||
const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
expect(result.current.crdAvailable).toBe(false);
|
||||
expect(result.current.devicePlugins).toHaveLength(0);
|
||||
// Inner CRD error should NOT be bubbled up to the top-level error field
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
it('increments refreshKey and re-runs the effect when refresh() is called', async () => {
|
||||
vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([[], null] as any);
|
||||
vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[], null] as any);
|
||||
vi.mocked(ApiProxy.request).mockResolvedValue({ items: [] });
|
||||
|
||||
const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||
|
||||
const callCountBefore = vi.mocked(ApiProxy.request).mock.calls.length;
|
||||
|
||||
await act(async () => {
|
||||
result.current.refresh();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const callCountAfter = vi.mocked(ApiProxy.request).mock.calls.length;
|
||||
expect(callCountAfter).toBeGreaterThan(callCountBefore);
|
||||
});
|
||||
});
|
||||
|
||||
it('treats a hanging CRD request as unavailable after 2s timeout', async () => {
|
||||
vi.useFakeTimers();
|
||||
const nodeWrapper = { jsonData: {} };
|
||||
vi.mocked(K8s.ResourceClasses.Node.useList).mockReturnValue([[nodeWrapper], null] as any);
|
||||
vi.mocked(K8s.ResourceClasses.Pod.useList).mockReturnValue([[nodeWrapper], null] as any);
|
||||
vi.mocked(ApiProxy.request)
|
||||
.mockReturnValueOnce(new Promise(() => {}))
|
||||
.mockResolvedValueOnce({ items: [] })
|
||||
.mockResolvedValueOnce({ items: [] })
|
||||
.mockResolvedValueOnce({ items: [] });
|
||||
|
||||
const { result } = renderHook(() => useIntelGpuContext(), { wrapper: Wrapper });
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
|
||||
vi.advanceTimersByTime(2000);
|
||||
await act(async () => {});
|
||||
expect(result.current.crdAvailable).toBe(false);
|
||||
expect(result.current.loading).toBe(false);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -69,6 +69,18 @@ export function useIntelGpuContext(): IntelGpuContextValue {
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DEFAULT_REQUEST_TIMEOUT_MS = 2_000;
|
||||
|
||||
/** Wraps a promise with a timeout, rejecting if it doesn't settle within ms. */
|
||||
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise<T>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`Request timed out after ${ms}ms`)), ms)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/** Extract raw Kubernetes JSON from Headlamp KubeObject wrappers. */
|
||||
const extractJsonData = (items: unknown[]): unknown[] =>
|
||||
items.map(item =>
|
||||
@@ -108,8 +120,11 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode }
|
||||
try {
|
||||
// GpuDevicePlugin CRDs — graceful degradation if CRD not installed
|
||||
try {
|
||||
const pluginList = await ApiProxy.request(
|
||||
`/apis/${INTEL_DEVICE_PLUGIN_API_GROUP}/${INTEL_DEVICE_PLUGIN_API_VERSION}/gpudeviceplugins`
|
||||
const pluginList = await withTimeout(
|
||||
ApiProxy.request(
|
||||
`/apis/${INTEL_DEVICE_PLUGIN_API_GROUP}/${INTEL_DEVICE_PLUGIN_API_VERSION}/gpudeviceplugins`
|
||||
),
|
||||
DEFAULT_REQUEST_TIMEOUT_MS
|
||||
);
|
||||
if (!cancelled && isKubeList(pluginList)) {
|
||||
setCrdAvailable(true);
|
||||
@@ -139,7 +154,7 @@ export function IntelGpuDataProvider({ children }: { children: React.ReactNode }
|
||||
|
||||
for (const url of pluginPodSelectors) {
|
||||
try {
|
||||
const list = await ApiProxy.request(url);
|
||||
const list = await withTimeout(ApiProxy.request(url), DEFAULT_REQUEST_TIMEOUT_MS);
|
||||
if (!cancelled && isKubeList(list)) {
|
||||
const gpuPluginPods = filterIntelGpuPluginPods(list.items);
|
||||
foundPluginPods.push(...gpuPluginPods);
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { IntelGpuContextValue, useIntelGpuContext } from '../api/IntelGpuDataContext';
|
||||
import { GpuDevicePlugin, IntelGpuPod } from '../api/k8s';
|
||||
import DevicePluginsPage from './DevicePluginsPage';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||
Loader: ({ title }: { title: string }) => <div data-testid="loader">{title}</div>,
|
||||
SectionBox: ({ title, children }: { title: string; children?: React.ReactNode }) => (
|
||||
<section>
|
||||
<h2>{title}</h2>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
SectionHeader: ({ title }: { title: string }) => <h1>{title}</h1>,
|
||||
NameValueTable: ({
|
||||
rows,
|
||||
}: {
|
||||
rows: Array<{ name: React.ReactNode; value: React.ReactNode }>;
|
||||
}) => (
|
||||
<dl>
|
||||
{rows.map((r, i) => (
|
||||
<div key={i}>
|
||||
<dt>{r.name}</dt>
|
||||
<dd>{r.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
),
|
||||
SimpleTable: ({
|
||||
columns,
|
||||
data,
|
||||
}: {
|
||||
columns: Array<{ label: string; getter: (item: unknown) => React.ReactNode }>;
|
||||
data: unknown[];
|
||||
}) => (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(c => (
|
||||
<th key={c.label}>{c.label}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item, i) => (
|
||||
<tr key={i}>
|
||||
{columns.map(c => (
|
||||
<td key={c.label}>{c.getter(item)}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
),
|
||||
StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => (
|
||||
<span data-status={status}>{children}</span>
|
||||
),
|
||||
PercentageBar: () => <div data-testid="percentage-bar" />,
|
||||
}));
|
||||
|
||||
vi.mock('../api/IntelGpuDataContext', () => ({
|
||||
useIntelGpuContext: vi.fn(),
|
||||
}));
|
||||
|
||||
function makeContext(overrides: Partial<IntelGpuContextValue> = {}): IntelGpuContextValue {
|
||||
return {
|
||||
devicePlugins: [],
|
||||
pluginInstalled: false,
|
||||
gpuNodes: [],
|
||||
gpuPods: [],
|
||||
pluginPods: [],
|
||||
crdAvailable: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const samplePlugin: GpuDevicePlugin = {
|
||||
kind: 'GpuDevicePlugin',
|
||||
metadata: {
|
||||
name: 'intel-gpu-plugin',
|
||||
uid: 'uid-dp-1',
|
||||
creationTimestamp: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
spec: {
|
||||
image: 'intel/intel-gpu-plugin:latest',
|
||||
sharedDevNum: 4,
|
||||
enableMonitoring: true,
|
||||
preferredAllocationPolicy: 'balanced',
|
||||
},
|
||||
status: {
|
||||
desiredNumberScheduled: 3,
|
||||
numberReady: 3,
|
||||
},
|
||||
};
|
||||
|
||||
const pluginPod: IntelGpuPod = {
|
||||
metadata: {
|
||||
name: 'intel-gpu-plugin-abc12',
|
||||
namespace: 'kube-system',
|
||||
uid: 'uid-pp-1',
|
||||
},
|
||||
spec: { nodeName: 'worker-1' },
|
||||
status: {
|
||||
phase: 'Running',
|
||||
conditions: [{ type: 'Ready', status: 'True' }],
|
||||
},
|
||||
};
|
||||
|
||||
describe('DevicePluginsPage', () => {
|
||||
it('shows loader when loading=true', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: true }));
|
||||
render(<DevicePluginsPage />);
|
||||
expect(screen.getByTestId('loader')).toHaveTextContent('Loading device plugin data...');
|
||||
});
|
||||
|
||||
it('shows "CRD Not Available" section when crdAvailable=false', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({ loading: false, crdAvailable: false })
|
||||
);
|
||||
render(<DevicePluginsPage />);
|
||||
expect(screen.getByText('CRD Not Available')).toBeInTheDocument();
|
||||
expect(screen.getByText(/GpuDevicePlugin CRD.*is not installed/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "No Device Plugins" section when crdAvailable=true but devicePlugins empty', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({ loading: false, crdAvailable: true, devicePlugins: [] })
|
||||
);
|
||||
render(<DevicePluginsPage />);
|
||||
expect(screen.getByText('No Device Plugins')).toBeInTheDocument();
|
||||
expect(screen.getByText(/No GpuDevicePlugin resources found/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows plugin detail section when crdAvailable=true and devicePlugins present', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({
|
||||
loading: false,
|
||||
crdAvailable: true,
|
||||
devicePlugins: [samplePlugin],
|
||||
})
|
||||
);
|
||||
render(<DevicePluginsPage />);
|
||||
expect(screen.getByText('GpuDevicePlugin: intel-gpu-plugin')).toBeInTheDocument();
|
||||
expect(screen.getByText('intel/intel-gpu-plugin:latest')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Plugin Daemon Pods" table when pluginPods present', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({
|
||||
loading: false,
|
||||
crdAvailable: true,
|
||||
devicePlugins: [samplePlugin],
|
||||
pluginPods: [pluginPod],
|
||||
})
|
||||
);
|
||||
render(<DevicePluginsPage />);
|
||||
expect(screen.getByText('Plugin Daemon Pods')).toBeInTheDocument();
|
||||
expect(screen.getByText('intel-gpu-plugin-abc12')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error section when error is set', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({ loading: false, crdAvailable: true, error: 'fetch error' })
|
||||
);
|
||||
render(<DevicePluginsPage />);
|
||||
expect(screen.getByText('fetch error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,214 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { IntelGpuContextValue, useIntelGpuContext } from '../api/IntelGpuDataContext';
|
||||
import { fetchGpuMetrics, GpuChipMetrics, GpuMetrics } from '../api/metrics';
|
||||
import MetricsPage from './MetricsPage';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||
Loader: ({ title }: { title: string }) => <div data-testid="loader">{title}</div>,
|
||||
SectionBox: ({ title, children }: { title: string; children?: React.ReactNode }) => (
|
||||
<section>
|
||||
<h2>{title}</h2>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
SectionHeader: ({ title }: { title: string }) => <h1>{title}</h1>,
|
||||
NameValueTable: ({
|
||||
rows,
|
||||
}: {
|
||||
rows: Array<{ name: React.ReactNode; value: React.ReactNode }>;
|
||||
}) => (
|
||||
<dl>
|
||||
{rows.map((r, i) => (
|
||||
<div key={i}>
|
||||
<dt>{r.name}</dt>
|
||||
<dd>{r.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
),
|
||||
SimpleTable: ({
|
||||
columns,
|
||||
data,
|
||||
}: {
|
||||
columns: Array<{ label: string; getter: (item: unknown) => React.ReactNode }>;
|
||||
data: unknown[];
|
||||
}) => (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(c => (
|
||||
<th key={c.label}>{c.label}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item, i) => (
|
||||
<tr key={i}>
|
||||
{columns.map(c => (
|
||||
<td key={c.label}>{c.getter(item)}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
),
|
||||
StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => (
|
||||
<span data-status={status}>{children}</span>
|
||||
),
|
||||
PercentageBar: () => <div data-testid="percentage-bar" />,
|
||||
}));
|
||||
|
||||
vi.mock('../api/IntelGpuDataContext', () => ({
|
||||
useIntelGpuContext: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../api/metrics', () => ({
|
||||
fetchGpuMetrics: vi.fn(),
|
||||
formatWatts: (w: number) => `${w.toFixed(1)} W`,
|
||||
formatPercent: (used: number, max: number) =>
|
||||
max <= 0 ? '—' : `${Math.round((used / max) * 100)}%`,
|
||||
}));
|
||||
|
||||
function makeContext(overrides: Partial<IntelGpuContextValue> = {}): IntelGpuContextValue {
|
||||
return {
|
||||
devicePlugins: [],
|
||||
pluginInstalled: false,
|
||||
gpuNodes: [],
|
||||
gpuPods: [],
|
||||
pluginPods: [],
|
||||
crdAvailable: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeMetrics(chips: GpuChipMetrics[]): GpuMetrics {
|
||||
return {
|
||||
chips,
|
||||
fetchedAt: new Date('2025-03-21T10:00:00Z').toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
const sampleChip: GpuChipMetrics = {
|
||||
nodeName: 'gpu-node-1',
|
||||
chip: '0000:09:01_0',
|
||||
instance: '192.168.1.10:9100',
|
||||
powerWatts: 45.3,
|
||||
powerMaxWatts: 120.0,
|
||||
};
|
||||
|
||||
describe('MetricsPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('shows loader when ctxLoading=true but heading is visible immediately', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: true }));
|
||||
// fetchGpuMetrics should never be called in loading state
|
||||
vi.mocked(fetchGpuMetrics).mockResolvedValue(null);
|
||||
render(<MetricsPage />);
|
||||
// Heading renders immediately, loader appears below it while waiting for context
|
||||
expect(screen.getByText('Intel GPU — Metrics')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('loader')).toHaveTextContent('Loading Intel GPU data...');
|
||||
});
|
||||
|
||||
it('shows "Prometheus Unreachable" section when fetchGpuMetrics returns null', async () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false }));
|
||||
vi.mocked(fetchGpuMetrics).mockResolvedValue(null);
|
||||
|
||||
render(<MetricsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Prometheus Unreachable')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows "No i915 Metrics in Prometheus" when fetchGpuMetrics returns empty chips', async () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false }));
|
||||
vi.mocked(fetchGpuMetrics).mockResolvedValue(makeMetrics([]));
|
||||
|
||||
render(<MetricsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('No i915 Metrics in Prometheus')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows chip cards with node name when fetchGpuMetrics returns chips', async () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false }));
|
||||
vi.mocked(fetchGpuMetrics).mockResolvedValue(makeMetrics([sampleChip]));
|
||||
|
||||
render(<MetricsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
// GpuChipCard title format: "{nodeName} — {chip}"
|
||||
expect(screen.getByText('gpu-node-1 — 0000:09:01_0')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('always renders MetricRequirements section', async () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false }));
|
||||
vi.mocked(fetchGpuMetrics).mockResolvedValue(makeMetrics([]));
|
||||
|
||||
render(<MetricsPage />);
|
||||
|
||||
// The MetricRequirements section box is titled "Metric Availability"
|
||||
expect(screen.getByText('Metric Availability')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows GPU Power Summary section when chips are present', async () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false }));
|
||||
vi.mocked(fetchGpuMetrics).mockResolvedValue(makeMetrics([sampleChip]));
|
||||
|
||||
render(<MetricsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('GPU Power Summary')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('re-triggers fetch when refresh button is clicked', async () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false }));
|
||||
vi.mocked(fetchGpuMetrics).mockResolvedValue(makeMetrics([]));
|
||||
|
||||
render(<MetricsPage />);
|
||||
|
||||
// Wait for initial fetch to complete
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(fetchGpuMetrics)).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const callsBefore = vi.mocked(fetchGpuMetrics).mock.calls.length;
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /refresh metrics/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(vi.mocked(fetchGpuMetrics).mock.calls.length).toBeGreaterThan(callsBefore);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows "Intel GPU — Metrics" heading', async () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false }));
|
||||
vi.mocked(fetchGpuMetrics).mockResolvedValue(makeMetrics([]));
|
||||
|
||||
render(<MetricsPage />);
|
||||
|
||||
expect(screen.getByText('Intel GPU — Metrics')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows power values for chip cards', async () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false }));
|
||||
vi.mocked(fetchGpuMetrics).mockResolvedValue(makeMetrics([sampleChip]));
|
||||
|
||||
render(<MetricsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
// formatWatts mock: "45.3 W" and "120.0 W"
|
||||
expect(screen.getAllByText(/45\.3 W/).length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -230,10 +230,6 @@ export default function MetricsPage() {
|
||||
};
|
||||
}, [ctxLoading, fetchSeq]);
|
||||
|
||||
if (ctxLoading) {
|
||||
return <Loader title="Loading Intel GPU data..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -247,7 +243,7 @@ export default function MetricsPage() {
|
||||
<SectionHeader title="Intel GPU — Metrics" />
|
||||
<button
|
||||
onClick={() => void doFetch()}
|
||||
disabled={fetching}
|
||||
disabled={fetching || ctxLoading}
|
||||
aria-label="Refresh metrics"
|
||||
style={{
|
||||
padding: '6px 16px',
|
||||
@@ -255,15 +251,18 @@ export default function MetricsPage() {
|
||||
color: 'var(--mui-palette-primary-main, #0071c5)',
|
||||
border: '1px solid var(--mui-palette-primary-main, #0071c5)',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
cursor: fetching || ctxLoading ? 'not-allowed' : 'pointer',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
opacity: fetching || ctxLoading ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{fetching ? 'Refreshing…' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{ctxLoading && <Loader title="Loading Intel GPU data..." />}
|
||||
|
||||
<MetricRequirements />
|
||||
|
||||
{fetching && !metrics && <Loader title="Querying Prometheus for GPU metrics..." />}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { IntelGpuContextValue, useIntelGpuContext } from '../api/IntelGpuDataContext';
|
||||
import { IntelGpuPod } from '../api/k8s';
|
||||
import NodeDetailSection from './NodeDetailSection';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||
SectionBox: ({ title, children }: { title: string; children?: React.ReactNode }) => (
|
||||
<section>
|
||||
<h2>{title}</h2>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
NameValueTable: ({
|
||||
rows,
|
||||
}: {
|
||||
rows: Array<{ name: React.ReactNode; value: React.ReactNode }>;
|
||||
}) => (
|
||||
<dl>
|
||||
{rows.map((r, i) => (
|
||||
<div key={i}>
|
||||
<dt>{r.name}</dt>
|
||||
<dd>{r.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
),
|
||||
StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => (
|
||||
<span data-status={status}>{children}</span>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../api/IntelGpuDataContext', () => ({
|
||||
useIntelGpuContext: vi.fn(),
|
||||
}));
|
||||
|
||||
function makeContext(overrides: Partial<IntelGpuContextValue> = {}): IntelGpuContextValue {
|
||||
return {
|
||||
devicePlugins: [],
|
||||
pluginInstalled: false,
|
||||
gpuNodes: [],
|
||||
gpuPods: [],
|
||||
pluginPods: [],
|
||||
crdAvailable: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// A raw GPU node (matches IntelGpuNode shape) with capacity/allocatable
|
||||
const gpuNodeRaw = {
|
||||
kind: 'Node',
|
||||
metadata: {
|
||||
name: 'gpu-node-1',
|
||||
labels: { 'intel.feature.node.kubernetes.io/gpu': 'true' },
|
||||
},
|
||||
status: {
|
||||
capacity: { 'gpu.intel.com/i915': '2', cpu: '8' },
|
||||
allocatable: { 'gpu.intel.com/i915': '2', cpu: '8' },
|
||||
nodeInfo: {
|
||||
kernelVersion: '5.15.0-generic',
|
||||
osImage: 'Ubuntu 22.04.3 LTS',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// A non-GPU node — no labels, no gpu.intel.com capacity
|
||||
const nonGpuNodeRaw = {
|
||||
kind: 'Node',
|
||||
metadata: {
|
||||
name: 'plain-node-1',
|
||||
labels: {},
|
||||
},
|
||||
status: {
|
||||
capacity: { cpu: '4', memory: '8Gi' },
|
||||
allocatable: { cpu: '4', memory: '8Gi' },
|
||||
},
|
||||
};
|
||||
|
||||
describe('NodeDetailSection', () => {
|
||||
it('renders nothing for a non-GPU node (no Intel GPU labels or capacity)', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext());
|
||||
const { container } = render(<NodeDetailSection resource={nonGpuNodeRaw} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders nothing for a non-GPU node passed via jsonData wrapper', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext());
|
||||
const { container } = render(<NodeDetailSection resource={{ jsonData: nonGpuNodeRaw }} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders "Intel GPU" section for a GPU node provided via jsonData', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false, gpuPods: [] }));
|
||||
render(<NodeDetailSection resource={{ jsonData: gpuNodeRaw }} />);
|
||||
expect(screen.getByText('Intel GPU')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "Intel GPU" section for a GPU node provided directly', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false, gpuPods: [] }));
|
||||
render(<NodeDetailSection resource={gpuNodeRaw} />);
|
||||
expect(screen.getByText('Intel GPU')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders capacity and allocatable rows', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false, gpuPods: [] }));
|
||||
render(<NodeDetailSection resource={gpuNodeRaw} />);
|
||||
// GPU (i915) capacity and allocatable rows
|
||||
expect(screen.getByText('GPU (i915) (capacity)')).toBeInTheDocument();
|
||||
expect(screen.getByText('GPU (i915) (allocatable)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "None" for GPU Workload Pods when no pods are on the node and not loading', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false, gpuPods: [] }));
|
||||
render(<NodeDetailSection resource={gpuNodeRaw} />);
|
||||
expect(screen.getByText('None')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Loading…" for GPU Workload Pods when context is loading', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: true, gpuPods: [] }));
|
||||
render(<NodeDetailSection resource={gpuNodeRaw} />);
|
||||
expect(screen.getByText('Loading…')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('lists pod names when GPU pods are scheduled on the node', () => {
|
||||
const gpuPod: IntelGpuPod = {
|
||||
metadata: { name: 'my-gpu-pod', namespace: 'default', uid: 'uid-pod-1' },
|
||||
spec: {
|
||||
nodeName: 'gpu-node-1',
|
||||
containers: [{ name: 'main', resources: { requests: { 'gpu.intel.com/i915': '1' } } }],
|
||||
},
|
||||
status: { phase: 'Running' },
|
||||
};
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({ loading: false, gpuPods: [gpuPod] })
|
||||
);
|
||||
render(<NodeDetailSection resource={gpuNodeRaw} />);
|
||||
expect(screen.getByText('my-gpu-pod')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { IntelGpuContextValue, useIntelGpuContext } from '../api/IntelGpuDataContext';
|
||||
import { IntelGpuNode } from '../api/k8s';
|
||||
import NodesPage from './NodesPage';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||
Loader: ({ title }: { title: string }) => <div data-testid="loader">{title}</div>,
|
||||
SectionBox: ({ title, children }: { title: string; children?: React.ReactNode }) => (
|
||||
<section>
|
||||
<h2>{title}</h2>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
SectionHeader: ({ title }: { title: string }) => <h1>{title}</h1>,
|
||||
NameValueTable: ({
|
||||
rows,
|
||||
}: {
|
||||
rows: Array<{ name: React.ReactNode; value: React.ReactNode }>;
|
||||
}) => (
|
||||
<dl>
|
||||
{rows.map((r, i) => (
|
||||
<div key={i}>
|
||||
<dt>{r.name}</dt>
|
||||
<dd>{r.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
),
|
||||
SimpleTable: ({
|
||||
columns,
|
||||
data,
|
||||
}: {
|
||||
columns: Array<{ label: string; getter: (item: unknown) => React.ReactNode }>;
|
||||
data: unknown[];
|
||||
}) => (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(c => (
|
||||
<th key={c.label}>{c.label}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item, i) => (
|
||||
<tr key={i}>
|
||||
{columns.map(c => (
|
||||
<td key={c.label}>{c.getter(item)}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
),
|
||||
StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => (
|
||||
<span data-status={status}>{children}</span>
|
||||
),
|
||||
PercentageBar: () => <div data-testid="percentage-bar" />,
|
||||
}));
|
||||
|
||||
vi.mock('../api/IntelGpuDataContext', () => ({
|
||||
useIntelGpuContext: vi.fn(),
|
||||
}));
|
||||
|
||||
function makeContext(overrides: Partial<IntelGpuContextValue> = {}): IntelGpuContextValue {
|
||||
return {
|
||||
devicePlugins: [],
|
||||
pluginInstalled: false,
|
||||
gpuNodes: [],
|
||||
gpuPods: [],
|
||||
pluginPods: [],
|
||||
crdAvailable: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const gpuNode: IntelGpuNode = {
|
||||
metadata: {
|
||||
name: 'gpu-node-1',
|
||||
uid: 'uid-001',
|
||||
labels: { 'intel.feature.node.kubernetes.io/gpu': 'true' },
|
||||
creationTimestamp: '2025-01-01T00:00:00Z',
|
||||
},
|
||||
status: {
|
||||
capacity: { 'gpu.intel.com/i915': '2', cpu: '8' },
|
||||
allocatable: { 'gpu.intel.com/i915': '2', cpu: '8' },
|
||||
conditions: [{ type: 'Ready', status: 'True' }],
|
||||
nodeInfo: {
|
||||
osImage: 'Ubuntu 22.04',
|
||||
kernelVersion: '5.15.0',
|
||||
kubeletVersion: 'v1.28.0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('NodesPage', () => {
|
||||
it('shows loader when loading=true', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: true }));
|
||||
render(<NodesPage />);
|
||||
expect(screen.getByTestId('loader')).toHaveTextContent('Loading GPU node data...');
|
||||
});
|
||||
|
||||
it('shows "No GPU Nodes Found" when gpuNodes is empty', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false, gpuNodes: [] }));
|
||||
render(<NodesPage />);
|
||||
expect(screen.getByText('No GPU Nodes Found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "GPU Node Summary" section and per-node detail card when gpuNodes present', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({ loading: false, gpuNodes: [gpuNode] })
|
||||
);
|
||||
render(<NodesPage />);
|
||||
expect(screen.getByText('GPU Node Summary')).toBeInTheDocument();
|
||||
// Node name appears in both the summary table and the detail card section header
|
||||
expect(screen.getAllByText('gpu-node-1').length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('renders a detail card for each GPU node', () => {
|
||||
const secondNode: IntelGpuNode = {
|
||||
metadata: {
|
||||
name: 'gpu-node-2',
|
||||
uid: 'uid-002',
|
||||
labels: { 'intel.feature.node.kubernetes.io/gpu': 'true' },
|
||||
},
|
||||
status: {
|
||||
capacity: { 'gpu.intel.com/i915': '1' },
|
||||
allocatable: { 'gpu.intel.com/i915': '1' },
|
||||
conditions: [{ type: 'Ready', status: 'True' }],
|
||||
},
|
||||
};
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({ loading: false, gpuNodes: [gpuNode, secondNode] })
|
||||
);
|
||||
render(<NodesPage />);
|
||||
// Node names appear in both the summary table cell and the detail card heading
|
||||
expect(screen.getAllByText('gpu-node-1').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText('gpu-node-2').length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('shows error section when error is set', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({ loading: false, error: 'node fetch failed', gpuNodes: [] })
|
||||
);
|
||||
render(<NodesPage />);
|
||||
expect(screen.getByText('node fetch failed')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,198 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { IntelGpuContextValue, useIntelGpuContext } from '../api/IntelGpuDataContext';
|
||||
import { GpuDevicePlugin, IntelGpuNode, IntelGpuPod } from '../api/k8s';
|
||||
import OverviewPage from './OverviewPage';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||
Loader: ({ title }: { title: string }) => <div data-testid="loader">{title}</div>,
|
||||
SectionBox: ({ title, children }: { title: string; children?: React.ReactNode }) => (
|
||||
<section>
|
||||
<h2>{title}</h2>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
SectionHeader: ({ title }: { title: string }) => <h1>{title}</h1>,
|
||||
NameValueTable: ({
|
||||
rows,
|
||||
}: {
|
||||
rows: Array<{ name: React.ReactNode; value: React.ReactNode }>;
|
||||
}) => (
|
||||
<dl>
|
||||
{rows.map((r, i) => (
|
||||
<div key={i}>
|
||||
<dt>{r.name}</dt>
|
||||
<dd>{r.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
),
|
||||
SimpleTable: ({
|
||||
columns,
|
||||
data,
|
||||
}: {
|
||||
columns: Array<{ label: string; getter: (item: unknown) => React.ReactNode }>;
|
||||
data: unknown[];
|
||||
}) => (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(c => (
|
||||
<th key={c.label}>{c.label}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item, i) => (
|
||||
<tr key={i}>
|
||||
{columns.map(c => (
|
||||
<td key={c.label}>{c.getter(item)}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
),
|
||||
StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => (
|
||||
<span data-status={status}>{children}</span>
|
||||
),
|
||||
PercentageBar: () => <div data-testid="percentage-bar" />,
|
||||
}));
|
||||
|
||||
vi.mock('../api/IntelGpuDataContext', () => ({
|
||||
useIntelGpuContext: vi.fn(),
|
||||
}));
|
||||
|
||||
function makeContext(overrides: Partial<IntelGpuContextValue> = {}): IntelGpuContextValue {
|
||||
return {
|
||||
devicePlugins: [],
|
||||
pluginInstalled: false,
|
||||
gpuNodes: [],
|
||||
gpuPods: [],
|
||||
pluginPods: [],
|
||||
crdAvailable: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('OverviewPage', () => {
|
||||
it('shows loader when loading=true', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: true }));
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByTestId('loader')).toHaveTextContent('Loading Intel GPU data...');
|
||||
});
|
||||
|
||||
it('shows "Plugin Not Detected" when not loading, no plugin installed, no nodes', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({ loading: false, pluginInstalled: false, gpuNodes: [] })
|
||||
);
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText('Plugin Not Detected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error content when error is set', () => {
|
||||
const errorMsg = 'something went wrong';
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({ loading: false, error: errorMsg, pluginInstalled: true })
|
||||
);
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText(errorMsg)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Intel GPU — Overview" heading when gpuNodes present and pluginInstalled', () => {
|
||||
const node: IntelGpuNode = {
|
||||
metadata: {
|
||||
name: 'gpu-node-1',
|
||||
labels: { 'intel.feature.node.kubernetes.io/gpu': 'true' },
|
||||
},
|
||||
status: {
|
||||
capacity: { 'gpu.intel.com/i915': '1' },
|
||||
allocatable: { 'gpu.intel.com/i915': '1' },
|
||||
},
|
||||
};
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({ loading: false, pluginInstalled: true, gpuNodes: [node] })
|
||||
);
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText('Intel GPU — Overview')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls refresh() when refresh button is clicked', () => {
|
||||
const refresh = vi.fn();
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({ loading: false, pluginInstalled: true, refresh })
|
||||
);
|
||||
render(<OverviewPage />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /refresh intel gpu data/i }));
|
||||
expect(refresh).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows CRD notice when crdAvailable=false and pluginInstalled=true', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({ loading: false, pluginInstalled: true, crdAvailable: false })
|
||||
);
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText('Notice')).toBeInTheDocument();
|
||||
expect(screen.getByText(/GpuDevicePlugin CRD not found/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Device Plugin Status" table when crdAvailable=true and devicePlugins present', () => {
|
||||
const plugin: GpuDevicePlugin = {
|
||||
kind: 'GpuDevicePlugin',
|
||||
metadata: { name: 'my-plugin', uid: 'uid-1' },
|
||||
spec: { enableMonitoring: true, sharedDevNum: 2 },
|
||||
status: { desiredNumberScheduled: 1, numberReady: 1 },
|
||||
};
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({
|
||||
loading: false,
|
||||
pluginInstalled: true,
|
||||
crdAvailable: true,
|
||||
devicePlugins: [plugin],
|
||||
})
|
||||
);
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText('Device Plugin Status')).toBeInTheDocument();
|
||||
expect(screen.getByText('my-plugin')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Plugin Daemon Pods" table when pluginPods present', () => {
|
||||
const pod: IntelGpuPod = {
|
||||
metadata: { name: 'plugin-pod-1', namespace: 'kube-system', uid: 'uid-pp-1' },
|
||||
spec: { nodeName: 'node-1' },
|
||||
status: { phase: 'Running' },
|
||||
};
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({ loading: false, pluginInstalled: true, pluginPods: [pod] })
|
||||
);
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText('Plugin Daemon Pods')).toBeInTheDocument();
|
||||
expect(screen.getByText('plugin-pod-1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Active GPU Pods" table when running GPU pods exist', () => {
|
||||
const pod: IntelGpuPod = {
|
||||
metadata: { name: 'workload-pod-1', namespace: 'default', uid: 'uid-wp-1' },
|
||||
spec: {
|
||||
nodeName: 'gpu-node-1',
|
||||
containers: [
|
||||
{
|
||||
name: 'main',
|
||||
resources: { requests: { 'gpu.intel.com/i915': '1' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
status: { phase: 'Running' },
|
||||
};
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({ loading: false, pluginInstalled: true, gpuPods: [pod] })
|
||||
);
|
||||
render(<OverviewPage />);
|
||||
expect(screen.getByText('Active GPU Pods')).toBeInTheDocument();
|
||||
expect(screen.getByText('workload-pod-1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import PodDetailSection from './PodDetailSection';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||
SectionBox: ({ title, children }: { title: string; children?: React.ReactNode }) => (
|
||||
<section>
|
||||
<h2>{title}</h2>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
NameValueTable: ({
|
||||
rows,
|
||||
}: {
|
||||
rows: Array<{ name: React.ReactNode; value: React.ReactNode }>;
|
||||
}) => (
|
||||
<dl>
|
||||
{rows.map((r, i) => (
|
||||
<div key={i}>
|
||||
<dt>{r.name}</dt>
|
||||
<dd>{r.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
),
|
||||
StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => (
|
||||
<span data-status={status}>{children}</span>
|
||||
),
|
||||
}));
|
||||
|
||||
// PodDetailSection does NOT use the context — no need to mock IntelGpuDataContext
|
||||
|
||||
// A non-GPU pod (no gpu.intel.com resources)
|
||||
const nonGpuPodRaw = {
|
||||
kind: 'Pod',
|
||||
metadata: { name: 'plain-pod', namespace: 'default' },
|
||||
spec: {
|
||||
containers: [{ name: 'main', resources: { requests: { cpu: '100m', memory: '128Mi' } } }],
|
||||
},
|
||||
status: { phase: 'Running' },
|
||||
};
|
||||
|
||||
// A GPU-requesting pod
|
||||
const gpuPodRaw = {
|
||||
kind: 'Pod',
|
||||
metadata: { name: 'gpu-workload', namespace: 'default' },
|
||||
spec: {
|
||||
nodeName: 'gpu-node-1',
|
||||
containers: [
|
||||
{
|
||||
name: 'trainer',
|
||||
resources: {
|
||||
requests: { 'gpu.intel.com/i915': '1', cpu: '2' },
|
||||
limits: { 'gpu.intel.com/i915': '1', cpu: '2' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
status: { phase: 'Running' },
|
||||
};
|
||||
|
||||
// A pod with limits only (no requests)
|
||||
const gpuPodLimitsOnly = {
|
||||
kind: 'Pod',
|
||||
metadata: { name: 'limits-only-pod', namespace: 'default' },
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'app',
|
||||
resources: {
|
||||
limits: { 'gpu.intel.com/i915': '1' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
status: { phase: 'Pending' },
|
||||
};
|
||||
|
||||
describe('PodDetailSection', () => {
|
||||
it('renders nothing for a non-GPU pod', () => {
|
||||
const { container } = render(<PodDetailSection resource={nonGpuPodRaw} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders nothing for a non-GPU pod passed via jsonData', () => {
|
||||
const { container } = render(<PodDetailSection resource={{ jsonData: nonGpuPodRaw }} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders "Intel GPU Resources" section for a GPU-requesting pod via jsonData', () => {
|
||||
render(<PodDetailSection resource={{ jsonData: gpuPodRaw }} />);
|
||||
expect(screen.getByText('Intel GPU Resources')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "Intel GPU Resources" section for a GPU-requesting pod provided directly', () => {
|
||||
render(<PodDetailSection resource={gpuPodRaw} />);
|
||||
expect(screen.getByText('Intel GPU Resources')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows container GPU resource request rows', () => {
|
||||
render(<PodDetailSection resource={gpuPodRaw} />);
|
||||
// Row label: "{containerName} → {resourceName} request"
|
||||
expect(screen.getByText('trainer → GPU (i915) request')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows phase status label for Running phase', () => {
|
||||
render(<PodDetailSection resource={gpuPodRaw} />);
|
||||
const statusEl = screen.getByText('Running');
|
||||
expect(statusEl).toHaveAttribute('data-status', 'success');
|
||||
});
|
||||
|
||||
it('shows phase status label for Pending phase', () => {
|
||||
render(<PodDetailSection resource={gpuPodLimitsOnly} />);
|
||||
const statusEl = screen.getByText('Pending');
|
||||
expect(statusEl).toHaveAttribute('data-status', 'warning');
|
||||
});
|
||||
|
||||
it('still renders when a container has limits only and no requests', () => {
|
||||
render(<PodDetailSection resource={gpuPodLimitsOnly} />);
|
||||
expect(screen.getByText('Intel GPU Resources')).toBeInTheDocument();
|
||||
// limits-only pod: the request row should show '—' since requests key is absent
|
||||
expect(screen.getByText('app → GPU (i915) request')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows scheduled node name', () => {
|
||||
render(<PodDetailSection resource={gpuPodRaw} />);
|
||||
expect(screen.getByText('gpu-node-1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows GPU container count', () => {
|
||||
render(<PodDetailSection resource={gpuPodRaw} />);
|
||||
const label = screen.getByText('GPU Containers');
|
||||
expect(label).toBeInTheDocument();
|
||||
// The value '1' is rendered in the sibling <dd>; verify via parent row
|
||||
expect(label.closest('div')).toHaveTextContent('1');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { IntelGpuContextValue, useIntelGpuContext } from '../api/IntelGpuDataContext';
|
||||
import { IntelGpuPod } from '../api/k8s';
|
||||
import PodsPage from './PodsPage';
|
||||
|
||||
vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({
|
||||
Loader: ({ title }: { title: string }) => <div data-testid="loader">{title}</div>,
|
||||
SectionBox: ({ title, children }: { title: string; children?: React.ReactNode }) => (
|
||||
<section>
|
||||
<h2>{title}</h2>
|
||||
{children}
|
||||
</section>
|
||||
),
|
||||
SectionHeader: ({ title }: { title: string }) => <h1>{title}</h1>,
|
||||
NameValueTable: ({
|
||||
rows,
|
||||
}: {
|
||||
rows: Array<{ name: React.ReactNode; value: React.ReactNode }>;
|
||||
}) => (
|
||||
<dl>
|
||||
{rows.map((r, i) => (
|
||||
<div key={i}>
|
||||
<dt>{r.name}</dt>
|
||||
<dd>{r.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
),
|
||||
SimpleTable: ({
|
||||
columns,
|
||||
data,
|
||||
}: {
|
||||
columns: Array<{ label: string; getter: (item: unknown) => React.ReactNode }>;
|
||||
data: unknown[];
|
||||
}) => (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(c => (
|
||||
<th key={c.label}>{c.label}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item, i) => (
|
||||
<tr key={i}>
|
||||
{columns.map(c => (
|
||||
<td key={c.label}>{c.getter(item)}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
),
|
||||
StatusLabel: ({ status, children }: { status: string; children?: React.ReactNode }) => (
|
||||
<span data-status={status}>{children}</span>
|
||||
),
|
||||
PercentageBar: () => <div data-testid="percentage-bar" />,
|
||||
}));
|
||||
|
||||
vi.mock('../api/IntelGpuDataContext', () => ({
|
||||
useIntelGpuContext: vi.fn(),
|
||||
}));
|
||||
|
||||
function makeContext(overrides: Partial<IntelGpuContextValue> = {}): IntelGpuContextValue {
|
||||
return {
|
||||
devicePlugins: [],
|
||||
pluginInstalled: false,
|
||||
gpuNodes: [],
|
||||
gpuPods: [],
|
||||
pluginPods: [],
|
||||
crdAvailable: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
refresh: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeRunningPod(name: string): IntelGpuPod {
|
||||
return {
|
||||
metadata: { name, namespace: 'default', uid: `uid-${name}` },
|
||||
spec: {
|
||||
nodeName: 'gpu-node-1',
|
||||
containers: [
|
||||
{
|
||||
name: 'main',
|
||||
resources: { requests: { 'gpu.intel.com/i915': '1' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
status: { phase: 'Running' },
|
||||
};
|
||||
}
|
||||
|
||||
function makePendingPod(name: string): IntelGpuPod {
|
||||
return {
|
||||
metadata: { name, namespace: 'default', uid: `uid-${name}` },
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'main',
|
||||
resources: { requests: { 'gpu.intel.com/i915': '1' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
status: {
|
||||
phase: 'Pending',
|
||||
containerStatuses: [
|
||||
{
|
||||
name: 'main',
|
||||
ready: false,
|
||||
restartCount: 0,
|
||||
state: { waiting: { reason: 'Unschedulable' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('PodsPage', () => {
|
||||
it('shows loader when loading=true', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: true }));
|
||||
render(<PodsPage />);
|
||||
expect(screen.getByTestId('loader')).toHaveTextContent('Loading GPU pod data...');
|
||||
});
|
||||
|
||||
it('shows "No GPU Pods Found" when gpuPods is empty', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false, gpuPods: [] }));
|
||||
render(<PodsPage />);
|
||||
expect(screen.getByText('No GPU Pods Found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows summary section with total count when pods present', () => {
|
||||
const pods = [makeRunningPod('pod-1'), makeRunningPod('pod-2')];
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false, gpuPods: pods }));
|
||||
render(<PodsPage />);
|
||||
expect(screen.getByText('Summary')).toBeInTheDocument();
|
||||
// 'Total GPU Pods' label is present; '2' appears in multiple places (row value + status label)
|
||||
expect(screen.getByText('Total GPU Pods')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('2').length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('shows "Attention: Pending GPU Pods" section when pending pods exist', () => {
|
||||
const pods = [makePendingPod('pending-pod-1')];
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false, gpuPods: pods }));
|
||||
render(<PodsPage />);
|
||||
expect(screen.getByText('Attention: Pending GPU Pods')).toBeInTheDocument();
|
||||
// Pod name appears in both the main "All GPU Pods" table and the pending attention table
|
||||
expect(screen.getAllByText('pending-pod-1').length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('shows error section when error is set', () => {
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(
|
||||
makeContext({ loading: false, error: 'pod list failed', gpuPods: [] })
|
||||
);
|
||||
render(<PodsPage />);
|
||||
expect(screen.getByText('pod list failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "All GPU Pods" table with pod name when pods present', () => {
|
||||
const pods = [makeRunningPod('my-workload')];
|
||||
vi.mocked(useIntelGpuContext).mockReturnValue(makeContext({ loading: false, gpuPods: pods }));
|
||||
render(<PodsPage />);
|
||||
expect(screen.getByText('All GPU Pods')).toBeInTheDocument();
|
||||
expect(screen.getByText('my-workload')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+34
-34
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* headlamp-intel-gpu-plugin — entry point.
|
||||
* intel-gpu-plugin — entry point.
|
||||
*
|
||||
* Registers sidebar entries, routes, detail view sections, and table column
|
||||
* processors for Intel GPU device plugin visibility in Headlamp.
|
||||
@@ -34,49 +34,49 @@ import PodsPage from './components/PodsPage';
|
||||
|
||||
registerSidebarEntry({
|
||||
parent: null,
|
||||
name: 'headlamp-intel-gpu',
|
||||
label: 'headlamp-intel-gpu',
|
||||
url: '/headlamp-intel-gpu',
|
||||
name: 'intel-gpu',
|
||||
label: 'intel-gpu',
|
||||
url: '/intel-gpu',
|
||||
icon: 'mdi:gpu',
|
||||
});
|
||||
|
||||
registerSidebarEntry({
|
||||
parent: 'headlamp-intel-gpu',
|
||||
name: 'headlamp-intel-gpu-overview',
|
||||
parent: 'intel-gpu',
|
||||
name: 'intel-gpu-overview',
|
||||
label: 'Overview',
|
||||
url: '/headlamp-intel-gpu',
|
||||
url: '/intel-gpu',
|
||||
icon: 'mdi:view-dashboard',
|
||||
});
|
||||
|
||||
registerSidebarEntry({
|
||||
parent: 'headlamp-intel-gpu',
|
||||
name: 'headlamp-intel-gpu-device-plugins',
|
||||
parent: 'intel-gpu',
|
||||
name: 'intel-gpu-device-plugins',
|
||||
label: 'Device Plugins',
|
||||
url: '/headlamp-intel-gpu/device-plugins',
|
||||
url: '/intel-gpu/device-plugins',
|
||||
icon: 'mdi:chip',
|
||||
});
|
||||
|
||||
registerSidebarEntry({
|
||||
parent: 'headlamp-intel-gpu',
|
||||
name: 'headlamp-intel-gpu-nodes',
|
||||
parent: 'intel-gpu',
|
||||
name: 'intel-gpu-nodes',
|
||||
label: 'GPU Nodes',
|
||||
url: '/headlamp-intel-gpu/nodes',
|
||||
url: '/intel-gpu/nodes',
|
||||
icon: 'mdi:server',
|
||||
});
|
||||
|
||||
registerSidebarEntry({
|
||||
parent: 'headlamp-intel-gpu',
|
||||
name: 'headlamp-intel-gpu-pods',
|
||||
parent: 'intel-gpu',
|
||||
name: 'intel-gpu-pods',
|
||||
label: 'GPU Pods',
|
||||
url: '/headlamp-intel-gpu/pods',
|
||||
url: '/intel-gpu/pods',
|
||||
icon: 'mdi:cube-outline',
|
||||
});
|
||||
|
||||
registerSidebarEntry({
|
||||
parent: 'headlamp-intel-gpu',
|
||||
name: 'headlamp-intel-gpu-metrics',
|
||||
parent: 'intel-gpu',
|
||||
name: 'intel-gpu-metrics',
|
||||
label: 'Metrics',
|
||||
url: '/headlamp-intel-gpu/metrics',
|
||||
url: '/intel-gpu/metrics',
|
||||
icon: 'mdi:chart-line',
|
||||
});
|
||||
|
||||
@@ -85,9 +85,9 @@ registerSidebarEntry({
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
registerRoute({
|
||||
path: '/headlamp-intel-gpu',
|
||||
sidebar: 'headlamp-intel-gpu-overview',
|
||||
name: 'headlamp-intel-gpu-overview',
|
||||
path: '/intel-gpu',
|
||||
sidebar: 'intel-gpu-overview',
|
||||
name: 'intel-gpu-overview',
|
||||
exact: true,
|
||||
component: () => (
|
||||
<IntelGpuDataProvider>
|
||||
@@ -97,9 +97,9 @@ registerRoute({
|
||||
});
|
||||
|
||||
registerRoute({
|
||||
path: '/headlamp-intel-gpu/device-plugins',
|
||||
sidebar: 'headlamp-intel-gpu-device-plugins',
|
||||
name: 'headlamp-intel-gpu-device-plugins',
|
||||
path: '/intel-gpu/device-plugins',
|
||||
sidebar: 'intel-gpu-device-plugins',
|
||||
name: 'intel-gpu-device-plugins',
|
||||
exact: true,
|
||||
component: () => (
|
||||
<IntelGpuDataProvider>
|
||||
@@ -109,9 +109,9 @@ registerRoute({
|
||||
});
|
||||
|
||||
registerRoute({
|
||||
path: '/headlamp-intel-gpu/nodes',
|
||||
sidebar: 'headlamp-intel-gpu-nodes',
|
||||
name: 'headlamp-intel-gpu-nodes',
|
||||
path: '/intel-gpu/nodes',
|
||||
sidebar: 'intel-gpu-nodes',
|
||||
name: 'intel-gpu-nodes',
|
||||
exact: true,
|
||||
component: () => (
|
||||
<IntelGpuDataProvider>
|
||||
@@ -121,9 +121,9 @@ registerRoute({
|
||||
});
|
||||
|
||||
registerRoute({
|
||||
path: '/headlamp-intel-gpu/pods',
|
||||
sidebar: 'headlamp-intel-gpu-pods',
|
||||
name: 'headlamp-intel-gpu-pods',
|
||||
path: '/intel-gpu/pods',
|
||||
sidebar: 'intel-gpu-pods',
|
||||
name: 'intel-gpu-pods',
|
||||
exact: true,
|
||||
component: () => (
|
||||
<IntelGpuDataProvider>
|
||||
@@ -133,9 +133,9 @@ registerRoute({
|
||||
});
|
||||
|
||||
registerRoute({
|
||||
path: '/headlamp-intel-gpu/metrics',
|
||||
sidebar: 'headlamp-intel-gpu-metrics',
|
||||
name: 'headlamp-intel-gpu-metrics',
|
||||
path: '/intel-gpu/metrics',
|
||||
sidebar: 'intel-gpu-metrics',
|
||||
name: 'intel-gpu-metrics',
|
||||
exact: true,
|
||||
component: () => (
|
||||
<IntelGpuDataProvider>
|
||||
|
||||
@@ -6,5 +6,8 @@ export default defineConfig({
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./vitest.setup.ts'],
|
||||
exclude: ['e2e/**', 'node_modules/**'],
|
||||
env: {
|
||||
NODE_ENV: 'test',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user