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:
+34
-8
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
+6
-8
@@ -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<string, string>;
|
||||
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: "<csi-vol-id>-<pool>-..." — pool is in volumeAttributes
|
||||
// We rely on volumeAttributes.pool instead; this just provides a fallback.
|
||||
return handle;
|
||||
}
|
||||
|
||||
@@ -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)';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,12 @@ import { useRookCephContext } from '../api/RookCephDataContext';
|
||||
function BlockPoolDetail({ pool, onClose }: { pool: CephBlockPool; onClose: () => void }) {
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="drawer-title-blockpool"
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
}}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
@@ -38,7 +44,7 @@ function BlockPoolDetail({ pool, onClose }: { pool: CephBlockPool; onClose: () =
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
<strong>{pool.metadata.name}</strong>
|
||||
<strong id="drawer-title-blockpool">{pool.metadata.name}</strong>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
|
||||
@@ -47,10 +47,14 @@ const ROOK_APP_LABELS = new Set([
|
||||
'rook-ceph-mgr',
|
||||
'rook-ceph-mds',
|
||||
'rook-ceph-rgw',
|
||||
// Legacy CSI labels (pre-Rook 1.12)
|
||||
'csi-rbdplugin-provisioner',
|
||||
'csi-cephfsplugin-provisioner',
|
||||
'csi-rbdplugin',
|
||||
'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> = {
|
||||
@@ -64,6 +68,8 @@ const ROLE_LABELS: Record<string, string> = {
|
||||
'csi-cephfsplugin-provisioner': 'CSI CephFS Provisioner',
|
||||
'csi-rbdplugin': 'CSI RBD 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) {
|
||||
|
||||
@@ -110,9 +110,16 @@ export default function ClusterStatusCard({
|
||||
{
|
||||
name: 'Used',
|
||||
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}
|
||||
/>
|
||||
|
||||
@@ -17,6 +17,12 @@ import { useRookCephContext } from '../api/RookCephDataContext';
|
||||
function FilesystemDetail({ fs, onClose }: { fs: CephFilesystem; onClose: () => void }) {
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="drawer-title-filesystem"
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
}}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
@@ -38,7 +44,7 @@ function FilesystemDetail({ fs, onClose }: { fs: CephFilesystem; onClose: () =>
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
<strong>{fs.metadata.name}</strong>
|
||||
<strong id="drawer-title-filesystem">{fs.metadata.name}</strong>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
|
||||
@@ -15,12 +15,16 @@ import { CephObjectStore, formatAge, phaseToStatus } from '../api/k8s';
|
||||
import { useRookCephContext } from '../api/RookCephDataContext';
|
||||
|
||||
function ObjectStoreDetail({ store, onClose }: { store: CephObjectStore; onClose: () => void }) {
|
||||
const endpoints = (store.status as unknown as Record<string, unknown>)?.endpoints as
|
||||
| { insecure?: string[]; secure?: string[] }
|
||||
| undefined;
|
||||
const endpoints = store.status?.endpoints;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="drawer-title-objectstore"
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
}}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
@@ -42,7 +46,7 @@ function ObjectStoreDetail({ store, onClose }: { store: CephObjectStore; onClose
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
<strong>{store.metadata.name}</strong>
|
||||
<strong id="drawer-title-objectstore">{store.metadata.name}</strong>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
formatAge,
|
||||
formatBytes,
|
||||
healthToStatus,
|
||||
parseStorageToBytes,
|
||||
phaseToStatus,
|
||||
storageClassType,
|
||||
} from '../api/k8s';
|
||||
@@ -162,36 +163,40 @@ export default function OverviewPage() {
|
||||
{/* Storage type distribution */}
|
||||
{storageClasses.length > 0 && (
|
||||
<SectionBox title="Storage Summary">
|
||||
{storageClasses.length > 0 && (
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '8px',
|
||||
fontSize: '14px',
|
||||
color: 'var(--mui-palette-text-secondary)',
|
||||
}}
|
||||
>
|
||||
StorageClass Type Distribution
|
||||
</div>
|
||||
<PercentageBar
|
||||
data={[
|
||||
...(rbdClasses.length > 0
|
||||
? [{ name: 'Block (RBD)', value: rbdClasses.length, fill: '#1976d2' }]
|
||||
: []),
|
||||
...(cephfsClasses.length > 0
|
||||
? [
|
||||
{
|
||||
name: 'Filesystem (CephFS)',
|
||||
value: cephfsClasses.length,
|
||||
fill: '#9c27b0',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
total={storageClasses.length}
|
||||
/>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '8px',
|
||||
fontSize: '14px',
|
||||
color: 'var(--mui-palette-text-secondary)',
|
||||
}}
|
||||
>
|
||||
StorageClass Type Distribution
|
||||
</div>
|
||||
)}
|
||||
<PercentageBar
|
||||
data={[
|
||||
...(rbdClasses.length > 0
|
||||
? [
|
||||
{
|
||||
name: 'Block (RBD)',
|
||||
value: rbdClasses.length,
|
||||
fill: 'var(--mui-palette-primary-main, #1976d2)',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(cephfsClasses.length > 0
|
||||
? [
|
||||
{
|
||||
name: 'Filesystem (CephFS)',
|
||||
value: cephfsClasses.length,
|
||||
fill: 'var(--mui-palette-secondary-main, #9c27b0)',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
total={storageClasses.length}
|
||||
/>
|
||||
</div>
|
||||
<NameValueTable
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ function StorageClassDetail({
|
||||
const type = storageClassType(sc);
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="drawer-title-storageclass"
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
}}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
@@ -47,7 +53,7 @@ function StorageClassDetail({
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
<strong>{sc.metadata.name}</strong>
|
||||
<strong id="drawer-title-storageclass">{sc.metadata.name}</strong>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
|
||||
@@ -18,6 +18,12 @@ function PVDetail({ pv, onClose }: { pv: RookCephPersistentVolume; onClose: () =
|
||||
const attrs = pv.spec.csi?.volumeAttributes ?? {};
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="drawer-title-pv"
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
}}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
@@ -39,7 +45,7 @@ function PVDetail({ pv, onClose }: { pv: RookCephPersistentVolume; onClose: () =
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
<strong>{pv.metadata.name}</strong>
|
||||
<strong id="drawer-title-pv">{pv.metadata.name}</strong>
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
|
||||
+30
-3
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
registerAppBarAction,
|
||||
registerDetailsViewSection,
|
||||
registerResourceTableColumnsProcessor,
|
||||
registerRoute,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
} from '@kinvolk/headlamp-plugin/lib';
|
||||
import React from 'react';
|
||||
import { RookCephDataProvider } from './api/RookCephDataContext';
|
||||
import AppBarClusterBadge from './components/AppBarClusterBadge';
|
||||
import BlockPoolsPage from './components/BlockPoolsPage';
|
||||
import CephPodDetailSection from './components/CephPodDetailSection';
|
||||
import FilesystemsPage from './components/FilesystemsPage';
|
||||
@@ -72,6 +74,22 @@ registerSidebarEntry({
|
||||
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({
|
||||
parent: 'rook-ceph',
|
||||
name: 'rook-ceph-pods',
|
||||
@@ -80,6 +98,16 @@ registerSidebarEntry({
|
||||
icon: 'mdi:cube-outline',
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// App bar action — cluster health badge
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
registerAppBarAction(() => (
|
||||
<RookCephDataProvider>
|
||||
<AppBarClusterBadge />
|
||||
</RookCephDataProvider>
|
||||
));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Routes
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -132,10 +160,9 @@ registerRoute({
|
||||
),
|
||||
});
|
||||
|
||||
// Storage Classes and Volumes pages accessible via direct URL
|
||||
registerRoute({
|
||||
path: '/rook-ceph/storage-classes',
|
||||
sidebar: 'rook-ceph-overview',
|
||||
sidebar: 'rook-ceph-storage-classes',
|
||||
name: 'rook-ceph-storage-classes',
|
||||
exact: true,
|
||||
component: () => (
|
||||
@@ -147,7 +174,7 @@ registerRoute({
|
||||
|
||||
registerRoute({
|
||||
path: '/rook-ceph/volumes',
|
||||
sidebar: 'rook-ceph-overview',
|
||||
sidebar: 'rook-ceph-volumes',
|
||||
name: 'rook-ceph-volumes',
|
||||
exact: true,
|
||||
component: () => (
|
||||
|
||||
Reference in New Issue
Block a user