From c30fc18b4376f10c69809b646d206801879bb475 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Wed, 18 Feb 2026 21:23:53 -0500 Subject: [PATCH] feat: add Filesystems/ObjectStores pages, fix CSI selectors, remove app bar badge (#2) - Remove AppBarClusterBadge registration (top-bar health bubble) - Fix CSI pod selectors to match actual pod labels in this cluster (was: csi-rbdplugin-provisioner, now: rook-ceph.rbd.csi.ceph.com-ctrlplugin) - Add FilesystemsPage with detail drawer (Active MDS, data pools, status) - Add ObjectStoresPage with detail drawer (gateway port, instances, endpoints) - Register Filesystems and Object Stores as sidebar entries with routes - Enhance PodsPage OSD table with OSD ID, device class, store type, and failure domain columns from pod labels Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-authored-by: Claude Co-authored-by: Happy --- src/api/RookCephDataContext.tsx | 10 +- src/api/k8s.test.ts | 8 +- src/api/k8s.ts | 4 +- src/components/FilesystemsPage.tsx | 164 ++++++++++++++++++++++++++++ src/components/ObjectStoresPage.tsx | 160 +++++++++++++++++++++++++++ src/components/PodsPage.tsx | 36 +++++- src/index.tsx | 53 +++++++-- 7 files changed, 412 insertions(+), 23 deletions(-) create mode 100644 src/components/FilesystemsPage.tsx create mode 100644 src/components/ObjectStoresPage.tsx diff --git a/src/api/RookCephDataContext.tsx b/src/api/RookCephDataContext.tsx index 9f34b01..70d85c8 100644 --- a/src/api/RookCephDataContext.tsx +++ b/src/api/RookCephDataContext.tsx @@ -18,16 +18,16 @@ import { filterRookCephStorageClasses, isKubeList, ROOK_CEPH_NAMESPACE, - RookCephPersistentVolume, - RookCephPVC, - RookCephPod, - RookCephStorageClass, ROOK_CSI_CEPHFS_SELECTOR, ROOK_CSI_RBD_SELECTOR, ROOK_MGR_SELECTOR, ROOK_MON_SELECTOR, - ROOK_OSD_SELECTOR, ROOK_OPERATOR_SELECTOR, + ROOK_OSD_SELECTOR, + RookCephPersistentVolume, + RookCephPod, + RookCephPVC, + RookCephStorageClass, } from './k8s'; // --------------------------------------------------------------------------- diff --git a/src/api/k8s.test.ts b/src/api/k8s.test.ts index 4a0c468..c804700 100644 --- a/src/api/k8s.test.ts +++ b/src/api/k8s.test.ts @@ -1,11 +1,14 @@ import { describe, expect, it } from 'vitest'; import { filterRookCephPersistentVolumes, + filterRookCephPVCs, filterRookCephStorageClasses, - formatAge, + findBoundPv, formatAccessModes, + formatAge, formatBytes, formatStorageType, + getPodRestarts, healthToStatus, isKubeList, isPodReady, @@ -17,9 +20,6 @@ import { ROOK_CEPH_CEPHFS_PROVISIONER, ROOK_CEPH_RBD_PROVISIONER, storageClassType, - filterRookCephPVCs, - findBoundPv, - getPodRestarts, } from './k8s'; describe('isRookCephProvisioner', () => { diff --git a/src/api/k8s.ts b/src/api/k8s.ts index fbb808a..d30d505 100644 --- a/src/api/k8s.ts +++ b/src/api/k8s.ts @@ -39,8 +39,8 @@ export const ROOK_OSD_SELECTOR = 'app=rook-ceph-osd'; export const ROOK_MGR_SELECTOR = 'app=rook-ceph-mgr'; export const ROOK_MDS_SELECTOR = 'app=rook-ceph-mds'; export const ROOK_RGW_SELECTOR = 'app=rook-ceph-rgw'; -export const ROOK_CSI_RBD_SELECTOR = 'app=csi-rbdplugin-provisioner'; -export const ROOK_CSI_CEPHFS_SELECTOR = 'app=csi-cephfsplugin-provisioner'; +export const ROOK_CSI_RBD_SELECTOR = 'app=rook-ceph.rbd.csi.ceph.com-ctrlplugin'; +export const ROOK_CSI_CEPHFS_SELECTOR = 'app=rook-ceph.cephfs.csi.ceph.com-ctrlplugin'; // --------------------------------------------------------------------------- // Generic Kubernetes object base shapes diff --git a/src/components/FilesystemsPage.tsx b/src/components/FilesystemsPage.tsx new file mode 100644 index 0000000..0690fb0 --- /dev/null +++ b/src/components/FilesystemsPage.tsx @@ -0,0 +1,164 @@ +/** + * FilesystemsPage — lists CephFilesystem resources. + */ + +import { + Loader, + NameValueTable, + SectionBox, + SectionHeader, + SimpleTable, + StatusLabel, +} from '@kinvolk/headlamp-plugin/lib/CommonComponents'; +import React, { useState } from 'react'; +import { CephFilesystem, formatAge, phaseToStatus } from '../api/k8s'; +import { useRookCephContext } from '../api/RookCephDataContext'; + +function FilesystemDetail({ fs, onClose }: { fs: CephFilesystem; onClose: () => void }) { + return ( +
+
+ {fs.metadata.name} + +
+ + + {fs.status?.phase ?? 'Unknown'} + + ), + }, + { name: 'Age', value: formatAge(fs.metadata.creationTimestamp) }, + ]} + /> + + + + + {fs.spec?.dataPools && fs.spec.dataPools.length > 0 && ( + + {fs.spec.dataPools.map((pool, i) => ( + + ))} + + )} + {fs.spec?.metadataPool && ( + + + + )} + {fs.status?.info && Object.keys(fs.status.info).length > 0 && ( + + ({ name: k, value: v }))} + /> + + )} +
+ ); +} + +export default function FilesystemsPage() { + const { filesystems, loading, error } = useRookCephContext(); + const [selected, setSelected] = useState(null); + + if (loading) return ; + + return ( + <> + + + {error && ( + + {error} }]} /> + + )} + + {filesystems.length === 0 ? ( + + + + ) : ( + + ( + + ), + }, + { + label: 'Phase', + getter: (f: CephFilesystem) => ( + + {f.status?.phase ?? 'Unknown'} + + ), + }, + { 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} + /> + + )} + + {selected && ( + <> +
setSelected(null)} + /> + setSelected(null)} /> + + )} + + ); +} diff --git a/src/components/ObjectStoresPage.tsx b/src/components/ObjectStoresPage.tsx new file mode 100644 index 0000000..6f76dc6 --- /dev/null +++ b/src/components/ObjectStoresPage.tsx @@ -0,0 +1,160 @@ +/** + * ObjectStoresPage — lists CephObjectStore resources. + */ + +import { + Loader, + NameValueTable, + SectionBox, + SectionHeader, + SimpleTable, + StatusLabel, +} from '@kinvolk/headlamp-plugin/lib/CommonComponents'; +import React, { useState } from 'react'; +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)?.endpoints as + | { insecure?: string[]; secure?: string[] } + | undefined; + + return ( +
+
+ {store.metadata.name} + +
+ + + {store.status?.phase ?? 'Unknown'} + + ), + }, + { name: 'Age', value: formatAge(store.metadata.creationTimestamp) }, + ]} + /> + + + + + {(endpoints?.insecure?.length || endpoints?.secure?.length) ? ( + + + + ) : null} + {store.status?.info && Object.keys(store.status.info).length > 0 && ( + + ({ name: k, value: v }))} + /> + + )} +
+ ); +} + +export default function ObjectStoresPage() { + const { objectStores, loading, error } = useRookCephContext(); + const [selected, setSelected] = useState(null); + + if (loading) return ; + + return ( + <> + + + {error && ( + + {error} }]} /> + + )} + + {objectStores.length === 0 ? ( + + + + ) : ( + + ( + + ), + }, + { + label: 'Phase', + getter: (o: CephObjectStore) => ( + + {o.status?.phase ?? 'Unknown'} + + ), + }, + { 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} + /> + + )} + + {selected && ( + <> +
setSelected(null)} + /> + setSelected(null)} /> + + )} + + ); +} diff --git a/src/components/PodsPage.tsx b/src/components/PodsPage.tsx index 8bf4de1..268cd8b 100644 --- a/src/components/PodsPage.tsx +++ b/src/components/PodsPage.tsx @@ -39,6 +39,40 @@ function PodTable({ pods, title }: { pods: RookCephPod[]; title: string }) { ); } +function OsdTable({ pods }: { pods: RookCephPod[] }) { + if (pods.length === 0) return null; + return ( + + p.metadata.labels?.['osd'] ?? p.metadata.name }, + { + label: 'Status', + getter: (p) => { + const st = isPodReady(p) ? 'success' : p.status?.phase === 'Pending' ? 'warning' : 'error'; + return ( + + {p.status?.phase ?? 'Unknown'} + + ); + }, + }, + { + label: 'Node', + 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) }, + ]} + data={pods} + /> + + ); +} + export default function PodsPage() { const { operatorPods, @@ -84,7 +118,7 @@ export default function PodsPage() { - + diff --git a/src/index.tsx b/src/index.tsx index 87883c5..88111e8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,7 +6,6 @@ */ import { - registerAppBarAction, registerDetailsViewSection, registerResourceTableColumnsProcessor, registerRoute, @@ -14,10 +13,11 @@ 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'; import { buildPVColumns, buildStorageClassColumns } from './components/integrations/StorageClassColumns'; +import ObjectStoresPage from './components/ObjectStoresPage'; import OverviewPage from './components/OverviewPage'; import PodsPage from './components/PodsPage'; import PVCDetailSection from './components/PVCDetailSection'; @@ -53,6 +53,22 @@ registerSidebarEntry({ icon: 'mdi:database', }); +registerSidebarEntry({ + parent: 'rook-ceph', + name: 'rook-ceph-filesystems', + label: 'Filesystems', + url: '/rook-ceph/filesystems', + icon: 'mdi:folder-network', +}); + +registerSidebarEntry({ + parent: 'rook-ceph', + name: 'rook-ceph-objectstores', + label: 'Object Stores', + url: '/rook-ceph/object-stores', + icon: 'mdi:bucket', +}); + registerSidebarEntry({ parent: 'rook-ceph', name: 'rook-ceph-pods', @@ -89,6 +105,30 @@ registerRoute({ ), }); +registerRoute({ + path: '/rook-ceph/filesystems', + sidebar: 'rook-ceph-filesystems', + name: 'rook-ceph-filesystems', + exact: true, + component: () => ( + + + + ), +}); + +registerRoute({ + path: '/rook-ceph/object-stores', + sidebar: 'rook-ceph-objectstores', + name: 'rook-ceph-objectstores', + exact: true, + component: () => ( + + + + ), +}); + // Storage Classes and Volumes pages accessible via direct URL registerRoute({ path: '/rook-ceph/storage-classes', @@ -172,12 +212,3 @@ registerResourceTableColumnsProcessor(({ id, columns }) => { return columns; }); -// --------------------------------------------------------------------------- -// App bar action — cluster health badge -// --------------------------------------------------------------------------- - -registerAppBarAction(() => ( - - - -));