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