diff --git a/src/app/api/redis/command/route.ts b/src/app/api/redis/command/route.ts index c26b6f2..da39108 100644 --- a/src/app/api/redis/command/route.ts +++ b/src/app/api/redis/command/route.ts @@ -22,8 +22,9 @@ export async function POST(request: NextRequest) { } // Parse command string into array - const args = command.trim().split(/\s+/); - const result = await redis.call(...args); + const args: string[] = command.trim().split(/\s+/); + const [commandName, ...commandArgs] = args; + const result = await redis.call(commandName, ...commandArgs); return NextResponse.json({ result }); } catch (error) { diff --git a/src/app/api/redis/config/route.ts b/src/app/api/redis/config/route.ts index b8ad518..f8cf33c 100644 --- a/src/app/api/redis/config/route.ts +++ b/src/app/api/redis/config/route.ts @@ -16,8 +16,8 @@ export async function GET(request: NextRequest) { // Tentar buscar configurações (pode estar desabilitado) try { - const configs = await redis.config('GET', '*'); - + const configs = await redis.config('GET', '*') as string[]; + // Converter array [key, value, key, value] para objeto for (let i = 0; i < configs.length; i += 2) { configObj[configs[i]] = configs[i + 1]; diff --git a/src/app/api/redis/keys/route.ts b/src/app/api/redis/keys/route.ts index c2288e6..d6fd7ac 100644 --- a/src/app/api/redis/keys/route.ts +++ b/src/app/api/redis/keys/route.ts @@ -96,7 +96,7 @@ export async function GET(request: NextRequest) { } } else { // Load limited keys using SCAN - const keys = []; + const keys: Array<{ name: string; type: string; ttl: number; size: number }> = []; let cursor = '0'; let iterations = 0; const maxIterations = 100; // Limite de segurança para evitar loops infinitos diff --git a/src/app/api/redis/metrics/route.ts b/src/app/api/redis/metrics/route.ts index db0987e..3a3c713 100644 --- a/src/app/api/redis/metrics/route.ts +++ b/src/app/api/redis/metrics/route.ts @@ -42,7 +42,7 @@ export async function GET() { stats.clientRttP95 = latencies.p95; - const metrics = MetricsService.calculateMetrics(stats, sessionId, connectionId); + const metrics = MetricsService.calculateMetrics(stats, sessionId, connectionId ?? undefined); return NextResponse.json({ metrics }); } catch (error) { diff --git a/src/app/api/redis/monitor/route.ts b/src/app/api/redis/monitor/route.ts index aba57d6..b879df8 100644 --- a/src/app/api/redis/monitor/route.ts +++ b/src/app/api/redis/monitor/route.ts @@ -36,7 +36,7 @@ export async function GET(request: NextRequest) { // Iniciar monitor console.log('Monitor: Starting monitor command'); - monitorRedis.monitor((err, monitor) => { + monitorRedis.monitor((err: Error | null, monitor: any) => { console.log('Monitor: Callback called', { err: err?.message, hasMonitor: !!monitor }); if (err) { console.error('Monitor error:', err); diff --git a/src/app/api/redis/slowlog/route.ts b/src/app/api/redis/slowlog/route.ts index 24f8170..4c35bda 100644 --- a/src/app/api/redis/slowlog/route.ts +++ b/src/app/api/redis/slowlog/route.ts @@ -17,7 +17,7 @@ export async function GET(request: NextRequest) { // Pegar todos os itens do slowlog (ou um número grande suficiente) // O Redis SLOWLOG retorna em ordem cronológica, não por duração - const allSlowLog = await redis.slowlog('GET', 1000); + const allSlowLog = await redis.slowlog('GET', 1000) as any[]; // Ordenar do mais lento para o menos lento (duração decrescente) // slowLog format: [id, timestamp, duration, command, clientAddress, clientName] diff --git a/src/components/Dashboard.old.tsx b/src/components/Dashboard.old.tsx deleted file mode 100644 index 6d7f375..0000000 --- a/src/components/Dashboard.old.tsx +++ /dev/null @@ -1,1142 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { - Box, - Grid, - Card, - CardContent, - Typography, - LinearProgress, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Chip, - IconButton, - Alert, - Paper, -} from '@mui/material'; -import { - Refresh as RefreshIcon, - Memory as MemoryIcon, - Speed as SpeedIcon, - Storage as StorageIcon, - Timeline as TimelineIcon, - AccessTime as UptimeIcon, - DataUsage as DataIcon, - Wifi as NetworkIcon, - Block as BlockIcon, - Computer as ServerIcon, - Code as CommandIcon, - Save as PersistenceIcon, - Psychology as CpuIcon, - AccountTree as ReplicationIcon, - Folder as DatabaseIcon, - TrendingUp as TrendingUpIcon, -} from '@mui/icons-material'; -import { useDispatch, useSelector } from 'react-redux'; -import { RootState, AppDispatch } from '@/store'; -import { fetchStats, fetchInfo, fetchSlowLog } from '@/store/slices/statsSlice'; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts'; -import { format } from 'date-fns'; -import { useRouter } from 'next/navigation'; -import { useAlerts } from '@/hooks/useAlerts'; - -const Dashboard = () => { - const dispatch = useDispatch(); - const { stats, info, slowLog, isLoading, error, derivedCpuPercent, lastUpdated, evictionsPerSec, expiredPerSec, rejectedConnPerSec, clientRttP50, clientRttP95 } = useSelector( - (state: RootState) => state.stats - ); - const { activeConnection } = useSelector((state: RootState) => state.connection); - - const [metricsHistory, setMetricsHistory] = useState([]); - const [autoRefresh, setAutoRefresh] = useState(true); - const [keyTypesData, setKeyTypesData] = useState<{type: string; count: number}[]>([]); - const router = useRouter(); - const { alerts, hasAlerts, criticalCount, warningCount } = useAlerts(stats); - - useEffect(() => { - if (activeConnection?.connected) { - // Initial load with a small delay to ensure connection is ready - const timeoutId = setTimeout(() => { - handleRefresh(); - }, 100); - - if (autoRefresh) { - const interval = setInterval(() => { - // Only refresh if still connected - if (activeConnection?.connected) { - handleRefresh(); - } - }, 5000); - - return () => { - clearTimeout(timeoutId); - clearInterval(interval); - }; - } - - return () => clearTimeout(timeoutId); - } - }, [activeConnection?.connected, autoRefresh]); - - useEffect(() => { - if (error === 'REDIRECT_TO_CONNECTIONS') { - router.push('/?tab=connections'); - } - }, [error, router]); - - useEffect(() => { - if (stats) { - setMetricsHistory(prev => { - const newEntry = { - timestamp: Date.now(), - time: format(new Date(), 'HH:mm:ss'), - memory: stats.usedMemory, - ops: stats.instantaneousOpsPerSec, - clients: stats.connectedClients, - evictPerSec: evictionsPerSec || 0, - rejConnPerSec: rejectedConnPerSec || 0, - }; - - const updated = [...prev, newEntry]; - return updated.slice(-20); // Keep last 20 entries - }); - } - }, [stats]); - - useEffect(() => { - const fetchKeyTypes = async () => { - try { - const response = await fetch('/api/redis/keys?pattern=*&count=200', { - credentials: 'include', - }); - if (response.ok) { - const result = await response.json(); - const keys = result.keys || []; - - const typeCounts: Record = {}; - keys.forEach((key: any) => { - const type = key.type || 'unknown'; - typeCounts[type] = (typeCounts[type] || 0) + 1; - }); - - const data = Object.entries(typeCounts) - .map(([type, count]) => ({ type, count })) - .sort((a, b) => b.count - a.count) - .slice(0, 6); - - const othersCount = Object.values(typeCounts).reduce((sum, count) => sum + count, 0) - - data.reduce((sum, item) => sum + item.count, 0); - - if (othersCount > 0) { - data.push({ type: 'others', count: othersCount }); - } - - setKeyTypesData(data); - } - } catch (error) { - console.error('Error fetching key types:', error); - } - }; - - if (activeConnection?.connected && autoRefresh) { - fetchKeyTypes(); - const interval = setInterval(fetchKeyTypes, 30000); - return () => clearInterval(interval); - } - }, [activeConnection?.connected, autoRefresh]); - - const handleRefresh = async () => { - if (!activeConnection?.connected) { - return; // Silently return if no active connection - } - - try { - // Dispatch actions with error handling - const results = await Promise.allSettled([ - dispatch(fetchStats()), - dispatch(fetchInfo()), - dispatch(fetchSlowLog(10)) - ]); - - // Only log errors if they're not connection-related (502, 503, etc.) - results.forEach((result, index) => { - if (result.status === 'rejected') { - const error = result.reason; - // Only log if it's not a network/connection error - if (!error?.message?.includes('502') && - !error?.message?.includes('503') && - !error?.message?.includes('Failed to fetch')) { - const actions = ['fetchStats', 'fetchInfo', 'fetchSlowLog']; - console.error(`Error in ${actions[index]}:`, error); - } - } - }); - } catch (error) { - // Only log unexpected errors - if (!error?.toString().includes('502') && - !error?.toString().includes('Failed to fetch')) { - console.error('Unexpected error during dashboard refresh:', error); - } - } - }; - - const formatBytes = (bytes: number) => { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; - }; - - const formatNumber = (num: number) => { - return new Intl.NumberFormat().format(num); - }; - - const formatUptime = (seconds: number) => { - if (!seconds || isNaN(seconds)) return '0m'; - - const days = Math.floor(seconds / 86400); - const hours = Math.floor((seconds % 86400) / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - - if (days > 0) { - return `${days}d ${hours}h ${minutes}m`; - } else if (hours > 0) { - return `${hours}h ${minutes}m`; - } else { - return `${minutes}m`; - } - }; - - const formatNetworkBytes = (bytes: number) => { - if (!bytes || isNaN(bytes)) return '0 B'; - - const units = ['B', 'KB', 'MB', 'GB', 'TB']; - let size = bytes; - let unitIndex = 0; - - while (size >= 1024 && unitIndex < units.length - 1) { - size /= 1024; - unitIndex++; - } - - return `${size.toFixed(1)} ${units[unitIndex]}`; - }; - - if (!activeConnection) { - return ( - - - No active Redis connection - - - Please connect to a Redis server from the Connections tab to view the dashboard. - - - ); - } - - return ( - - {error && error !== 'REDIRECT_TO_CONNECTIONS' && ( - - {error} - - )} - - {hasAlerts && ( - - - - Critical Issues Detected - - - {alerts.slice(0, 7).map((alert, index) => ( -
  • - - {alert.message} - -
  • - ))} - {alerts.length > 7 && ( - - ... and {alerts.length - 7} more - - )} -
    -
    -
    - )} - - - - Redis Dashboard - {hasAlerts && ( - - {criticalCount > 0 && ( - } - /> - )} - {warningCount > 0 && ( - - )} - - )} - - - {lastUpdated && ( - - Last updated {lastUpdated} - - )} - setAutoRefresh(!autoRefresh)} - clickable - /> - - - - - - - - {/* Key Metrics */} - - - - - - Memory Usage - - - {stats?.usedMemoryHuman || '0B'} - - - {formatNumber(stats?.usedMemory || 0)} bytes - - {stats?.maxMemory && stats.maxMemory > 0 && ( - - {(() => { - const perc = (stats.usedMemory / stats.maxMemory) * 100; - const color = perc >= 90 ? 'error' : perc >= 80 ? 'warning' : 'success'; - return ; - })()} - - )} - - - - - - - - - - Operations/sec - - - {formatNumber(stats?.instantaneousOpsPerSec || 0)} - - - Current throughput - - - - - - - - - - - Connected Clients - - - {formatNumber(stats?.connectedClients || 0)} - - - Active connections - - - - - - - - - - - Event Latency - - - {clientRttP50 !== null ? `${clientRttP50.toFixed(2)}ms` : 'N/A'} - - - Client RTT P50: {clientRttP50 !== null ? `${clientRttP50.toFixed(2)}ms` : 'N/A'} - - - Client RTT P95: {clientRttP95 !== null ? `${clientRttP95.toFixed(2)}ms` : 'N/A'} - - - - - - {/* Server Info */} - - - - - - Uptime - - - {stats?.uptimeInDays || 0}d - - - {formatUptime(stats?.uptimeInSeconds || 0)} - - - - - - - - - - - Total Keys - - - {formatNumber(stats?.totalKeys || 0)} - - - {formatNumber(stats?.totalExpires || 0)} with TTL - - - - - - - - - - - Network Rate - - - ↓ {stats?.instantaneousInputKbps ? `${stats.instantaneousInputKbps.toFixed(1)} KB/s` : '0.0 KB/s'} - - - ↑ {stats?.instantaneousOutputKbps ? `${stats.instantaneousOutputKbps.toFixed(1)} KB/s` : '0.0 KB/s'} - - - - - - - - - - - CPU Usage - - - {derivedCpuPercent !== null ? `${derivedCpuPercent.toFixed(2)}%` : 'N/A'} - - - Real-time process usage - - - - - - {/* Performance Chart */} - - - - - Performance Metrics - - - - - - - - - - - - - - - - - - - {/* Key Types Distribution */} - - - - - Key Types Distribution - - - Sample of up to 200 keys - - {keyTypesData.length > 0 ? ( - - - - - - - - - - - - ) : ( - - - No key type data available - - - )} - - - - - {/* Infra Signals */} - - - - - - Infra Signals - - - No critical infrastructure signals detected - - - - - - {/* Cache Performance */} - - - - - Cache Performance - - - - Hits - - - {formatNumber(stats?.keyspaceHits || 0)} - - - - - - Misses - - - {formatNumber(stats?.keyspaceMisses || 0)} - - - - - - - {stats?.keyspaceHits && stats?.keyspaceMisses - ? ((stats.keyspaceHits / (stats.keyspaceHits + stats.keyspaceMisses)) * 100).toFixed(1) - : '0'}% - - - - - - - - - - - - {/* Slow Log */} - - - - - Slow Log (Last 10 Commands) - - - - - - ID - Timestamp - Duration (μs) - Command - Client - - - - {slowLog.length === 0 ? ( - - - - No slow commands recorded - - - - ) : ( - slowLog.map((entry) => ( - - {entry.id} - - {format(new Date(entry.timestamp * 1000), 'HH:mm:ss')} - - - 10000 ? 'error' : 'warning'} - size="small" - /> - - - {entry.command.join(' ')} - - - {entry.clientAddress || 'N/A'} - - - )) - )} - -
    -
    -
    -
    -
    - - {/* Memory Details */} - - - - - Memory Details - - - - Used Memory - {stats?.usedMemoryHuman} - - - Max Memory - {stats?.maxMemoryHuman || 'Unlimited'} - - - Fragmentation Ratio - - - {(stats?.memoryFragmentationRatio || 1).toFixed(2)} - - {(stats?.memoryFragmentationRatio || 1) > 1.5 && ( - - )} - - - {stats?.maxMemory && stats.maxMemory > 0 && ( - - { - const perc = (stats.usedMemory / stats.maxMemory) * 100; - return perc >= 90 ? 'error' : perc >= 80 ? 'warning' : 'primary'; - })()} - /> - - )} - - - - - - {/* Replication & Sync */} - - - - - Replication & Sync - - - Full Syncs - {formatNumber(stats?.syncFull || 0)} - - - Partial Sync OK - {formatNumber(stats?.syncPartialOk || 0)} - - - Partial Sync Errors - {formatNumber(stats?.syncPartialErr || 0)} - - - Rejected Connections - - {formatNumber(stats?.rejectedConnections || 0)} - - - - Latest Fork Time - - {stats?.latestForkUsec ? `${(stats.latestForkUsec / 1000).toFixed(1)}ms` : 'N/A'} - - - - - - - {/* Pub/Sub & Client Info */} - - - - - Pub/Sub & Clients - - - Pub/Sub Channels - {formatNumber(stats?.pubsubChannels || 0)} - - - Pub/Sub Patterns - {formatNumber(stats?.pubsubPatterns || 0)} - - - Connected Clients - {formatNumber(stats?.connectedClients || 0)} - - - Blocked Clients - - {formatNumber(stats?.blockedClients || 0)} - - - - Tracking Clients - {formatNumber(stats?.trackingClients || 0)} - - - Rejected Connections - - {formatNumber(stats?.rejectedConnections || 0)} - - - - - - - {/* Keyspace Summary */} - - - - - Keyspace Summary - - - Total Keys - {formatNumber(stats?.totalKeys || 0)} - - - Keys with TTL - {formatNumber(stats?.totalExpires || 0)} - - - TTL Percentage - - {stats?.totalKeys ? ((stats.totalExpires / stats.totalKeys) * 100).toFixed(1) : '0'}% - - - - Expired Keys - {formatNumber(stats?.expiredKeys || 0)} - - - Evicted Keys - - {formatNumber(stats?.evictedKeys || 0)} - - - - - - - {/* Server Information */} - - - - - - - Server Information - - - - Redis Version - - {stats?.redisVersion || info?.server?.redis_version || 'N/A'} - - - - Mode - - {stats?.redisMode || info?.server?.redis_mode || 'standalone'} - - - - Operating System - - {stats?.os || info?.server?.os || 'N/A'} - - - - Architecture - - {stats?.archBits ? `${stats.archBits} bits` : - info?.server?.arch_bits ? `${info.server.arch_bits} bits` : 'N/A'} - - - - Process ID - - {stats?.processId || info?.server?.process_id || 'N/A'} - - - - TCP Port - - {stats?.tcpPort || info?.server?.tcp_port || 'N/A'} - - - - - - - {/* CPU Usage */} - - - - - - - CPU Usage - - - - System CPU Time - - {info?.cpu?.used_cpu_sys ? `${parseFloat(info.cpu.used_cpu_sys).toFixed(2)}s` : - stats?.usedCpuSys ? `${stats.usedCpuSys.toFixed(2)}s` : '0s'} - - - - User CPU Time - - {info?.cpu?.used_cpu_user ? `${parseFloat(info.cpu.used_cpu_user).toFixed(2)}s` : - stats?.usedCpuUser ? `${stats.usedCpuUser.toFixed(2)}s` : '0s'} - - - - CPU Percentage (avg since start) - - {info?.cpu?.used_cpu_sys && info?.server?.uptime_in_seconds ? - `${Math.min((parseFloat(info.cpu.used_cpu_sys) / parseInt(info.server.uptime_in_seconds)) * 100, 100).toFixed(2)}%` : - 'N/A (uptime required)'} - - - - Server Uptime - - {info?.server?.uptime_in_seconds ? - `${parseInt(info.server.uptime_in_seconds).toLocaleString()}s (${Math.floor(parseInt(info.server.uptime_in_seconds) / 86400)}d)` : - 'N/A'} - - - - System CPU (Children) - - {stats?.usedCpuSysChildren ? `${stats.usedCpuSysChildren.toFixed(2)}s` : '0s'} - - - - User CPU (Children) - - {stats?.usedCpuUserChildren ? `${stats.usedCpuUserChildren.toFixed(2)}s` : '0s'} - - - - Total CPU Time - - {info?.cpu?.used_cpu_sys && info?.cpu?.used_cpu_user - ? `${(parseFloat(info.cpu.used_cpu_sys) + parseFloat(info.cpu.used_cpu_user)).toFixed(2)}s` - : 'N/A'} - - - - - - - {/* Command Statistics */} - - - - - - - Top Commands - - - - - - - Command - Calls - % Total - Avg Time (μs) - Total Time - - - - {stats?.commandStats?.length ? ( - stats.commandStats.map((cmd) => ( - - - {cmd.command} - - {formatNumber(cmd.calls)} - - 20 ? 'primary' : cmd.percentage > 5 ? 'secondary' : 'default'} - size="small" - /> - - {cmd.usecPerCall.toFixed(1)} - {formatNumber(cmd.usec)} - - )) - ) : ( - - - - No command statistics available - - - - )} - -
    -
    -
    -
    -
    - - {/* Memory Policy & Status */} - - - - - - - Memory Policy & Status - - - - Eviction Policy - - - - Evicted Keys - - {formatNumber(stats?.evictedKeys || 0)} - - - - Expired Keys - - {formatNumber(stats?.expiredKeys || 0)} - - - - - - - {/* Persistence Status */} - - - - - - - Persistence Status - - - - RDB Last Save - - {stats?.rdbLastSaveTime - ? format(new Date(stats.rdbLastSaveTime * 1000), 'HH:mm:ss') - : 'Never'} - - - - Changes Since Save - - {formatNumber(stats?.rdbChangesSinceLastSave || 0)} - - - - Last Bgsave Status - - - - AOF Enabled - - - {stats?.aofEnabled && ( - <> - - AOF Current Size - {formatBytes(stats?.aofCurrentSize || 0)} - - - AOF Rewrite - - - - )} - - - - - {/* Database Breakdown */} - - - - - - - Database Breakdown - - - {stats?.databases?.length ? ( - - {stats.databases.map((db) => ( - - - Database {db.db} - - - Keys - - {formatNumber(db.keys)} - - - - With TTL - - {formatNumber(db.expires)} ({db.keys > 0 ? ((db.expires / db.keys) * 100).toFixed(1) : '0'}%) - - - {db.avgTtl > 0 && ( - - Avg TTL - - {Math.round(db.avgTtl / 1000)}s - - - )} - - ))} - - ) : ( - - - No database information available - - - )} - - - -
    -
    - ); -}; - -export default Dashboard; diff --git a/src/components/KeysBrowser.backup.tsx b/src/components/KeysBrowser.backup.tsx deleted file mode 100644 index cb9d391..0000000 --- a/src/components/KeysBrowser.backup.tsx +++ /dev/null @@ -1,631 +0,0 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -// Componentes do Material-UI -import * as React from 'react'; -import { useRef, useState, useEffect } from 'react'; -import { styled } from '@mui/material/styles'; -import Typography from '@mui/material/Typography'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; - -// Estilos inline para substituir styled-components -const containerStyle = { - display: 'flex', - flexDirection: 'column', - width: '100%', - height: '100%', - overflow: 'hidden' as const -}; - -const styledBoxStyle = { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '100%', - width: '100%' -}; -import IconButton from '@mui/material/IconButton'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemText from '@mui/material/ListItemText'; -import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; -import Chip from '@mui/material/Chip'; -import Button from '@mui/material/Button'; -import InputAdornment from '@mui/material/InputAdornment'; -import Alert from '@mui/material/Alert'; -import CircularProgress from '@mui/material/CircularProgress'; -import ToggleButton from '@mui/material/ToggleButton'; -import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; -import Tooltip from '@mui/material/Tooltip'; -import Dialog from '@mui/material/Dialog'; -import DialogTitle from '@mui/material/DialogTitle'; -import DialogContent from '@mui/material/DialogContent'; -import DialogContentText from '@mui/material/DialogContentText'; -import DialogActions from '@mui/material/DialogActions'; -import LinearProgress from '@mui/material/LinearProgress'; -import { - Search as SearchIcon, - Refresh as RefreshIcon, - Delete as DeleteIcon, - Edit as EditIcon, - ViewList as ViewListIcon, - AccountTree as TreeViewIcon, - ArrowBack as ArrowBackIcon, - GetApp as LoadAllIcon, - Key as KeyIcon, -} from '@mui/icons-material'; -import { useDispatch, useSelector } from 'react-redux'; -import { RootState, AppDispatch } from '@/store'; -import { - fetchKeys, - fetchAllKeys, - fetchValue, - setSearchPattern, - setSelectedKey, - deleteKey, - setLoadingProgress, - resetLoadingProgress, - setKeys, - setTotalKeys, - setLoadedKeysJson, - clearLoadedKeysJson, -} from '@/store/slices/keysSlice'; -import ValueEditor from './ValueEditor'; -import TreeView from './TreeView'; -import TreeStats from './TreeStats'; -import SeparatorSelector from './SeparatorSelector'; -import { useTreeView } from '@/hooks/useTreeView'; -import { useLoadAllKeysWithProgress } from '@/hooks/useLoadAllKeysWithProgress'; -import { useLoadAllKeysWithPolling } from '@/hooks/useLoadAllKeysWithPolling'; -import { useSimplePolling } from '@/hooks/useSimplePolling'; -import { useConnectionErrorHandler } from '@/hooks/useConnectionErrorHandler'; -import { useTTLCountdown } from '@/hooks/useTTLCountdown'; -import ErrorModal from './ErrorModal'; -import LoadingProgressModal from './LoadingProgressModal'; -import VirtualizedKeysList from './VirtualizedKeysList'; -import { RedisDataType } from '@/types/redis'; -import { formatTTL } from '@/utils/timeFormatter'; - -const KeysBrowser = () => { - const dispatch = useDispatch(); - const router = useRouter(); - const { keys, selectedKey, selectedValue, searchPattern, isLoading, isLoadingValue, isLoadingAllKeys, totalKeys, error, loadingProgress, loadedKeysJson } = useSelector( - (state: RootState) => state.keys - ); - const { activeConnection } = useSelector((state: RootState) => state.connection); - const listContainerRef = useRef(null); - const [listHeight, setListHeight] = useState(600); - const [localSearchPattern, setLocalSearchPattern] = useState(searchPattern); - const [deleteDialog, setDeleteDialog] = useState<{ - open: boolean; - keyName?: string; - keyNames?: string[]; - type: 'single' | 'bulk'; - }>({ open: false, type: 'single' }); - const [goToKeyDialog, setGoToKeyDialog] = useState<{ - open: boolean; - keyName: string; - }>({ open: false, keyName: '' }); - const goToKeyInputRef = useRef(null); - const { - treeNodes, - viewMode, - setViewMode, - detectedSeparator, - customSeparator, - setCustomSeparator, - activeSeparator, - expandedNodes: treeExpandedNodes, - toggleExpand, - expandAllChildren, - collapseAllChildren - } = useTreeView(keys); - - const { loadAllKeysWithProgress, cancelLoadAllKeys } = useLoadAllKeysWithProgress(); - const { loadAllKeysWithPolling, cancelLoadAllKeys: cancelPolling } = useLoadAllKeysWithPolling(); - const { loadAllKeysSimple, cancelSimple } = useSimplePolling(); - const { handleFetchError, errorModal, closeErrorModal } = useConnectionErrorHandler(); - - // TTL Countdown - ativa apenas quando há conexão ativa - useTTLCountdown(!!activeConnection?.connected); - - // Ref para evitar chamadas duplicadas - const lastConnectionIdRef = useRef(null); - const isInitialLoadRef = useRef(true); - - useEffect(() => { - // Evitar chamada na montagem inicial se não há conexão - if (!activeConnection?.connected) { - isInitialLoadRef.current = false; - return; - } - - // Evitar chamada duplicada para a mesma conexão - if (lastConnectionIdRef.current === activeConnection.id) { - return; - } - - lastConnectionIdRef.current = activeConnection.id; - isInitialLoadRef.current = false; - - // Só recarregar se não houver chaves carregadas - if (keys.length === 0) { - handleRefresh(); - } - }, [activeConnection?.id, activeConnection?.connected]); - - // Calcular altura disponível para a lista - useEffect(() => { - const updateHeight = () => { - if (listContainerRef.current) { - const rect = listContainerRef.current.getBoundingClientRect(); - setListHeight(rect.height); - } - }; - - updateHeight(); - window.addEventListener('resize', updateHeight); - - // Pequeno delay para garantir que o layout foi renderizado - const timer = setTimeout(updateHeight, 100); - - return () => { - window.removeEventListener('resize', updateHeight); - clearTimeout(timer); - }; - }, [viewMode, keys.length]); - - // Função para focar no input após a transição do Dialog - const handleDialogEntered = () => { - if (goToKeyInputRef.current) { - goToKeyInputRef.current.focus(); - } - }; - - const handleRefresh = () => { - if (activeConnection) { - dispatch(fetchKeys({ pattern: searchPattern, count: 1000 })); - } - }; - - const handleLoadAllKeys = async () => { - if (activeConnection) { - console.log(' Carregando todas as chaves...'); - try { - // Use simple polling method - console.log(' Usando método simples...'); - const keys = await loadAllKeysSimple(searchPattern); - - // Update progress to show we're preparing the file - dispatch(setLoadingProgress({ - isActive: true, - phase: 'completing', - message: 'Preparando arquivo JSON...', - progress: 100, - total: keys.length, - current: keys.length, - })); - - // Small delay to show the "preparing" message - await new Promise(resolve => setTimeout(resolve, 300)); - - // Process JSON data BEFORE marking as complete - const jsonData = JSON.stringify(keys, null, 2); - dispatch(setLoadedKeysJson({ - json: jsonData, - fileName: `redis-keys-${activeConnection.name}-${new Date().toISOString().split('T')[0]}.json` - })); - - // Wait a bit to ensure Redux state is updated - await new Promise(resolve => setTimeout(resolve, 100)); - - // Now mark as complete - modal will open with JSON already loaded - dispatch(setLoadingProgress({ - isActive: false, - phase: 'complete', - message: `${keys.length} chaves carregadas com sucesso!`, - progress: 100, - total: keys.length, - current: keys.length, - })); - - console.log(` Carregamento concluído: ${keys.length} chaves`); - - } catch (error) { - console.error(' Erro ao carregar chaves:', error); - if (error instanceof Error && !error.message.includes('cancelada')) { - dispatch(resetLoadingProgress()); - } - } - } - }; - - const handleCancelLoading = () => { - console.log(' Usuário solicitou cancelamento...'); - - // Use simple cancellation - cancelSimple(); - }; - - const handleSearch = () => { - dispatch(setSearchPattern(localSearchPattern)); - dispatch(fetchKeys({ pattern: localSearchPattern, count: 1000 })); - }; - - const handleKeySelect = (keyName: string) => { - // Navegar para a rota de edição - router.push(`/browser/edit/${encodeURIComponent(keyName)}`); - }; - - const handleGoToKey = () => { - setGoToKeyDialog({ open: true, keyName: '' }); - }; - - const handleConfirmGoToKey = () => { - const keyName = goToKeyDialog.keyName.trim(); - if (keyName) { - handleKeySelect(keyName); - setGoToKeyDialog({ open: false, keyName: '' }); - } - }; - - const handleKeyDelete = (keyName: string) => { - console.log('🔄 handleKeyDelete chamado para:', keyName); - console.log('🔄 Estado atual do deleteDialog:', deleteDialog); - setDeleteDialog({ - open: true, - keyName, - type: 'single' - }); - console.log('✅ Modal de exclusão deve estar aberto agora'); - }; - - const handleConfirmDelete = async () => { - if (deleteDialog.type === 'single' && deleteDialog.keyName) { - await dispatch(deleteKey(deleteDialog.keyName)); - handleRefresh(); - } else if (deleteDialog.type === 'bulk' && deleteDialog.keyNames) { - try { - // Deletar chaves em paralelo para melhor performance - await Promise.all(deleteDialog.keyNames.map(keyName => dispatch(deleteKey(keyName)))); - handleRefresh(); - } catch (error) { - console.error('Bulk delete failed:', error); - } - } - setDeleteDialog({ open: false, type: 'single' }); - }; - - const handleBulkKeyDelete = (keyNames: string[]) => { - setDeleteDialog({ - open: true, - keyNames, - type: 'bulk' - }); - }; - - const getTypeColor = (type: RedisDataType) => { - const colors = { - string: 'primary', - hash: 'secondary', - list: 'success', - set: 'warning', - zset: 'info', - stream: 'error', - json: 'default', - } as const; - return colors[type] || 'default'; - }; - - const formatSize = (size: number) => { - if (size < 1024) return `${size} B`; - if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`; - return `${(size / (1024 * 1024)).toFixed(1)} MB`; - }; - - if (!activeConnection) { - return ( -
    - - No active Redis connection - -
    - ); - } - - // Renderização condicional removida - agora usa rotas - - return ( -
    - {error && ( - - {error} - - )} - - {/* Navegação de chaves em tela cheia */} - - -
    - newMode && setViewMode(newMode)} - size="small" - > - - - - - - - - - - - - -
    - - - - - - - - - - -
    - - - setLocalSearchPattern(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && handleSearch()} - InputProps={{ - endAdornment: ( - - - - - - ), - }} - sx={{ mb: 2 }} - /> - - {viewMode === 'tree' && keys.length > 0 && ( -
    - - - -
    - )} - -
    - {viewMode === 'tree' && keys.length > 0 ? ( - - ) : ( - - {keys.length} keys found - - )} - - -
    - {isLoading || isLoadingAllKeys ? ( -
    - -
    - ) : viewMode === 'tree' ? ( - - ) : ( - - )} -
    - - - - {/* Modal de Confirmação de Delete - Para visualização de navegação */} - setDeleteDialog({ open: false, type: 'single' })} - aria-labelledby="delete-dialog-title" - aria-describedby="delete-dialog-description" - > - - Confirmar Exclusão - - - - {deleteDialog.type === 'single' && deleteDialog.keyName ? ( - <>Tem certeza que deseja excluir a chave "{deleteDialog.keyName}"? - ) : deleteDialog.type === 'bulk' && deleteDialog.keyNames ? ( - <>Tem certeza que deseja excluir {deleteDialog.keyNames.length} chave{deleteDialog.keyNames.length > 1 ? 's' : ''}? - ) : null} -

    - Esta ação não pode ser desfeita. -
    -
    - - - -
    - - {/* Loading Progress Modal */} - { - dispatch(resetLoadingProgress()); - dispatch(clearLoadedKeysJson()); - } : handleCancelLoading} - /> - - - {/* Go to Key Modal */} - setGoToKeyDialog({ open: false, keyName: '' })} - onTransitionEnd={handleDialogEntered} - aria-labelledby="go-to-key-dialog-title" - maxWidth="sm" - fullWidth - > - - Acessar Chave Específica - - - - Digite o nome exato da chave que deseja visualizar: - - setGoToKeyDialog({ ...goToKeyDialog, keyName: e.target.value })} - onKeyPress={(e) => { - if (e.key === 'Enter') { - handleConfirmGoToKey(); - } - }} - sx={{ mt: 1 }} - /> - - - - - - - - {/* Delete Confirmation Modal */} - setDeleteDialog({ open: false, type: 'single' })} - aria-labelledby="delete-dialog-title" - aria-describedby="delete-dialog-description" - > - - Confirmar Exclusão - - - - {deleteDialog.type === 'single' && deleteDialog.keyName ? ( - <>Tem certeza que deseja excluir a chave "{deleteDialog.keyName}"? - ) : deleteDialog.type === 'bulk' && deleteDialog.keyNames ? ( - <>Tem certeza que deseja excluir {deleteDialog.keyNames.length} chave{deleteDialog.keyNames.length > 1 ? 's' : ''}? - ) : null} -

    - Esta ação não pode ser desfeita. -
    -
    - - - - -
    - - ); -}; - -export default KeysBrowser; diff --git a/src/components/KeysBrowser.tsx b/src/components/KeysBrowser.tsx index ef9a626..c97963b 100644 --- a/src/components/KeysBrowser.tsx +++ b/src/components/KeysBrowser.tsx @@ -300,6 +300,7 @@ const KeysBrowser = () => { zset: 'info', stream: 'error', json: 'default', + none: 'default', } as const; return colors[type] || 'default'; }; @@ -438,7 +439,7 @@ const KeysBrowser = () => { ) : ( { {/* Loading Progress Modal */} { const formatTimestamp = (timestamp: number) => { const date = new Date(timestamp); - return date.toLocaleTimeString('en-US', { - hour12: false, - hour: '2-digit', - minute: '2-digit', + // fractionalSecondDigits is supported at runtime but absent from the ES2017 lib types + const options: Intl.DateTimeFormatOptions & { fractionalSecondDigits?: number } = { + hour12: false, + hour: '2-digit', + minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3, - }); + }; + return date.toLocaleTimeString('en-US', options); }; if (!activeConnection?.connected) { @@ -217,10 +219,9 @@ const Monitor = () => { )} - diff --git a/src/components/TreeView.tsx b/src/components/TreeView.tsx index 3241920..9fd5403 100644 --- a/src/components/TreeView.tsx +++ b/src/components/TreeView.tsx @@ -39,7 +39,7 @@ import { formatTTL } from '@/utils/timeFormatter'; import KeyTypeIcon from './KeyTypeIcon'; import { useDispatch } from 'react-redux'; import { removeKeysLocally } from '@/store/slices/keysSlice'; -import { AppDispatch } from '@/store/store'; +import { AppDispatch } from '@/store'; interface TreeViewProps { nodes: TreeNode[]; @@ -177,6 +177,7 @@ const TreeView = ({ zset: 'info', stream: 'error', json: 'default', + none: 'default', } as const; return colors[type] || 'default'; }; diff --git a/src/services/metrics.ts b/src/services/metrics.ts index dca7f3a..bcbaf2b 100644 --- a/src/services/metrics.ts +++ b/src/services/metrics.ts @@ -42,8 +42,8 @@ export class MetricsService { // CPU% = (delta_cpu_time / delta_real_time) * 100 let cpuPercentage = 0; if (session.previousStats) { - const deltaCpuSys = stats.usedCpuSys - session.previousStats.usedCpuSys; - const deltaCpuUser = stats.usedCpuUser - session.previousStats.usedCpuUser; + const deltaCpuSys = (stats.usedCpuSys ?? 0) - (session.previousStats.usedCpuSys ?? 0); + const deltaCpuUser = (stats.usedCpuUser ?? 0) - (session.previousStats.usedCpuUser ?? 0); const deltaCpuTotal = deltaCpuSys + deltaCpuUser; // CPU% = (tempo de CPU usado / tempo real decorrido) * 100 diff --git a/src/types/redis.ts b/src/types/redis.ts index 7a3c23f..4620395 100644 --- a/src/types/redis.ts +++ b/src/types/redis.ts @@ -17,7 +17,7 @@ export interface RedisKey { encoding?: string; } -export type RedisDataType = 'string' | 'hash' | 'list' | 'set' | 'zset' | 'stream' | 'json'; +export type RedisDataType = 'string' | 'hash' | 'list' | 'set' | 'zset' | 'stream' | 'json' | 'none'; export interface RedisValue { type: RedisDataType; @@ -126,8 +126,10 @@ export interface RedisStats { usedCpuSysPercent?: number; usedCpuUserPercent?: number; instantaneousCpuPercent?: number; // Real-time CPU usage - clientRttP50?: number; // Client RTT P50 latency - clientRttP95?: number; // Client RTT P95 latency + instantaneousInputKbps?: number; // Network input rate + instantaneousOutputKbps?: number; // Network output rate + clientRttP50?: number | null; // Client RTT P50 latency + clientRttP95?: number | null; // Client RTT P95 latency // Replication Advanced role?: string;