Files
GSM/temp/MetricsChart.jsx
Alexander Zielonka 2b1fbb9f02 Initial commit: Homelab documentation
- infrastructure.md: Network topology, server overview, credentials
- gsm.md: Gameserver Monitor detailed documentation
- todo.md: Project roadmap and completed tasks
- CLAUDE.md: AI assistant context
- temp/: Frontend component backups

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 06:16:05 +01:00

193 lines
6.8 KiB
JavaScript

import { useState, useEffect } from 'react'
import { AreaChart, Area, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
import { getMetricsHistory } from '../api'
import { useUser } from '../context/UserContext'
const GRAFANA_URL = 'https://grafana.dimension47.de'
export default function MetricsChart({ serverId, serverName, expanded = false }) {
const { token } = useUser()
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [range, setRange] = useState('1h')
useEffect(() => {
const fetchData = async () => {
try {
const metrics = await getMetricsHistory(token, serverId, range)
setData(metrics)
} catch (err) {
console.error('Failed to fetch metrics:', err)
} finally {
setLoading(false)
}
}
fetchData()
const interval = setInterval(fetchData, 60000)
return () => clearInterval(interval)
}, [token, serverId, range])
const formatTime = (timestamp) => {
const date = new Date(timestamp)
return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
}
const formatBytes = (bytes) => {
if (bytes < 1024) return bytes.toFixed(0) + ' B/s'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB/s'
return (bytes / 1024 / 1024).toFixed(1) + ' MB/s'
}
if (loading) {
return (
<div className="bg-black/50 border border-[#00ff41]/20 rounded p-4">
<div className="h-20 flex items-center justify-center">
<span className="text-[#00ff41]/50 font-mono text-sm animate-pulse">LOADING_METRICS...</span>
</div>
</div>
)
}
if (!data || data.cpu.length === 0) {
return (
<div className="bg-black/50 border border-[#00ff41]/20 rounded p-4">
<div className="h-20 flex items-center justify-center">
<span className="text-[#00ff41]/30 font-mono text-sm">NO_DATA_AVAILABLE</span>
</div>
</div>
)
}
const combinedData = data.cpu.map((point, i) => ({
timestamp: point.timestamp,
cpu: point.value,
memory: data.memory[i]?.value || 0,
networkRx: data.networkRx[i]?.value || 0,
networkTx: data.networkTx[i]?.value || 0
}))
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div className="bg-black/95 border border-[#00ff41]/50 rounded p-3 font-mono text-xs">
<div className="text-[#00ff41]/60 mb-2">{formatTime(label)}</div>
{payload.map((entry, i) => (
<div key={i} className="flex justify-between gap-4" style={{ color: entry.color }}>
<span>{entry.name}:</span>
<span className="font-bold">
{entry.name.includes('Network') ? formatBytes(entry.value) : entry.value.toFixed(1) + '%'}
</span>
</div>
))}
</div>
)
}
return null
}
const ChartCard = ({ title, dataKey, color, domain, formatter }) => (
<div className="bg-black/50 border border-[#00ff41]/20 rounded p-4">
<div className="flex justify-between items-center mb-3">
<span className="text-[#00ff41]/60 font-mono text-xs">{title}</span>
<span className="text-[#00ff41] font-mono text-sm font-bold">
{formatter
? formatter(combinedData[combinedData.length - 1]?.[dataKey] || 0)
: (combinedData[combinedData.length - 1]?.[dataKey] || 0).toFixed(1) + '%'
}
</span>
</div>
<ResponsiveContainer width="100%" height={expanded ? 150 : 80}>
<AreaChart data={combinedData}>
<defs>
<linearGradient id={`gradient-${dataKey}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={color} stopOpacity={0.4}/>
<stop offset="95%" stopColor={color} stopOpacity={0.05}/>
</linearGradient>
</defs>
{expanded && (
<>
<XAxis
dataKey="timestamp"
tickFormatter={formatTime}
tick={{ fill: '#00ff41', fontSize: 10, opacity: 0.5 }}
axisLine={{ stroke: '#00ff41', opacity: 0.2 }}
tickLine={false}
minTickGap={50}
/>
<YAxis
domain={domain || [0, 'auto']}
tick={{ fill: '#00ff41', fontSize: 10, opacity: 0.5 }}
axisLine={{ stroke: '#00ff41', opacity: 0.2 }}
tickLine={false}
width={40}
tickFormatter={formatter ? (v) => formatBytes(v).split(' ')[0] : (v) => v.toFixed(0)}
/>
<Tooltip content={<CustomTooltip />} />
</>
)}
<Area
type="monotone"
dataKey={dataKey}
name={title}
stroke={color}
strokeWidth={2}
fill={`url(#gradient-${dataKey})`}
isAnimationActive={true}
animationDuration={500}
/>
</AreaChart>
</ResponsiveContainer>
</div>
)
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<span className="text-[#00ff41]/60 font-mono text-xs">
// METRICS_HISTORY
</span>
<div className="flex items-center gap-3">
<select
value={range}
onChange={(e) => setRange(e.target.value)}
className="input-matrix px-3 py-1 rounded font-mono text-xs"
>
<option value="15m">15 MIN</option>
<option value="1h">1 HOUR</option>
<option value="6h">6 HOURS</option>
<option value="24h">24 HOURS</option>
</select>
<a
href={`${GRAFANA_URL}/d/rYdddlPWk/node-exporter-full?var-job=${serverId}`}
target="_blank"
rel="noopener noreferrer"
className="btn-matrix px-3 py-1 text-xs font-mono flex items-center gap-2"
>
<span>📊</span>
GRAFANA
</a>
</div>
</div>
{/* Charts */}
{expanded ? (
<div className="space-y-4">
<ChartCard title="CPU_USAGE" dataKey="cpu" color="#00ff41" domain={[0, 100]} />
<ChartCard title="RAM_USAGE" dataKey="memory" color="#00ffff" domain={[0, 100]} />
<div className="grid grid-cols-2 gap-4">
<ChartCard title="NETWORK_RX" dataKey="networkRx" color="#ff00ff" formatter={formatBytes} />
<ChartCard title="NETWORK_TX" dataKey="networkTx" color="#ffff00" formatter={formatBytes} />
</div>
</div>
) : (
<div className="grid grid-cols-2 gap-3">
<ChartCard title="CPU" dataKey="cpu" color="#00ff41" domain={[0, 100]} />
<ChartCard title="RAM" dataKey="memory" color="#00ffff" domain={[0, 100]} />
</div>
)}
</div>
)
}