Files
headlamp-rook-plugin/src/components/ClusterStatusCard.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

187 lines
6.0 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
? 'var(--mui-palette-error-main, #f44336)'
: 'var(--mui-palette-primary-main, #1976d2)',
},
{
name: 'Free',
value: bytesAvail,
fill: 'var(--mui-palette-action-disabledBackground, #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>
);
}