import { useMemo, useState } from 'react'; import { useParams, Link } from 'react-router'; import { RefreshCw, ChevronRight } from 'lucide-react'; import { StatCard, StatusDot, Badge, LineChart, AreaChart, BarChart, EventFeed, Spinner, EmptyState, SectionHeader, MonoText, LogViewer, ButtonGroup, useGlobalFilters, } from '@cameleer/design-system'; import type { FeedEvent, LogEntry, ButtonGroupItem } from '@cameleer/design-system'; import styles from './AgentInstance.module.css'; import { useAgents, useAgentEvents } from '../../api/queries/agents'; import { useApplicationLogs } from '../../api/queries/logs'; import { useStatsTimeseries } from '../../api/queries/executions'; import { useAgentMetrics } from '../../api/queries/agent-metrics'; const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [ { value: 'error', label: 'Error', color: 'var(--error)' }, { value: 'warn', label: 'Warn', color: 'var(--warning)' }, { value: 'info', label: 'Info', color: 'var(--success)' }, { value: 'debug', label: 'Debug', color: 'var(--running)' }, { value: 'trace', label: 'Trace', color: 'var(--text-muted)' }, ]; function mapLogLevel(level: string): LogEntry['level'] { switch (level?.toUpperCase()) { case 'ERROR': return 'error'; case 'WARN': case 'WARNING': return 'warn'; case 'DEBUG': return 'debug'; case 'TRACE': return 'trace'; default: return 'info'; } } export default function AgentInstance() { const { appId, instanceId } = useParams(); const { timeRange } = useGlobalFilters(); const [logSearch, setLogSearch] = useState(''); const [logLevels, setLogLevels] = useState>(new Set()); const [logSortAsc, setLogSortAsc] = useState(false); const [eventSortAsc, setEventSortAsc] = useState(false); const [logRefreshTo, setLogRefreshTo] = useState(); const [eventRefreshTo, setEventRefreshTo] = useState(); const timeFrom = timeRange.start.toISOString(); const timeTo = timeRange.end.toISOString(); const { data: agents, isLoading } = useAgents(undefined, appId); const { data: events } = useAgentEvents(appId, instanceId, 50, eventRefreshTo); const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, undefined, appId); const agent = useMemo( () => (agents || []).find((a: any) => a.id === instanceId) as any, [agents, instanceId], ); // Stat card metrics (latest 1 bucket) const { data: latestMetrics } = useAgentMetrics( agent?.id || null, ['jvm.cpu.process', 'jvm.memory.heap.used', 'jvm.memory.heap.max'], 1, ); const cpuPct = latestMetrics?.metrics?.['jvm.cpu.process']?.[0]?.value; const heapUsed = latestMetrics?.metrics?.['jvm.memory.heap.used']?.[0]?.value; const heapMax = latestMetrics?.metrics?.['jvm.memory.heap.max']?.[0]?.value; const memPct = heapMax ? (heapUsed! / heapMax) * 100 : undefined; // Chart metrics (60 buckets) const { data: jvmMetrics } = useAgentMetrics( agent?.id || null, ['jvm.cpu.process', 'jvm.memory.heap.used', 'jvm.memory.heap.max', 'jvm.threads.count', 'jvm.gc.time'], 60, ); const chartData = useMemo( () => (timeseries?.buckets || []).map((b: any) => ({ time: new Date(b.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), throughput: b.totalCount, latency: b.avgDurationMs, errors: b.failedCount, })), [timeseries], ); const feedEvents = useMemo(() => { const mapped = (events || []) .filter((e: any) => !instanceId || e.instanceId === instanceId) .map((e: any) => ({ id: String(e.id), severity: e.eventType === 'WENT_DEAD' ? ('error' as const) : e.eventType === 'WENT_STALE' ? ('warning' as const) : e.eventType === 'RECOVERED' ? ('success' as const) : ('running' as const), message: `${e.eventType}${e.detail ? ' \u2014 ' + e.detail : ''}`, timestamp: new Date(e.timestamp), })); return eventSortAsc ? mapped.toReversed() : mapped; }, [events, instanceId, eventSortAsc]); // JVM chart series helpers const cpuSeries = useMemo(() => { const pts = jvmMetrics?.metrics?.['jvm.cpu.process']; if (!pts?.length) return null; return [{ label: 'CPU %', data: pts.map((p: any, i: number) => ({ x: i, y: p.value * 100 })) }]; }, [jvmMetrics]); const heapSeries = useMemo(() => { const pts = jvmMetrics?.metrics?.['jvm.memory.heap.used']; if (!pts?.length) return null; return [{ label: 'Heap MB', data: pts.map((p: any, i: number) => ({ x: i, y: p.value / (1024 * 1024) })) }]; }, [jvmMetrics]); const threadSeries = useMemo(() => { const pts = jvmMetrics?.metrics?.['jvm.threads.count']; if (!pts?.length) return null; return [{ label: 'Threads', data: pts.map((p: any, i: number) => ({ x: i, y: p.value })) }]; }, [jvmMetrics]); const gcSeries = useMemo(() => { const pts = jvmMetrics?.metrics?.['jvm.gc.time']; if (!pts?.length) return null; return [{ label: 'GC ms', data: pts.map((p: any) => ({ x: String(p.time ?? ''), y: p.value })) }]; }, [jvmMetrics]); const throughputSeries = useMemo( () => chartData.length ? [{ label: 'Throughput', data: chartData.map((d: any, i: number) => ({ x: i, y: d.throughput })) }] : null, [chartData], ); const errorSeries = useMemo( () => chartData.length ? [{ label: 'Errors', data: chartData.map((d: any, i: number) => ({ x: i, y: d.errors })) }] : null, [chartData], ); // Application logs const { data: rawLogs } = useApplicationLogs(appId, instanceId, { toOverride: logRefreshTo }); const logEntries = useMemo(() => { const mapped = (rawLogs || []).map((l) => ({ timestamp: l.timestamp ?? '', level: mapLogLevel(l.level), message: l.message ?? '', })); return logSortAsc ? mapped.toReversed() : mapped; }, [rawLogs, logSortAsc]); const searchLower = logSearch.toLowerCase(); const filteredLogs = logEntries .filter((l) => logLevels.size === 0 || logLevels.has(l.level)) .filter((l) => !searchLower || l.message.toLowerCase().includes(searchLower)); if (isLoading) return ; const statusVariant = agent?.status === 'LIVE' ? 'live' : agent?.status === 'STALE' ? 'stale' : 'dead'; const statusColor: 'success' | 'warning' | 'error' = agent?.status === 'LIVE' ? 'success' : agent?.status === 'STALE' ? 'warning' : 'error'; const cpuDisplay = cpuPct != null ? (cpuPct * 100).toFixed(0) : null; const heapUsedMB = heapUsed != null ? (heapUsed / (1024 * 1024)).toFixed(0) : null; const heapMaxMB = heapMax != null ? (heapMax / (1024 * 1024)).toFixed(0) : null; return (
{/* Stat strip — 5 columns */}
85 ? 'error' : Number(cpuDisplay) > 70 ? 'warning' : 'success' : undefined } /> 85 ? 'error' : memPct > 70 ? 'warning' : 'success' : undefined } detail={ heapUsedMB != null && heapMaxMB != null ? `${heapUsedMB} MB / ${heapMaxMB} MB` : undefined } /> 0 ? 'error' : 'success'} />
{/* Scope trail + badges */} {agent && ( <>
All Agents {appId} {agent.name} {agent.version && }
{/* Process info card */}
Process Information
{agent.capabilities?.jvmVersion && ( <> JVM {agent.capabilities.jvmVersion} )} {agent.capabilities?.camelVersion && ( <> Camel {agent.capabilities.camelVersion} )} {agent.capabilities?.springBootVersion && ( <> Spring Boot {agent.capabilities.springBootVersion} )} Started {agent.registeredAt ? new Date(agent.registeredAt).toLocaleString() : '\u2014'} {agent.capabilities && ( <> Capabilities {Object.entries(agent.capabilities) .filter(([, v]) => typeof v === 'boolean' && v) .map(([k]) => ( ))} )}
{/* Routes */} {(agent.routeIds?.length ?? 0) > 0 && ( <> Routes
{(agent.routeIds || []).map((r: string) => ( ))}
)} )} {/* Charts grid — 3x2 */}
CPU Usage {cpuDisplay != null ? `${cpuDisplay}% current` : ''}
{cpuSeries ? ( ) : ( )}
Memory (Heap) {heapUsedMB != null && heapMaxMB != null ? `${heapUsedMB} MB / ${heapMaxMB} MB` : ''}
{heapSeries ? ( ) : ( )}
Throughput {agent?.tps != null ? `${agent.tps.toFixed(1)} msg/s` : ''}
{throughputSeries ? ( ) : ( )}
Error Rate {agent?.errorRate != null ? `${(agent.errorRate * 100).toFixed(1)}%` : ''}
{errorSeries ? ( ) : ( )}
Thread Count {threadSeries ? `${threadSeries[0].data[threadSeries[0].data.length - 1]?.y.toFixed(0)} active` : ''}
{threadSeries ? ( ) : ( )}
GC Pauses
{gcSeries ? ( ) : ( )}
{/* Log + Timeline side by side */}
Application Log
{logEntries.length} entries
setLogSearch(e.target.value)} aria-label="Search logs" /> {logSearch && ( )}
{logLevels.size > 0 && ( )}
{filteredLogs.length > 0 ? ( ) : (
{logSearch || logLevels.size > 0 ? 'No matching log entries' : 'No log entries available'}
)}
Timeline
{feedEvents.length} events
{feedEvents.length > 0 ? ( ) : (
No events in the selected time range.
)}
); } function formatUptime(seconds?: number): string { if (!seconds) return '\u2014'; const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const mins = Math.floor((seconds % 3600) / 60); if (days > 0) return `${days}d ${hours}h`; if (hours > 0) return `${hours}h ${mins}m`; return `${mins}m`; }