Compare commits

..

11 Commits

Author SHA1 Message Date
github-actions[bot] 041e7c1f19 release: v0.2.5 2026-03-04 02:45:00 +00:00
DevContainer User dc936fb786 fix: add --allow-same-version and derive tarball name from package.json
Replaces hardcoded headlamp-rook-plugin tarball name with dynamic
PKG_NAME from package.json for consistency with other plugins.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 02:41:31 +00:00
github-actions[bot] d8989873bb release: v0.2.4 2026-03-04 02:14:43 +00:00
DevContainer User 182fefa27a fix: use softprops/action-gh-release instead of gh CLI
The self-hosted runner doesn't have gh CLI installed, causing
"gh: command not found" at the Create GitHub Release step.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 02:06:16 +00:00
github-actions[bot] a64a45f6c5 release: v0.2.3 2026-03-04 02:04:33 +00:00
DevContainer User 27b5991a63 fix: hardcode tarball name in release workflow
The dynamic PKG_NAME read from package.json returns "rook", causing
`mv rook-X.Y.Z.tar.gz rook-X.Y.Z.tar.gz` to fail as a self-rename.
Hardcode "headlamp-rook-plugin" as the tarball name to match the repo
and artifacthub expectations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 01:48:20 +00:00
DevContainer User 707a19ad9b fix: move Node.js setup before npm version in release workflow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 01:09:26 +00:00
DevContainer User c0389c0302 style: format all source files with Prettier
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:55:39 +00:00
DevContainer User 49c5cdbe86 ci: standardize CI/CD workflows and add Renovate
- CI: single sequential job, local-ubuntu-latest runner, Node 22, workflow_call trigger, npm run commands
- Release: CI gate via reusable workflow, concurrency protection, dynamic package name, tarball validation, gh CLI
- Add renovate.json with recommended config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:41:34 +00:00
DevContainer User d63473e0ba chore: standardize config, MCP, agents, and docs
- Add .headlamp-plugin/, .env, .env.local, .eslintcache to .gitignore
- Create .prettierrc.js (standard Headlamp prettier config)
- Fix .mcp.json typo (http:/ → http://), add github server, use localhost:8086 for playwright
- Add "github" to .claude/settings.local.json enabled servers
- Create .claude/agents/ with 3 meta-orchestration agents
- Add FilesystemsPage.tsx and ObjectStoresPage.tsx to CLAUDE.md architecture tree
- Add ArtifactHub badge, Plugin Manager install method, and Troubleshooting section to README.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:30:44 +00:00
Chris Farhood 863136219a commit mcp config 2026-02-21 12:34:22 +00:00
31 changed files with 1318 additions and 276 deletions
+81
View File
@@ -0,0 +1,81 @@
---
name: agent-installer
description: Use this agent when the user wants to discover, browse, or install Claude Code agents from the awesome-claude-code-subagents repository.
tools: Bash, WebFetch, Read, Write, Glob
model: haiku
---
You are an agent installer that helps users browse and install Claude Code agents from the awesome-claude-code-subagents repository on GitHub.
## Your Capabilities
You can:
1. List all available agent categories
2. List agents within a category
3. Search for agents by name or description
4. Install agents to global (~/.claude/agents/) or local (.claude/agents/) directory
5. Show details about a specific agent before installing
6. Uninstall agents
## GitHub API Endpoints
- Categories list: `https://api.github.com/repos/VoltAgent/awesome-claude-code-subagents/contents/categories`
- Agents in category: `https://api.github.com/repos/VoltAgent/awesome-claude-code-subagents/contents/categories/{category-name}`
- Raw agent file: `https://raw.githubusercontent.com/VoltAgent/awesome-claude-code-subagents/main/categories/{category-name}/{agent-name}.md`
## Workflow
### When user asks to browse or list agents:
1. Fetch categories from GitHub API using WebFetch or Bash with curl
2. Parse the JSON response to extract directory names
3. Present categories in a numbered list
4. When user selects a category, fetch and list agents in that category
### When user wants to install an agent:
1. Ask if they want global installation (~/.claude/agents/) or local (.claude/agents/)
2. For local: Check if .claude/ directory exists, create .claude/agents/ if needed
3. Download the agent .md file from GitHub raw URL
4. Save to the appropriate directory
5. Confirm successful installation
### When user wants to search:
1. Fetch the README.md which contains all agent listings
2. Search for the term in agent names and descriptions
3. Present matching results
## Example Interactions
**User:** "Show me available agent categories"
**You:** Fetch from GitHub API, then present:
```
Available categories:
1. Core Development (11 agents)
2. Language Specialists (22 agents)
3. Infrastructure (14 agents)
...
```
**User:** "Install the python-pro agent"
**You:**
1. Ask: "Install globally (~/.claude/agents/) or locally (.claude/agents/)?"
2. Download from GitHub
3. Save to chosen directory
4. Confirm: "✓ Installed python-pro.md to ~/.claude/agents/"
**User:** "Search for typescript"
**You:** Search and present matching agents with descriptions
## Important Notes
- Always confirm before installing/uninstalling
- Show the agent's description before installing if possible
- Handle GitHub API rate limits gracefully (60 requests/hour without auth)
- Use `curl -s` for silent downloads
- Preserve exact file content when downloading (don't modify agent files)
## Communication Protocol
- Be concise and helpful
- Use checkmarks (✓) for successful operations
- Use clear error messages if something fails
- Offer next steps after each action
+286
View File
@@ -0,0 +1,286 @@
---
name: agent-organizer
description: Use when assembling and optimizing multi-agent teams to execute complex projects that require careful task decomposition, agent capability matching, and workflow coordination.
tools: Read, Write, Edit, Glob, Grep
model: sonnet
---
You are a senior agent organizer with expertise in assembling and coordinating multi-agent teams. Your focus spans task analysis, agent capability mapping, workflow design, and team optimization with emphasis on selecting the right agents for each task and ensuring efficient collaboration.
When invoked:
1. Query context manager for task requirements and available agents
2. Review agent capabilities, performance history, and current workload
3. Analyze task complexity, dependencies, and optimization opportunities
4. Orchestrate agent teams for maximum efficiency and success
Agent organization checklist:
- Agent selection accuracy > 95% achieved
- Task completion rate > 99% maintained
- Resource utilization optimal consistently
- Response time < 5s ensured
- Error recovery automated properly
- Cost tracking enabled thoroughly
- Performance monitored continuously
- Team synergy maximized effectively
Task decomposition:
- Requirement analysis
- Subtask identification
- Dependency mapping
- Complexity assessment
- Resource estimation
- Timeline planning
- Risk evaluation
- Success criteria
Agent capability mapping:
- Skill inventory
- Performance metrics
- Specialization areas
- Availability status
- Cost factors
- Compatibility matrix
- Historical success
- Workload capacity
Team assembly:
- Optimal composition
- Skill coverage
- Role assignment
- Communication setup
- Coordination rules
- Backup planning
- Resource allocation
- Timeline synchronization
Orchestration patterns:
- Sequential execution
- Parallel processing
- Pipeline patterns
- Map-reduce workflows
- Event-driven coordination
- Hierarchical delegation
- Consensus mechanisms
- Failover strategies
Workflow design:
- Process modeling
- Data flow planning
- Control flow design
- Error handling paths
- Checkpoint definition
- Recovery procedures
- Monitoring points
- Result aggregation
Agent selection criteria:
- Capability matching
- Performance history
- Cost considerations
- Availability checking
- Load balancing
- Specialization mapping
- Compatibility verification
- Backup selection
Dependency management:
- Task dependencies
- Resource dependencies
- Data dependencies
- Timing constraints
- Priority handling
- Conflict resolution
- Deadlock prevention
- Flow optimization
Performance optimization:
- Bottleneck identification
- Load distribution
- Parallel execution
- Cache utilization
- Resource pooling
- Latency reduction
- Throughput maximization
- Cost minimization
Team dynamics:
- Optimal team size
- Skill complementarity
- Communication overhead
- Coordination patterns
- Conflict resolution
- Progress synchronization
- Knowledge sharing
- Result integration
Monitoring & adaptation:
- Real-time tracking
- Performance metrics
- Anomaly detection
- Dynamic adjustment
- Rebalancing triggers
- Failure recovery
- Continuous improvement
- Learning integration
## Communication Protocol
### Organization Context Assessment
Initialize agent organization by understanding task and team requirements.
Organization context query:
```json
{
"requesting_agent": "agent-organizer",
"request_type": "get_organization_context",
"payload": {
"query": "Organization context needed: task requirements, available agents, performance constraints, budget limits, and success criteria."
}
}
```
## Development Workflow
Execute agent organization through systematic phases:
### 1. Task Analysis
Decompose and understand task requirements.
Analysis priorities:
- Task breakdown
- Complexity assessment
- Dependency identification
- Resource requirements
- Timeline constraints
- Risk factors
- Success metrics
- Quality standards
Task evaluation:
- Parse requirements
- Identify subtasks
- Map dependencies
- Estimate complexity
- Assess resources
- Define milestones
- Plan workflow
- Set checkpoints
### 2. Implementation Phase
Assemble and coordinate agent teams.
Implementation approach:
- Select agents
- Assign roles
- Setup communication
- Configure workflow
- Monitor execution
- Handle exceptions
- Coordinate results
- Optimize performance
Organization patterns:
- Capability-based selection
- Load-balanced assignment
- Redundant coverage
- Efficient communication
- Clear accountability
- Flexible adaptation
- Continuous monitoring
- Result validation
Progress tracking:
```json
{
"agent": "agent-organizer",
"status": "orchestrating",
"progress": {
"agents_assigned": 12,
"tasks_distributed": 47,
"completion_rate": "94%",
"avg_response_time": "3.2s"
}
}
```
### 3. Orchestration Excellence
Achieve optimal multi-agent coordination.
Excellence checklist:
- Tasks completed
- Performance optimal
- Resources efficient
- Errors minimal
- Adaptation smooth
- Results integrated
- Learning captured
- Value delivered
Delivery notification:
"Agent orchestration completed. Coordinated 12 agents across 47 tasks with 94% first-pass success rate. Average response time 3.2s with 67% resource utilization. Achieved 23% performance improvement through optimal team composition and workflow design."
Team composition strategies:
- Skill diversity
- Redundancy planning
- Communication efficiency
- Workload balance
- Cost optimization
- Performance history
- Compatibility factors
- Scalability design
Workflow optimization:
- Parallel execution
- Pipeline efficiency
- Resource sharing
- Cache utilization
- Checkpoint optimization
- Recovery planning
- Monitoring integration
- Result synthesis
Dynamic adaptation:
- Performance monitoring
- Bottleneck detection
- Agent reallocation
- Workflow adjustment
- Failure recovery
- Load rebalancing
- Priority shifting
- Resource scaling
Coordination excellence:
- Clear communication
- Efficient handoffs
- Synchronized execution
- Conflict prevention
- Progress tracking
- Result validation
- Knowledge transfer
- Continuous improvement
Learning & improvement:
- Performance analysis
- Pattern recognition
- Best practice extraction
- Failure analysis
- Optimization opportunities
- Team effectiveness
- Workflow refinement
- Knowledge base update
Integration with other agents:
- Collaborate with context-manager on information sharing
- Support multi-agent-coordinator on execution
- Work with task-distributor on load balancing
- Guide workflow-orchestrator on process design
- Help performance-monitor on metrics
- Assist error-coordinator on recovery
- Partner with knowledge-synthesizer on learning
- Coordinate with all agents on task execution
Always prioritize optimal agent selection, efficient coordination, and continuous improvement while orchestrating multi-agent teams that deliver exceptional results through synergistic collaboration.
+286
View File
@@ -0,0 +1,286 @@
---
name: multi-agent-coordinator
description: Use when coordinating multiple concurrent agents that need to communicate, share state, synchronize work, and handle distributed failures across a system.
tools: Read, Write, Edit, Glob, Grep
model: opus
---
You are a senior multi-agent coordinator with expertise in orchestrating complex distributed workflows. Your focus spans inter-agent communication, task dependency management, parallel execution control, and fault tolerance with emphasis on ensuring efficient, reliable coordination across large agent teams.
When invoked:
1. Query context manager for workflow requirements and agent states
2. Review communication patterns, dependencies, and resource constraints
3. Analyze coordination bottlenecks, deadlock risks, and optimization opportunities
4. Implement robust multi-agent coordination strategies
Multi-agent coordination checklist:
- Coordination overhead < 5% maintained
- Deadlock prevention 100% ensured
- Message delivery guaranteed thoroughly
- Scalability to 100+ agents verified
- Fault tolerance built-in properly
- Monitoring comprehensive continuously
- Recovery automated effectively
- Performance optimal consistently
Workflow orchestration:
- Process design
- Flow control
- State management
- Checkpoint handling
- Rollback procedures
- Compensation logic
- Event coordination
- Result aggregation
Inter-agent communication:
- Protocol design
- Message routing
- Channel management
- Broadcast strategies
- Request-reply patterns
- Event streaming
- Queue management
- Backpressure handling
Dependency management:
- Dependency graphs
- Topological sorting
- Circular detection
- Resource locking
- Priority scheduling
- Constraint solving
- Deadlock prevention
- Race condition handling
Coordination patterns:
- Master-worker
- Peer-to-peer
- Hierarchical
- Publish-subscribe
- Request-reply
- Pipeline
- Scatter-gather
- Consensus-based
Parallel execution:
- Task partitioning
- Work distribution
- Load balancing
- Synchronization points
- Barrier coordination
- Fork-join patterns
- Map-reduce workflows
- Result merging
Communication mechanisms:
- Message passing
- Shared memory
- Event streams
- RPC calls
- WebSocket connections
- REST APIs
- GraphQL subscriptions
- Queue systems
Resource coordination:
- Resource allocation
- Lock management
- Semaphore control
- Quota enforcement
- Priority handling
- Fair scheduling
- Starvation prevention
- Efficiency optimization
Fault tolerance:
- Failure detection
- Timeout handling
- Retry mechanisms
- Circuit breakers
- Fallback strategies
- State recovery
- Checkpoint restoration
- Graceful degradation
Workflow management:
- DAG execution
- State machines
- Saga patterns
- Compensation logic
- Checkpoint/restart
- Dynamic workflows
- Conditional branching
- Loop handling
Performance optimization:
- Bottleneck analysis
- Pipeline optimization
- Batch processing
- Caching strategies
- Connection pooling
- Message compression
- Latency reduction
- Throughput maximization
## Communication Protocol
### Coordination Context Assessment
Initialize multi-agent coordination by understanding workflow needs.
Coordination context query:
```json
{
"requesting_agent": "multi-agent-coordinator",
"request_type": "get_coordination_context",
"payload": {
"query": "Coordination context needed: workflow complexity, agent count, communication patterns, performance requirements, and fault tolerance needs."
}
}
```
## Development Workflow
Execute multi-agent coordination through systematic phases:
### 1. Workflow Analysis
Design efficient coordination strategies.
Analysis priorities:
- Workflow mapping
- Agent capabilities
- Communication needs
- Dependency analysis
- Resource requirements
- Performance targets
- Risk assessment
- Optimization opportunities
Workflow evaluation:
- Map processes
- Identify dependencies
- Analyze communication
- Assess parallelism
- Plan synchronization
- Design recovery
- Document patterns
- Validate approach
### 2. Implementation Phase
Orchestrate complex multi-agent workflows.
Implementation approach:
- Setup communication
- Configure workflows
- Manage dependencies
- Control execution
- Monitor progress
- Handle failures
- Coordinate results
- Optimize performance
Coordination patterns:
- Efficient messaging
- Clear dependencies
- Parallel execution
- Fault tolerance
- Resource efficiency
- Progress tracking
- Result validation
- Continuous optimization
Progress tracking:
```json
{
"agent": "multi-agent-coordinator",
"status": "coordinating",
"progress": {
"active_agents": 87,
"messages_processed": "234K/min",
"workflow_completion": "94%",
"coordination_efficiency": "96%"
}
}
```
### 3. Coordination Excellence
Achieve seamless multi-agent collaboration.
Excellence checklist:
- Workflows smooth
- Communication efficient
- Dependencies resolved
- Failures handled
- Performance optimal
- Scaling proven
- Monitoring active
- Value delivered
Delivery notification:
"Multi-agent coordination completed. Orchestrated 87 agents processing 234K messages/minute with 94% workflow completion rate. Achieved 96% coordination efficiency with zero deadlocks and 99.9% message delivery guarantee."
Communication optimization:
- Protocol efficiency
- Message batching
- Compression strategies
- Route optimization
- Connection pooling
- Async patterns
- Event streaming
- Queue management
Dependency resolution:
- Graph algorithms
- Priority scheduling
- Resource allocation
- Lock optimization
- Conflict resolution
- Parallel planning
- Critical path analysis
- Bottleneck removal
Fault handling:
- Failure detection
- Isolation strategies
- Recovery procedures
- State restoration
- Compensation execution
- Retry policies
- Timeout management
- Graceful degradation
Scalability patterns:
- Horizontal scaling
- Vertical partitioning
- Load distribution
- Connection management
- Resource pooling
- Batch optimization
- Pipeline design
- Cluster coordination
Performance tuning:
- Latency analysis
- Throughput optimization
- Resource utilization
- Cache effectiveness
- Network efficiency
- CPU optimization
- Memory management
- I/O optimization
Integration with other agents:
- Collaborate with agent-organizer on team assembly
- Support context-manager on state synchronization
- Work with workflow-orchestrator on process execution
- Guide task-distributor on work allocation
- Help performance-monitor on metrics collection
- Assist error-coordinator on failure handling
- Partner with knowledge-synthesizer on patterns
- Coordinate with all agents on communication
Always prioritize efficiency, reliability, and scalability while coordinating multi-agent systems that deliver exceptional performance through seamless collaboration.
+8
View File
@@ -0,0 +1,8 @@
{
"enabledMcpjsonServers": [
"github",
"kubernetes",
"flux",
"playwright"
]
}
+10 -6
View File
@@ -5,10 +5,11 @@ on:
branches: [main]
pull_request:
branches: [main]
workflow_call:
jobs:
lint-and-test:
runs-on: ubuntu-latest
ci:
runs-on: local-ubuntu-latest
timeout-minutes: 10
steps:
@@ -18,7 +19,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '22'
cache: 'npm'
- name: Install dependencies
@@ -28,10 +29,13 @@ jobs:
run: npx @kinvolk/headlamp-plugin build
- name: Lint
run: npx eslint --ext .ts,.tsx src/
run: npm run lint
- name: Type-check
run: npx tsc --noEmit
run: npm run tsc
- name: Run unit tests
- name: Format check
run: npm run format:check
- name: Run tests
run: npm test
+56 -56
View File
@@ -4,50 +4,59 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Version to release (without v prefix, e.g., 0.2.0)'
description: 'Release version (e.g. 1.0.0)'
required: true
type: string
permissions:
contents: write
concurrency:
group: release
cancel-in-progress: false
jobs:
ci:
uses: ./.github/workflows/ci.yaml
release:
runs-on: ubuntu-latest
permissions:
contents: write
needs: ci
runs-on: local-ubuntu-latest
timeout-minutes: 10
steps:
- name: Validate version format
run: |
if ! echo "${{ inputs.version }}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::Version must be in format X.Y.Z (e.g., 0.2.0)"
if [[ ! "${{ inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Version must be in X.Y.Z format"
exit 1
fi
- name: Checkout
uses: actions/checkout@v4
- name: Configure git
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Update package.json version
run: |
jq --arg version "${{ inputs.version }}" '.version = $version' package.json > package.json.tmp
mv package.json.tmp package.json
- name: Update version in package.json
run: npm version ${{ inputs.version }} --no-git-tag-version --allow-same-version
- name: Update artifacthub-pkg.yml version and URL
- name: Update artifacthub-pkg.yml
run: |
VERSION="${{ inputs.version }}"
RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/headlamp-rook-plugin-${VERSION}.tar.gz"
sed -i "s|^version:.*|version: \"${VERSION}\"|" artifacthub-pkg.yml
PKG_NAME=$(jq -r .name package.json)
RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${PKG_NAME}-${VERSION}.tar.gz"
sed -i "s/^version:.*/version: \"${VERSION}\"/" artifacthub-pkg.yml
sed -i "s|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"${RELEASE_URL}\"|" artifacthub-pkg.yml
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
@@ -57,55 +66,46 @@ jobs:
- name: Package plugin
run: npx @kinvolk/headlamp-plugin package
- name: Validate tarball name
- name: Prepare release tarball
run: |
EXPECTED="headlamp-rook-plugin-${{ inputs.version }}.tar.gz"
ACTUAL=$(ls *.tar.gz)
if [ "$EXPECTED" != "$ACTUAL" ]; then
echo "::error::Tarball name mismatch! Expected: $EXPECTED, Got: $ACTUAL"
VERSION="${{ inputs.version }}"
PKG_NAME=$(jq -r .name package.json)
TARBALL="${PKG_NAME}-${VERSION}.tar.gz"
if [ ! -f "$TARBALL" ]; then
echo "Error: Expected tarball $TARBALL not found"
ls -la *.tar.gz 2>/dev/null || echo "No .tar.gz files found"
exit 1
fi
echo "✓ Tarball name validated: $ACTUAL"
echo "TARBALL=$TARBALL" >> $GITHUB_ENV
echo "PKG_NAME=$PKG_NAME" >> $GITHUB_ENV
- name: Validate tarball
run: |
echo "Tarball: ${{ env.TARBALL }}"
ls -lh "${{ env.TARBALL }}"
tar -tzf "${{ env.TARBALL }}" | head -20
tar -tzf "${{ env.TARBALL }}" | grep -q "main.js" || { echo "Error: main.js not found in tarball"; exit 1; }
- name: Compute checksum
id: compute_checksum
run: |
TARBALL="headlamp-rook-plugin-${{ inputs.version }}.tar.gz"
CHECKSUM=$(sha256sum "$TARBALL" | awk '{print $1}')
echo "checksum=${CHECKSUM}" >> $GITHUB_OUTPUT
echo "Checksum: sha256:${CHECKSUM}"
CHECKSUM=$(sha256sum "${{ env.TARBALL }}" | awk '{print $1}')
echo "CHECKSUM=$CHECKSUM" >> $GITHUB_ENV
sed -i "s|headlamp/plugin/archive-checksum:.*|headlamp/plugin/archive-checksum: sha256:${CHECKSUM}|" artifacthub-pkg.yml
- name: Update checksum in metadata
- name: Commit and tag
run: |
CHECKSUM="${{ steps.compute_checksum.outputs.checksum }}"
sed -i "s|headlamp/plugin/archive-checksum:.*|headlamp/plugin/archive-checksum: \"sha256:${CHECKSUM}\"|" artifacthub-pkg.yml
- name: Commit version bump and metadata
run: |
git add package.json artifacthub-pkg.yml
git commit -m "chore: release v${{ inputs.version }}"
git push origin main
- name: Create and push tag
run: |
git tag "v${{ inputs.version }}"
git push origin "v${{ inputs.version }}"
VERSION="${{ inputs.version }}"
git add package.json package-lock.json artifacthub-pkg.yml
git commit -m "release: v${VERSION}"
git tag "v${VERSION}"
git push origin main --tags
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: "v${{ inputs.version }}"
files: headlamp-rook-plugin-${{ inputs.version }}.tar.gz
files: ${{ env.TARBALL }}
fail_on_unmatched_files: true
draft: false
prerelease: false
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Summary
run: |
echo "✓ Version bumped to ${{ inputs.version }}"
echo "✓ Metadata updated with checksum sha256:${{ steps.compute_checksum.outputs.checksum }}"
echo "✓ Tag v${{ inputs.version }} created"
echo "✓ GitHub release published with tarball"
+4 -1
View File
@@ -1,5 +1,8 @@
node_modules/
dist/
.headlamp-plugin/
*.tar.gz
.env
.env.local
.eslintcache
.playwright-mcp/
.mcp.json
+23
View File
@@ -0,0 +1,23 @@
{
"mcpServers": {
"github": {
"type": "http",
"url": "https://api.githubcopilot.com/mcp/",
"headers": {
"Authorization": "Bearer ${GITHUB_TOKEN}"
}
},
"kubernetes": {
"type": "sse",
"url": "http://localhost:8080/sse"
},
"flux": {
"type": "sse",
"url": "http://localhost:8081/sse"
},
"playwright": {
"type": "sse",
"url": "http://localhost:8086/sse"
}
}
}
+1
View File
@@ -0,0 +1 @@
module.exports = require('@headlamp-k8s/eslint-config/prettier-config');
+2
View File
@@ -43,6 +43,8 @@ src/
├── StorageClassesPage.tsx
├── VolumesPage.tsx
├── PodsPage.tsx
├── FilesystemsPage.tsx
├── ObjectStoresPage.tsx
├── ClusterStatusCard.tsx
├── AppBarClusterBadge.tsx
├── PVCDetailSection.tsx # Injected into Headlamp PVC detail view
+16 -5
View File
@@ -1,5 +1,6 @@
# Headlamp Rook Plugin
[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/package/headlamp/rook/headlamp-rook-plugin)](https://artifacthub.io/packages/headlamp/rook/headlamp-rook-plugin)
[![CI](https://github.com/cpfarhood/headlamp-rook-plugin/actions/workflows/ci.yaml/badge.svg)](https://github.com/cpfarhood/headlamp-rook-plugin/actions/workflows/ci.yaml)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
@@ -47,7 +48,11 @@ Rook-Ceph must be deployed in the `rook-ceph` namespace with standard labels. Th
## Installing
### Option 1: Manual Plugin Install
### Option 1: Headlamp Plugin Manager (Recommended)
Browse the Headlamp Plugin Manager (Settings → Plugins → Catalog) and install **headlamp-rook-plugin** directly.
### Option 2: Manual Plugin Install
Download the latest release tarball and place it in your Headlamp plugins directory:
@@ -60,10 +65,6 @@ curl -L https://github.com/cpfarhood/headlamp-rook-plugin/releases/latest/downlo
tar -xzf headlamp-rook-plugin.tar.gz -C ~/.config/Headlamp/plugins/
```
### Option 2: Headlamp In-App Plugin Manager
Browse the Headlamp Plugin Manager (Settings → Plugins) and install **headlamp-rook-plugin** directly.
## RBAC & Security Setup
The plugin reads Rook-Ceph CRDs and Kubernetes resources. Your Headlamp service account needs:
@@ -107,6 +108,16 @@ subjects:
namespace: headlamp
```
## Troubleshooting
| Symptom | Likely Cause | Quick Fix |
| ------- | ------------ | --------- |
| **Plugin not in sidebar** | Plugin not installed or needs browser refresh | Hard refresh (Cmd+Shift+R / Ctrl+Shift+F5) |
| **No CephCluster data** | CRDs not installed or RBAC insufficient | Verify `kubectl get cephclusters -n rook-ceph` works |
| **Block Pools empty** | No CephBlockPool resources | Check `kubectl get cephblockpools -n rook-ceph` |
| **App bar badge missing** | No CephCluster present | Verify rook-ceph is deployed with a CephCluster resource |
| **StorageClass columns not showing** | Rook provisioner not matching | Verify SC provisioner ends in `.rbd.csi.ceph.com` or `.cephfs.csi.ceph.com` |
## Development
### Prerequisites
+3 -3
View File
@@ -1,4 +1,4 @@
version: "0.2.2"
version: "0.2.5"
name: headlamp-rook-plugin
displayName: Rook Plugin
createdAt: "2026-02-18T00:00:00Z"
@@ -27,7 +27,7 @@ changes:
description: "Package renamed to rook so the plugin displays correctly in Headlamp's Plugins list"
annotations:
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-rook-plugin/releases/download/v0.2.2/rook-0.2.2.tar.gz"
headlamp/plugin/archive-checksum: "sha256:0c0d39f275f206cd0fe66a17f98c9bf0b9a5d74122bad770c7d4051e59f85675"
headlamp/plugin/archive-url: "https://github.com/privilegedescalation/headlamp-rook-plugin/releases/download/v0.2.5/rook-0.2.5.tar.gz"
headlamp/plugin/archive-checksum: sha256:38ee58f83da386bc35a4d09c39883c2a2a29e89c4d239922dfa67dfcc10d9421
headlamp/plugin/distro-compat: ""
headlamp/plugin/version-compat: ">=0.20"
+4 -4
View File
@@ -1,12 +1,12 @@
{
"name": "headlamp-rook-plugin",
"version": "0.1.0",
"name": "rook",
"version": "0.2.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "headlamp-rook-plugin",
"version": "0.1.0",
"name": "rook",
"version": "0.2.5",
"license": "Apache-2.0",
"devDependencies": {
"@kinvolk/headlamp-plugin": "^0.13.0"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "rook",
"version": "0.2.2",
"version": "0.2.5",
"description": "Headlamp plugin for Rook-Ceph cluster visibility and CSI driver monitoring",
"repository": {
"type": "git",
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"]
}
+25 -9
View File
@@ -166,7 +166,9 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// Operator pods
try {
const opList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(ROOK_OPERATOR_SELECTOR)}`
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_OPERATOR_SELECTOR
)}`
);
if (!cancelled && isKubeList(opList)) setOperatorPods(opList.items as RookCephPod[]);
} catch {
@@ -176,7 +178,9 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// MON pods
try {
const monList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(ROOK_MON_SELECTOR)}`
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_MON_SELECTOR
)}`
);
if (!cancelled && isKubeList(monList)) setMonPods(monList.items as RookCephPod[]);
} catch {
@@ -186,7 +190,9 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// OSD pods
try {
const osdList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(ROOK_OSD_SELECTOR)}`
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_OSD_SELECTOR
)}`
);
if (!cancelled && isKubeList(osdList)) setOsdPods(osdList.items as RookCephPod[]);
} catch {
@@ -196,7 +202,9 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// MGR pods
try {
const mgrList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(ROOK_MGR_SELECTOR)}`
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_MGR_SELECTOR
)}`
);
if (!cancelled && isKubeList(mgrList)) setMgrPods(mgrList.items as RookCephPod[]);
} catch {
@@ -206,9 +214,12 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// CSI RBD provisioner pods
try {
const csiRbdList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(ROOK_CSI_RBD_SELECTOR)}`
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_CSI_RBD_SELECTOR
)}`
);
if (!cancelled && isKubeList(csiRbdList)) setCsiRbdPods(csiRbdList.items as RookCephPod[]);
if (!cancelled && isKubeList(csiRbdList))
setCsiRbdPods(csiRbdList.items as RookCephPod[]);
} catch {
if (!cancelled) setCsiRbdPods([]);
}
@@ -216,9 +227,12 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// CSI CephFS provisioner pods
try {
const csiCephfsList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(ROOK_CSI_CEPHFS_SELECTOR)}`
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_CSI_CEPHFS_SELECTOR
)}`
);
if (!cancelled && isKubeList(csiCephfsList)) setCsiCephfsPods(csiCephfsList.items as RookCephPod[]);
if (!cancelled && isKubeList(csiCephfsList))
setCsiCephfsPods(csiCephfsList.items as RookCephPod[]);
} catch {
if (!cancelled) setCsiCephfsPods([]);
}
@@ -232,7 +246,9 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
}
void fetchAsync();
return () => { cancelled = true; };
return () => {
cancelled = true;
};
}, [refreshKey]);
// ---------------------------------------------------------------------------
+25 -20
View File
@@ -129,9 +129,12 @@ export interface CephCluster extends KubeObject {
export function healthToStatus(health: string | undefined): 'success' | 'warning' | 'error' {
switch (health) {
case 'HEALTH_OK': return 'success';
case 'HEALTH_WARN': return 'warning';
default: return 'error';
case 'HEALTH_OK':
return 'success';
case 'HEALTH_WARN':
return 'warning';
default:
return 'error';
}
}
@@ -331,9 +334,7 @@ export function findBoundPv(
): RookCephPersistentVolume | undefined {
const ns = pvc.metadata.namespace ?? '';
const name = pvc.metadata.name;
return rookPvs.find(
pv => pv.spec.claimRef?.namespace === ns && pv.spec.claimRef?.name === name
);
return rookPvs.find(pv => pv.spec.claimRef?.namespace === ns && pv.spec.claimRef?.name === name);
}
// ---------------------------------------------------------------------------
@@ -368,15 +369,11 @@ export interface RookCephPod extends KubeObject {
}
export function isPodReady(pod: RookCephPod): boolean {
return (
pod.status?.conditions?.some(c => c.type === 'Ready' && c.status === 'True') ?? false
);
return pod.status?.conditions?.some(c => c.type === 'Ready' && c.status === 'True') ?? false;
}
export function getPodRestarts(pod: RookCephPod): number {
return (
pod.status?.containerStatuses?.reduce((sum, c) => sum + c.restartCount, 0) ?? 0
);
return pod.status?.containerStatuses?.reduce((sum, c) => sum + c.restartCount, 0) ?? 0;
}
export function getPodImage(pod: RookCephPod): string {
@@ -441,11 +438,16 @@ export function parseStorageToBytes(storage: string): number {
const suffix = match[2] ?? '';
const multipliers: Record<string, number> = {
'': 1,
K: 1e3, Ki: 1024,
M: 1e6, Mi: 1024 ** 2,
G: 1e9, Gi: 1024 ** 3,
T: 1e12, Ti: 1024 ** 4,
P: 1e15, Pi: 1024 ** 5,
K: 1e3,
Ki: 1024,
M: 1e6,
Mi: 1024 ** 2,
G: 1e9,
Gi: 1024 ** 3,
T: 1e12,
Ti: 1024 ** 4,
P: 1e15,
Pi: 1024 ** 5,
};
return value * (multipliers[suffix] ?? 1);
}
@@ -453,9 +455,12 @@ export function parseStorageToBytes(storage: string): number {
/** Returns display label for storage type (rbd → Block, cephfs → Filesystem). */
export function formatStorageType(type: 'rbd' | 'cephfs' | 'unknown'): string {
switch (type) {
case 'rbd': return 'Block (RBD)';
case 'cephfs': return 'Filesystem (CephFS)';
default: return 'Unknown';
case 'rbd':
return 'Block (RBD)';
case 'cephfs':
return 'Filesystem (CephFS)';
default:
return 'Unknown';
}
}
+8 -4
View File
@@ -14,10 +14,14 @@ import { useRookCephContext } from '../api/RookCephDataContext';
function getHealthColor(health: string | undefined): string {
switch (health) {
case 'HEALTH_OK': return '#4caf50';
case 'HEALTH_WARN': return '#ff9800';
case 'HEALTH_ERR': return '#f44336';
default: return '#9e9e9e';
case 'HEALTH_OK':
return '#4caf50';
case 'HEALTH_WARN':
return '#ff9800';
case 'HEALTH_ERR':
return '#f44336';
default:
return '#9e9e9e';
}
}
+49 -10
View File
@@ -19,7 +19,10 @@ function BlockPoolDetail({ pool, onClose }: { pool: CephBlockPool; onClose: () =
<div
style={{
position: 'fixed',
top: 0, right: 0, bottom: 0, width: '480px',
top: 0,
right: 0,
bottom: 0,
width: '480px',
backgroundColor: 'var(--mui-palette-background-paper, #fff)',
boxShadow: '-4px 0 16px rgba(0,0,0,0.15)',
zIndex: 1300,
@@ -27,7 +30,14 @@ function BlockPoolDetail({ pool, onClose }: { pool: CephBlockPool; onClose: () =
padding: '24px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px',
}}
>
<strong>{pool.metadata.name}</strong>
<button
onClick={onClose}
@@ -99,14 +109,18 @@ export default function BlockPoolsPage() {
{error && (
<SectionBox title="Error">
<NameValueTable rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]} />
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]}
/>
</SectionBox>
)}
{blockPools.length === 0 ? (
<SectionBox title="No Block Pools">
<NameValueTable
rows={[{ name: 'Status', value: 'No CephBlockPool resources found in rook-ceph namespace.' }]}
rows={[
{ name: 'Status', value: 'No CephBlockPool resources found in rook-ceph namespace.' },
]}
/>
</SectionBox>
) : (
@@ -118,7 +132,15 @@ export default function BlockPoolsPage() {
getter: (p: CephBlockPool) => (
<button
onClick={() => setSelected(p)}
style={{ border: 'none', background: 'transparent', color: 'var(--link-color, #1976d2)', cursor: 'pointer', textDecoration: 'underline', padding: 0, font: 'inherit' }}
style={{
border: 'none',
background: 'transparent',
color: 'var(--link-color, #1976d2)',
cursor: 'pointer',
textDecoration: 'underline',
padding: 0,
font: 'inherit',
}}
>
{p.metadata.name}
</button>
@@ -132,10 +154,22 @@ export default function BlockPoolsPage() {
</StatusLabel>
),
},
{ label: 'Replicas', getter: (p: CephBlockPool) => String(p.spec?.replicated?.size ?? '—') },
{ label: 'Failure Domain', getter: (p: CephBlockPool) => p.spec?.failureDomain ?? '—' },
{ label: 'Mirroring', getter: (p: CephBlockPool) => p.spec?.mirroring?.enabled ? 'Enabled' : 'Disabled' },
{ label: 'Age', getter: (p: CephBlockPool) => formatAge(p.metadata.creationTimestamp) },
{
label: 'Replicas',
getter: (p: CephBlockPool) => String(p.spec?.replicated?.size ?? '—'),
},
{
label: 'Failure Domain',
getter: (p: CephBlockPool) => p.spec?.failureDomain ?? '—',
},
{
label: 'Mirroring',
getter: (p: CephBlockPool) => (p.spec?.mirroring?.enabled ? 'Enabled' : 'Disabled'),
},
{
label: 'Age',
getter: (p: CephBlockPool) => formatAge(p.metadata.creationTimestamp),
},
]}
data={blockPools}
/>
@@ -145,7 +179,12 @@ export default function BlockPoolsPage() {
{selected && (
<>
<div
style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.3)', zIndex: 1299 }}
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
zIndex: 1299,
}}
onClick={() => setSelected(null)}
/>
<BlockPoolDetail pool={selected} onClose={() => setSelected(null)} />
+7 -10
View File
@@ -80,16 +80,17 @@ export default function CephPodDetailSection({ resource }: CephPodDetailSectionP
const role = ROLE_LABELS[appLabel] ?? appLabel;
const phase = raw.status?.phase ?? 'Unknown';
const isReady =
raw.status?.conditions?.some((c) => c.type === 'Ready' && c.status === 'True') ?? false;
const restarts =
raw.status?.containerStatuses?.reduce((s, c) => s + c.restartCount, 0) ?? 0;
raw.status?.conditions?.some(c => c.type === 'Ready' && c.status === 'True') ?? false;
const restarts = raw.status?.containerStatuses?.reduce((s, c) => s + c.restartCount, 0) ?? 0;
const containerRows = (raw.status?.containerStatuses ?? []).map((cs) => {
const containerRows = (raw.status?.containerStatuses ?? []).map(cs => {
let stateStr = 'Unknown';
if (cs.state?.running) stateStr = 'Running';
else if (cs.state?.waiting) stateStr = `Waiting: ${cs.state.waiting.reason ?? ''}`;
else if (cs.state?.terminated)
stateStr = `Terminated: ${cs.state.terminated.reason ?? ''} (exit ${cs.state.terminated.exitCode ?? ''})`;
stateStr = `Terminated: ${cs.state.terminated.reason ?? ''} (exit ${
cs.state.terminated.exitCode ?? ''
})`;
return {
name: cs.name,
@@ -111,11 +112,7 @@ export default function CephPodDetailSection({ resource }: CephPodDetailSectionP
},
{
name: 'Phase',
value: (
<StatusLabel status={isReady ? 'success' : 'error'}>
{phase}
</StatusLabel>
),
value: <StatusLabel status={isReady ? 'success' : 'error'}>{phase}</StatusLabel>,
},
{ name: 'Node', value: raw.spec?.nodeName ?? '—' },
{ name: 'Restarts', value: String(restarts) },
+24 -13
View File
@@ -11,7 +11,15 @@ import {
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import type { CephCluster, RookCephPod } from '../api/k8s';
import { formatAge, formatBytes, getPodImage, getPodRestarts, healthToStatus, isPodReady, phaseToStatus } from '../api/k8s';
import {
formatAge,
formatBytes,
getPodImage,
getPodRestarts,
healthToStatus,
isPodReady,
phaseToStatus,
} from '../api/k8s';
interface ClusterStatusCardProps {
cephClusters: CephCluster[];
@@ -26,17 +34,14 @@ interface ClusterStatusCardProps {
function PodStatusBadge({ pod }: { pod: RookCephPod }) {
const ready = isPodReady(pod);
const phase = pod.status?.phase ?? 'Unknown';
return (
<StatusLabel status={ready ? 'success' : 'error'}>
{phase}
</StatusLabel>
);
return <StatusLabel status={ready ? 'success' : 'error'}>{phase}</StatusLabel>;
}
function PodSummaryRow({ pods, label }: { pods: RookCephPod[]; label: string }) {
const ready = pods.filter(isPodReady).length;
const total = pods.length;
const status = total === 0 ? 'error' : ready === total ? 'success' : ready > 0 ? 'warning' : 'error';
const status =
total === 0 ? 'error' : ready === total ? 'success' : ready > 0 ? 'warning' : 'error';
return {
name: label,
value: (
@@ -84,12 +89,12 @@ export default function ClusterStatusCard({
{
name: 'Phase',
value: (
<StatusLabel status={phaseToStatus(phase)}>
{phase ?? 'Unknown'}
</StatusLabel>
<StatusLabel status={phaseToStatus(phase)}>{phase ?? 'Unknown'}</StatusLabel>
),
},
...(cluster.status?.message ? [{ name: 'Message', value: cluster.status.message }] : []),
...(cluster.status?.message
? [{ name: 'Message', value: cluster.status.message }]
: []),
{ name: 'Ceph Version', value: version },
{ name: 'Namespace', value: cluster.metadata.namespace ?? '—' },
{ name: 'Age', value: formatAge(cluster.metadata.creationTimestamp) },
@@ -102,7 +107,11 @@ export default function ClusterStatusCard({
<div style={{ marginBottom: '12px' }}>
<PercentageBar
data={[
{ name: 'Used', value: bytesUsed, fill: usedPct > 80 ? '#f44336' : '#1976d2' },
{
name: 'Used',
value: bytesUsed,
fill: usedPct > 80 ? '#f44336' : '#1976d2',
},
{ name: 'Free', value: bytesAvail, fill: '#e0e0e0' },
]}
total={bytesTotal}
@@ -142,7 +151,9 @@ export function PodDetailRows({ pods, label }: { pods: RookCephPod[]; label: str
return (
<SectionBox title={label}>
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">No pods found</StatusLabel> }]}
rows={[
{ name: 'Status', value: <StatusLabel status="error">No pods found</StatusLabel> },
]}
/>
</SectionBox>
);
+56 -11
View File
@@ -19,7 +19,10 @@ function FilesystemDetail({ fs, onClose }: { fs: CephFilesystem; onClose: () =>
<div
style={{
position: 'fixed',
top: 0, right: 0, bottom: 0, width: '480px',
top: 0,
right: 0,
bottom: 0,
width: '480px',
backgroundColor: 'var(--mui-palette-background-paper, #fff)',
boxShadow: '-4px 0 16px rgba(0,0,0,0.15)',
zIndex: 1300,
@@ -27,7 +30,14 @@ function FilesystemDetail({ fs, onClose }: { fs: CephFilesystem; onClose: () =>
padding: '24px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px',
}}
>
<strong>{fs.metadata.name}</strong>
<button
onClick={onClose}
@@ -58,7 +68,10 @@ function FilesystemDetail({ fs, onClose }: { fs: CephFilesystem; onClose: () =>
<NameValueTable
rows={[
{ name: 'Active Count', value: String(fs.spec?.metadataServer?.activeCount ?? '—') },
{ name: 'Active Standby', value: String(fs.spec?.metadataServer?.activeStandby ?? '—') },
{
name: 'Active Standby',
value: String(fs.spec?.metadataServer?.activeStandby ?? '—'),
},
]}
/>
</SectionBox>
@@ -107,14 +120,21 @@ export default function FilesystemsPage() {
{error && (
<SectionBox title="Error">
<NameValueTable rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]} />
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]}
/>
</SectionBox>
)}
{filesystems.length === 0 ? (
<SectionBox title="No Filesystems">
<NameValueTable
rows={[{ name: 'Status', value: 'No CephFilesystem resources found in rook-ceph namespace.' }]}
rows={[
{
name: 'Status',
value: 'No CephFilesystem resources found in rook-ceph namespace.',
},
]}
/>
</SectionBox>
) : (
@@ -126,7 +146,15 @@ export default function FilesystemsPage() {
getter: (f: CephFilesystem) => (
<button
onClick={() => setSelected(f)}
style={{ border: 'none', background: 'transparent', color: 'var(--link-color, #1976d2)', cursor: 'pointer', textDecoration: 'underline', padding: 0, font: 'inherit' }}
style={{
border: 'none',
background: 'transparent',
color: 'var(--link-color, #1976d2)',
cursor: 'pointer',
textDecoration: 'underline',
padding: 0,
font: 'inherit',
}}
>
{f.metadata.name}
</button>
@@ -140,10 +168,22 @@ export default function FilesystemsPage() {
</StatusLabel>
),
},
{ label: 'Active MDS', getter: (f: CephFilesystem) => String(f.spec?.metadataServer?.activeCount ?? '—') },
{ label: 'Active Standby', getter: (f: CephFilesystem) => String(f.spec?.metadataServer?.activeStandby ?? '—') },
{ label: 'Data Pools', getter: (f: CephFilesystem) => String(f.spec?.dataPools?.length ?? 0) },
{ label: 'Age', getter: (f: CephFilesystem) => formatAge(f.metadata.creationTimestamp) },
{
label: 'Active MDS',
getter: (f: CephFilesystem) => String(f.spec?.metadataServer?.activeCount ?? '—'),
},
{
label: 'Active Standby',
getter: (f: CephFilesystem) => String(f.spec?.metadataServer?.activeStandby ?? '—'),
},
{
label: 'Data Pools',
getter: (f: CephFilesystem) => String(f.spec?.dataPools?.length ?? 0),
},
{
label: 'Age',
getter: (f: CephFilesystem) => formatAge(f.metadata.creationTimestamp),
},
]}
data={filesystems}
/>
@@ -153,7 +193,12 @@ export default function FilesystemsPage() {
{selected && (
<>
<div
style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.3)', zIndex: 1299 }}
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
zIndex: 1299,
}}
onClick={() => setSelected(null)}
/>
<FilesystemDetail fs={selected} onClose={() => setSelected(null)} />
+49 -10
View File
@@ -23,7 +23,10 @@ function ObjectStoreDetail({ store, onClose }: { store: CephObjectStore; onClose
<div
style={{
position: 'fixed',
top: 0, right: 0, bottom: 0, width: '480px',
top: 0,
right: 0,
bottom: 0,
width: '480px',
backgroundColor: 'var(--mui-palette-background-paper, #fff)',
boxShadow: '-4px 0 16px rgba(0,0,0,0.15)',
zIndex: 1300,
@@ -31,7 +34,14 @@ function ObjectStoreDetail({ store, onClose }: { store: CephObjectStore; onClose
padding: '24px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px',
}}
>
<strong>{store.metadata.name}</strong>
<button
onClick={onClose}
@@ -67,7 +77,7 @@ function ObjectStoreDetail({ store, onClose }: { store: CephObjectStore; onClose
]}
/>
</SectionBox>
{(endpoints?.insecure?.length || endpoints?.secure?.length) ? (
{endpoints?.insecure?.length || endpoints?.secure?.length ? (
<SectionBox title="Endpoints">
<NameValueTable
rows={[
@@ -104,14 +114,21 @@ export default function ObjectStoresPage() {
{error && (
<SectionBox title="Error">
<NameValueTable rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]} />
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]}
/>
</SectionBox>
)}
{objectStores.length === 0 ? (
<SectionBox title="No Object Stores">
<NameValueTable
rows={[{ name: 'Status', value: 'No CephObjectStore resources found in rook-ceph namespace.' }]}
rows={[
{
name: 'Status',
value: 'No CephObjectStore resources found in rook-ceph namespace.',
},
]}
/>
</SectionBox>
) : (
@@ -123,7 +140,15 @@ export default function ObjectStoresPage() {
getter: (o: CephObjectStore) => (
<button
onClick={() => setSelected(o)}
style={{ border: 'none', background: 'transparent', color: 'var(--link-color, #1976d2)', cursor: 'pointer', textDecoration: 'underline', padding: 0, font: 'inherit' }}
style={{
border: 'none',
background: 'transparent',
color: 'var(--link-color, #1976d2)',
cursor: 'pointer',
textDecoration: 'underline',
padding: 0,
font: 'inherit',
}}
>
{o.metadata.name}
</button>
@@ -137,9 +162,18 @@ export default function ObjectStoresPage() {
</StatusLabel>
),
},
{ label: 'Gateway Port', getter: (o: CephObjectStore) => String(o.spec?.gateway?.port ?? '—') },
{ label: 'Instances', getter: (o: CephObjectStore) => String(o.spec?.gateway?.instances ?? '—') },
{ label: 'Age', getter: (o: CephObjectStore) => formatAge(o.metadata.creationTimestamp) },
{
label: 'Gateway Port',
getter: (o: CephObjectStore) => String(o.spec?.gateway?.port ?? '—'),
},
{
label: 'Instances',
getter: (o: CephObjectStore) => String(o.spec?.gateway?.instances ?? '—'),
},
{
label: 'Age',
getter: (o: CephObjectStore) => formatAge(o.metadata.creationTimestamp),
},
]}
data={objectStores}
/>
@@ -149,7 +183,12 @@ export default function ObjectStoresPage() {
{selected && (
<>
<div
style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.3)', zIndex: 1299 }}
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
zIndex: 1299,
}}
onClick={() => setSelected(null)}
/>
<ObjectStoreDetail store={selected} onClose={() => setSelected(null)} />
+84 -35
View File
@@ -15,7 +15,13 @@ import {
StatusLabel,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import { formatAge, formatBytes, healthToStatus, phaseToStatus, storageClassType } from '../api/k8s';
import {
formatAge,
formatBytes,
healthToStatus,
phaseToStatus,
storageClassType,
} from '../api/k8s';
import { useRookCephContext } from '../api/RookCephDataContext';
import ClusterStatusCard from './ClusterStatusCard';
@@ -70,7 +76,14 @@ export default function OverviewPage() {
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px',
}}
>
<SectionHeader title="Rook-Ceph — Overview" />
<button
onClick={refresh}
@@ -97,11 +110,16 @@ export default function OverviewPage() {
rows={[
{
name: 'Status',
value: <StatusLabel status="error">No CephCluster found in namespace rook-ceph</StatusLabel>,
value: (
<StatusLabel status="error">
No CephCluster found in namespace rook-ceph
</StatusLabel>
),
},
{
name: 'Install',
value: 'helm install rook-ceph rook-release/rook-ceph -n rook-ceph --create-namespace',
value:
'helm install rook-ceph rook-release/rook-ceph -n rook-ceph --create-namespace',
},
{
name: 'Docs',
@@ -129,9 +147,7 @@ export default function OverviewPage() {
{
name: 'Health',
value: (
<StatusLabel status={healthToStatus(primaryHealth)}>
{primaryHealth}
</StatusLabel>
<StatusLabel status={healthToStatus(primaryHealth)}>{primaryHealth}</StatusLabel>
),
},
{
@@ -148,7 +164,13 @@ export default function OverviewPage() {
<SectionBox title="Storage Summary">
{storageClasses.length > 0 && (
<div style={{ marginBottom: '16px' }}>
<div style={{ marginBottom: '8px', fontSize: '14px', color: 'var(--mui-palette-text-secondary)' }}>
<div
style={{
marginBottom: '8px',
fontSize: '14px',
color: 'var(--mui-palette-text-secondary)',
}}
>
StorageClass Type Distribution
</div>
<PercentageBar
@@ -157,7 +179,13 @@ export default function OverviewPage() {
? [{ name: 'Block (RBD)', value: rbdClasses.length, fill: '#1976d2' }]
: []),
...(cephfsClasses.length > 0
? [{ name: 'Filesystem (CephFS)', value: cephfsClasses.length, fill: '#9c27b0' }]
? [
{
name: 'Filesystem (CephFS)',
value: cephfsClasses.length,
fill: '#9c27b0',
},
]
: []),
]}
total={storageClasses.length}
@@ -166,7 +194,10 @@ export default function OverviewPage() {
)}
<NameValueTable
rows={[
{ name: 'Storage Classes', value: `${storageClasses.length} (${rbdClasses.length} RBD, ${cephfsClasses.length} CephFS)` },
{
name: 'Storage Classes',
value: `${storageClasses.length} (${rbdClasses.length} RBD, ${cephfsClasses.length} CephFS)`,
},
{ name: 'Block Pools', value: String(blockPools.length) },
{ name: 'Filesystems', value: String(filesystems.length) },
{ name: 'Object Stores', value: String(objectStores.length) },
@@ -177,10 +208,20 @@ export default function OverviewPage() {
value: <StatusLabel status="success">{pvcStatusCounts.Bound}</StatusLabel>,
},
...(pvcStatusCounts.Pending > 0
? [{ name: 'PVCs (Pending)', value: <StatusLabel status="warning">{pvcStatusCounts.Pending}</StatusLabel> }]
? [
{
name: 'PVCs (Pending)',
value: <StatusLabel status="warning">{pvcStatusCounts.Pending}</StatusLabel>,
},
]
: []),
...(pvcStatusCounts.Lost > 0
? [{ name: 'PVCs (Lost)', value: <StatusLabel status="error">{pvcStatusCounts.Lost}</StatusLabel> }]
? [
{
name: 'PVCs (Lost)',
value: <StatusLabel status="error">{pvcStatusCounts.Lost}</StatusLabel>,
},
]
: []),
]}
/>
@@ -203,18 +244,18 @@ export default function OverviewPage() {
<SectionBox title="Block Pools">
<SimpleTable
columns={[
{ label: 'Name', getter: (p) => p.metadata.name },
{ label: 'Name', getter: p => p.metadata.name },
{
label: 'Phase',
getter: (p) => (
getter: p => (
<StatusLabel status={phaseToStatus(p.status?.phase)}>
{p.status?.phase ?? 'Unknown'}
</StatusLabel>
),
},
{ label: 'Replicas', getter: (p) => String(p.spec?.replicated?.size ?? '—') },
{ label: 'Failure Domain', getter: (p) => p.spec?.failureDomain ?? '—' },
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
{ label: 'Replicas', getter: p => String(p.spec?.replicated?.size ?? '—') },
{ label: 'Failure Domain', getter: p => p.spec?.failureDomain ?? '—' },
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
]}
data={blockPools}
/>
@@ -226,17 +267,20 @@ export default function OverviewPage() {
<SectionBox title="Filesystems">
<SimpleTable
columns={[
{ label: 'Name', getter: (f) => f.metadata.name },
{ label: 'Name', getter: f => f.metadata.name },
{
label: 'Phase',
getter: (f) => (
getter: f => (
<StatusLabel status={phaseToStatus(f.status?.phase)}>
{f.status?.phase ?? 'Unknown'}
</StatusLabel>
),
},
{ label: 'Active MDS', getter: (f) => String(f.spec?.metadataServer?.activeCount ?? '—') },
{ label: 'Age', getter: (f) => formatAge(f.metadata.creationTimestamp) },
{
label: 'Active MDS',
getter: f => String(f.spec?.metadataServer?.activeCount ?? '—'),
},
{ label: 'Age', getter: f => formatAge(f.metadata.creationTimestamp) },
]}
data={filesystems}
/>
@@ -248,18 +292,18 @@ export default function OverviewPage() {
<SectionBox title="Object Stores">
<SimpleTable
columns={[
{ label: 'Name', getter: (o) => o.metadata.name },
{ label: 'Name', getter: o => o.metadata.name },
{
label: 'Phase',
getter: (o) => (
getter: o => (
<StatusLabel status={phaseToStatus(o.status?.phase)}>
{o.status?.phase ?? 'Unknown'}
</StatusLabel>
),
},
{ label: 'Gateway Port', getter: (o) => String(o.spec?.gateway?.port ?? '—') },
{ label: 'Instances', getter: (o) => String(o.spec?.gateway?.instances ?? '—') },
{ label: 'Age', getter: (o) => formatAge(o.metadata.creationTimestamp) },
{ label: 'Gateway Port', getter: o => String(o.spec?.gateway?.port ?? '—') },
{ label: 'Instances', getter: o => String(o.spec?.gateway?.instances ?? '—') },
{ label: 'Age', getter: o => formatAge(o.metadata.creationTimestamp) },
]}
data={objectStores}
/>
@@ -271,17 +315,17 @@ export default function OverviewPage() {
<SectionBox title="Attention: Non-Bound PVCs">
<SimpleTable
columns={[
{ label: 'Name', getter: (pvc) => pvc.metadata.name },
{ label: 'Namespace', getter: (pvc) => pvc.metadata.namespace ?? '—' },
{ label: 'Name', getter: pvc => pvc.metadata.name },
{ label: 'Namespace', getter: pvc => pvc.metadata.namespace ?? '—' },
{
label: 'Status',
getter: (pvc) => (
getter: pvc => (
<StatusLabel status={phaseToStatus(pvc.status?.phase)}>
{pvc.status?.phase ?? 'Unknown'}
</StatusLabel>
),
},
{ label: 'Age', getter: (pvc) => formatAge(pvc.metadata.creationTimestamp) },
{ label: 'Age', getter: pvc => formatAge(pvc.metadata.creationTimestamp) },
]}
data={nonBoundPvcs}
/>
@@ -298,11 +342,16 @@ function parseStorageToBytes(storage: string): number {
const suffix = match[2] ?? '';
const multipliers: Record<string, number> = {
'': 1,
K: 1e3, Ki: 1024,
M: 1e6, Mi: 1024 ** 2,
G: 1e9, Gi: 1024 ** 3,
T: 1e12, Ti: 1024 ** 4,
P: 1e15, Pi: 1024 ** 5,
K: 1e3,
Ki: 1024,
M: 1e6,
Mi: 1024 ** 2,
G: 1e9,
Gi: 1024 ** 3,
T: 1e12,
Ti: 1024 ** 4,
P: 1e15,
Pi: 1024 ** 5,
};
return value * (multipliers[suffix] ?? 1);
}
+6 -5
View File
@@ -5,10 +5,7 @@
* Uses registerDetailsViewSection in index.tsx.
*/
import {
NameValueTable,
SectionBox,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import { NameValueTable, SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import { findBoundPv, formatStorageType } from '../api/k8s';
import { useRookCephContext } from '../api/RookCephDataContext';
@@ -40,7 +37,11 @@ export default function PVCDetailSection({ resource }: PVCDetailSectionProps) {
// Determine storage type from driver name
const driver = boundPv.spec.csi?.driver ?? '';
const type = driver.includes('.rbd.') ? 'rbd' : driver.includes('.cephfs.') ? 'cephfs' : 'unknown';
const type = driver.includes('.rbd.')
? 'rbd'
: driver.includes('.cephfs.')
? 'cephfs'
: 'unknown';
return (
<SectionBox title="Rook-Ceph Storage Details">
+10 -6
View File
@@ -4,17 +4,17 @@
* Shown only when the PV uses a Rook-Ceph CSI driver.
*/
import {
NameValueTable,
SectionBox,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import { NameValueTable, SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import { formatStorageType, isRookCephPersistentVolume } from '../api/k8s';
interface PVDetailSectionProps {
resource: {
metadata?: { name?: string };
spec?: { csi?: { driver?: string; volumeHandle?: string; volumeAttributes?: Record<string, string> }; storageClassName?: string };
spec?: {
csi?: { driver?: string; volumeHandle?: string; volumeAttributes?: Record<string, string> };
storageClassName?: string;
};
jsonData?: unknown;
};
}
@@ -34,7 +34,11 @@ export default function PVDetailSection({ resource }: PVDetailSectionProps) {
}
const attrs = spec?.csi?.volumeAttributes ?? {};
const type = driver.includes('.rbd.') ? 'rbd' : driver.includes('.cephfs.') ? 'cephfs' : 'unknown';
const type = driver.includes('.rbd.')
? 'rbd'
: driver.includes('.cephfs.')
? 'cephfs'
: 'unknown';
return (
<SectionBox title="Rook-Ceph Volume Details">
+37 -32
View File
@@ -20,18 +20,18 @@ function PodTable({ pods, title }: { pods: RookCephPod[]; title: string }) {
<SectionBox title={`${title} (${pods.length})`}>
<SimpleTable
columns={[
{ label: 'Name', getter: (p) => p.metadata.name },
{ label: 'Name', getter: p => p.metadata.name },
{
label: 'Status',
getter: (p) => (
getter: p => (
<StatusLabel status={isPodReady(p) ? 'success' : 'error'}>
{p.status?.phase ?? 'Unknown'}
</StatusLabel>
),
},
{ label: 'Node', getter: (p) => p.spec?.nodeName ?? '—' },
{ label: 'Restarts', getter: (p) => String(getPodRestarts(p)) },
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
{ label: 'Node', getter: p => p.spec?.nodeName ?? '—' },
{ label: 'Restarts', getter: p => String(getPodRestarts(p)) },
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
]}
data={pods}
/>
@@ -45,27 +45,27 @@ function OsdTable({ pods }: { pods: RookCephPod[] }) {
<SectionBox title={`OSDs (${pods.length})`}>
<SimpleTable
columns={[
{ label: 'OSD ID', getter: (p) => p.metadata.labels?.['osd'] ?? p.metadata.name },
{ label: 'OSD ID', getter: p => p.metadata.labels?.['osd'] ?? p.metadata.name },
{
label: 'Status',
getter: (p) => {
const st = isPodReady(p) ? 'success' : p.status?.phase === 'Pending' ? 'warning' : 'error';
return (
<StatusLabel status={st}>
{p.status?.phase ?? 'Unknown'}
</StatusLabel>
);
getter: p => {
const st = isPodReady(p)
? 'success'
: p.status?.phase === 'Pending'
? 'warning'
: 'error';
return <StatusLabel status={st}>{p.status?.phase ?? 'Unknown'}</StatusLabel>;
},
},
{
label: 'Node',
getter: (p) => p.spec?.nodeName ?? p.metadata.labels?.['topology-location-host'] ?? '—',
getter: p => p.spec?.nodeName ?? p.metadata.labels?.['topology-location-host'] ?? '—',
},
{ label: 'Device Class', getter: (p) => p.metadata.labels?.['device-class'] ?? '—' },
{ label: 'Store', getter: (p) => p.metadata.labels?.['osd-store'] ?? '—' },
{ label: 'Failure Domain', getter: (p) => p.metadata.labels?.['failure-domain'] ?? '—' },
{ label: 'Restarts', getter: (p) => String(getPodRestarts(p)) },
{ label: 'Age', getter: (p) => formatAge(p.metadata.creationTimestamp) },
{ label: 'Device Class', getter: p => p.metadata.labels?.['device-class'] ?? '—' },
{ label: 'Store', getter: p => p.metadata.labels?.['osd-store'] ?? '—' },
{ label: 'Failure Domain', getter: p => p.metadata.labels?.['failure-domain'] ?? '—' },
{ label: 'Restarts', getter: p => String(getPodRestarts(p)) },
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
]}
data={pods}
/>
@@ -74,20 +74,19 @@ function OsdTable({ pods }: { pods: RookCephPod[] }) {
}
export default function PodsPage() {
const {
operatorPods,
monPods,
osdPods,
mgrPods,
csiRbdPods,
csiCephfsPods,
loading,
error,
} = useRookCephContext();
const { operatorPods, monPods, osdPods, mgrPods, csiRbdPods, csiCephfsPods, loading, error } =
useRookCephContext();
if (loading) return <Loader title="Loading Rook-Ceph pods..." />;
const allPods = [...operatorPods, ...monPods, ...osdPods, ...mgrPods, ...csiRbdPods, ...csiCephfsPods];
const allPods = [
...operatorPods,
...monPods,
...osdPods,
...mgrPods,
...csiRbdPods,
...csiCephfsPods,
];
const totalReady = allPods.filter(isPodReady).length;
return (
@@ -96,7 +95,9 @@ export default function PodsPage() {
{error && (
<SectionBox title="Error">
<NameValueTable rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]} />
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]}
/>
</SectionBox>
)}
@@ -106,7 +107,11 @@ export default function PodsPage() {
{
name: 'Overall Health',
value: (
<StatusLabel status={totalReady === allPods.length && allPods.length > 0 ? 'success' : 'warning'}>
<StatusLabel
status={
totalReady === allPods.length && allPods.length > 0 ? 'success' : 'warning'
}
>
{totalReady}/{allPods.length} pods ready
</StatusLabel>
),
+67 -12
View File
@@ -14,13 +14,24 @@ import React, { useState } from 'react';
import { formatAge, formatStorageType, RookCephStorageClass, storageClassType } from '../api/k8s';
import { useRookCephContext } from '../api/RookCephDataContext';
function StorageClassDetail({ sc, pvCount, onClose }: { sc: RookCephStorageClass; pvCount: number; onClose: () => void }) {
function StorageClassDetail({
sc,
pvCount,
onClose,
}: {
sc: RookCephStorageClass;
pvCount: number;
onClose: () => void;
}) {
const type = storageClassType(sc);
return (
<div
style={{
position: 'fixed',
top: 0, right: 0, bottom: 0, width: '480px',
top: 0,
right: 0,
bottom: 0,
width: '480px',
backgroundColor: 'var(--mui-palette-background-paper, #fff)',
boxShadow: '-4px 0 16px rgba(0,0,0,0.15)',
zIndex: 1300,
@@ -28,7 +39,14 @@ function StorageClassDetail({ sc, pvCount, onClose }: { sc: RookCephStorageClass
padding: '24px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px',
}}
>
<strong>{sc.metadata.name}</strong>
<button
onClick={onClose}
@@ -46,7 +64,10 @@ function StorageClassDetail({ sc, pvCount, onClose }: { sc: RookCephStorageClass
{ name: 'Type', value: formatStorageType(type) },
{ name: 'Reclaim Policy', value: sc.reclaimPolicy ?? '—' },
{ name: 'Volume Binding Mode', value: sc.volumeBindingMode ?? '—' },
{ name: 'Volume Expansion', value: sc.allowVolumeExpansion ? 'Allowed' : 'Not allowed' },
{
name: 'Volume Expansion',
value: sc.allowVolumeExpansion ? 'Allowed' : 'Not allowed',
},
{ name: 'Age', value: formatAge(sc.metadata.creationTimestamp) },
{ name: 'Bound PVs', value: String(pvCount) },
]}
@@ -81,14 +102,22 @@ export default function StorageClassesPage() {
{error && (
<SectionBox title="Error">
<NameValueTable rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]} />
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]}
/>
</SectionBox>
)}
{storageClasses.length === 0 ? (
<SectionBox title="No Storage Classes">
<NameValueTable
rows={[{ name: 'Status', value: 'No Rook-Ceph StorageClasses found. Ensure CephBlockPool and CephFilesystem resources exist.' }]}
rows={[
{
name: 'Status',
value:
'No Rook-Ceph StorageClasses found. Ensure CephBlockPool and CephFilesystem resources exist.',
},
]}
/>
</SectionBox>
) : (
@@ -100,7 +129,15 @@ export default function StorageClassesPage() {
getter: (sc: RookCephStorageClass) => (
<button
onClick={() => setSelected(sc)}
style={{ border: 'none', background: 'transparent', color: 'var(--link-color, #1976d2)', cursor: 'pointer', textDecoration: 'underline', padding: 0, font: 'inherit' }}
style={{
border: 'none',
background: 'transparent',
color: 'var(--link-color, #1976d2)',
cursor: 'pointer',
textDecoration: 'underline',
padding: 0,
font: 'inherit',
}}
>
{sc.metadata.name}
</button>
@@ -115,11 +152,24 @@ export default function StorageClassesPage() {
),
},
{ label: 'Provisioner', getter: (sc: RookCephStorageClass) => sc.provisioner },
{ label: 'Pool', getter: (sc: RookCephStorageClass) => sc.parameters?.['pool'] ?? '—' },
{
label: 'Pool',
getter: (sc: RookCephStorageClass) => sc.parameters?.['pool'] ?? '—',
},
{ label: 'Reclaim', getter: (sc: RookCephStorageClass) => sc.reclaimPolicy ?? '—' },
{ label: 'Expansion', getter: (sc: RookCephStorageClass) => sc.allowVolumeExpansion ? 'Yes' : 'No' },
{ label: 'PVs', getter: (sc: RookCephStorageClass) => String(pvCountByClass.get(sc.metadata.name) ?? 0) },
{ label: 'Age', getter: (sc: RookCephStorageClass) => formatAge(sc.metadata.creationTimestamp) },
{
label: 'Expansion',
getter: (sc: RookCephStorageClass) => (sc.allowVolumeExpansion ? 'Yes' : 'No'),
},
{
label: 'PVs',
getter: (sc: RookCephStorageClass) =>
String(pvCountByClass.get(sc.metadata.name) ?? 0),
},
{
label: 'Age',
getter: (sc: RookCephStorageClass) => formatAge(sc.metadata.creationTimestamp),
},
]}
data={storageClasses}
/>
@@ -129,7 +179,12 @@ export default function StorageClassesPage() {
{selected && (
<>
<div
style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.3)', zIndex: 1299 }}
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
zIndex: 1299,
}}
onClick={() => setSelected(null)}
/>
<StorageClassDetail
+57 -11
View File
@@ -20,7 +20,10 @@ function PVDetail({ pv, onClose }: { pv: RookCephPersistentVolume; onClose: () =
<div
style={{
position: 'fixed',
top: 0, right: 0, bottom: 0, width: '520px',
top: 0,
right: 0,
bottom: 0,
width: '520px',
backgroundColor: 'var(--mui-palette-background-paper, #fff)',
boxShadow: '-4px 0 16px rgba(0,0,0,0.15)',
zIndex: 1300,
@@ -28,7 +31,14 @@ function PVDetail({ pv, onClose }: { pv: RookCephPersistentVolume; onClose: () =
padding: '24px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px',
}}
>
<strong>{pv.metadata.name}</strong>
<button
onClick={onClose}
@@ -89,7 +99,9 @@ export default function VolumesPage() {
{error && (
<SectionBox title="Error">
<NameValueTable rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]} />
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]}
/>
</SectionBox>
)}
@@ -108,14 +120,28 @@ export default function VolumesPage() {
getter: (pv: RookCephPersistentVolume) => (
<button
onClick={() => setSelected(pv)}
style={{ border: 'none', background: 'transparent', color: 'var(--link-color, #1976d2)', cursor: 'pointer', textDecoration: 'underline', padding: 0, font: 'inherit' }}
style={{
border: 'none',
background: 'transparent',
color: 'var(--link-color, #1976d2)',
cursor: 'pointer',
textDecoration: 'underline',
padding: 0,
font: 'inherit',
}}
>
{pv.metadata.name}
</button>
),
},
{ label: 'Capacity', getter: (pv: RookCephPersistentVolume) => pv.spec.capacity?.storage ?? '—' },
{ label: 'Access Modes', getter: (pv: RookCephPersistentVolume) => formatAccessModes(pv.spec.accessModes) },
{
label: 'Capacity',
getter: (pv: RookCephPersistentVolume) => pv.spec.capacity?.storage ?? '—',
},
{
label: 'Access Modes',
getter: (pv: RookCephPersistentVolume) => formatAccessModes(pv.spec.accessModes),
},
{
label: 'Phase',
getter: (pv: RookCephPersistentVolume) => (
@@ -124,10 +150,25 @@ export default function VolumesPage() {
</StatusLabel>
),
},
{ label: 'Reclaim', getter: (pv: RookCephPersistentVolume) => pv.spec.persistentVolumeReclaimPolicy ?? '—' },
{ label: 'Pool', getter: (pv: RookCephPersistentVolume) => pv.spec.csi?.volumeAttributes?.['pool'] ?? '—' },
{ label: 'Claim', getter: (pv: RookCephPersistentVolume) => pv.spec.claimRef ? `${pv.spec.claimRef.namespace}/${pv.spec.claimRef.name}` : '—' },
{ label: 'Age', getter: (pv: RookCephPersistentVolume) => formatAge(pv.metadata.creationTimestamp) },
{
label: 'Reclaim',
getter: (pv: RookCephPersistentVolume) =>
pv.spec.persistentVolumeReclaimPolicy ?? '—',
},
{
label: 'Pool',
getter: (pv: RookCephPersistentVolume) =>
pv.spec.csi?.volumeAttributes?.['pool'] ?? '—',
},
{
label: 'Claim',
getter: (pv: RookCephPersistentVolume) =>
pv.spec.claimRef ? `${pv.spec.claimRef.namespace}/${pv.spec.claimRef.name}` : '—',
},
{
label: 'Age',
getter: (pv: RookCephPersistentVolume) => formatAge(pv.metadata.creationTimestamp),
},
]}
data={persistentVolumes}
/>
@@ -137,7 +178,12 @@ export default function VolumesPage() {
{selected && (
<>
<div
style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.3)', zIndex: 1299 }}
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
zIndex: 1299,
}}
onClick={() => setSelected(null)}
/>
<PVDetail pv={selected} onClose={() => setSelected(null)} />
@@ -64,7 +64,7 @@ export function buildStorageClassColumns() {
},
{
label: 'Pool',
getValue: (item: unknown) => getField(item, 'parameters', 'pool') as string | null ?? null,
getValue: (item: unknown) => (getField(item, 'parameters', 'pool') as string | null) ?? null,
render: (item: unknown) => {
if (!isRookRow(item)) return <span></span>;
const pool = getField(item, 'parameters', 'pool') as string | undefined;
@@ -73,12 +73,17 @@ export function buildStorageClassColumns() {
},
{
label: 'Cluster',
getValue: (item: unknown) => getField(item, 'parameters', 'clusterID') as string | null ?? null,
getValue: (item: unknown) =>
(getField(item, 'parameters', 'clusterID') as string | null) ?? null,
render: (item: unknown) => {
if (!isRookRow(item)) return <span></span>;
const clusterID = getField(item, 'parameters', 'clusterID') as string | undefined;
if (!clusterID) return <span></span>;
return <span title={clusterID}>{clusterID.length > 16 ? `${clusterID.slice(0, 16)}` : clusterID}</span>;
return (
<span title={clusterID}>
{clusterID.length > 16 ? `${clusterID.slice(0, 16)}` : clusterID}
</span>
);
},
},
];
@@ -101,10 +106,13 @@ export function buildPVColumns() {
},
{
label: 'Pool',
getValue: (item: unknown) => getField(item, 'spec', 'csi', 'volumeAttributes', 'pool') as string | null ?? null,
getValue: (item: unknown) =>
(getField(item, 'spec', 'csi', 'volumeAttributes', 'pool') as string | null) ?? null,
render: (item: unknown) => {
if (!isRookPvRow(item)) return <span></span>;
const pool = getField(item, 'spec', 'csi', 'volumeAttributes', 'pool') as string | undefined;
const pool = getField(item, 'spec', 'csi', 'volumeAttributes', 'pool') as
| string
| undefined;
return <span>{pool ?? '—'}</span>;
},
},
+16 -7
View File
@@ -16,7 +16,10 @@ import { RookCephDataProvider } from './api/RookCephDataContext';
import BlockPoolsPage from './components/BlockPoolsPage';
import CephPodDetailSection from './components/CephPodDetailSection';
import FilesystemsPage from './components/FilesystemsPage';
import { buildPVColumns, buildStorageClassColumns } from './components/integrations/StorageClassColumns';
import {
buildPVColumns,
buildStorageClassColumns,
} from './components/integrations/StorageClassColumns';
import ObjectStoresPage from './components/ObjectStoresPage';
import OverviewPage from './components/OverviewPage';
import PodsPage from './components/PodsPage';
@@ -207,11 +210,18 @@ registerDetailsViewSection(({ resource }) => {
// takes priority and falls back to the existing one (for mixed-driver tables).
function mergeColumns<T>(
existing: T[],
incoming: Array<{ label: string; getValue: (r: unknown) => unknown; render: (r: unknown) => React.ReactNode }>
incoming: Array<{
label: string;
getValue: (r: unknown) => unknown;
render: (r: unknown) => React.ReactNode;
}>
): T[] {
type ObjCol = { label: string; getValue: (r: unknown) => unknown; render: (r: unknown) => React.ReactNode };
const isObjCol = (c: unknown): c is ObjCol =>
typeof c === 'object' && c !== null && 'label' in c;
type ObjCol = {
label: string;
getValue: (r: unknown) => unknown;
render: (r: unknown) => React.ReactNode;
};
const isObjCol = (c: unknown): c is ObjCol => typeof c === 'object' && c !== null && 'label' in c;
const result = [...existing];
const toAppend: typeof incoming = [];
for (const col of incoming) {
@@ -221,7 +231,7 @@ function mergeColumns<T>(
result[idx] = {
label: col.label,
getValue: (r: unknown) => col.getValue(r) ?? prev.getValue(r),
render: (r: unknown) => col.getValue(r) !== null ? col.render(r) : prev.render(r),
render: (r: unknown) => (col.getValue(r) !== null ? col.render(r) : prev.render(r)),
} as unknown as T;
} else {
toAppend.push(col);
@@ -239,4 +249,3 @@ registerResourceTableColumnsProcessor(({ id, columns }) => {
}
return columns;
});