Replace custom log entry rendering (MonoText, Badge, per-entry HTML) with the LogViewer composite component. Map mock data to LogEntry interface, remove formatLogTime helper, and clean up unused CSS classes and imports (Card, CodeBlock, ProgressBar, sectionHeaderRow, sectionTitle, fdRow). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
307 lines
14 KiB
TypeScript
307 lines
14 KiB
TypeScript
import { useMemo } from 'react'
|
|
import { useParams, Link } from 'react-router-dom'
|
|
import styles from './AgentInstance.module.css'
|
|
|
|
// Layout
|
|
import { AppShell } from '../../design-system/layout/AppShell/AppShell'
|
|
import { Sidebar } from '../../design-system/layout/Sidebar/Sidebar'
|
|
import { TopBar } from '../../design-system/layout/TopBar/TopBar'
|
|
|
|
// Composites
|
|
import { LineChart } from '../../design-system/composites/LineChart/LineChart'
|
|
import { AreaChart } from '../../design-system/composites/AreaChart/AreaChart'
|
|
import { EventFeed } from '../../design-system/composites/EventFeed/EventFeed'
|
|
import { Tabs } from '../../design-system/composites/Tabs/Tabs'
|
|
import { LogViewer } from '../../design-system/composites/LogViewer/LogViewer'
|
|
import type { LogEntry } from '../../design-system/composites/LogViewer/LogViewer'
|
|
|
|
// Primitives
|
|
import { StatusDot } from '../../design-system/primitives/StatusDot/StatusDot'
|
|
import { MonoText } from '../../design-system/primitives/MonoText/MonoText'
|
|
import { Badge } from '../../design-system/primitives/Badge/Badge'
|
|
import { StatCard } from '../../design-system/primitives/StatCard/StatCard'
|
|
import { SectionHeader } from '../../design-system/primitives/SectionHeader/SectionHeader'
|
|
|
|
// Global filters
|
|
import { useGlobalFilters } from '../../design-system/providers/GlobalFilterProvider'
|
|
|
|
// Data
|
|
import { agents } from '../../mocks/agents'
|
|
import { SIDEBAR_APPS } from '../../mocks/sidebar'
|
|
import { agentEvents } from '../../mocks/agentEvents'
|
|
|
|
import { useState } from 'react'
|
|
|
|
// ── Mock trend data ──────────────────────────────────────────────────────────
|
|
|
|
function buildTimeSeries(baseValue: number, variance: number, points = 30) {
|
|
const now = Date.now()
|
|
const interval = (6 * 60 * 60 * 1000) / points
|
|
return Array.from({ length: points }, (_, i) => ({
|
|
x: new Date(now - (points - i) * interval),
|
|
y: Math.max(0, baseValue + (Math.random() - 0.5) * variance * 2),
|
|
}))
|
|
}
|
|
|
|
function buildMemoryHistory(currentPct: number) {
|
|
return [
|
|
{ label: 'Heap Used', data: buildTimeSeries(currentPct * 0.7, 10) },
|
|
{ label: 'Heap Total', data: buildTimeSeries(currentPct * 0.9, 5) },
|
|
]
|
|
}
|
|
|
|
// ── Mock log entries ─────────────────────────────────────────────────────────
|
|
|
|
function buildLogEntries(agentName: string): LogEntry[] {
|
|
const now = Date.now()
|
|
const MIN = 60_000
|
|
return [
|
|
{ timestamp: new Date(now - 1 * MIN).toISOString(), level: 'info', message: `[o.a.c.impl.DefaultCamelContext] Route order-validation started and consuming from: direct:validate` },
|
|
{ timestamp: new Date(now - 2 * MIN).toISOString(), level: 'info', message: `[o.a.c.impl.DefaultCamelContext] Total 3 routes, of which 3 are started` },
|
|
{ timestamp: new Date(now - 5 * MIN).toISOString(), level: 'warn', message: `[o.a.c.processor.errorhandler] Failed delivery for exchangeId: ID-${agentName}-1710847200000-0-1. Exhausted after 3 attempts.` },
|
|
{ timestamp: new Date(now - 8 * MIN).toISOString(), level: 'info', message: `[o.a.c.health.HealthCheckHelper] Health check [routes] is UP` },
|
|
{ timestamp: new Date(now - 12 * MIN).toISOString(), level: 'info', message: `[o.a.c.health.HealthCheckHelper] Health check [consumers] is UP` },
|
|
{ timestamp: new Date(now - 15 * MIN).toISOString(), level: 'debug', message: `[o.a.c.component.kafka] KafkaConsumer[order-events] poll returned 42 records in 18ms` },
|
|
{ timestamp: new Date(now - 18 * MIN).toISOString(), level: 'info', message: `[o.a.c.impl.engine.InternalRouteStartup] Route order-enrichment started and consuming from: kafka:order-events` },
|
|
{ timestamp: new Date(now - 25 * MIN).toISOString(), level: 'warn', message: `[o.a.c.component.http] HTTP endpoint https://payment-api.internal/verify returned 503 — will retry` },
|
|
{ timestamp: new Date(now - 30 * MIN).toISOString(), level: 'info', message: `[o.a.c.impl.DefaultCamelContext] Apache Camel ${agentName} (CamelContext) is starting` },
|
|
{ timestamp: new Date(now - 32 * MIN).toISOString(), level: 'info', message: `[org.springframework.boot] Started ${agentName} in 4.231 seconds (process running for 4.892)` },
|
|
]
|
|
}
|
|
|
|
// ── Mock JVM / process info ──────────────────────────────────────────────────
|
|
|
|
function buildProcessInfo(agent: typeof agents[0]) {
|
|
return {
|
|
jvmVersion: 'OpenJDK 21.0.2+13',
|
|
camelVersion: '4.4.0',
|
|
springBootVersion: '3.2.4',
|
|
pid: Math.floor(1000 + Math.random() * 90000),
|
|
startTime: new Date(Date.now() - parseDuration(agent.uptime)).toISOString(),
|
|
heapMax: '512 MB',
|
|
heapUsed: `${Math.round(512 * agent.memoryUsagePct / 100)} MB`,
|
|
nonHeapUsed: `${Math.round(80 + Math.random() * 40)} MB`,
|
|
threadCount: Math.floor(20 + Math.random() * 30),
|
|
peakThreads: Math.floor(45 + Math.random() * 20),
|
|
gcCollections: Math.floor(Math.random() * 500),
|
|
gcPauseTotal: `${(Math.random() * 2).toFixed(2)}s`,
|
|
classesLoaded: Math.floor(8000 + Math.random() * 4000),
|
|
openFileDescriptors: Math.floor(50 + Math.random() * 200),
|
|
maxFileDescriptors: 65536,
|
|
}
|
|
}
|
|
|
|
function parseDuration(s: string): number {
|
|
let ms = 0
|
|
const dMatch = s.match(/(\d+)d/)
|
|
const hMatch = s.match(/(\d+)h/)
|
|
const mMatch = s.match(/(\d+)m/)
|
|
if (dMatch) ms += parseInt(dMatch[1]) * 86400000
|
|
if (hMatch) ms += parseInt(hMatch[1]) * 3600000
|
|
if (mMatch) ms += parseInt(mMatch[1]) * 60000
|
|
return ms || 60000
|
|
}
|
|
|
|
// ── Component ────────────────────────────────────────────────────────────────
|
|
|
|
const LOG_TABS = [
|
|
{ label: 'All', value: 'all' },
|
|
{ label: 'Warnings', value: 'warn' },
|
|
{ label: 'Errors', value: 'error' },
|
|
]
|
|
|
|
export function AgentInstance() {
|
|
const { appId, instanceId } = useParams<{ appId: string; instanceId: string }>()
|
|
const { isInTimeRange } = useGlobalFilters()
|
|
const [logFilter, setLogFilter] = useState('all')
|
|
|
|
const agent = agents.find((a) => a.appId === appId && a.id === instanceId)
|
|
|
|
const instanceEvents = useMemo(() => {
|
|
if (!agent) return []
|
|
return agentEvents
|
|
.filter((e) => e.searchText?.toLowerCase().includes(agent.name.toLowerCase()))
|
|
.filter((e) => isInTimeRange(e.timestamp))
|
|
}, [agent, isInTimeRange])
|
|
|
|
if (!agent) {
|
|
return (
|
|
<AppShell sidebar={<Sidebar apps={SIDEBAR_APPS} />}>
|
|
<TopBar breadcrumb={[{ label: 'Agents', href: '/agents' }, { label: 'Not Found' }]} environment="PRODUCTION" user={{ name: 'hendrik' }} />
|
|
<div className={styles.content}>
|
|
<div className={styles.notFound}>Agent instance not found.</div>
|
|
</div>
|
|
</AppShell>
|
|
)
|
|
}
|
|
|
|
const processInfo = buildProcessInfo(agent)
|
|
const logEntries = buildLogEntries(agent.name)
|
|
const filteredLogs = logFilter === 'all'
|
|
? logEntries
|
|
: logEntries.filter((l) => l.level === logFilter)
|
|
|
|
const cpuData = buildTimeSeries(agent.cpuUsagePct, 15)
|
|
const memSeries = buildMemoryHistory(agent.memoryUsagePct)
|
|
const tpsSeries = [{ label: 'Throughput', data: buildTimeSeries(agent.tps, 5) }]
|
|
const errorSeries = [{ label: 'Errors', data: buildTimeSeries(agent.errorRate ? parseFloat(agent.errorRate) : 0.2, 2), color: 'var(--error)' }]
|
|
const threadSeries = [{ label: 'Threads', data: buildTimeSeries(processInfo.threadCount, 8) }]
|
|
const gcSeries = [{ label: 'GC Pause', data: buildTimeSeries(4, 6) }]
|
|
|
|
const statusVariant = agent.status === 'live' ? 'live' : agent.status === 'stale' ? 'stale' : 'dead'
|
|
const statusColor = agent.status === 'live' ? 'success' : agent.status === 'stale' ? 'warning' : 'error'
|
|
|
|
return (
|
|
<AppShell sidebar={<Sidebar apps={SIDEBAR_APPS} />}>
|
|
<TopBar
|
|
breadcrumb={[
|
|
{ label: 'Applications', href: '/apps' },
|
|
{ label: 'Agents', href: '/agents' },
|
|
{ label: appId!, href: `/agents/${appId}` },
|
|
{ label: instanceId! },
|
|
]}
|
|
environment="PRODUCTION"
|
|
user={{ name: 'hendrik' }}
|
|
/>
|
|
|
|
<div className={styles.content}>
|
|
{/* Stat strip — 5 columns matching /agents */}
|
|
<div className={styles.statStrip}>
|
|
<StatCard label="CPU" value={`${agent.cpuUsagePct}%`} accent={agent.cpuUsagePct > 85 ? 'error' : agent.cpuUsagePct > 70 ? 'warning' : 'success'} />
|
|
<StatCard label="Memory" value={`${agent.memoryUsagePct}%`} accent={agent.memoryUsagePct > 85 ? 'error' : agent.memoryUsagePct > 70 ? 'warning' : 'success'} detail={`${processInfo.heapUsed} / ${processInfo.heapMax}`} />
|
|
<StatCard label="Throughput" value={`${agent.tps.toFixed(1)}/s`} accent="amber" detail="msg/s" />
|
|
<StatCard label="Errors" value={agent.errorRate ?? '0 err/h'} accent={agent.errorRate ? 'error' : 'success'} />
|
|
<StatCard label="Uptime" value={agent.uptime || '—'} accent="running" detail={`since ${new Date(processInfo.startTime).toLocaleDateString()}`} />
|
|
</div>
|
|
|
|
{/* Scope trail + badges */}
|
|
<div className={styles.scopeTrail}>
|
|
<Link to="/agents" className={styles.scopeLink}>All Agents</Link>
|
|
<span className={styles.scopeSep}>▸</span>
|
|
<Link to={`/agents/${appId}`} className={styles.scopeLink}>{appId}</Link>
|
|
<span className={styles.scopeSep}>▸</span>
|
|
<span className={styles.scopeCurrent}>{agent.name}</span>
|
|
<Badge label={agent.status.toUpperCase()} color={statusColor} />
|
|
<Badge label={agent.version} color="auto" variant="outlined" />
|
|
<Badge label={`${agent.activeRoutes}/${agent.totalRoutes} routes`} color={agent.activeRoutes < agent.totalRoutes ? 'warning' : 'success'} />
|
|
</div>
|
|
|
|
{/* Process info card — right below stat strip */}
|
|
<div className={styles.processCard}>
|
|
<SectionHeader>Process Information</SectionHeader>
|
|
<div className={styles.processGrid}>
|
|
<span className={styles.processLabel}>JVM</span>
|
|
<MonoText size="xs">{processInfo.jvmVersion}</MonoText>
|
|
|
|
<span className={styles.processLabel}>Camel</span>
|
|
<MonoText size="xs">{processInfo.camelVersion}</MonoText>
|
|
|
|
<span className={styles.processLabel}>Spring Boot</span>
|
|
<MonoText size="xs">{processInfo.springBootVersion}</MonoText>
|
|
|
|
<span className={styles.processLabel}>Started</span>
|
|
<MonoText size="xs">{new Date(processInfo.startTime).toLocaleString()}</MonoText>
|
|
|
|
<span className={styles.processLabel}>File Descriptors</span>
|
|
<MonoText size="xs">{processInfo.openFileDescriptors} / {processInfo.maxFileDescriptors.toLocaleString()}</MonoText>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Charts grid — 3x2 (CPU, Memory, Throughput, Errors, Threads, GC) */}
|
|
<div className={styles.chartsGrid}>
|
|
<div className={styles.chartCard}>
|
|
<div className={styles.chartHeader}>
|
|
<span className={styles.chartTitle}>CPU Usage</span>
|
|
<span className={styles.chartMeta}>{agent.cpuUsagePct}% current</span>
|
|
</div>
|
|
<AreaChart
|
|
series={[{ label: 'CPU %', data: cpuData }]}
|
|
height={160}
|
|
yLabel="%"
|
|
thresholdValue={85}
|
|
thresholdLabel="Alert"
|
|
/>
|
|
</div>
|
|
|
|
<div className={styles.chartCard}>
|
|
<div className={styles.chartHeader}>
|
|
<span className={styles.chartTitle}>Memory (Heap)</span>
|
|
<span className={styles.chartMeta}>{processInfo.heapUsed} / {processInfo.heapMax}</span>
|
|
</div>
|
|
<AreaChart
|
|
series={memSeries}
|
|
height={160}
|
|
yLabel="MB"
|
|
/>
|
|
</div>
|
|
|
|
<div className={styles.chartCard}>
|
|
<div className={styles.chartHeader}>
|
|
<span className={styles.chartTitle}>Throughput</span>
|
|
<span className={styles.chartMeta}>{agent.tps.toFixed(1)} msg/s</span>
|
|
</div>
|
|
<LineChart
|
|
series={tpsSeries}
|
|
height={160}
|
|
yLabel="msg/s"
|
|
/>
|
|
</div>
|
|
|
|
<div className={styles.chartCard}>
|
|
<div className={styles.chartHeader}>
|
|
<span className={styles.chartTitle}>Error Rate</span>
|
|
<span className={styles.chartMeta}>{agent.errorRate ?? '0 err/h'}</span>
|
|
</div>
|
|
<LineChart
|
|
series={errorSeries}
|
|
height={160}
|
|
yLabel="err/h"
|
|
/>
|
|
</div>
|
|
|
|
<div className={styles.chartCard}>
|
|
<div className={styles.chartHeader}>
|
|
<span className={styles.chartTitle}>Thread Count</span>
|
|
<span className={styles.chartMeta}>{processInfo.threadCount} active</span>
|
|
</div>
|
|
<LineChart series={threadSeries} height={160} yLabel="threads" />
|
|
</div>
|
|
|
|
<div className={styles.chartCard}>
|
|
<div className={styles.chartHeader}>
|
|
<span className={styles.chartTitle}>GC Pauses</span>
|
|
<span className={styles.chartMeta}>{processInfo.gcPauseTotal} total</span>
|
|
</div>
|
|
<LineChart series={gcSeries} height={160} yLabel="ms" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Log + Timeline side by side */}
|
|
<div className={styles.bottomRow}>
|
|
{/* Log viewer */}
|
|
<div className={styles.logCard}>
|
|
<div className={styles.logHeader}>
|
|
<SectionHeader>Application Log</SectionHeader>
|
|
<Tabs tabs={LOG_TABS} active={logFilter} onChange={setLogFilter} />
|
|
</div>
|
|
<LogViewer entries={filteredLogs} maxHeight={360} />
|
|
</div>
|
|
|
|
{/* Timeline */}
|
|
<div className={styles.timelineCard}>
|
|
<div className={styles.timelineHeader}>
|
|
<span className={styles.chartTitle}>Timeline</span>
|
|
<span className={styles.chartMeta}>{instanceEvents.length} events</span>
|
|
</div>
|
|
{instanceEvents.length > 0 ? (
|
|
<EventFeed events={instanceEvents} />
|
|
) : (
|
|
<div className={styles.logEmpty}>No events in the selected time range.</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AppShell>
|
|
)
|
|
}
|