import React, { useState, useEffect, useRef } from 'react'; import { Card, Row, Col, Statistic, Button, Tag, Typography, Spin } from 'antd'; import { AreaChart, Area, XAxis, YAxis, Tooltip as RechartsTooltip, ResponsiveContainer, CartesianGrid, Legend, LineChart, Line } from 'recharts'; import { DesktopOutlined, DashboardOutlined, ApiOutlined, HddOutlined, ReloadOutlined, PlayCircleOutlined, PauseCircleOutlined } from '@ant-design/icons'; import { useStore } from '../store'; import { SavedConnection } from '../types'; import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig'; import { t, type I18nParams } from '../i18n'; import { useOptionalI18n } from '../i18n/provider'; import { RedisGetServerInfo } from '../../wailsjs/go/app/App'; const { Title, Text } = Typography; interface RedisMonitorProps { connectionId: string; redisDB: number; } // Data point for charts interface MetricPoint { time: string; qps: number; memory: number; // in MB memory_rss: number; // in MB clients: number; cpuSys: number; cpuUser: number; hitRate: number; keys: number; } const MAX_HISTORY_POINTS = 60; // Keep up to 60 data points const RedisMonitor: React.FC = ({ connectionId, redisDB }) => { const connections = useStore(state => state.connections); const theme = useStore(state => state.theme); const darkMode = theme === 'dark'; const i18n = useOptionalI18n(); const i18nLanguage = i18n?.language; const tr = (key: string, params?: I18nParams) => t(key, params, i18nLanguage); const [isRunning, setIsRunning] = useState(true); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [history, setHistory] = useState([]); const [currentInfo, setCurrentInfo] = useState>({}); // Ref to track if component is mounted to prevent state updates after unmount const mountedRef = useRef(true); // Interval ref const intervalRef = useRef | null>(null); const connection = connections.find((c: SavedConnection) => c.id === connectionId); const formatFetchError = (detail?: unknown) => tr('redis_monitor.message.fetch_failed', { detail: String(detail || tr('common.unknown')), }); const fetchMetrics = async () => { if (!connection) return; try { const config = buildRpcConnectionConfig(connection.config, { redisDB }); const res = await RedisGetServerInfo(config); if (!mountedRef.current) return; if (!res.success) { setError(formatFetchError(res.message)); return; } setError(null); const infoMap = res.data as Record; setCurrentInfo(infoMap); const now = new Date(); const timeStr = now.toLocaleTimeString([], { hour12: false, second: '2-digit' }); // Parse values const qps = parseInt(infoMap['instantaneous_ops_per_sec'] || '0', 10); const memBytes = parseInt(infoMap['used_memory'] || '0', 10); const memRssBytes = parseInt(infoMap['used_memory_rss'] || '0', 10); const clients = parseInt(infoMap['connected_clients'] || '0', 10); const cpuSys = parseFloat(infoMap['used_cpu_sys'] || '0'); const cpuUser = parseFloat(infoMap['used_cpu_user'] || '0'); const hits = parseInt(infoMap['keyspace_hits'] || '0', 10); const misses = parseInt(infoMap['keyspace_misses'] || '0', 10); const hitRate = (hits + misses) > 0 ? (hits / (hits + misses)) * 100 : 0; let keys = 0; Object.keys(infoMap).forEach(k => { if (k.startsWith('db')) { const m = infoMap[k].match(/keys=(\d+)/); if (m) keys += parseInt(m[1], 10); } }); const point: MetricPoint = { time: timeStr, qps, memory: parseFloat((memBytes / 1024 / 1024).toFixed(2)), memory_rss: parseFloat((memRssBytes / 1024 / 1024).toFixed(2)), clients, cpuSys: parseFloat(cpuSys.toFixed(2)), cpuUser: parseFloat(cpuUser.toFixed(2)), hitRate: parseFloat(hitRate.toFixed(2)), keys }; setHistory(prev => { const next = [...prev, point]; if (next.length > MAX_HISTORY_POINTS) { return next.slice(next.length - MAX_HISTORY_POINTS); } return next; }); if (loading) setLoading(false); } catch (err: any) { if (mountedRef.current) { setError(formatFetchError(err?.message || err)); if (loading) setLoading(false); } } }; useEffect(() => { mountedRef.current = true; fetchMetrics(); // initial fetch return () => { mountedRef.current = false; if (intervalRef.current) clearInterval(intervalRef.current); }; }, []); useEffect(() => { if (intervalRef.current) { clearInterval(intervalRef.current); } if (isRunning) { intervalRef.current = setInterval(fetchMetrics, 2000); // 2 second interval } return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isRunning, connectionId, redisDB, connection, i18nLanguage]); if (!connection) { return
{tr('redis_monitor.state.connection_not_found')}
; } // Determine styles for charts based on theme const chartTextColor = darkMode ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.65)'; const chartGridColor = darkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'; const cardBgColor = darkMode ? '#1f1f1f' : '#ffffff'; const getFormatMemoryString = (bytes: string) => { const val = parseInt(bytes || '0', 10); if (val > 1024*1024*1024) return (val/1024/1024/1024).toFixed(2) + ' GB'; if (val > 1024*1024) return (val/1024/1024).toFixed(2) + ' MB'; if (val > 1024) return (val/1024).toFixed(2) + ' KB'; return val + ' B'; }; const getUptimeString = (seconds: string) => { const d = parseInt(seconds || '0', 10); if (d < 60) return `${d}s`; if (d < 3600) return `${Math.floor(d/60)}m ${d%60}s`; if (d < 86400) return `${Math.floor(d/3600)}h ${Math.floor((d%3600)/60)}m`; return `${Math.floor(d/86400)}d ${Math.floor((d%86400)/3600)}h`; }; return (
<DashboardOutlined style={{ marginRight: 8, color: '#1677ff' }} /> {tr('redis_monitor.title.instance')} {connection.name} {currentInfo.redis_version && ` • Redis ${currentInfo.redis_version}`} {currentInfo.os && ` • ${currentInfo.os}`}
{error && {error}} {loading && !error && }
{tr('redis_monitor.metric.memory_used')}} value={getFormatMemoryString(currentInfo.used_memory || '0')} valueStyle={{ color: '#eb2f96', fontWeight: 600 }} suffix={{tr('redis_monitor.metric.memory_peak', { value: getFormatMemoryString(currentInfo.used_memory_peak || '0') })}} /> {tr('redis_monitor.metric.clients')}} value={currentInfo.connected_clients || '0'} valueStyle={{ color: '#1677ff', fontWeight: 600 }} suffix={{tr('redis_monitor.metric.blocked_clients', { value: currentInfo.blocked_clients || '0' })}} /> {tr('redis_monitor.metric.ops')}} value={currentInfo.instantaneous_ops_per_sec || '0'} valueStyle={{ color: '#52c41a', fontWeight: 600 }} suffix={cmds/s} /> {tr('redis_monitor.metric.uptime')}} value={getUptimeString(currentInfo.uptime_in_seconds || '0')} valueStyle={{ color: '#fa8c16', fontWeight: 600 }} suffix={{tr('redis_monitor.metric.days', { value: currentInfo.uptime_in_days || '0' })}} /> [`${value} MB`]} /> [`${value} s`]} />
{['redis_version', 'os', 'arch_bits', 'multiplexing_api', 'gcc_version', 'run_id', 'tcp_port', 'uptime_in_days', 'hz', 'lru_clock', 'role', 'maxmemory_human', 'maxmemory_policy', 'mem_fragmentation_ratio', 'keyspace_hits', 'keyspace_misses', 'total_connections_received'].map(key => ( currentInfo[key] ? (
{key} {currentInfo[key]}
) : null ))}
); }; export default RedisMonitor;