/**
* 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 && (
<>
>
)}
>
);
}