Files
headlamp-tns-csi-plugin/src/components/PVCDetailSection.tsx
T
Chris Farhood e5d1fcb11c Implement headlamp-tns-csi-plugin
Full plugin implementation with 6 pages, K8s resource filtering,
Prometheus metrics parsing, kbench benchmark runner, and 67 unit tests.

## Pages
- Overview: driver health, storage summary, protocol distribution chart, non-Bound PVC alerts
- Storage Classes: tns-csi SC table with slide-in detail panel + protocol notes
- Volumes: PV table with full CSI attribute detail panel
- Snapshots: VolumeSnapshot CRDs with graceful degradation if not installed
- Metrics: Prometheus text format parser + WebSocket/Volume/CSI operation cards
- Benchmark: kbench Job+PVC lifecycle, FIO log parser, past benchmarks list

## API modules
- k8s.ts: typed resource shapes, filtering helpers, formatting utilities
- metrics.ts: Prometheus text format parser, tns-csi metric extraction
- kbench.ts: Job/PVC manifests, lifecycle management, FIO summary parser
- TnsCsiDataContext.tsx: shared React context with memoized filtered resources

## Quality
- TypeScript strict mode, zero any, discriminated union for benchmark state
- 67 tests passing (vitest + @testing-library/react)
- registerDetailsViewSection injects TNS-CSI details on PVC pages
- Graceful degradation for missing CSIDriver and VolumeSnapshot CRDs

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>
2026-02-18 07:45:19 -05:00

68 lines
2.1 KiB
TypeScript

/**
* PVCDetailSection — injected into Headlamp's PVC detail view.
*
* Shown only when the bound PV uses tns.csi.io as the CSI driver.
* Uses registerDetailsViewSection in index.tsx.
*/
import {
NameValueTable,
SectionBox,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import { useTnsCsiContext } from '../api/TnsCsiDataContext';
import { findBoundPv, formatProtocol } from '../api/k8s';
interface PVCDetailSectionProps {
resource: {
metadata?: { name?: string; namespace?: string };
spec?: { volumeName?: string; storageClassName?: string };
};
}
export default function PVCDetailSection({ resource }: PVCDetailSectionProps) {
const { persistentVolumes, persistentVolumeClaims, loading } = useTnsCsiContext();
if (loading) return null;
// Find this PVC in our filtered list
const pvcName = resource.metadata?.name;
const pvcNamespace = resource.metadata?.namespace;
const matchedPvc = persistentVolumeClaims.find(
pvc => pvc.metadata.name === pvcName && pvc.metadata.namespace === pvcNamespace
);
if (!matchedPvc) {
// Not a tns-csi PVC — render nothing
return null;
}
const boundPv = findBoundPv(matchedPvc, persistentVolumes);
if (!boundPv) return null;
const attrs = boundPv.spec.csi?.volumeAttributes ?? {};
const protocol = formatProtocol(attrs['protocol']);
return (
<SectionBox title="TNS-CSI Storage Details">
<NameValueTable
rows={[
{ name: 'Driver', value: 'tns.csi.io' },
{ name: 'Protocol', value: protocol },
{ name: 'Server', value: attrs['server'] ?? '—' },
{ name: 'Storage Class', value: boundPv.spec.storageClassName ?? '—' },
{ name: 'Volume Handle', value: boundPv.spec.csi?.volumeHandle ?? '—' },
...(Object.entries(attrs)
.filter(([k]) => !['protocol', 'server'].includes(k))
.map(([k, v]) => ({ name: k, value: v ?? '—' }))
),
{
name: 'PV Name',
value: boundPv.metadata.name,
},
]}
/>
</SectionBox>
);
}