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

342 lines
11 KiB
TypeScript

/**
* OverviewPage — main dashboard for the Rook-Ceph plugin.
*
* Shows: cluster health, capacity overview, storage resource counts,
* daemon pod summary, and non-Bound PVC alerts.
*/
import {
Loader,
NameValueTable,
PercentageBar,
SectionBox,
SectionHeader,
SimpleTable,
StatusLabel,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import {
formatAge,
formatBytes,
healthToStatus,
parseStorageToBytes,
phaseToStatus,
storageClassType,
} from '../api/k8s';
import { useRookCephContext } from '../api/RookCephDataContext';
import ClusterStatusCard from './ClusterStatusCard';
export default function OverviewPage() {
const {
cephClusters,
clusterInstalled,
blockPools,
filesystems,
objectStores,
storageClasses,
persistentVolumes,
persistentVolumeClaims,
operatorPods,
monPods,
osdPods,
mgrPods,
csiRbdPods,
csiCephfsPods,
loading,
error,
refresh,
} = useRookCephContext();
if (loading) {
return <Loader title="Loading Rook-Ceph data..." />;
}
// Storage summary
const rbdClasses = storageClasses.filter(sc => storageClassType(sc) === 'rbd');
const cephfsClasses = storageClasses.filter(sc => storageClassType(sc) === 'cephfs');
const totalCapacityBytes = persistentVolumes.reduce((sum, pv) => {
const cap = pv.spec.capacity?.storage ?? '0';
return sum + parseStorageToBytes(cap);
}, 0);
const pvcStatusCounts = { Bound: 0, Pending: 0, Lost: 0, Other: 0 };
for (const pvc of persistentVolumeClaims) {
const phase = pvc.status?.phase ?? 'Other';
if (phase === 'Bound') pvcStatusCounts.Bound++;
else if (phase === 'Pending') pvcStatusCounts.Pending++;
else if (phase === 'Lost') pvcStatusCounts.Lost++;
else pvcStatusCounts.Other++;
}
const nonBoundPvcs = persistentVolumeClaims.filter(pvc => pvc.status?.phase !== 'Bound');
// Primary cluster health (first cluster)
const primaryCluster = cephClusters[0];
const primaryHealth = primaryCluster?.status?.ceph?.health;
return (
<>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '20px',
}}
>
<SectionHeader title="Rook-Ceph — Overview" />
<button
onClick={refresh}
aria-label="Refresh Rook-Ceph data"
style={{
padding: '6px 16px',
backgroundColor: 'transparent',
color: 'var(--mui-palette-primary-main, #1976d2)',
border: '1px solid var(--mui-palette-primary-main, #1976d2)',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '13px',
fontWeight: 500,
}}
>
Refresh
</button>
</div>
{/* Cluster not detected */}
{!clusterInstalled && !loading && (
<SectionBox title="Rook-Ceph Not Detected">
<NameValueTable
rows={[
{
name: 'Status',
value: (
<StatusLabel status="error">
No CephCluster found in namespace rook-ceph
</StatusLabel>
),
},
{
name: 'Install',
value:
'helm install rook-ceph rook-release/rook-ceph -n rook-ceph --create-namespace',
},
{
name: 'Docs',
value: 'https://rook.io/docs/rook/latest/Getting-Started/quickstart/',
},
]}
/>
</SectionBox>
)}
{/* Error state */}
{error && (
<SectionBox title="Error">
<NameValueTable
rows={[{ name: 'Status', value: <StatusLabel status="error">{error}</StatusLabel> }]}
/>
</SectionBox>
)}
{/* Quick health summary banner when cluster is installed */}
{clusterInstalled && primaryHealth && (
<SectionBox title="Cluster Health">
<NameValueTable
rows={[
{
name: 'Health',
value: (
<StatusLabel status={healthToStatus(primaryHealth)}>{primaryHealth}</StatusLabel>
),
},
{
name: 'Clusters',
value: String(cephClusters.length),
},
]}
/>
</SectionBox>
)}
{/* Storage type distribution */}
{storageClasses.length > 0 && (
<SectionBox title="Storage Summary">
<div style={{ marginBottom: '16px' }}>
<div
style={{
marginBottom: '8px',
fontSize: '14px',
color: 'var(--mui-palette-text-secondary)',
}}
>
StorageClass Type Distribution
</div>
<PercentageBar
data={[
...(rbdClasses.length > 0
? [
{
name: 'Block (RBD)',
value: rbdClasses.length,
fill: 'var(--mui-palette-primary-main, #1976d2)',
},
]
: []),
...(cephfsClasses.length > 0
? [
{
name: 'Filesystem (CephFS)',
value: cephfsClasses.length,
fill: 'var(--mui-palette-secondary-main, #9c27b0)',
},
]
: []),
]}
total={storageClasses.length}
/>
</div>
<NameValueTable
rows={[
{
name: 'Storage Classes',
value: `${storageClasses.length} (${rbdClasses.length} RBD, ${cephfsClasses.length} CephFS)`,
},
{ name: 'Block Pools', value: String(blockPools.length) },
{ name: 'Filesystems', value: String(filesystems.length) },
{ name: 'Object Stores', value: String(objectStores.length) },
{ name: 'Persistent Volumes', value: String(persistentVolumes.length) },
{ name: 'Total PV Capacity', value: formatBytes(totalCapacityBytes) },
{
name: 'PVCs (Bound)',
value: <StatusLabel status="success">{pvcStatusCounts.Bound}</StatusLabel>,
},
...(pvcStatusCounts.Pending > 0
? [
{
name: 'PVCs (Pending)',
value: <StatusLabel status="warning">{pvcStatusCounts.Pending}</StatusLabel>,
},
]
: []),
...(pvcStatusCounts.Lost > 0
? [
{
name: 'PVCs (Lost)',
value: <StatusLabel status="error">{pvcStatusCounts.Lost}</StatusLabel>,
},
]
: []),
]}
/>
</SectionBox>
)}
{/* Cluster status + capacity + daemon health */}
<ClusterStatusCard
cephClusters={cephClusters}
operatorPods={operatorPods}
monPods={monPods}
osdPods={osdPods}
mgrPods={mgrPods}
csiRbdPods={csiRbdPods}
csiCephfsPods={csiCephfsPods}
/>
{/* Block pools table */}
{blockPools.length > 0 && (
<SectionBox title="Block Pools">
<SimpleTable
columns={[
{ label: 'Name', getter: p => p.metadata.name },
{
label: 'Phase',
getter: p => (
<StatusLabel status={phaseToStatus(p.status?.phase)}>
{p.status?.phase ?? 'Unknown'}
</StatusLabel>
),
},
{ label: 'Replicas', getter: p => String(p.spec?.replicated?.size ?? '—') },
{ label: 'Failure Domain', getter: p => p.spec?.failureDomain ?? '—' },
{ label: 'Age', getter: p => formatAge(p.metadata.creationTimestamp) },
]}
data={blockPools}
/>
</SectionBox>
)}
{/* Filesystems table */}
{filesystems.length > 0 && (
<SectionBox title="Filesystems">
<SimpleTable
columns={[
{ label: 'Name', getter: f => f.metadata.name },
{
label: 'Phase',
getter: f => (
<StatusLabel status={phaseToStatus(f.status?.phase)}>
{f.status?.phase ?? 'Unknown'}
</StatusLabel>
),
},
{
label: 'Active MDS',
getter: f => String(f.spec?.metadataServer?.activeCount ?? '—'),
},
{ label: 'Age', getter: f => formatAge(f.metadata.creationTimestamp) },
]}
data={filesystems}
/>
</SectionBox>
)}
{/* Object stores table */}
{objectStores.length > 0 && (
<SectionBox title="Object Stores">
<SimpleTable
columns={[
{ label: 'Name', getter: o => o.metadata.name },
{
label: 'Phase',
getter: o => (
<StatusLabel status={phaseToStatus(o.status?.phase)}>
{o.status?.phase ?? 'Unknown'}
</StatusLabel>
),
},
{ label: 'Gateway Port', getter: o => String(o.spec?.gateway?.port ?? '—') },
{ label: 'Instances', getter: o => String(o.spec?.gateway?.instances ?? '—') },
{ label: 'Age', getter: o => formatAge(o.metadata.creationTimestamp) },
]}
data={objectStores}
/>
</SectionBox>
)}
{/* Non-bound PVCs warning */}
{nonBoundPvcs.length > 0 && (
<SectionBox title="Attention: Non-Bound PVCs">
<SimpleTable
columns={[
{ label: 'Name', getter: pvc => pvc.metadata.name },
{ label: 'Namespace', getter: pvc => pvc.metadata.namespace ?? '—' },
{
label: 'Status',
getter: pvc => (
<StatusLabel status={phaseToStatus(pvc.status?.phase)}>
{pvc.status?.phase ?? 'Unknown'}
</StatusLabel>
),
},
{ label: 'Age', getter: pvc => formatAge(pvc.metadata.creationTimestamp) },
]}
data={nonBoundPvcs}
/>
</SectionBox>
)}
</>
);
}