- 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>
193 lines
6.8 KiB
JavaScript
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>
|
|
)
|
|
}
|