25175b65b8
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>
111 lines
4.6 KiB
TypeScript
111 lines
4.6 KiB
TypeScript
/**
|
|
* StorageClassColumns — registerResourceTableColumnsProcessor integration.
|
|
*
|
|
* Adds Rook-Ceph-specific columns to the native Headlamp StorageClass table
|
|
* ('headlamp-storageclasses') and PV table ('headlamp-persistentvolumes').
|
|
* Non-Rook-Ceph rows show '—'.
|
|
*/
|
|
|
|
import React from 'react';
|
|
import { isRookCephProvisioner, formatStorageType } from '../../api/k8s';
|
|
|
|
/** Safely read a nested field from either a KubeObject instance or plain object. */
|
|
function getField(item: unknown, ...path: string[]): unknown {
|
|
if (!item || typeof item !== 'object') return undefined;
|
|
const obj = item as Record<string, unknown>;
|
|
// KubeObject instances store raw JSON in .jsonData
|
|
const raw =
|
|
'jsonData' in obj && obj['jsonData'] && typeof obj['jsonData'] === 'object'
|
|
? (obj['jsonData'] as Record<string, unknown>)
|
|
: obj;
|
|
let cur: unknown = raw;
|
|
for (const key of path) {
|
|
if (!cur || typeof cur !== 'object') return undefined;
|
|
cur = (cur as Record<string, unknown>)[key];
|
|
}
|
|
return cur;
|
|
}
|
|
|
|
function isRookRow(item: unknown): boolean {
|
|
const provisioner = getField(item, 'provisioner') as string | undefined;
|
|
return typeof provisioner === 'string' && isRookCephProvisioner(provisioner);
|
|
}
|
|
|
|
function isRookPvRow(item: unknown): boolean {
|
|
const driver = getField(item, 'spec', 'csi', 'driver') as string | undefined;
|
|
return typeof driver === 'string' && isRookCephProvisioner(driver);
|
|
}
|
|
|
|
export function buildStorageClassColumns() {
|
|
return [
|
|
{
|
|
label: 'Rook Type',
|
|
getValue: (item: unknown) => {
|
|
if (!isRookRow(item)) return null;
|
|
const provisioner = getField(item, 'provisioner') as string | undefined;
|
|
if (!provisioner) return null;
|
|
const type = provisioner.includes('.rbd.') ? 'rbd' : provisioner.includes('.cephfs.') ? 'cephfs' : 'unknown';
|
|
return formatStorageType(type as 'rbd' | 'cephfs' | 'unknown');
|
|
},
|
|
render: (item: unknown) => {
|
|
if (!isRookRow(item)) return <span>—</span>;
|
|
const provisioner = getField(item, 'provisioner') as string | undefined;
|
|
if (!provisioner) return <span>—</span>;
|
|
const type = provisioner.includes('.rbd.') ? 'rbd' : provisioner.includes('.cephfs.') ? 'cephfs' : 'unknown';
|
|
return <span style={{ color: '#1976d2', fontWeight: 500 }}>{formatStorageType(type as 'rbd' | 'cephfs' | 'unknown')}</span>;
|
|
},
|
|
},
|
|
{
|
|
label: 'Pool',
|
|
getValue: (item: unknown) => getField(item, 'parameters', 'pool') as string | null ?? null,
|
|
render: (item: unknown) => {
|
|
if (!isRookRow(item)) return <span>—</span>;
|
|
const pool = getField(item, 'parameters', 'pool') as string | undefined;
|
|
return <span>{pool ?? '—'}</span>;
|
|
},
|
|
},
|
|
{
|
|
label: 'Cluster ID',
|
|
getValue: (item: unknown) => getField(item, 'parameters', 'clusterID') as string | null ?? null,
|
|
render: (item: unknown) => {
|
|
if (!isRookRow(item)) return <span>—</span>;
|
|
const clusterID = getField(item, 'parameters', 'clusterID') as string | undefined;
|
|
if (!clusterID) return <span>—</span>;
|
|
// Truncate long cluster IDs
|
|
return <span title={clusterID}>{clusterID.length > 16 ? `${clusterID.slice(0, 16)}…` : clusterID}</span>;
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
export function buildPVColumns() {
|
|
return [
|
|
{
|
|
label: 'Rook Type',
|
|
getValue: (item: unknown) => {
|
|
if (!isRookPvRow(item)) return null;
|
|
const driver = getField(item, 'spec', 'csi', 'driver') as string | undefined;
|
|
if (!driver) return null;
|
|
const type = driver.includes('.rbd.') ? 'rbd' : driver.includes('.cephfs.') ? 'cephfs' : 'unknown';
|
|
return formatStorageType(type as 'rbd' | 'cephfs' | 'unknown');
|
|
},
|
|
render: (item: unknown) => {
|
|
if (!isRookPvRow(item)) return <span>—</span>;
|
|
const driver = getField(item, 'spec', 'csi', 'driver') as string | undefined;
|
|
if (!driver) return <span>—</span>;
|
|
const type = driver.includes('.rbd.') ? 'rbd' : driver.includes('.cephfs.') ? 'cephfs' : 'unknown';
|
|
return <span style={{ color: '#1976d2', fontWeight: 500 }}>{formatStorageType(type as 'rbd' | 'cephfs' | 'unknown')}</span>;
|
|
},
|
|
},
|
|
{
|
|
label: 'Pool',
|
|
getValue: (item: unknown) => getField(item, 'spec', 'csi', 'volumeAttributes', 'pool') as string | null ?? null,
|
|
render: (item: unknown) => {
|
|
if (!isRookPvRow(item)) return <span>—</span>;
|
|
const pool = getField(item, 'spec', 'csi', 'volumeAttributes', 'pool') as string | undefined;
|
|
return <span>{pool ?? '—'}</span>;
|
|
},
|
|
},
|
|
];
|
|
}
|