311 lines
13 KiB
TypeScript
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>
|
||
|
|
)
|
||
|
|
}
|