Files
headlamp-rook-plugin/src/components/ClusterStatusCard.tsx
T
Chris Farhood 25175b65b8 feat: initial release of headlamp-rook-ceph-plugin v0.1.0
Headlamp plugin for Rook-Ceph cluster visibility.

Pages:
- Overview dashboard: CephCluster health, capacity bar, resource counts
  (block pools, filesystems, object stores, PVs, PVCs), daemon pod
  health summary, non-Bound PVC alerts
- Block Pools: CephBlockPool table with replication, failure domain,
  mirroring; slide-in detail panel
- Pods: all Rook-Ceph daemon pods grouped by role with ready/total counts

Native Headlamp integrations:
- StorageClass table: Rook Type, Pool, Cluster ID columns
- PV table: Rook Type, Pool columns
- PVC detail injection: driver, type, pool, volume handle
- PV detail injection: CSI volume attributes
- Pod detail injection: Ceph daemon role badge
- App bar badge: cluster health (HEALTH_OK/WARN/ERR), color-coded

API / architecture:
- src/api/k8s.ts: types + filters for ceph.rook.io/v1 CRDs; handles
  both default rook-ceph.* and custom-namespace provisioner strings
- src/api/RookCephDataContext.tsx: shared context provider; fetches
  CephCluster, CephBlockPool, CephFilesystem, CephObjectStore CRDs
  plus daemon pods via label selectors
- 37 unit tests (vitest + @testing-library/react)
- TypeScript strict mode, zero any types
- CI + release GitHub Actions workflows

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-18 16:55:39 -05:00

169 lines
5.6 KiB
TypeScript

/**
* ClusterStatusCard — reusable component showing Rook-Ceph cluster health.
* Displays CephCluster health, phase, capacity, version, and daemon pod counts.
*/
import {
NameValueTable,
PercentageBar,
SectionBox,
StatusLabel,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import type { CephCluster, RookCephPod } from '../api/k8s';
import { formatAge, formatBytes, getPodImage, getPodRestarts, healthToStatus, isPodReady, phaseToStatus } from '../api/k8s';
interface ClusterStatusCardProps {
cephClusters: CephCluster[];
operatorPods: RookCephPod[];
monPods: RookCephPod[];
osdPods: RookCephPod[];
mgrPods: RookCephPod[];
csiRbdPods: RookCephPod[];
csiCephfsPods: RookCephPod[];
}
function PodStatusBadge({ pod }: { pod: RookCephPod }) {
const ready = isPodReady(pod);
const phase = pod.status?.phase ?? 'Unknown';
return (
<StatusLabel status={ready ? 'success' : 'error'}>
{phase}
</StatusLabel>
);
}
function PodSummaryRow({ pods, label }: { pods: RookCephPod[]; label: string }) {
const ready = pods.filter(isPodReady).length;
const total = pods.length;
const status = total === 0 ? 'error' : ready === total ? 'success' : ready > 0 ? 'warning' : 'error';
return {
name: label,
value: (
<StatusLabel status={status}>
{total === 0 ? 'None found' : `${ready}/${total} ready`}
</StatusLabel>
),
};
}
export default function ClusterStatusCard({
cephClusters,
operatorPods,
monPods,
osdPods,
mgrPods,
csiRbdPods,
csiCephfsPods,
}: ClusterStatusCardProps) {
return (
<>
{cephClusters.map(cluster => {
const health = cluster.status?.ceph?.health;
const phase = cluster.status?.phase;
const capacity = cluster.status?.ceph?.capacity;
const version = cluster.status?.version?.version ?? '—';
const bytesTotal = capacity?.bytesTotal ?? 0;
const bytesUsed = capacity?.bytesUsed ?? 0;
const bytesAvail = capacity?.bytesAvailable ?? 0;
const usedPct = bytesTotal > 0 ? Math.round((bytesUsed / bytesTotal) * 100) : 0;
return (
<React.Fragment key={cluster.metadata.name}>
<SectionBox title={`CephCluster: ${cluster.metadata.name}`}>
<NameValueTable
rows={[
{
name: 'Health',
value: (
<StatusLabel status={healthToStatus(health)}>
{health ?? 'Unknown'}
</StatusLabel>
),
},
{
name: 'Phase',
value: (
<StatusLabel status={phaseToStatus(phase)}>
{phase ?? 'Unknown'}
</StatusLabel>
),
},
...(cluster.status?.message ? [{ name: 'Message', value: cluster.status.message }] : []),
{ name: 'Ceph Version', value: version },
{ name: 'Namespace', value: cluster.metadata.namespace ?? '—' },
{ name: 'Age', value: formatAge(cluster.metadata.creationTimestamp) },
]}
/>
</SectionBox>
{bytesTotal > 0 && (
<SectionBox title="Cluster Capacity">
<div style={{ marginBottom: '12px' }}>
<PercentageBar
data={[
{ name: 'Used', value: bytesUsed, fill: usedPct > 80 ? '#f44336' : '#1976d2' },
{ name: 'Free', value: bytesAvail, fill: '#e0e0e0' },
]}
total={bytesTotal}
/>
</div>
<NameValueTable
rows={[
{ name: 'Total', value: formatBytes(bytesTotal) },
{ name: 'Used', value: `${formatBytes(bytesUsed)} (${usedPct}%)` },
{ name: 'Available', value: formatBytes(bytesAvail) },
]}
/>
</SectionBox>
)}
</React.Fragment>
);
})}
<SectionBox title="Daemon Health">
<NameValueTable
rows={[
PodSummaryRow({ pods: operatorPods, label: 'Operator' }),
PodSummaryRow({ pods: monPods, label: 'Monitors (MON)' }),
PodSummaryRow({ pods: osdPods, label: 'OSDs' }),
PodSummaryRow({ pods: mgrPods, label: 'Managers (MGR)' }),
PodSummaryRow({ pods: csiRbdPods, label: 'CSI RBD Provisioner' }),
PodSummaryRow({ pods: csiCephfsPods, label: 'CSI CephFS Provisioner' }),
]}
/>
</SectionBox>
</>
);
}
export function PodDetailRows({ pods, label }: { pods: RookCephPod[]; label: string }) {
if (pods.length === 0) {
return (
<SectionBox title={label}>
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">No pods found</StatusLabel> }]}
/>
</SectionBox>
);
}
return (
<SectionBox title={`${label} (${pods.length})`}>
{pods.map(pod => (
<NameValueTable
key={pod.metadata.name}
rows={[
{ name: 'Pod', value: pod.metadata.name },
{ name: 'Node', value: pod.spec?.nodeName ?? '—' },
{ name: 'Status', value: <PodStatusBadge pod={pod} /> },
{ name: 'Restarts', value: String(getPodRestarts(pod)) },
{ name: 'Image', value: getPodImage(pod) },
{ name: 'Age', value: formatAge(pod.metadata.creationTimestamp) },
]}
/>
))}
</SectionBox>
);
}