feat: native Headlamp integration, TrueNAS API, docs, and CI for v0.2.0
Native Headlamp integrations: - registerResourceTableColumnsProcessor: add Protocol/Pool/Server columns to native StorageClass table and Protocol/Volume Handle to PV table - registerDetailsViewSection: inject TNS-CSI section into PV detail pages - registerDetailsViewSection: inject driver role/status into tns-csi Pod pages - registerDetailsViewHeaderAction: Benchmark shortcut on StorageClass detail - registerAppBarAction: driver health badge (N/Nc M/Mn, color-coded) - Trim sidebar from 6 → 4 entries (Overview, Snapshots, Metrics, Benchmark) TrueNAS API integration: - src/api/truenas.ts: ConfigStore-backed settings, WebSocket JSON-RPC client for pool.query (auth.login_with_api_key + pool.query) - src/components/TnsCsiSettings.tsx: API key + server override settings UI with connection test button - TnsCsiDataContext: fetch real pool stats (size/allocated/free/status) - OverviewPage: three-tier pool capacity display (real data → error → metrics fallback) Documentation: - README, CHANGELOG, CONTRIBUTING, SECURITY - docs/: architecture, deployment (Helm), getting-started, user-guide, troubleshooting CI: - .github/workflows/ci.yaml: lint + type-check + test on PR/push - .github/workflows/release.yaml: workflow_dispatch versioned release 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>
This commit is contained in:
@@ -58,6 +58,8 @@ export default function OverviewPage() {
|
||||
persistentVolumeClaims,
|
||||
controllerPods,
|
||||
nodePods,
|
||||
poolStats,
|
||||
poolStatsError,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
@@ -109,6 +111,27 @@ export default function OverviewPage() {
|
||||
const chartData = protocolChartData(storageClasses);
|
||||
const totalScs = storageClasses.length;
|
||||
|
||||
// Capacity by pool: join volumeCapacityBytes samples (volume_id, protocol)
|
||||
// with PV volumeHandle → pool name from volumeAttributes.
|
||||
const capacityByPool: Map<string, number> = React.useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
if (!metrics) return map;
|
||||
// Build lookup: volumeHandle → pool name
|
||||
const handleToPool = new Map<string, string>();
|
||||
for (const pv of persistentVolumes) {
|
||||
const handle = pv.spec.csi?.volumeHandle;
|
||||
const pool = pv.spec.csi?.volumeAttributes?.['pool'];
|
||||
if (handle && pool) handleToPool.set(handle, pool);
|
||||
}
|
||||
for (const sample of metrics.volumeCapacityBytes) {
|
||||
const volumeId = sample.labels['volume_id'];
|
||||
if (!volumeId) continue;
|
||||
const pool = handleToPool.get(volumeId) ?? 'unknown';
|
||||
map.set(pool, (map.get(pool) ?? 0) + sample.value);
|
||||
}
|
||||
return map;
|
||||
}, [metrics, persistentVolumes]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
@@ -234,6 +257,66 @@ export default function OverviewPage() {
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
{/* Pool capacity — real data from TrueNAS API when configured */}
|
||||
{poolStats.length > 0 && (
|
||||
<SectionBox title="Pool Capacity">
|
||||
<SimpleTable
|
||||
columns={[
|
||||
{ label: 'Pool', getter: (p) => p.name },
|
||||
{
|
||||
label: 'Status',
|
||||
getter: (p) => (
|
||||
<StatusLabel status={p.status === 'ONLINE' ? 'success' : 'warning'}>
|
||||
{p.status}
|
||||
</StatusLabel>
|
||||
),
|
||||
},
|
||||
{ label: 'Total', getter: (p) => formatBytes(p.size) },
|
||||
{ label: 'Used', getter: (p) => formatBytes(p.allocated) },
|
||||
{ label: 'Free', getter: (p) => formatBytes(p.free) },
|
||||
{
|
||||
label: 'Used %',
|
||||
getter: (p) => p.size > 0
|
||||
? `${Math.round((p.allocated / p.size) * 100)}%`
|
||||
: '—',
|
||||
},
|
||||
]}
|
||||
data={poolStats}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
|
||||
{poolStatsError && (
|
||||
<SectionBox title="Pool Capacity Unavailable">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Error',
|
||||
value: <StatusLabel status="warning">{poolStatsError}</StatusLabel>,
|
||||
},
|
||||
{
|
||||
name: 'Note',
|
||||
value: 'Check your TrueNAS API key and server address in plugin settings.',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
|
||||
{/* Provisioned capacity by pool (from Prometheus metrics — shown when TrueNAS API not configured) */}
|
||||
{poolStats.length === 0 && !poolStatsError && capacityByPool.size > 0 && (
|
||||
<SectionBox title="Provisioned Capacity by Pool">
|
||||
<NameValueTable
|
||||
rows={[...capacityByPool.entries()]
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([pool, bytes]) => ({
|
||||
name: pool,
|
||||
value: formatBytes(bytes),
|
||||
}))}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
|
||||
{/* Non-bound PVCs warning */}
|
||||
{nonBoundPvcs.length > 0 && (
|
||||
<SectionBox title="Attention: Non-Bound PVCs">
|
||||
|
||||
Reference in New Issue
Block a user