62c24e3857
- 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>
187 lines
6.0 KiB
TypeScript
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>
|
|
);
|
|
}
|