letsbe-hub/src/components/admin/server-metrics-panel.tsx

311 lines
13 KiB
TypeScript

'use client'
import { useState } from 'react'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import {
Loader2,
Activity,
Cpu,
HardDrive,
Network,
Download,
Upload,
RefreshCw,
ExternalLink,
} from 'lucide-react'
import Link from 'next/link'
import { useServerMetrics } from '@/hooks/use-netcup'
interface ServerMetricsPanelProps {
netcupServerId: string
serverName?: string
}
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B'
if (!Number.isFinite(bytes) || bytes < 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
function formatBytesPerSec(bps: number): string {
return formatBytes(bps) + '/s'
}
function MetricsBar({ value, max = 100, gradient }: {
value: number
max?: number
gradient?: string
}) {
const percentage = Math.min((value / max) * 100, 100)
const getThresholdColor = () => {
if (percentage > 90) return 'bg-gradient-to-r from-red-500 to-red-600'
if (percentage > 75) return 'bg-gradient-to-r from-amber-500 to-orange-500'
return gradient || 'bg-gradient-to-r from-blue-500 to-blue-600'
}
return (
<div className="h-1.5 bg-muted/60 rounded-full overflow-hidden ring-1 ring-inset ring-black/5">
<div
className={`h-full ${getThresholdColor()} transition-all duration-500 ease-out rounded-full`}
style={{ width: `${percentage}%` }}
/>
</div>
)
}
export function ServerMetricsPanel({ netcupServerId, serverName }: ServerMetricsPanelProps) {
const [hours, setHours] = useState(24)
const { data: metrics, isLoading, isFetching, error, refetch } = useServerMetrics(netcupServerId, hours, !!netcupServerId)
if (!netcupServerId) {
return null
}
const isInitialLoading = isLoading && !metrics
if (error && !metrics) {
return (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-900/30">
<Activity className="h-4 w-4 text-blue-600 dark:text-blue-400" />
</div>
<div>
<CardTitle className="text-lg">Server Metrics</CardTitle>
<CardDescription>Performance monitoring</CardDescription>
</div>
</div>
</div>
</CardHeader>
<CardContent>
<div className="rounded-lg border border-destructive/20 bg-destructive/5 py-6 text-center">
<p className="text-muted-foreground text-sm mb-2">Failed to load metrics</p>
<Button variant="outline" size="sm" onClick={() => refetch()} className="gap-2">
<RefreshCw className="h-4 w-4" />
Retry
</Button>
</div>
</CardContent>
</Card>
)
}
return (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-900/30">
<Activity className="h-4 w-4 text-blue-600 dark:text-blue-400" />
</div>
<div>
<CardTitle className="text-lg">Server Metrics</CardTitle>
<CardDescription className="flex items-center gap-1">
Performance monitoring
{serverName && (
<Link
href={`/admin/servers/netcup/${netcupServerId}`}
className="text-xs text-primary hover:underline ml-2 inline-flex items-center gap-1"
>
View full details
<ExternalLink className="h-3 w-3" />
</Link>
)}
</CardDescription>
</div>
</div>
<div className="flex items-center gap-2">
{/* Period selector */}
<div className="flex items-center gap-0.5 p-0.5 bg-muted/50 rounded-lg">
{[1, 6, 24, 168].map((h) => (
<Button
key={h}
variant={hours === h ? 'default' : 'ghost'}
size="sm"
className={`h-7 px-2 text-xs font-medium transition-all ${
hours === h ? 'shadow-sm' : 'hover:bg-muted'
}`}
onClick={() => setHours(h)}
disabled={isFetching}
>
{h === 168 ? '7d' : `${h}h`}
</Button>
))}
</div>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
onClick={() => refetch()}
disabled={isFetching}
>
<RefreshCw className={`h-3.5 w-3.5 ${isFetching ? 'animate-spin' : ''}`} />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
{isInitialLoading ? (
<div className="grid gap-4 md:grid-cols-3">
{[
{ icon: Cpu, label: 'CPU', color: 'text-blue-500 bg-blue-100 dark:bg-blue-900/30' },
{ icon: HardDrive, label: 'Disk I/O', color: 'text-emerald-500 bg-emerald-100 dark:bg-emerald-900/30' },
{ icon: Network, label: 'Network', color: 'text-violet-500 bg-violet-100 dark:bg-violet-900/30' },
].map(({ icon: Icon, label, color }) => (
<div key={label} className="rounded-lg border bg-card p-4">
<div className="flex items-center gap-2 mb-3">
<div className={`p-1.5 rounded-md ${color}`}>
<Icon className="h-3.5 w-3.5" />
</div>
<span className="text-sm font-medium">{label}</span>
</div>
<div className="flex items-center justify-center py-4">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
</div>
))}
</div>
) : !metrics ? (
<div className="rounded-lg border bg-muted/20 py-8 text-center">
<Activity className="h-8 w-8 mx-auto text-muted-foreground/50 mb-2" />
<p className="text-muted-foreground text-sm">No metrics data available</p>
<p className="text-xs text-muted-foreground/70 mt-1">
Metrics will appear once the server is running
</p>
</div>
) : (
<div className={`grid gap-4 md:grid-cols-3 transition-opacity duration-200 ${isFetching ? 'opacity-60' : ''}`}>
{/* CPU metrics */}
<div className="rounded-lg border bg-gradient-to-br from-card to-blue-50/30 dark:to-blue-950/10 p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-md bg-blue-100 dark:bg-blue-900/30">
<Cpu className="h-3.5 w-3.5 text-blue-600 dark:text-blue-400" />
</div>
<span className="text-sm font-medium">CPU</span>
</div>
<span className="text-xl font-bold tabular-nums text-blue-600 dark:text-blue-400">
{metrics.cpu.average}%
</span>
</div>
<MetricsBar
value={metrics.cpu.average}
gradient="bg-gradient-to-r from-blue-500 to-blue-600"
/>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>Peak: {metrics.cpu.max}%</span>
<span>{metrics.cpu.dataPoints.length} samples</span>
</div>
</div>
{/* Disk I/O metrics */}
{metrics.disk && (metrics.disk.readBps.length > 0 || metrics.disk.writeBps.length > 0) ? (
<div className="rounded-lg border bg-gradient-to-br from-card to-emerald-50/30 dark:to-emerald-950/10 p-4 space-y-3">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-md bg-emerald-100 dark:bg-emerald-900/30">
<HardDrive className="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-400" />
</div>
<span className="text-sm font-medium">Disk I/O</span>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Download className="h-2.5 w-2.5" />
Read
</div>
<p className="text-sm font-semibold tabular-nums">
{metrics.disk.readBps.length > 0
? formatBytesPerSec(metrics.disk.readBps[metrics.disk.readBps.length - 1]?.value || 0)
: 'N/A'}
</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Upload className="h-2.5 w-2.5" />
Write
</div>
<p className="text-sm font-semibold tabular-nums">
{metrics.disk.writeBps.length > 0
? formatBytesPerSec(metrics.disk.writeBps[metrics.disk.writeBps.length - 1]?.value || 0)
: 'N/A'}
</p>
</div>
</div>
</div>
) : (
<div className="rounded-lg border bg-gradient-to-br from-card to-emerald-50/30 dark:to-emerald-950/10 p-4">
<div className="flex items-center gap-2 mb-3">
<div className="p-1.5 rounded-md bg-emerald-100 dark:bg-emerald-900/30">
<HardDrive className="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-400" />
</div>
<span className="text-sm font-medium">Disk I/O</span>
</div>
<p className="text-xs text-muted-foreground text-center py-2">No data</p>
</div>
)}
{/* Network metrics */}
{metrics.network && (metrics.network.rxBps.length > 0 || metrics.network.txBps.length > 0) ? (
<div className="rounded-lg border bg-gradient-to-br from-card to-violet-50/30 dark:to-violet-950/10 p-4 space-y-3">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-md bg-violet-100 dark:bg-violet-900/30">
<Network className="h-3.5 w-3.5 text-violet-600 dark:text-violet-400" />
</div>
<span className="text-sm font-medium">Network</span>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Download className="h-2.5 w-2.5" />
RX
</div>
<p className="text-sm font-semibold tabular-nums">
{metrics.network.rxBps.length > 0
? formatBytesPerSec(metrics.network.rxBps[metrics.network.rxBps.length - 1]?.value || 0)
: 'N/A'}
</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Upload className="h-2.5 w-2.5" />
TX
</div>
<p className="text-sm font-semibold tabular-nums">
{metrics.network.txBps.length > 0
? formatBytesPerSec(metrics.network.txBps[metrics.network.txBps.length - 1]?.value || 0)
: 'N/A'}
</p>
</div>
</div>
</div>
) : (
<div className="rounded-lg border bg-gradient-to-br from-card to-violet-50/30 dark:to-violet-950/10 p-4">
<div className="flex items-center gap-2 mb-3">
<div className="p-1.5 rounded-md bg-violet-100 dark:bg-violet-900/30">
<Network className="h-3.5 w-3.5 text-violet-600 dark:text-violet-400" />
</div>
<span className="text-sm font-medium">Network</span>
</div>
<p className="text-xs text-muted-foreground text-center py-2">No data</p>
</div>
)}
</div>
)}
</CardContent>
</Card>
)
}