/** * VolumesPage — lists tns-csi PersistentVolumes with PVC cross-reference. * Slide-in detail panel shows full CSI attributes. */ import { Loader, NameValueTable, SectionBox, SectionHeader, SimpleTable, StatusLabel, } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import React, { useCallback, useEffect, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import type { TnsCsiPersistentVolume } from '../api/k8s'; import { formatAccessModes, formatAge, formatProtocol, phaseToStatus } from '../api/k8s'; import { useTnsCsiContext } from '../api/TnsCsiDataContext'; // --------------------------------------------------------------------------- // Detail panel // --------------------------------------------------------------------------- interface VolumeDetailPanelProps { pv: TnsCsiPersistentVolume; onClose: () => void; } function VolumeDetailPanel({ pv, onClose }: VolumeDetailPanelProps) { const [isMaximized, setIsMaximized] = React.useState(false); const drawerClass = `tns-csi-pv-drawer-${pv.metadata.name}`; const csi = pv.spec.csi; const attrs = csi?.volumeAttributes ?? {}; const claim = pv.spec.claimRef; return ( <>

{pv.metadata.name}

{pv.status?.phase ?? 'Unknown'} ), }, { name: 'Capacity', value: pv.spec.capacity?.storage ?? '—' }, { name: 'Access Modes', value: formatAccessModes(pv.spec.accessModes) }, { name: 'Reclaim Policy', value: pv.spec.persistentVolumeReclaimPolicy ?? '—' }, { name: 'Storage Class', value: pv.spec.storageClassName ?? '—' }, { name: 'Age', value: formatAge(pv.metadata.creationTimestamp) }, ]} /> {claim && ( )} !['protocol', 'server'].includes(k)) .map(([k, v]) => ({ name: k, value: v ?? '—' })), ]} /> {/* Volume adoption note */} {pv.metadata.annotations?.['tns-csi.io/adoptable'] === 'true' && ( This volume can be adopted cross-cluster ), }, ]} /> )}
); } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export default function VolumesPage() { const location = useLocation(); const history = useHistory(); const { persistentVolumes, loading, error } = useTnsCsiContext(); const [selectedName, setSelectedName] = useState(location.hash.slice(1) || null); useEffect(() => { setSelectedName(location.hash.slice(1) || null); }, [location.hash]); const openVolume = (name: string) => { setSelectedName(name); history.push(`${location.pathname}#${name}`); }; const closeVolume = useCallback(() => { setSelectedName(null); history.push(location.pathname); }, [history, location.pathname]); useEffect(() => { if (!selectedName) return; const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') closeVolume(); }; window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); }, [selectedName, closeVolume]); if (loading) return ; if (error) { return ( <> {error} }]} /> ); } const selectedPv = selectedName ? persistentVolumes.find(pv => pv.metadata.name === selectedName) ?? null : null; return ( <> ( ), }, { label: 'PVC', getter: (pv: TnsCsiPersistentVolume) => { const claim = pv.spec.claimRef; return claim ? `${claim.namespace}/${claim.name}` : '—'; }, }, { label: 'Protocol', getter: (pv: TnsCsiPersistentVolume) => formatProtocol(pv.spec.csi?.volumeAttributes?.['protocol']), }, { label: 'Capacity', getter: (pv: TnsCsiPersistentVolume) => pv.spec.capacity?.storage ?? '—', }, { label: 'Access Modes', getter: (pv: TnsCsiPersistentVolume) => formatAccessModes(pv.spec.accessModes), }, { label: 'Reclaim', getter: (pv: TnsCsiPersistentVolume) => pv.spec.persistentVolumeReclaimPolicy ?? '—', }, { label: 'Status', getter: (pv: TnsCsiPersistentVolume) => ( {pv.status?.phase ?? 'Unknown'} ), }, { label: 'Age', getter: (pv: TnsCsiPersistentVolume) => formatAge(pv.metadata.creationTimestamp), }, ]} data={persistentVolumes} emptyMessage="No tns-csi PersistentVolumes found." /> {selectedPv && ( <>
)} ); }