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(() => (
-
-
-
-));