fix: register AppBarClusterBadge, fix CSI label mismatch, improve accessibility and theme support

- Register AppBarClusterBadge via registerAppBarAction (was dead code)
- Add Rook 1.12+ CSI pod labels to CephPodDetailSection alongside legacy labels
- Add sidebar entries for Storage Classes and Volumes pages
- Add role="dialog", aria-modal, aria-labelledby, and Escape key to all detail drawers
- Replace hardcoded hex colors with CSS custom properties for dark/light theme compat
- Remove duplicate parseStorageToBytes from OverviewPage (import from k8s.ts)
- Add endpoints field to CephObjectStoreStatus interface (remove unsafe cast)
- Use ROOK_CEPH_API_GROUP/VERSION constants in API URL construction
- Hoist extractJsonData to module level
- Remove dead extractPoolFromVolumeHandle function
- Fix redundant storageClasses.length guard in OverviewPage
- Fix lint indent warnings
- Update CLAUDE.md and CHANGELOG.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
DevContainer User
2026-03-04 12:55:37 +00:00
parent fea6df6719
commit 62c24e3857
14 changed files with 185 additions and 100 deletions
+34 -8
View File
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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 ## [0.2.2] - 2026-02-19
### Changed ### 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 - TypeScript strict mode with zero `any` types
- ESLint + Prettier code quality tooling - ESLint + Prettier code quality tooling
[Unreleased]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.2.2...HEAD [Unreleased]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.2.6...HEAD
[0.2.2]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.2.1...v0.2.2 [0.2.6]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.2.5...v0.2.6
[0.2.1]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.2.0...v0.2.1 [0.2.2]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.2.1...v0.2.2
[0.2.0]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.1.3...v0.2.0 [0.2.1]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.2.0...v0.2.1
[0.1.3]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.1.2...v0.1.3 [0.2.0]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.1.3...v0.2.0
[0.1.2]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.1.1...v0.1.2 [0.1.3]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.1.2...v0.1.3
[0.1.1]: https://github.com/cpfarhood/headlamp-rook-plugin/compare/v0.1.0...v0.1.1 [0.1.2]: https://github.com/privilegedescalation/headlamp-rook-plugin/compare/v0.1.1...v0.1.2
[0.1.0]: https://github.com/cpfarhood/headlamp-rook-plugin/releases/tag/v0.1.0 [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
+6 -4
View File
@@ -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. Headlamp plugin for Rook-Ceph cluster visibility.
- **Plugin name**: `headlamp-rook-plugin` - **Plugin name**: `rook`
- **Rook-Ceph API group**: `ceph.rook.io/v1` - **Rook-Ceph API group**: `ceph.rook.io/v1`
- **Default namespace**: `rook-ceph` - **Default namespace**: `rook-ceph`
- **Reference plugin**: `../headlamp-tns-csi-plugin` - **Reference plugin**: `../headlamp-tns-csi-plugin`
@@ -33,7 +33,7 @@ All tests and `tsc` must pass before committing.
``` ```
src/ src/
├── index.tsx # Plugin entry: registerRoute, registerSidebarEntry, etc. ├── index.tsx # Plugin entry: registerRoute, registerSidebarEntry, registerAppBarAction, etc.
├── api/ ├── api/
│ ├── k8s.ts # Types + filtering helpers (ceph.rook.io) │ ├── k8s.ts # Types + filtering helpers (ceph.rook.io)
│ └── RookCephDataContext.tsx # Shared React context provider │ └── RookCephDataContext.tsx # Shared React context provider
@@ -46,7 +46,7 @@ src/
├── FilesystemsPage.tsx ├── FilesystemsPage.tsx
├── ObjectStoresPage.tsx ├── ObjectStoresPage.tsx
├── ClusterStatusCard.tsx ├── ClusterStatusCard.tsx
├── AppBarClusterBadge.tsx ├── AppBarClusterBadge.tsx # Cluster health badge in Headlamp top nav bar
├── PVCDetailSection.tsx # Injected into Headlamp PVC detail view ├── PVCDetailSection.tsx # Injected into Headlamp PVC detail view
├── PVDetailSection.tsx # Injected into Headlamp PV detail view ├── PVDetailSection.tsx # Injected into Headlamp PV detail view
├── CephPodDetailSection.tsx # Injected into Headlamp Pod 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` - RBD provisioner: `rook-ceph.rbd.csi.ceph.com`
- CephFS provisioner: `rook-ceph.cephfs.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` - 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 ## Code conventions
+20 -13
View File
@@ -17,6 +17,8 @@ import {
filterRookCephPVCs, filterRookCephPVCs,
filterRookCephStorageClasses, filterRookCephStorageClasses,
isKubeList, isKubeList,
ROOK_CEPH_API_GROUP,
ROOK_CEPH_API_VERSION,
ROOK_CEPH_NAMESPACE, ROOK_CEPH_NAMESPACE,
ROOK_CSI_CEPHFS_SELECTOR, ROOK_CSI_CEPHFS_SELECTOR,
ROOK_CSI_RBD_SELECTOR, ROOK_CSI_RBD_SELECTOR,
@@ -79,6 +81,19 @@ export function useRookCephContext(): RookCephContextValue {
return ctx; 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 // Provider
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -118,7 +133,7 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// CephCluster CRDs // CephCluster CRDs
try { try {
const clusterList = await ApiProxy.request( 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)) { if (!cancelled && isKubeList(clusterList)) {
setCephClusters(clusterList.items as CephCluster[]); setCephClusters(clusterList.items as CephCluster[]);
@@ -130,7 +145,7 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// CephBlockPool CRDs // CephBlockPool CRDs
try { try {
const poolList = await ApiProxy.request( 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)) { if (!cancelled && isKubeList(poolList)) {
setBlockPools(poolList.items as CephBlockPool[]); setBlockPools(poolList.items as CephBlockPool[]);
@@ -142,7 +157,7 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// CephFilesystem CRDs // CephFilesystem CRDs
try { try {
const fsList = await ApiProxy.request( 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)) { if (!cancelled && isKubeList(fsList)) {
setFilesystems(fsList.items as CephFilesystem[]); setFilesystems(fsList.items as CephFilesystem[]);
@@ -154,7 +169,7 @@ export function RookCephDataProvider({ children }: { children: React.ReactNode }
// CephObjectStore CRDs // CephObjectStore CRDs
try { try {
const osList = await ApiProxy.request( 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)) { if (!cancelled && isKubeList(osList)) {
setObjectStores(osList.items as CephObjectStore[]); 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 // Derived / filtered values — memoized to avoid recomputation on every render
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Headlamp useList() returns KubeObject class instances that store raw // Uses module-level extractJsonData below
// 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
);
const storageClasses = useMemo(() => { const storageClasses = useMemo(() => {
if (!allStorageClasses) return []; if (!allStorageClasses) return [];
+6 -8
View File
@@ -209,10 +209,16 @@ export interface CephObjectStoreSpec {
gateway?: { port?: number; securePort?: number; instances?: number }; gateway?: { port?: number; securePort?: number; instances?: number };
} }
export interface CephObjectStoreEndpoints {
insecure?: string[];
secure?: string[];
}
export interface CephObjectStoreStatus { export interface CephObjectStoreStatus {
phase?: string; phase?: string;
conditions?: CephClusterCondition[]; conditions?: CephClusterCondition[];
info?: Record<string, string>; info?: Record<string, string>;
endpoints?: CephObjectStoreEndpoints;
} }
export interface CephObjectStore extends KubeObject { export interface CephObjectStore extends KubeObject {
@@ -463,11 +469,3 @@ export function formatStorageType(type: 'rbd' | 'cephfs' | 'unknown'): string {
return 'Unknown'; 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: "<csi-vol-id>-<pool>-..." — pool is in volumeAttributes
// We rely on volumeAttributes.pool instead; this just provides a fallback.
return handle;
}
+4 -4
View File
@@ -15,13 +15,13 @@ import { useRookCephContext } from '../api/RookCephDataContext';
function getHealthColor(health: string | undefined): string { function getHealthColor(health: string | undefined): string {
switch (health) { switch (health) {
case 'HEALTH_OK': case 'HEALTH_OK':
return '#4caf50'; return 'var(--mui-palette-success-main, #4caf50)';
case 'HEALTH_WARN': case 'HEALTH_WARN':
return '#ff9800'; return 'var(--mui-palette-warning-main, #ff9800)';
case 'HEALTH_ERR': case 'HEALTH_ERR':
return '#f44336'; return 'var(--mui-palette-error-main, #f44336)';
default: default:
return '#9e9e9e'; return 'var(--mui-palette-action-disabled, #9e9e9e)';
} }
} }
+7 -1
View File
@@ -17,6 +17,12 @@ import { useRookCephContext } from '../api/RookCephDataContext';
function BlockPoolDetail({ pool, onClose }: { pool: CephBlockPool; onClose: () => void }) { function BlockPoolDetail({ pool, onClose }: { pool: CephBlockPool; onClose: () => void }) {
return ( return (
<div <div
role="dialog"
aria-modal="true"
aria-labelledby="drawer-title-blockpool"
onKeyDown={e => {
if (e.key === 'Escape') onClose();
}}
style={{ style={{
position: 'fixed', position: 'fixed',
top: 0, top: 0,
@@ -38,7 +44,7 @@ function BlockPoolDetail({ pool, onClose }: { pool: CephBlockPool; onClose: () =
marginBottom: '16px', marginBottom: '16px',
}} }}
> >
<strong>{pool.metadata.name}</strong> <strong id="drawer-title-blockpool">{pool.metadata.name}</strong>
<button <button
onClick={onClose} onClick={onClose}
aria-label="Close" aria-label="Close"
+6
View File
@@ -47,10 +47,14 @@ const ROOK_APP_LABELS = new Set([
'rook-ceph-mgr', 'rook-ceph-mgr',
'rook-ceph-mds', 'rook-ceph-mds',
'rook-ceph-rgw', 'rook-ceph-rgw',
// Legacy CSI labels (pre-Rook 1.12)
'csi-rbdplugin-provisioner', 'csi-rbdplugin-provisioner',
'csi-cephfsplugin-provisioner', 'csi-cephfsplugin-provisioner',
'csi-rbdplugin', 'csi-rbdplugin',
'csi-cephfsplugin', 'csi-cephfsplugin',
// New CSI labels (Rook 1.12+)
'rook-ceph.rbd.csi.ceph.com-ctrlplugin',
'rook-ceph.cephfs.csi.ceph.com-ctrlplugin',
]); ]);
const ROLE_LABELS: Record<string, string> = { const ROLE_LABELS: Record<string, string> = {
@@ -64,6 +68,8 @@ const ROLE_LABELS: Record<string, string> = {
'csi-cephfsplugin-provisioner': 'CSI CephFS Provisioner', 'csi-cephfsplugin-provisioner': 'CSI CephFS Provisioner',
'csi-rbdplugin': 'CSI RBD Node Plugin', 'csi-rbdplugin': 'CSI RBD Node Plugin',
'csi-cephfsplugin': 'CSI CephFS Node Plugin', 'csi-cephfsplugin': 'CSI CephFS Node Plugin',
'rook-ceph.rbd.csi.ceph.com-ctrlplugin': 'CSI RBD Provisioner',
'rook-ceph.cephfs.csi.ceph.com-ctrlplugin': 'CSI CephFS Provisioner',
}; };
export default function CephPodDetailSection({ resource }: CephPodDetailSectionProps) { export default function CephPodDetailSection({ resource }: CephPodDetailSectionProps) {
+9 -2
View File
@@ -110,9 +110,16 @@ export default function ClusterStatusCard({
{ {
name: 'Used', name: 'Used',
value: bytesUsed, value: bytesUsed,
fill: usedPct > 80 ? '#f44336' : '#1976d2', fill:
usedPct > 80
? 'var(--mui-palette-error-main, #f44336)'
: 'var(--mui-palette-primary-main, #1976d2)',
},
{
name: 'Free',
value: bytesAvail,
fill: 'var(--mui-palette-action-disabledBackground, #e0e0e0)',
}, },
{ name: 'Free', value: bytesAvail, fill: '#e0e0e0' },
]} ]}
total={bytesTotal} total={bytesTotal}
/> />
+7 -1
View File
@@ -17,6 +17,12 @@ import { useRookCephContext } from '../api/RookCephDataContext';
function FilesystemDetail({ fs, onClose }: { fs: CephFilesystem; onClose: () => void }) { function FilesystemDetail({ fs, onClose }: { fs: CephFilesystem; onClose: () => void }) {
return ( return (
<div <div
role="dialog"
aria-modal="true"
aria-labelledby="drawer-title-filesystem"
onKeyDown={e => {
if (e.key === 'Escape') onClose();
}}
style={{ style={{
position: 'fixed', position: 'fixed',
top: 0, top: 0,
@@ -38,7 +44,7 @@ function FilesystemDetail({ fs, onClose }: { fs: CephFilesystem; onClose: () =>
marginBottom: '16px', marginBottom: '16px',
}} }}
> >
<strong>{fs.metadata.name}</strong> <strong id="drawer-title-filesystem">{fs.metadata.name}</strong>
<button <button
onClick={onClose} onClick={onClose}
aria-label="Close" aria-label="Close"
+8 -4
View File
@@ -15,12 +15,16 @@ import { CephObjectStore, formatAge, phaseToStatus } from '../api/k8s';
import { useRookCephContext } from '../api/RookCephDataContext'; import { useRookCephContext } from '../api/RookCephDataContext';
function ObjectStoreDetail({ store, onClose }: { store: CephObjectStore; onClose: () => void }) { function ObjectStoreDetail({ store, onClose }: { store: CephObjectStore; onClose: () => void }) {
const endpoints = (store.status as unknown as Record<string, unknown>)?.endpoints as const endpoints = store.status?.endpoints;
| { insecure?: string[]; secure?: string[] }
| undefined;
return ( return (
<div <div
role="dialog"
aria-modal="true"
aria-labelledby="drawer-title-objectstore"
onKeyDown={e => {
if (e.key === 'Escape') onClose();
}}
style={{ style={{
position: 'fixed', position: 'fixed',
top: 0, top: 0,
@@ -42,7 +46,7 @@ function ObjectStoreDetail({ store, onClose }: { store: CephObjectStore; onClose
marginBottom: '16px', marginBottom: '16px',
}} }}
> >
<strong>{store.metadata.name}</strong> <strong id="drawer-title-objectstore">{store.metadata.name}</strong>
<button <button
onClick={onClose} onClick={onClose}
aria-label="Close" aria-label="Close"
+9 -25
View File
@@ -19,6 +19,7 @@ import {
formatAge, formatAge,
formatBytes, formatBytes,
healthToStatus, healthToStatus,
parseStorageToBytes,
phaseToStatus, phaseToStatus,
storageClassType, storageClassType,
} from '../api/k8s'; } from '../api/k8s';
@@ -162,7 +163,6 @@ export default function OverviewPage() {
{/* Storage type distribution */} {/* Storage type distribution */}
{storageClasses.length > 0 && ( {storageClasses.length > 0 && (
<SectionBox title="Storage Summary"> <SectionBox title="Storage Summary">
{storageClasses.length > 0 && (
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<div <div
style={{ style={{
@@ -176,14 +176,20 @@ export default function OverviewPage() {
<PercentageBar <PercentageBar
data={[ data={[
...(rbdClasses.length > 0 ...(rbdClasses.length > 0
? [{ name: 'Block (RBD)', value: rbdClasses.length, fill: '#1976d2' }] ? [
{
name: 'Block (RBD)',
value: rbdClasses.length,
fill: 'var(--mui-palette-primary-main, #1976d2)',
},
]
: []), : []),
...(cephfsClasses.length > 0 ...(cephfsClasses.length > 0
? [ ? [
{ {
name: 'Filesystem (CephFS)', name: 'Filesystem (CephFS)',
value: cephfsClasses.length, value: cephfsClasses.length,
fill: '#9c27b0', fill: 'var(--mui-palette-secondary-main, #9c27b0)',
}, },
] ]
: []), : []),
@@ -191,7 +197,6 @@ export default function OverviewPage() {
total={storageClasses.length} total={storageClasses.length}
/> />
</div> </div>
)}
<NameValueTable <NameValueTable
rows={[ rows={[
{ {
@@ -334,24 +339,3 @@ export default function OverviewPage() {
</> </>
); );
} }
function parseStorageToBytes(storage: string): number {
const match = /^(\d+(?:\.\d+)?)\s*(Ki|Mi|Gi|Ti|Pi|K|M|G|T|P)?$/.exec(storage.trim());
if (!match) return 0;
const value = parseFloat(match[1]);
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,
};
return value * (multipliers[suffix] ?? 1);
}
+7 -1
View File
@@ -26,6 +26,12 @@ function StorageClassDetail({
const type = storageClassType(sc); const type = storageClassType(sc);
return ( return (
<div <div
role="dialog"
aria-modal="true"
aria-labelledby="drawer-title-storageclass"
onKeyDown={e => {
if (e.key === 'Escape') onClose();
}}
style={{ style={{
position: 'fixed', position: 'fixed',
top: 0, top: 0,
@@ -47,7 +53,7 @@ function StorageClassDetail({
marginBottom: '16px', marginBottom: '16px',
}} }}
> >
<strong>{sc.metadata.name}</strong> <strong id="drawer-title-storageclass">{sc.metadata.name}</strong>
<button <button
onClick={onClose} onClick={onClose}
aria-label="Close" aria-label="Close"
+7 -1
View File
@@ -18,6 +18,12 @@ function PVDetail({ pv, onClose }: { pv: RookCephPersistentVolume; onClose: () =
const attrs = pv.spec.csi?.volumeAttributes ?? {}; const attrs = pv.spec.csi?.volumeAttributes ?? {};
return ( return (
<div <div
role="dialog"
aria-modal="true"
aria-labelledby="drawer-title-pv"
onKeyDown={e => {
if (e.key === 'Escape') onClose();
}}
style={{ style={{
position: 'fixed', position: 'fixed',
top: 0, top: 0,
@@ -39,7 +45,7 @@ function PVDetail({ pv, onClose }: { pv: RookCephPersistentVolume; onClose: () =
marginBottom: '16px', marginBottom: '16px',
}} }}
> >
<strong>{pv.metadata.name}</strong> <strong id="drawer-title-pv">{pv.metadata.name}</strong>
<button <button
onClick={onClose} onClick={onClose}
aria-label="Close" aria-label="Close"
+30 -3
View File
@@ -6,6 +6,7 @@
*/ */
import { import {
registerAppBarAction,
registerDetailsViewSection, registerDetailsViewSection,
registerResourceTableColumnsProcessor, registerResourceTableColumnsProcessor,
registerRoute, registerRoute,
@@ -13,6 +14,7 @@ import {
} from '@kinvolk/headlamp-plugin/lib'; } from '@kinvolk/headlamp-plugin/lib';
import React from 'react'; import React from 'react';
import { RookCephDataProvider } from './api/RookCephDataContext'; import { RookCephDataProvider } from './api/RookCephDataContext';
import AppBarClusterBadge from './components/AppBarClusterBadge';
import BlockPoolsPage from './components/BlockPoolsPage'; import BlockPoolsPage from './components/BlockPoolsPage';
import CephPodDetailSection from './components/CephPodDetailSection'; import CephPodDetailSection from './components/CephPodDetailSection';
import FilesystemsPage from './components/FilesystemsPage'; import FilesystemsPage from './components/FilesystemsPage';
@@ -72,6 +74,22 @@ registerSidebarEntry({
icon: 'mdi:bucket', icon: 'mdi:bucket',
}); });
registerSidebarEntry({
parent: 'rook-ceph',
name: 'rook-ceph-storage-classes',
label: 'Storage Classes',
url: '/rook-ceph/storage-classes',
icon: 'mdi:database-settings',
});
registerSidebarEntry({
parent: 'rook-ceph',
name: 'rook-ceph-volumes',
label: 'Volumes',
url: '/rook-ceph/volumes',
icon: 'mdi:harddisk',
});
registerSidebarEntry({ registerSidebarEntry({
parent: 'rook-ceph', parent: 'rook-ceph',
name: 'rook-ceph-pods', name: 'rook-ceph-pods',
@@ -80,6 +98,16 @@ registerSidebarEntry({
icon: 'mdi:cube-outline', icon: 'mdi:cube-outline',
}); });
// ---------------------------------------------------------------------------
// App bar action — cluster health badge
// ---------------------------------------------------------------------------
registerAppBarAction(() => (
<RookCephDataProvider>
<AppBarClusterBadge />
</RookCephDataProvider>
));
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Routes // Routes
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -132,10 +160,9 @@ registerRoute({
), ),
}); });
// Storage Classes and Volumes pages accessible via direct URL
registerRoute({ registerRoute({
path: '/rook-ceph/storage-classes', path: '/rook-ceph/storage-classes',
sidebar: 'rook-ceph-overview', sidebar: 'rook-ceph-storage-classes',
name: 'rook-ceph-storage-classes', name: 'rook-ceph-storage-classes',
exact: true, exact: true,
component: () => ( component: () => (
@@ -147,7 +174,7 @@ registerRoute({
registerRoute({ registerRoute({
path: '/rook-ceph/volumes', path: '/rook-ceph/volumes',
sidebar: 'rook-ceph-overview', sidebar: 'rook-ceph-volumes',
name: 'rook-ceph-volumes', name: 'rook-ceph-volumes',
exact: true, exact: true,
component: () => ( component: () => (