/** * StorageClassesPage — lists tns-csi StorageClasses with a slide-in detail panel. * * Pattern mirrors headlamp-polaris-plugin's NamespacesListView: * click row → detail drawer, Escape to close, URL hash state. */ 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 { TnsCsiStorageClass } from '../api/k8s'; import { formatProtocol } from '../api/k8s'; import { useTnsCsiContext } from '../api/TnsCsiDataContext'; // --------------------------------------------------------------------------- // Detail drawer // --------------------------------------------------------------------------- interface StorageClassDetailPanelProps { sc: TnsCsiStorageClass; pvCount: number; onClose: () => void; } function StorageClassDetailPanel({ sc, pvCount, onClose }: StorageClassDetailPanelProps) { const [isMaximized, setIsMaximized] = React.useState(false); const params = sc.parameters ?? {}; const protocol = formatProtocol(params.protocol); const drawerClass = `tns-csi-sc-drawer-${sc.metadata.name}`; return ( <>

{sc.metadata.name}

{sc.allowVolumeExpansion ? 'Yes' : 'No'} ), }, { name: 'Delete Strategy', value: params.deleteStrategy ?? '—' }, { name: 'Encryption', value: params.encryption === 'true' ? ( Enabled ) : ( Disabled ), }, { name: 'Provisioner', value: sc.provisioner }, { name: 'Bound PVs', value: String(pvCount) }, ]} /> {/* Protocol-specific notes */} {params.protocol && ( )}
); } function protocolNotes(protocol: string): Array<{ name: string; value: React.ReactNode }> { const lower = protocol.toLowerCase(); if (lower === 'nfs') { return [ { name: 'Prerequisite', value: 'nfs-common (Debian/Ubuntu) or nfs-utils (RHEL/Fedora) required on all nodes', }, { name: 'Access Modes', value: 'Supports RWO, RWX, RWOP' }, ]; } if (lower === 'nvmeof') { return [ { name: 'Prerequisite', value: 'nvme-cli + kernel modules nvme-tcp and nvme-fabrics required on all nodes', }, { name: 'Networking', value: 'Static IP required — DHCP is not supported for NVMe-oF' }, { name: 'Access Modes', value: 'Supports RWO, RWOP' }, ]; } if (lower === 'iscsi') { return [ { name: 'Prerequisite', value: 'open-iscsi required on all nodes' }, { name: 'Access Modes', value: 'Supports RWO, RWOP' }, ]; } return []; } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export default function StorageClassesPage() { const location = useLocation(); const history = useHistory(); const { storageClasses, persistentVolumes, loading, error } = useTnsCsiContext(); const [selectedName, setSelectedName] = useState(location.hash.slice(1) || null); useEffect(() => { setSelectedName(location.hash.slice(1) || null); }, [location.hash]); const openSc = (name: string) => { setSelectedName(name); history.push(`${location.pathname}#${name}`); }; const closeSc = useCallback(() => { setSelectedName(null); history.push(location.pathname); }, [history, location.pathname]); useEffect(() => { if (!selectedName) return; const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') closeSc(); }; window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); }, [selectedName, closeSc]); if (loading) return ; if (error) { return ( <> {error} }]} /> ); } // Build PV count per StorageClass const pvCountBySc = new Map(); for (const pv of persistentVolumes) { const scName = pv.spec.storageClassName ?? ''; pvCountBySc.set(scName, (pvCountBySc.get(scName) ?? 0) + 1); } const selectedSc = selectedName ? storageClasses.find(sc => sc.metadata.name === selectedName) ?? null : null; return ( <> ( ), }, { label: 'Protocol', getter: (sc: TnsCsiStorageClass) => formatProtocol(sc.parameters?.protocol), }, { label: 'Pool', getter: (sc: TnsCsiStorageClass) => sc.parameters?.pool ?? '—' }, { label: 'Server', getter: (sc: TnsCsiStorageClass) => sc.parameters?.server ?? '—' }, { label: 'Reclaim Policy', getter: (sc: TnsCsiStorageClass) => sc.reclaimPolicy ?? '—', }, { label: 'Expansion', getter: (sc: TnsCsiStorageClass) => ( {sc.allowVolumeExpansion ? 'Yes' : 'No'} ), }, { label: 'PVs', getter: (sc: TnsCsiStorageClass) => String(pvCountBySc.get(sc.metadata.name) ?? 0), }, ]} data={storageClasses} emptyMessage="No tns-csi StorageClasses found." /> {selectedSc && ( <>
)} ); }