'use client'
import { useEffect, useState, useCallback, useRef } from 'react'
import { RefreshCw, Cpu, MemoryStick, HardDrive, Network, Activity, CheckCircle2, AlertTriangle, XCircle } from 'lucide-react'
import { cn } from '@/lib/utils'
// ============================================================================
// Types
// ============================================================================
interface LiveStatsData {
cpuPercent: number | null
memoryPercent: number | null
memoryUsedMb: number | null
memoryTotalMb: number | null
diskReadMbps: number | null
diskWriteMbps: number | null
networkInMbps: number | null
networkOutMbps: number | null
containersRunning: number | null
containersStopped: number | null
timestamp?: Date | string
}
interface LiveStatsPanelProps {
data: LiveStatsData | null | undefined
isRefreshing: boolean
onRefresh: () => void
autoRefreshInterval?: number // in milliseconds, default 15000 (15 seconds)
}
interface GaugeConfig {
label: string
value: number | null
max: number
unit: string
icon: React.ReactNode
thresholds: {
warning: number // percentage at which to show warning (yellow)
critical: number // percentage at which to show critical (red)
}
description?: string
secondaryValue?: string
}
// ============================================================================
// Enhanced Circular Gauge Component
// ============================================================================
function CircularGauge({
label,
value,
max,
unit,
icon,
thresholds,
description,
secondaryValue,
isRefreshing
}: GaugeConfig & { isRefreshing: boolean }) {
const displayValue = value ?? 0
const percentage = Math.min((displayValue / max) * 100, 100)
// Determine status color based on thresholds
const getStatusColor = () => {
if (value === null) return { stroke: '#9ca3af', bg: '#f3f4f6', text: 'text-gray-500' }
if (percentage >= thresholds.critical) return { stroke: '#ef4444', bg: '#fef2f2', text: 'text-red-600' }
if (percentage >= thresholds.warning) return { stroke: '#f59e0b', bg: '#fffbeb', text: 'text-amber-600' }
return { stroke: '#22c55e', bg: '#f0fdf4', text: 'text-green-600' }
}
const colors = getStatusColor()
// SVG circle parameters
const size = 120
const strokeWidth = 8
const radius = (size - strokeWidth) / 2
const circumference = 2 * Math.PI * radius
const strokeDashoffset = circumference - (percentage / 100) * circumference
// Get status icon
const getStatusIcon = () => {
if (value === null) return null
if (percentage >= thresholds.critical) return
if (percentage >= thresholds.warning) return
return
}
return (
{/* Status indicator */}
{getStatusIcon()}
{/* Circular gauge */}
{/* Center content */}
{icon}
{value !== null ? displayValue.toFixed(1) : '--'}
{unit}
{/* Label and description */}
{label}
{description && (
{description}
)}
{secondaryValue && (
{secondaryValue}
)}
)
}
// ============================================================================
// Compact Stats Bar Component
// ============================================================================
function CompactStatBar({
label,
value,
max,
unit,
color
}: {
label: string
value: number | null
max: number
unit: string
color: string
}) {
const displayValue = value ?? 0
const percentage = Math.min((displayValue / max) * 100, 100)
return (
{label}
{value !== null ? `${displayValue.toFixed(1)} ${unit}` : '--'}
)
}
// ============================================================================
// Main Live Stats Panel
// ============================================================================
export function LiveStatsPanel({
data,
isRefreshing,
onRefresh,
autoRefreshInterval = 15000
}: LiveStatsPanelProps) {
const [lastUpdated, setLastUpdated] = useState(null)
const [countdown, setCountdown] = useState(autoRefreshInterval / 1000)
const intervalRef = useRef(null)
const countdownRef = useRef(null)
// Update last updated time when data changes
useEffect(() => {
if (data?.timestamp) {
setLastUpdated(new Date(data.timestamp))
}
}, [data?.timestamp])
// Auto-refresh interval
useEffect(() => {
// Initial refresh on mount
onRefresh()
// Set up auto-refresh interval
intervalRef.current = setInterval(() => {
onRefresh()
setCountdown(autoRefreshInterval / 1000)
}, autoRefreshInterval)
// Countdown timer
countdownRef.current = setInterval(() => {
setCountdown(prev => Math.max(0, prev - 1))
}, 1000)
return () => {
if (intervalRef.current) clearInterval(intervalRef.current)
if (countdownRef.current) clearInterval(countdownRef.current)
}
}, [autoRefreshInterval, onRefresh])
// Reset countdown when manually refreshing
const handleManualRefresh = useCallback(() => {
onRefresh()
setCountdown(autoRefreshInterval / 1000)
}, [onRefresh, autoRefreshInterval])
// Format time ago
const formatTimeAgo = (date: Date | null) => {
if (!date) return 'Never'
const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000)
if (seconds < 60) return `${seconds}s ago`
const minutes = Math.floor(seconds / 60)
if (minutes < 60) return `${minutes}m ago`
return `${Math.floor(minutes / 60)}h ago`
}
// Gauge configurations
const gauges: GaugeConfig[] = [
{
label: 'CPU Usage',
value: data?.cpuPercent ?? null,
max: 100,
unit: '%',
icon: ,
thresholds: { warning: 70, critical: 90 },
description: 'Processor utilization'
},
{
label: 'Memory',
value: data?.memoryPercent ?? null,
max: 100,
unit: '%',
icon: ,
thresholds: { warning: 75, critical: 90 },
description: 'RAM utilization',
secondaryValue: data?.memoryUsedMb && data?.memoryTotalMb
? `${(data.memoryUsedMb / 1024).toFixed(1)} / ${(data.memoryTotalMb / 1024).toFixed(1)} GB`
: undefined
},
{
label: 'Disk Read',
value: data?.diskReadMbps ?? null,
max: 100,
unit: 'MB/s',
icon: ,
thresholds: { warning: 50, critical: 80 },
description: 'Storage throughput'
},
{
label: 'Network In',
value: data?.networkInMbps ?? null,
max: 100,
unit: 'Mbps',
icon: ,
thresholds: { warning: 50, critical: 80 },
description: 'Inbound traffic'
}
]
return (
{/* Header */}
Live Stats
Last updated: {formatTimeAgo(lastUpdated)} • Next refresh in {countdown}s
{/* Main Gauges */}
{gauges.map((gauge) => (
))}
{/* Additional Stats */}
Additional Metrics
{/* Container counts */}
{(data?.containersRunning !== null || data?.containersStopped !== null) && (
{data?.containersRunning ?? 0} running
{data?.containersStopped ?? 0} stopped
{(data?.containersRunning ?? 0) + (data?.containersStopped ?? 0)} total containers
)}
)
}
export default LiveStatsPanel