feat: add AgentInstance detail page and improve AgentHealth
All checks were successful
Build & Publish / publish (push) Successful in 43s
All checks were successful
Build & Publish / publish (push) Successful in 43s
- New /agents/:appId/:instanceId page with process info, 3x2 charts grid (CPU, memory, throughput, errors, threads, GC), application log viewer with level filtering, and instance-scoped timeline - AgentHealth now uses slide-in DetailPanel for quick instance preview - Stat strip enhanced: colored StatusDot breakdowns, route ratio with state-colored values, Groups renamed to Applications - Unified page structure: stat strip → scope trail with inline badges (removed duplicate section headers from both pages) - StatCard value/detail props now accept ReactNode - Log and timeline displayed side by side Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { Metrics } from './pages/Metrics/Metrics'
|
|||||||
import { RouteDetail } from './pages/RouteDetail/RouteDetail'
|
import { RouteDetail } from './pages/RouteDetail/RouteDetail'
|
||||||
import { ExchangeDetail } from './pages/ExchangeDetail/ExchangeDetail'
|
import { ExchangeDetail } from './pages/ExchangeDetail/ExchangeDetail'
|
||||||
import { AgentHealth } from './pages/AgentHealth/AgentHealth'
|
import { AgentHealth } from './pages/AgentHealth/AgentHealth'
|
||||||
|
import { AgentInstance } from './pages/AgentInstance/AgentInstance'
|
||||||
import { Inventory } from './pages/Inventory/Inventory'
|
import { Inventory } from './pages/Inventory/Inventory'
|
||||||
import { AuditLog } from './pages/Admin/AuditLog/AuditLog'
|
import { AuditLog } from './pages/Admin/AuditLog/AuditLog'
|
||||||
import { OidcConfig } from './pages/Admin/OidcConfig/OidcConfig'
|
import { OidcConfig } from './pages/Admin/OidcConfig/OidcConfig'
|
||||||
@@ -85,6 +86,7 @@ export default function App() {
|
|||||||
<Route path="/metrics" element={<Metrics />} />
|
<Route path="/metrics" element={<Metrics />} />
|
||||||
<Route path="/routes/:id" element={<RouteDetail />} />
|
<Route path="/routes/:id" element={<RouteDetail />} />
|
||||||
<Route path="/exchanges/:id" element={<ExchangeDetail />} />
|
<Route path="/exchanges/:id" element={<ExchangeDetail />} />
|
||||||
|
<Route path="/agents/:appId/:instanceId" element={<AgentInstance />} />
|
||||||
<Route path="/agents/*" element={<AgentHealth />} />
|
<Route path="/agents/*" element={<AgentHealth />} />
|
||||||
<Route path="/admin" element={<Navigate to="/admin/rbac" replace />} />
|
<Route path="/admin" element={<Navigate to="/admin/rbac" replace />} />
|
||||||
<Route path="/admin/audit" element={<AuditLog />} />
|
<Route path="/admin/audit" element={<AuditLog />} />
|
||||||
|
|||||||
@@ -316,20 +316,15 @@ export function AgentHealth() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scope breadcrumb trail */}
|
{/* Scope trail + badges */}
|
||||||
{scope.level !== 'all' && (
|
|
||||||
<div className={styles.scopeTrail}>
|
<div className={styles.scopeTrail}>
|
||||||
|
{scope.level !== 'all' && (
|
||||||
|
<>
|
||||||
<Link to="/agents" className={styles.scopeLink}>All Agents</Link>
|
<Link to="/agents" className={styles.scopeLink}>All Agents</Link>
|
||||||
<span className={styles.scopeSep}>▸</span>
|
<span className={styles.scopeSep}>▸</span>
|
||||||
<span className={styles.scopeCurrent}>{scope.appId}</span>
|
<span className={styles.scopeCurrent}>{scope.appId}</span>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Section header */}
|
|
||||||
<div className={styles.sectionHeaderRow}>
|
|
||||||
<span className={styles.sectionTitle}>
|
|
||||||
{scope.level === 'all' ? 'Agents' : scope.appId}
|
|
||||||
</span>
|
|
||||||
<Badge
|
<Badge
|
||||||
label={`${liveCount}/${totalInstances} live`}
|
label={`${liveCount}/${totalInstances} live`}
|
||||||
color={deadCount > 0 ? 'error' : staleCount > 0 ? 'warning' : 'success'}
|
color={deadCount > 0 ? 'error' : staleCount > 0 ? 'warning' : 'success'}
|
||||||
|
|||||||
229
src/pages/AgentInstance/AgentInstance.module.css
Normal file
229
src/pages/AgentInstance/AgentInstance.module.css
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px 24px 40px;
|
||||||
|
min-width: 0;
|
||||||
|
background: var(--bg-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notFound {
|
||||||
|
padding: 60px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stat strip — 5 columns matching /agents */
|
||||||
|
.statStrip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scope trail — matches /agents */
|
||||||
|
.scopeTrail {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scopeLink {
|
||||||
|
color: var(--amber);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scopeLink:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scopeSep {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scopeCurrent {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section header — matches /agents */
|
||||||
|
.sectionHeaderRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Charts 3x2 grid */
|
||||||
|
.chartsGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartCard {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
padding: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartTitle {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartMeta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Process info card */
|
||||||
|
.processCard {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.processGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto 1fr;
|
||||||
|
gap: 6px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.processLabel {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fdRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Log + Timeline side by side */
|
||||||
|
.bottomRow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Log viewer */
|
||||||
|
.logCard {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logEntries {
|
||||||
|
max-height: 360px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logEntry {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 5px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logEntry:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logEntry:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logTime {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logLogger {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-faint);
|
||||||
|
max-width: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logMsg {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logEmpty {
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Timeline card */
|
||||||
|
.timelineCard {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timelineHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
326
src/pages/AgentInstance/AgentInstance.tsx
Normal file
326
src/pages/AgentInstance/AgentInstance.tsx
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
// 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 { ProgressBar } from '../../design-system/primitives/ProgressBar/ProgressBar'
|
||||||
|
import { SectionHeader } from '../../design-system/primitives/SectionHeader/SectionHeader'
|
||||||
|
import { Card } from '../../design-system/primitives/Card/Card'
|
||||||
|
import { CodeBlock } from '../../design-system/primitives/CodeBlock/CodeBlock'
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
const now = Date.now()
|
||||||
|
const MIN = 60_000
|
||||||
|
return [
|
||||||
|
{ ts: new Date(now - 1 * MIN).toISOString(), level: 'INFO', logger: 'o.a.c.impl.DefaultCamelContext', msg: `Route order-validation started and consuming from: direct:validate` },
|
||||||
|
{ ts: new Date(now - 2 * MIN).toISOString(), level: 'INFO', logger: 'o.a.c.impl.DefaultCamelContext', msg: `Total 3 routes, of which 3 are started` },
|
||||||
|
{ ts: new Date(now - 5 * MIN).toISOString(), level: 'WARN', logger: 'o.a.c.processor.errorhandler', msg: `Failed delivery for exchangeId: ID-${agentName}-1710847200000-0-1. Exhausted after 3 attempts.` },
|
||||||
|
{ ts: new Date(now - 8 * MIN).toISOString(), level: 'INFO', logger: 'o.a.c.health.HealthCheckHelper', msg: `Health check [routes] is UP` },
|
||||||
|
{ ts: new Date(now - 12 * MIN).toISOString(), level: 'INFO', logger: 'o.a.c.health.HealthCheckHelper', msg: `Health check [consumers] is UP` },
|
||||||
|
{ ts: new Date(now - 15 * MIN).toISOString(), level: 'DEBUG', logger: 'o.a.c.component.kafka', msg: `KafkaConsumer[order-events] poll returned 42 records in 18ms` },
|
||||||
|
{ ts: new Date(now - 18 * MIN).toISOString(), level: 'INFO', logger: 'o.a.c.impl.engine.InternalRouteStartup', msg: `Route order-enrichment started and consuming from: kafka:order-events` },
|
||||||
|
{ ts: new Date(now - 25 * MIN).toISOString(), level: 'WARN', logger: 'o.a.c.component.http', msg: `HTTP endpoint https://payment-api.internal/verify returned 503 — will retry` },
|
||||||
|
{ ts: new Date(now - 30 * MIN).toISOString(), level: 'INFO', logger: 'o.a.c.impl.DefaultCamelContext', msg: `Apache Camel ${agentName} (CamelContext) is starting` },
|
||||||
|
{ ts: new Date(now - 32 * MIN).toISOString(), level: 'INFO', logger: 'org.springframework.boot', msg: `Started ${agentName} in 4.231 seconds (process running for 4.892)` },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLogTime(iso: string): string {
|
||||||
|
return new Date(iso).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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.toUpperCase())
|
||||||
|
|
||||||
|
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>
|
||||||
|
<div className={styles.logEntries}>
|
||||||
|
{filteredLogs.map((entry, i) => (
|
||||||
|
<div key={i} className={styles.logEntry}>
|
||||||
|
<MonoText size="xs" className={styles.logTime}>{formatLogTime(entry.ts)}</MonoText>
|
||||||
|
<Badge
|
||||||
|
label={entry.level}
|
||||||
|
color={entry.level === 'WARN' ? 'warning' : entry.level === 'ERROR' ? 'error' : entry.level === 'DEBUG' ? 'auto' : 'success'}
|
||||||
|
/>
|
||||||
|
<MonoText size="xs" className={styles.logLogger}>{entry.logger}</MonoText>
|
||||||
|
<span className={styles.logMsg}>{entry.msg}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{filteredLogs.length === 0 && (
|
||||||
|
<div className={styles.logEmpty}>No log entries match the selected filter.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user