feat: align Agent Instance with mock — JVM charts, process info, stat cards, log placeholder
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
.statStrip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
.chartsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -84,9 +84,35 @@
|
||||
}
|
||||
|
||||
.infoCard {
|
||||
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;
|
||||
}
|
||||
|
||||
.infoGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.infoLabel {
|
||||
font-weight: 700;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.6px;
|
||||
color: var(--text-muted);
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.capTags {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.paneTitle {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import {
|
||||
StatCard, StatusDot, Badge,
|
||||
LineChart, AreaChart, EventFeed, Breadcrumb, Spinner,
|
||||
CodeBlock,
|
||||
StatCard, StatusDot, Badge, Card,
|
||||
LineChart, AreaChart, BarChart, EventFeed, Breadcrumb, Spinner, EmptyState,
|
||||
} from '@cameleer/design-system';
|
||||
import styles from './AgentInstance.module.css';
|
||||
import { useAgents, useAgentEvents } from '../../api/queries/agents';
|
||||
import { useStatsTimeseries } from '../../api/queries/executions';
|
||||
import { useAgentMetrics } from '../../api/queries/agent-metrics';
|
||||
import { useGlobalFilters } from '@cameleer/design-system';
|
||||
|
||||
export default function AgentInstance() {
|
||||
@@ -21,10 +21,28 @@ export default function AgentInstance() {
|
||||
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, undefined, appId);
|
||||
|
||||
const agent = useMemo(() =>
|
||||
(agents || []).find((a: any) => a.id === instanceId),
|
||||
(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' }),
|
||||
@@ -48,6 +66,41 @@ export default function AgentInstance() {
|
||||
[events, instanceId],
|
||||
);
|
||||
|
||||
// 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, i: number) => ({ x: String(i), 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],
|
||||
);
|
||||
|
||||
if (isLoading) return <Spinner size="lg" />;
|
||||
|
||||
return (
|
||||
@@ -64,15 +117,55 @@ export default function AgentInstance() {
|
||||
<StatusDot variant={agent.status === 'LIVE' ? 'live' : agent.status === 'STALE' ? 'stale' : 'dead'} />
|
||||
<h2>{agent.name}</h2>
|
||||
<Badge label={agent.status} color={agent.status === 'LIVE' ? 'success' : agent.status === 'STALE' ? 'warning' : 'error'} />
|
||||
{agent.version && <Badge label={agent.version} variant="outlined" />}
|
||||
</div>
|
||||
|
||||
<div className={styles.statStrip}>
|
||||
<StatCard label="TPS" value={agent.tps?.toFixed(1) ?? '0'} />
|
||||
<StatCard label="Error Rate" value={agent.errorRate ? `${(agent.errorRate * 100).toFixed(1)}%` : '0%'} accent={agent.errorRate > 0.05 ? 'error' : undefined} />
|
||||
<StatCard label="Active Routes" value={`${agent.activeRoutes ?? 0}/${agent.totalRoutes ?? 0}`} />
|
||||
<StatCard label="Uptime" value={formatUptime(agent.uptimeSeconds ?? 0)} />
|
||||
<StatCard label="CPU" value={cpuPct != null ? `${(cpuPct * 100).toFixed(0)}%` : '—'} />
|
||||
<StatCard label="Memory" value={memPct != null ? `${memPct.toFixed(0)}%` : '—'} />
|
||||
<StatCard label="Throughput" value={agent?.tps != null ? `${agent.tps.toFixed(1)}/s` : '—'} />
|
||||
<StatCard label="Errors" value={agent?.errorRate != null ? `${(agent.errorRate * 100).toFixed(1)}%` : '—'} accent={agent?.errorRate > 0 ? 'error' : undefined} />
|
||||
<StatCard label="Uptime" value={formatUptime(agent?.uptimeSeconds)} />
|
||||
</div>
|
||||
|
||||
<Card className={styles.infoCard}>
|
||||
<div className={styles.paneTitle}>Process Information</div>
|
||||
<div className={styles.infoGrid}>
|
||||
{agent?.capabilities?.jvmVersion && (
|
||||
<div>
|
||||
<span className={styles.infoLabel}>JVM</span>
|
||||
<span>{agent.capabilities.jvmVersion}</span>
|
||||
</div>
|
||||
)}
|
||||
{agent?.capabilities?.camelVersion && (
|
||||
<div>
|
||||
<span className={styles.infoLabel}>Camel</span>
|
||||
<span>{agent.capabilities.camelVersion}</span>
|
||||
</div>
|
||||
)}
|
||||
{agent?.capabilities?.springBootVersion && (
|
||||
<div>
|
||||
<span className={styles.infoLabel}>Spring Boot</span>
|
||||
<span>{agent.capabilities.springBootVersion}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className={styles.infoLabel}>Started</span>
|
||||
<span>{agent?.registeredAt ? new Date(agent.registeredAt).toLocaleString() : '—'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.infoLabel}>Capabilities</span>
|
||||
<span className={styles.capTags}>
|
||||
{Object.entries(agent?.capabilities || {})
|
||||
.filter(([, v]) => typeof v === 'boolean' && v)
|
||||
.map(([k]) => (
|
||||
<Badge key={k} label={k} variant="outlined" />
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className={styles.sectionTitle}>Routes</div>
|
||||
<div className={styles.routeBadges}>
|
||||
{(agent.routeIds || []).map((r: string) => (
|
||||
@@ -82,21 +175,45 @@ export default function AgentInstance() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{chartData.length > 0 && (
|
||||
<>
|
||||
<div className={styles.sectionTitle}>Performance</div>
|
||||
<div className={styles.chartsGrid}>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={styles.chartHeader}><div className={styles.chartTitle}>Throughput</div></div>
|
||||
<AreaChart series={[{ label: 'Throughput', data: chartData.map((d: any, i: number) => ({ x: i, y: d.throughput })) }]} height={200} />
|
||||
</div>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={styles.chartHeader}><div className={styles.chartTitle}>Latency</div></div>
|
||||
<LineChart series={[{ label: 'Latency', data: chartData.map((d: any, i: number) => ({ x: i, y: d.latency })) }]} height={200} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className={styles.sectionTitle}>Performance</div>
|
||||
<div className={styles.chartsGrid}>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={styles.chartHeader}><div className={styles.chartTitle}>CPU Usage</div></div>
|
||||
{cpuSeries
|
||||
? <AreaChart series={cpuSeries} yLabel="%" height={200} />
|
||||
: <EmptyState title="No data" description="No CPU metrics available" />}
|
||||
</div>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={styles.chartHeader}><div className={styles.chartTitle}>Memory Heap</div></div>
|
||||
{heapSeries
|
||||
? <AreaChart series={heapSeries} yLabel="MB" height={200} />
|
||||
: <EmptyState title="No data" description="No heap metrics available" />}
|
||||
</div>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={styles.chartHeader}><div className={styles.chartTitle}>Throughput</div></div>
|
||||
{throughputSeries
|
||||
? <AreaChart series={throughputSeries} height={200} />
|
||||
: <EmptyState title="No data" description="No throughput data in range" />}
|
||||
</div>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={styles.chartHeader}><div className={styles.chartTitle}>Error Rate</div></div>
|
||||
{errorSeries
|
||||
? <LineChart series={errorSeries} height={200} />
|
||||
: <EmptyState title="No data" description="No error data in range" />}
|
||||
</div>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={styles.chartHeader}><div className={styles.chartTitle}>Thread Count</div></div>
|
||||
{threadSeries
|
||||
? <LineChart series={threadSeries} height={200} />
|
||||
: <EmptyState title="No data" description="No thread metrics available" />}
|
||||
</div>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={styles.chartHeader}><div className={styles.chartTitle}>GC Pauses</div></div>
|
||||
{gcSeries
|
||||
? <BarChart series={gcSeries} yLabel="ms" height={200} />
|
||||
: <EmptyState title="No data" description="No GC metrics available" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{feedEvents.length > 0 && (
|
||||
<div className={styles.eventCard}>
|
||||
@@ -105,28 +222,17 @@ export default function AgentInstance() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agent && (
|
||||
<>
|
||||
<div className={styles.sectionTitle}>Agent Info</div>
|
||||
<div className={styles.infoCard}>
|
||||
<CodeBlock content={JSON.stringify({
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
group: agent.group,
|
||||
registeredAt: agent.registeredAt,
|
||||
lastHeartbeat: agent.lastHeartbeat,
|
||||
routeIds: agent.routeIds,
|
||||
}, null, 2)} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<EmptyState title="Application Logs" description="Application log streaming is not yet available" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatUptime(seconds: number): string {
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
||||
return `${Math.floor(seconds / 86400)}d ${Math.floor((seconds % 86400) / 3600)}h`;
|
||||
function formatUptime(seconds?: number): string {
|
||||
if (!seconds) return '—';
|
||||
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`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user