diff --git a/CHANGELOG.md b/CHANGELOG.md index 8abbf15..f8b229c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.6] - 2026-03-04 + +### Fixed + +- **AppBarClusterBadge registration** — cluster health badge in the Headlamp top nav bar was implemented but never registered; now wired up via `registerAppBarAction` +- **CSI pod label mismatch** — `CephPodDetailSection` now recognizes both legacy (`csi-rbdplugin-provisioner`) and Rook 1.12+ (`rook-ceph.rbd.csi.ceph.com-ctrlplugin`) CSI pod labels +- **Duplicate `parseStorageToBytes`** — removed local copy from `OverviewPage`; imports shared implementation from `k8s.ts` +- **ObjectStore endpoint type safety** — added `endpoints` field to `CephObjectStoreStatus` interface, eliminating unsafe double-cast +- **Redundant guard** — removed duplicate `storageClasses.length > 0` condition in `OverviewPage` + +### Added + +- **Sidebar entries** for Storage Classes and Volumes pages — both are now navigable from the sidebar instead of only accessible via direct URL +- **Drawer accessibility** — all detail panel drawers now include `role="dialog"`, `aria-modal`, `aria-labelledby`, and Escape key handling + +### Changed + +- **Theme-aware colors** — replaced hardcoded hex colors with CSS custom properties (`var(--mui-palette-*)`) in `AppBarClusterBadge`, `ClusterStatusCard`, and `OverviewPage` for dark/light theme compatibility +- **API URL constants** — `RookCephDataContext` now uses `ROOK_CEPH_API_GROUP` and `ROOK_CEPH_API_VERSION` constants instead of string literals +- **`extractJsonData` hoisted** — moved from inside the component render body to module-level function + +### Removed + +- **Dead code** — removed unused `extractPoolFromVolumeHandle` function from `k8s.ts` + ## [0.2.2] - 2026-02-19 ### Changed @@ -72,11 +97,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - TypeScript strict mode with zero `any` types - ESLint + Prettier code quality tooling -[Unreleased]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.2.2...HEAD -[0.2.2]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.2.1...v0.2.2 -[0.2.1]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.2.0...v0.2.1 -[0.2.0]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.1.3...v0.2.0 -[0.1.3]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.1.2...v0.1.3 -[0.1.2]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.1.1...v0.1.2 -[0.1.1]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.1.0...v0.1.1 -[0.1.0]: https://github.com/cpfarhood/headlamp-rook-plugin/releases/tag/v0.1.0 +[Unreleased]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.2.6...HEAD +[0.2.6]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.2.5...v0.2.6 +[0.2.2]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.2.1...v0.2.2 +[0.2.1]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.2.0...v0.2.1 +[0.2.0]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.1.3...v0.2.0 +[0.1.3]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.1.2...v0.1.3 +[0.1.2]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.1.1...v0.1.2 +[0.1.1]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.1.0...v0.1.1 +[0.1.0]: https://github.com/privilegedescalation/headlamp-rook-plugin/releases/tag/v0.1.0 diff --git a/CLAUDE.md b/CLAUDE.md index f953f6b..4f3b0cf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Headlamp plugin for Rook-Ceph cluster visibility. -- **Plugin name**: `headlamp-rook-plugin` +- **Plugin name**: `rook` - **Rook-Ceph API group**: `ceph.rook.io/v1` - **Default namespace**: `rook-ceph` - **Reference plugin**: `../headlamp-tns-csi-plugin` @@ -33,7 +33,7 @@ All tests and `tsc` must pass before committing. ``` src/ -├── index.tsx # Plugin entry: registerRoute, registerSidebarEntry, etc. +├── index.tsx # Plugin entry: registerRoute, registerSidebarEntry, registerAppBarAction, etc. ├── api/ │ ├── k8s.ts # Types + filtering helpers (ceph.rook.io) │ └── RookCephDataContext.tsx # Shared React context provider @@ -46,7 +46,7 @@ src/ ├── FilesystemsPage.tsx ├── ObjectStoresPage.tsx ├── ClusterStatusCard.tsx - ├── AppBarClusterBadge.tsx + ├── AppBarClusterBadge.tsx # Cluster health badge in Headlamp top nav bar ├── PVCDetailSection.tsx # Injected into Headlamp PVC detail view ├── PVDetailSection.tsx # Injected into Headlamp PV detail view ├── CephPodDetailSection.tsx # Injected into Headlamp Pod detail view @@ -71,7 +71,9 @@ All pages consume data exclusively via `useRookCephContext()`. The provider is r - RBD provisioner: `rook-ceph.rbd.csi.ceph.com` - CephFS provisioner: `rook-ceph.cephfs.csi.ceph.com` - Custom namespace provisioners: any string ending in `.rbd.csi.ceph.com` or `.cephfs.csi.ceph.com` -- Pod selectors: `app=rook-ceph-operator`, `app=rook-ceph-mon`, `app=rook-ceph-osd`, `app=rook-ceph-mgr`, `app=csi-rbdplugin-provisioner`, `app=csi-cephfsplugin-provisioner` +- Pod selectors: `app=rook-ceph-operator`, `app=rook-ceph-mon`, `app=rook-ceph-osd`, `app=rook-ceph-mgr`, `app=rook-ceph-mds`, `app=rook-ceph-rgw` +- CSI pod selectors (Rook 1.12+): `app=rook-ceph.rbd.csi.ceph.com-ctrlplugin`, `app=rook-ceph.cephfs.csi.ceph.com-ctrlplugin` +- CSI pod selectors (legacy): `app=csi-rbdplugin-provisioner`, `app=csi-cephfsplugin-provisioner`, `app=csi-rbdplugin`, `app=csi-cephfsplugin` ## Code conventions diff --git a/src/api/RookCephDataContext.tsx b/src/api/RookCephDataContext.tsx index ffa3169..9990d73 100644 --- a/src/api/RookCephDataContext.tsx +++ b/src/api/RookCephDataContext.tsx @@ -17,6 +17,8 @@ import { filterRookCephPVCs, filterRookCephStorageClasses, isKubeList, + ROOK_CEPH_API_GROUP, + ROOK_CEPH_API_VERSION, ROOK_CEPH_NAMESPACE, ROOK_CSI_CEPHFS_SELECTOR, ROOK_CSI_RBD_SELECTOR, @@ -79,6 +81,19 @@ export function useRookCephContext(): RookCephContextValue { return ctx; } +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** Unwrap Headlamp KubeObject class instances to their raw `.jsonData`. */ +function extractJsonData(items: unknown[]): unknown[] { + return items.map(item => + item && typeof item === 'object' && 'jsonData' in item + ? (item as { jsonData: unknown }).jsonData + : item + ); +} + // --------------------------------------------------------------------------- // Provider // --------------------------------------------------------------------------- @@ -118,7 +133,7 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode } // CephCluster CRDs try { const clusterList = await ApiProxy.request( - `/apis/ceph.rook.io/v1/namespaces/${ROOK_CEPH_NAMESPACE}/cephclusters` + `/apis/${ROOK_CEPH_API_GROUP}/${ROOK_CEPH_API_VERSION}/namespaces/${ROOK_CEPH_NAMESPACE}/cephclusters` ); if (!cancelled && isKubeList(clusterList)) { setCephClusters(clusterList.items as CephCluster[]); @@ -130,7 +145,7 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode } // CephBlockPool CRDs try { const poolList = await ApiProxy.request( - `/apis/ceph.rook.io/v1/namespaces/${ROOK_CEPH_NAMESPACE}/cephblockpools` + `/apis/${ROOK_CEPH_API_GROUP}/${ROOK_CEPH_API_VERSION}/namespaces/${ROOK_CEPH_NAMESPACE}/cephblockpools` ); if (!cancelled && isKubeList(poolList)) { setBlockPools(poolList.items as CephBlockPool[]); @@ -142,7 +157,7 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode } // CephFilesystem CRDs try { const fsList = await ApiProxy.request( - `/apis/ceph.rook.io/v1/namespaces/${ROOK_CEPH_NAMESPACE}/cephfilesystems` + `/apis/${ROOK_CEPH_API_GROUP}/${ROOK_CEPH_API_VERSION}/namespaces/${ROOK_CEPH_NAMESPACE}/cephfilesystems` ); if (!cancelled && isKubeList(fsList)) { setFilesystems(fsList.items as CephFilesystem[]); @@ -154,7 +169,7 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode } // CephObjectStore CRDs try { const osList = await ApiProxy.request( - `/apis/ceph.rook.io/v1/namespaces/${ROOK_CEPH_NAMESPACE}/cephobjectstores` + `/apis/${ROOK_CEPH_API_GROUP}/${ROOK_CEPH_API_VERSION}/namespaces/${ROOK_CEPH_NAMESPACE}/cephobjectstores` ); if (!cancelled && isKubeList(osList)) { setObjectStores(osList.items as CephObjectStore[]); @@ -255,15 +270,7 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode } // Derived / filtered values — memoized to avoid recomputation on every render // --------------------------------------------------------------------------- - // Headlamp useList() returns KubeObject class instances that store raw - // Kubernetes JSON under `.jsonData`. Extract it so our plain-object helpers - // work correctly. - const extractJsonData = (items: unknown[]): unknown[] => - items.map(item => - item && typeof item === 'object' && 'jsonData' in item - ? (item as { jsonData: unknown }).jsonData - : item - ); + // Uses module-level extractJsonData below const storageClasses = useMemo(() => { if (!allStorageClasses) return []; diff --git a/src/api/k8s.ts b/src/api/k8s.ts index 912439c..8d6a1a4 100644 --- a/src/api/k8s.ts +++ b/src/api/k8s.ts @@ -209,10 +209,16 @@ export interface CephObjectStoreSpec { gateway?: { port?: number; securePort?: number; instances?: number }; } +export interface CephObjectStoreEndpoints { + insecure?: string[]; + secure?: string[]; +} + export interface CephObjectStoreStatus { phase?: string; conditions?: CephClusterCondition[]; info?: Record; + endpoints?: CephObjectStoreEndpoints; } export interface CephObjectStore extends KubeObject { @@ -463,11 +469,3 @@ export function formatStorageType(type: 'rbd' | 'cephfs' | 'unknown'): string { return 'Unknown'; } } - -/** Extracts pool/subvolume group name from a Rook-Ceph PV volumeHandle. */ -export function extractPoolFromVolumeHandle(handle: string | undefined): string { - if (!handle) return '—'; - // RBD format: "--..." — pool is in volumeAttributes - // We rely on volumeAttributes.pool instead; this just provides a fallback. - return handle; -} diff --git a/src/components/AppBarClusterBadge.tsx b/src/components/AppBarClusterBadge.tsx index a2e38a2..b623a60 100644 --- a/src/components/AppBarClusterBadge.tsx +++ b/src/components/AppBarClusterBadge.tsx @@ -15,13 +15,13 @@ import { useRookCephContext } from '../api/RookCephDataContext'; function getHealthColor(health: string | undefined): string { switch (health) { case 'HEALTH_OK': - return '#4caf50'; + return 'var(--mui-palette-success-main, #4caf50)'; case 'HEALTH_WARN': - return '#ff9800'; + return 'var(--mui-palette-warning-main, #ff9800)'; case 'HEALTH_ERR': - return '#f44336'; + return 'var(--mui-palette-error-main, #f44336)'; default: - return '#9e9e9e'; + return 'var(--mui-palette-action-disabled, #9e9e9e)'; } } diff --git a/src/components/BlockPoolsPage.tsx b/src/components/BlockPoolsPage.tsx index 4be866a..66f0d35 100644 --- a/src/components/BlockPoolsPage.tsx +++ b/src/components/BlockPoolsPage.tsx @@ -17,6 +17,12 @@ import { useRookCephContext } from '../api/RookCephDataContext'; function BlockPoolDetail({ pool, onClose }: { pool: CephBlockPool; onClose: () => void }) { return (
{ + if (e.key === 'Escape') onClose(); + }} style={{ position: 'fixed', top: 0, @@ -38,7 +44,7 @@ function BlockPoolDetail({ pool, onClose }: { pool: CephBlockPool; onClose: () = marginBottom: '16px', }} > - {pool.metadata.name} + {pool.metadata.name}