Files
the-other-dude/frontend/src/components/network/VpnTab.tsx
Jason Staack b840047e19 feat: The Other Dude v9.0.1 — full-featured email system
ci: add GitHub Pages deployment workflow for docs site

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 19:30:44 -05:00

158 lines
5.1 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import { Shield, Lock, Globe } from 'lucide-react'
import { networkApi, type VpnTunnel } from '@/lib/networkApi'
import { Skeleton } from '@/components/ui/skeleton'
import { Badge } from '@/components/ui/badge'
interface VpnTabProps {
tenantId: string
deviceId: string
active: boolean
}
/** Format byte count to human-readable string. */
function formatBytes(bytes: string | null): string {
if (!bytes) return '--'
const n = parseInt(bytes, 10)
if (isNaN(n)) return bytes
if (n >= 1_073_741_824) return `${(n / 1_073_741_824).toFixed(1)} GB`
if (n >= 1_048_576) return `${(n / 1_048_576).toFixed(1)} MB`
if (n >= 1_024) return `${(n / 1_024).toFixed(1)} KB`
return `${n} B`
}
/** VPN type configuration for icons and colors. */
const VPN_TYPE_CONFIG = {
wireguard: {
icon: Shield,
label: 'WireGuard',
color: '#a855f7', // purple
},
ipsec: {
icon: Lock,
label: 'IPsec',
color: '#3b82f6', // blue
},
l2tp: {
icon: Globe,
label: 'L2TP',
color: '#22c55e', // green
},
} as const
function TunnelRow({ tunnel }: { tunnel: VpnTunnel }) {
const config = VPN_TYPE_CONFIG[tunnel.type]
const Icon = config.icon
const isUp = tunnel.status === 'connected' || tunnel.status === 'established'
return (
<tr className="border-b border-border last:border-b-0 hover:bg-elevated/30 transition-colors">
{/* Type */}
<td className="py-2.5 px-3">
<div className="flex items-center gap-2">
<span
className="flex items-center justify-center w-6 h-6 rounded"
style={{ backgroundColor: config.color + '20', color: config.color }}
>
<Icon className="w-3.5 h-3.5" />
</span>
<Badge color={config.color}>{config.label}</Badge>
</div>
</td>
{/* Remote Endpoint */}
<td className="py-2.5 px-3 font-mono text-xs text-text-primary">
{tunnel.remote_endpoint}
</td>
{/* Status */}
<td className="py-2.5 px-3">
<span
className={`inline-flex items-center gap-1.5 text-xs font-medium ${
isUp ? 'text-success' : 'text-text-muted'
}`}
>
<span
className={`w-1.5 h-1.5 rounded-full ${isUp ? 'bg-success' : 'bg-text-muted'}`}
/>
{tunnel.status}
</span>
</td>
{/* Uptime */}
<td className="py-2.5 px-3 text-xs text-text-secondary font-mono">
{tunnel.uptime ?? '--'}
</td>
{/* RX */}
<td className="py-2.5 px-3 text-xs text-text-secondary font-mono text-right">
{formatBytes(tunnel.rx_bytes)}
</td>
{/* TX */}
<td className="py-2.5 px-3 text-xs text-text-secondary font-mono text-right">
{formatBytes(tunnel.tx_bytes)}
</td>
</tr>
)
}
export function VpnTab({ tenantId, deviceId, active }: VpnTabProps) {
const { data, isLoading, error } = useQuery({
queryKey: ['vpn-tunnels', tenantId, deviceId],
queryFn: () => networkApi.getVpnTunnels(tenantId, deviceId),
refetchInterval: active ? 30_000 : false,
enabled: active,
})
if (isLoading) {
return (
<div className="mt-4 space-y-2">
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
</div>
)
}
if (error) {
return (
<div className="mt-4 rounded-lg border border-border bg-surface p-6 text-center text-sm text-error">
Failed to load VPN tunnels. The device may not support this feature.
</div>
)
}
if (!data || data.tunnels.length === 0) {
return (
<div className="mt-4 rounded-lg border border-border bg-surface p-8 text-center">
<Shield className="w-10 h-10 mx-auto mb-3 text-text-muted opacity-40" />
<p className="text-sm font-medium text-text-primary mb-1">
No active VPN tunnels
</p>
<p className="text-xs text-text-muted max-w-sm mx-auto">
VPN tunnels will appear here when WireGuard peers, IPsec SAs, or L2TP
connections are active on this device.
</p>
</div>
)
}
return (
<div className="mt-4 rounded-lg border border-border bg-surface overflow-hidden">
<table className="w-full text-left">
<thead>
<tr className="border-b border-border bg-elevated/50">
<th className="py-2 px-3 text-xs font-medium text-text-muted">Type</th>
<th className="py-2 px-3 text-xs font-medium text-text-muted">Remote Endpoint</th>
<th className="py-2 px-3 text-xs font-medium text-text-muted">Status</th>
<th className="py-2 px-3 text-xs font-medium text-text-muted">Uptime</th>
<th className="py-2 px-3 text-xs font-medium text-text-muted text-right">RX</th>
<th className="py-2 px-3 text-xs font-medium text-text-muted text-right">TX</th>
</tr>
</thead>
<tbody>
{data.tunnels.map((tunnel, i) => (
<TunnelRow key={`${tunnel.type}-${tunnel.remote_endpoint}-${i}`} tunnel={tunnel} />
))}
</tbody>
</table>
</div>
)
}