Files
headlamp-rook-plugin/src/api/RookCephDataContext.tsx
T
DevContainer User 62c24e3857 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>
2026-03-04 12:55:37 +00:00

355 lines
11 KiB
TypeScript

/**
* RookCephDataContext — shared data provider for Rook-Ceph Kubernetes resources.
*
* Fetches CephCluster, CephBlockPool, CephFilesystem, CephObjectStore CRDs
* plus StorageClasses, PVs, PVCs, and Rook-Ceph pods via Headlamp hooks and
* ApiProxy. Provides filtered data to all child pages via React context.
*/
import { ApiProxy, K8s } from '@kinvolk/headlamp-plugin/lib';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import {
CephBlockPool,
CephCluster,
CephFilesystem,
CephObjectStore,
filterRookCephPersistentVolumes,
filterRookCephPVCs,
filterRookCephStorageClasses,
isKubeList,
ROOK_CEPH_API_GROUP,
ROOK_CEPH_API_VERSION,
ROOK_CEPH_NAMESPACE,
ROOK_CSI_CEPHFS_SELECTOR,
ROOK_CSI_RBD_SELECTOR,
ROOK_MGR_SELECTOR,
ROOK_MON_SELECTOR,
ROOK_OPERATOR_SELECTOR,
ROOK_OSD_SELECTOR,
RookCephPersistentVolume,
RookCephPod,
RookCephPVC,
RookCephStorageClass,
} from './k8s';
// ---------------------------------------------------------------------------
// Context shape
// ---------------------------------------------------------------------------
export interface RookCephContextValue {
// Cluster presence
cephClusters: CephCluster[];
clusterInstalled: boolean;
// Core CRD resources
blockPools: CephBlockPool[];
filesystems: CephFilesystem[];
objectStores: CephObjectStore[];
// Core K8s resources (filtered to Rook-Ceph only)
storageClasses: RookCephStorageClass[];
persistentVolumes: RookCephPersistentVolume[];
persistentVolumeClaims: RookCephPVC[];
// Operator / daemon pods
operatorPods: RookCephPod[];
monPods: RookCephPod[];
osdPods: RookCephPod[];
mgrPods: RookCephPod[];
csiRbdPods: RookCephPod[];
csiCephfsPods: RookCephPod[];
// Loading / error state
loading: boolean;
error: string | null;
// Manual refresh trigger
refresh: () => void;
}
// ---------------------------------------------------------------------------
// Context
// ---------------------------------------------------------------------------
const RookCephContext = createContext<RookCephContextValue | null>(null);
export function useRookCephContext(): RookCephContextValue {
const ctx = useContext(RookCephContext);
if (!ctx) {
throw new Error('useRookCephContext must be used within a RookCephDataProvider');
}
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
// ---------------------------------------------------------------------------
export function RookCephDataProvider({ children }: { children: React.ReactNode }) {
// K8s resource hooks — Headlamp re-fetches on cluster changes automatically
const [allStorageClasses, scError] = K8s.ResourceClasses.StorageClass.useList();
const [allPvs, pvError] = K8s.ResourceClasses.PersistentVolume.useList();
const [allPvcs, pvcError] = K8s.ResourceClasses.PersistentVolumeClaim.useList({ namespace: '' });
// Async-fetched resources (CRDs, pods)
const [cephClusters, setCephClusters] = useState<CephCluster[]>([]);
const [blockPools, setBlockPools] = useState<CephBlockPool[]>([]);
const [filesystems, setFilesystems] = useState<CephFilesystem[]>([]);
const [objectStores, setObjectStores] = useState<CephObjectStore[]>([]);
const [operatorPods, setOperatorPods] = useState<RookCephPod[]>([]);
const [monPods, setMonPods] = useState<RookCephPod[]>([]);
const [osdPods, setOsdPods] = useState<RookCephPod[]>([]);
const [mgrPods, setMgrPods] = useState<RookCephPod[]>([]);
const [csiRbdPods, setCsiRbdPods] = useState<RookCephPod[]>([]);
const [csiCephfsPods, setCsiCephfsPods] = useState<RookCephPod[]>([]);
const [asyncLoading, setAsyncLoading] = useState(true);
const [asyncError, setAsyncError] = useState<string | null>(null);
const [refreshKey, setRefreshKey] = useState(0);
const refresh = useCallback(() => {
setRefreshKey(k => k + 1);
}, []);
useEffect(() => {
let cancelled = false;
async function fetchAsync() {
setAsyncLoading(true);
setAsyncError(null);
try {
// CephCluster CRDs
try {
const clusterList = await ApiProxy.request(
`/apis/${ROOK_CEPH_API_GROUP}/${ROOK_CEPH_API_VERSION}/namespaces/${ROOK_CEPH_NAMESPACE}/cephclusters`
);
if (!cancelled && isKubeList(clusterList)) {
setCephClusters(clusterList.items as CephCluster[]);
}
} catch {
if (!cancelled) setCephClusters([]);
}
// CephBlockPool CRDs
try {
const poolList = await ApiProxy.request(
`/apis/${ROOK_CEPH_API_GROUP}/${ROOK_CEPH_API_VERSION}/namespaces/${ROOK_CEPH_NAMESPACE}/cephblockpools`
);
if (!cancelled && isKubeList(poolList)) {
setBlockPools(poolList.items as CephBlockPool[]);
}
} catch {
if (!cancelled) setBlockPools([]);
}
// CephFilesystem CRDs
try {
const fsList = await ApiProxy.request(
`/apis/${ROOK_CEPH_API_GROUP}/${ROOK_CEPH_API_VERSION}/namespaces/${ROOK_CEPH_NAMESPACE}/cephfilesystems`
);
if (!cancelled && isKubeList(fsList)) {
setFilesystems(fsList.items as CephFilesystem[]);
}
} catch {
if (!cancelled) setFilesystems([]);
}
// CephObjectStore CRDs
try {
const osList = await ApiProxy.request(
`/apis/${ROOK_CEPH_API_GROUP}/${ROOK_CEPH_API_VERSION}/namespaces/${ROOK_CEPH_NAMESPACE}/cephobjectstores`
);
if (!cancelled && isKubeList(osList)) {
setObjectStores(osList.items as CephObjectStore[]);
}
} catch {
if (!cancelled) setObjectStores([]);
}
// Operator pods
try {
const opList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_OPERATOR_SELECTOR
)}`
);
if (!cancelled && isKubeList(opList)) setOperatorPods(opList.items as RookCephPod[]);
} catch {
if (!cancelled) setOperatorPods([]);
}
// MON pods
try {
const monList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_MON_SELECTOR
)}`
);
if (!cancelled && isKubeList(monList)) setMonPods(monList.items as RookCephPod[]);
} catch {
if (!cancelled) setMonPods([]);
}
// OSD pods
try {
const osdList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_OSD_SELECTOR
)}`
);
if (!cancelled && isKubeList(osdList)) setOsdPods(osdList.items as RookCephPod[]);
} catch {
if (!cancelled) setOsdPods([]);
}
// MGR pods
try {
const mgrList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_MGR_SELECTOR
)}`
);
if (!cancelled && isKubeList(mgrList)) setMgrPods(mgrList.items as RookCephPod[]);
} catch {
if (!cancelled) setMgrPods([]);
}
// CSI RBD provisioner pods
try {
const csiRbdList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_CSI_RBD_SELECTOR
)}`
);
if (!cancelled && isKubeList(csiRbdList))
setCsiRbdPods(csiRbdList.items as RookCephPod[]);
} catch {
if (!cancelled) setCsiRbdPods([]);
}
// CSI CephFS provisioner pods
try {
const csiCephfsList = await ApiProxy.request(
`/api/v1/namespaces/${ROOK_CEPH_NAMESPACE}/pods?labelSelector=${encodeURIComponent(
ROOK_CSI_CEPHFS_SELECTOR
)}`
);
if (!cancelled && isKubeList(csiCephfsList))
setCsiCephfsPods(csiCephfsList.items as RookCephPod[]);
} catch {
if (!cancelled) setCsiCephfsPods([]);
}
} catch (err: unknown) {
if (!cancelled) {
setAsyncError(err instanceof Error ? err.message : String(err));
}
} finally {
if (!cancelled) setAsyncLoading(false);
}
}
void fetchAsync();
return () => {
cancelled = true;
};
}, [refreshKey]);
// ---------------------------------------------------------------------------
// Derived / filtered values — memoized to avoid recomputation on every render
// ---------------------------------------------------------------------------
// Uses module-level extractJsonData below
const storageClasses = useMemo(() => {
if (!allStorageClasses) return [];
return filterRookCephStorageClasses(extractJsonData(allStorageClasses as unknown[]));
}, [allStorageClasses]);
const persistentVolumes = useMemo(() => {
if (!allPvs) return [];
return filterRookCephPersistentVolumes(extractJsonData(allPvs as unknown[]));
}, [allPvs]);
const persistentVolumeClaims = useMemo(() => {
if (!allPvcs || persistentVolumes.length === 0) return [];
return filterRookCephPVCs(
extractJsonData(allPvcs as unknown[]) as RookCephPVC[],
persistentVolumes
);
}, [allPvcs, persistentVolumes]);
// ---------------------------------------------------------------------------
// Combined loading / error state
// ---------------------------------------------------------------------------
const loading = asyncLoading || !allStorageClasses || !allPvs || !allPvcs;
const errors: string[] = [];
if (scError) errors.push(String(scError));
if (pvError) errors.push(String(pvError));
if (pvcError) errors.push(String(pvcError));
if (asyncError) errors.push(asyncError);
const error = errors.length > 0 ? errors.join('; ') : null;
const clusterInstalled = cephClusters.length > 0;
// ---------------------------------------------------------------------------
// Memoized context value
// ---------------------------------------------------------------------------
const value = useMemo<RookCephContextValue>(
() => ({
cephClusters,
clusterInstalled,
blockPools,
filesystems,
objectStores,
storageClasses,
persistentVolumes,
persistentVolumeClaims,
operatorPods,
monPods,
osdPods,
mgrPods,
csiRbdPods,
csiCephfsPods,
loading,
error,
refresh,
}),
[
cephClusters,
clusterInstalled,
blockPools,
filesystems,
objectStores,
storageClasses,
persistentVolumes,
persistentVolumeClaims,
operatorPods,
monPods,
osdPods,
mgrPods,
csiRbdPods,
csiCephfsPods,
loading,
error,
refresh,
]
);
return <RookCephContext.Provider value={value}>{children}</RookCephContext.Provider>;
}