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>
342 lines
11 KiB
TypeScript
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>
|
|
)}
|
|
</>
|
|
);
|
|
}
|