feat: replace UI with design system example pages wired to real API
Migrate all page components from the @cameleer/design-system v0.0.3 example UI, replacing mock data with real backend API hooks. This brings richer visuals (KpiStrip, GroupCard, RouteFlow, ProcessorTimeline, DateRangePicker, expandable rows) while preserving all existing API integration, auth, and routing infrastructure. Pages migrated: Dashboard, RoutesMetrics, RouteDetail, ExchangeDetail, AgentHealth, AgentInstance, OidcConfig, AuditLog, RBAC (Users/Groups/Roles). Also enhanced LayoutShell CommandPalette with real search data from catalog. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,12 @@
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px 24px 40px;
|
||||
min-width: 0;
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
/* Stat strip — 5 columns matching /agents */
|
||||
.statStrip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
@@ -5,18 +14,67 @@
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.agentHeader {
|
||||
/* Scope trail — matches /agents */
|
||||
.scopeTrail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 16px 0;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.agentHeader h2 {
|
||||
font-size: 18px;
|
||||
.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);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.capTags {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Route badges */
|
||||
.routeBadges {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
@@ -24,9 +82,10 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Charts 3x2 grid */
|
||||
.chartsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -53,14 +112,46 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 12px;
|
||||
.chartMeta {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.eventCard {
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* Empty state (shared) */
|
||||
.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);
|
||||
@@ -69,107 +160,12 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 420px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.eventCardHeader {
|
||||
.timelineHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.infoCard {
|
||||
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;
|
||||
}
|
||||
|
||||
.scopeTrail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.scopeLink {
|
||||
color: var(--text-accent, var(--text-primary));
|
||||
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;
|
||||
}
|
||||
|
||||
.paneTitle {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chartMeta {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.bottomSection {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.eventCount {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.emptyEvents {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useParams, Link } from 'react-router';
|
||||
import {
|
||||
StatCard, StatusDot, Badge, Card,
|
||||
LineChart, AreaChart, BarChart, EventFeed, Breadcrumb, Spinner, EmptyState,
|
||||
StatCard, StatusDot, Badge, LineChart, AreaChart, BarChart,
|
||||
EventFeed, Spinner, EmptyState, SectionHeader, MonoText,
|
||||
LogViewer, Tabs, useGlobalFilters,
|
||||
} from '@cameleer/design-system';
|
||||
import type { FeedEvent, LogEntry } 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';
|
||||
|
||||
const LOG_TABS = [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Warnings', value: 'warn' },
|
||||
{ label: 'Errors', value: 'error' },
|
||||
];
|
||||
|
||||
export default function AgentInstance() {
|
||||
const { appId, instanceId } = useParams();
|
||||
const { timeRange } = useGlobalFilters();
|
||||
const [logFilter, setLogFilter] = useState('all');
|
||||
const timeFrom = timeRange.start.toISOString();
|
||||
const timeTo = timeRange.end.toISOString();
|
||||
|
||||
@@ -20,8 +28,8 @@ export default function AgentInstance() {
|
||||
const { data: events } = useAgentEvents(appId, instanceId);
|
||||
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, undefined, appId);
|
||||
|
||||
const agent = useMemo(() =>
|
||||
(agents || []).find((a: any) => a.id === instanceId) as any,
|
||||
const agent = useMemo(
|
||||
() => (agents || []).find((a: any) => a.id === instanceId) as any,
|
||||
[agents, instanceId],
|
||||
);
|
||||
|
||||
@@ -43,26 +51,34 @@ export default function AgentInstance() {
|
||||
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,
|
||||
})),
|
||||
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(() =>
|
||||
(events || []).filter((e: any) => !instanceId || e.agentId === 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 ? ' — ' + e.detail : ''}`,
|
||||
timestamp: new Date(e.timestamp),
|
||||
})),
|
||||
const feedEvents = useMemo<FeedEvent[]>(
|
||||
() =>
|
||||
(events || [])
|
||||
.filter((e: any) => !instanceId || e.agentId === 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),
|
||||
})),
|
||||
[events, instanceId],
|
||||
);
|
||||
|
||||
@@ -88,194 +104,305 @@ export default function AgentInstance() {
|
||||
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 })) }];
|
||||
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,
|
||||
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,
|
||||
const errorSeries = useMemo(
|
||||
() =>
|
||||
chartData.length
|
||||
? [{ label: 'Errors', data: chartData.map((d: any, i: number) => ({ x: i, y: d.errors })) }]
|
||||
: null,
|
||||
[chartData],
|
||||
);
|
||||
|
||||
// Placeholder log entries (backend does not stream logs yet)
|
||||
const logEntries = useMemo<LogEntry[]>(() => [], []);
|
||||
const filteredLogs =
|
||||
logFilter === 'all' ? logEntries : logEntries.filter((l) => l.level === logFilter);
|
||||
|
||||
if (isLoading) return <Spinner size="lg" />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Breadcrumb items={[
|
||||
{ label: 'Agents', href: '/agents' },
|
||||
{ label: appId || '', href: `/agents/${appId}` },
|
||||
{ label: agent?.name || instanceId || '' },
|
||||
]} />
|
||||
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 (
|
||||
<div className={styles.content}>
|
||||
{/* Stat strip — 5 columns */}
|
||||
<div className={styles.statStrip}>
|
||||
<StatCard
|
||||
label="CPU"
|
||||
value={cpuDisplay != null ? `${cpuDisplay}%` : '\u2014'}
|
||||
accent={
|
||||
cpuDisplay != null
|
||||
? Number(cpuDisplay) > 85
|
||||
? 'error'
|
||||
: Number(cpuDisplay) > 70
|
||||
? 'warning'
|
||||
: 'success'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<StatCard
|
||||
label="Memory"
|
||||
value={memPct != null ? `${memPct.toFixed(0)}%` : '\u2014'}
|
||||
accent={
|
||||
memPct != null
|
||||
? memPct > 85
|
||||
? 'error'
|
||||
: memPct > 70
|
||||
? 'warning'
|
||||
: 'success'
|
||||
: undefined
|
||||
}
|
||||
detail={
|
||||
heapUsedMB != null && heapMaxMB != null
|
||||
? `${heapUsedMB} MB / ${heapMaxMB} MB`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<StatCard
|
||||
label="Throughput"
|
||||
value={agent?.tps != null ? `${agent.tps.toFixed(1)}/s` : '\u2014'}
|
||||
accent="amber"
|
||||
detail="msg/s"
|
||||
/>
|
||||
<StatCard
|
||||
label="Errors"
|
||||
value={agent?.errorRate != null ? `${(agent.errorRate * 100).toFixed(1)}%` : '\u2014'}
|
||||
accent={agent?.errorRate > 0 ? 'error' : 'success'}
|
||||
/>
|
||||
<StatCard
|
||||
label="Uptime"
|
||||
value={formatUptime(agent?.uptimeSeconds)}
|
||||
accent="running"
|
||||
detail={
|
||||
agent?.registeredAt
|
||||
? `since ${new Date(agent.registeredAt).toLocaleDateString()}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Scope trail + badges */}
|
||||
{agent && (
|
||||
<>
|
||||
<div className={styles.agentHeader}>
|
||||
<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="CPU" value={cpuPct != null ? `${(cpuPct * 100).toFixed(0)}%` : '—'} />
|
||||
<StatCard
|
||||
label="Memory"
|
||||
value={memPct != null ? `${memPct.toFixed(0)}%` : '—'}
|
||||
detail={heapUsed != null && heapMax != null ? `${(heapUsed / (1024 * 1024)).toFixed(0)} MB / ${(heapMax / (1024 * 1024)).toFixed(0)} MB` : undefined}
|
||||
/>
|
||||
<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)}
|
||||
detail={agent?.registeredAt ? `since ${new Date(agent.registeredAt).toLocaleDateString()}` : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.scopeTrail}>
|
||||
<a href="/agents" className={styles.scopeLink}>All Agents</a>
|
||||
<Link to="/agents" className={styles.scopeLink}>
|
||||
All Agents
|
||||
</Link>
|
||||
<span className={styles.scopeSep}>▸</span>
|
||||
<a href={`/agents/${appId}`} className={styles.scopeLink}>{appId}</a>
|
||||
<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={agent.status === 'LIVE' ? 'success' : agent.status === 'STALE' ? 'warning' : 'error'}
|
||||
/>
|
||||
{agent.version && <Badge label={agent.version} variant="outlined" />}
|
||||
<StatusDot variant={statusVariant} />
|
||||
<Badge label={agent.status} color={statusColor} />
|
||||
{agent.version && <Badge label={agent.version} variant="outlined" color="auto" />}
|
||||
<Badge
|
||||
label={`${agent.activeRoutes ?? (agent.routeIds?.length ?? 0)}/${agent.totalRoutes ?? (agent.routeIds?.length ?? 0)} routes`}
|
||||
color={(agent.activeRoutes ?? 0) < (agent.totalRoutes ?? 0) ? 'warning' : 'success'}
|
||||
color={
|
||||
(agent.activeRoutes ?? 0) < (agent.totalRoutes ?? 0) ? 'warning' : 'success'
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
{/* Process info card */}
|
||||
<div className={styles.processCard}>
|
||||
<SectionHeader>Process Information</SectionHeader>
|
||||
<div className={styles.processGrid}>
|
||||
{agent.capabilities?.jvmVersion && (
|
||||
<>
|
||||
<span className={styles.processLabel}>JVM</span>
|
||||
<MonoText size="xs">{agent.capabilities.jvmVersion}</MonoText>
|
||||
</>
|
||||
)}
|
||||
{agent?.capabilities?.camelVersion && (
|
||||
<div>
|
||||
<span className={styles.infoLabel}>Camel</span>
|
||||
<span>{agent.capabilities.camelVersion}</span>
|
||||
</div>
|
||||
{agent.capabilities?.camelVersion && (
|
||||
<>
|
||||
<span className={styles.processLabel}>Camel</span>
|
||||
<MonoText size="xs">{agent.capabilities.camelVersion}</MonoText>
|
||||
</>
|
||||
)}
|
||||
{agent?.capabilities?.springBootVersion && (
|
||||
<div>
|
||||
<span className={styles.infoLabel}>Spring Boot</span>
|
||||
<span>{agent.capabilities.springBootVersion}</span>
|
||||
</div>
|
||||
{agent.capabilities?.springBootVersion && (
|
||||
<>
|
||||
<span className={styles.processLabel}>Spring Boot</span>
|
||||
<MonoText size="xs">{agent.capabilities.springBootVersion}</MonoText>
|
||||
</>
|
||||
)}
|
||||
<span className={styles.processLabel}>Started</span>
|
||||
<MonoText size="xs">
|
||||
{agent.registeredAt ? new Date(agent.registeredAt).toLocaleString() : '\u2014'}
|
||||
</MonoText>
|
||||
{agent.capabilities && (
|
||||
<>
|
||||
<span className={styles.processLabel}>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>
|
||||
<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) => (
|
||||
<Badge key={r} label={r} color="auto" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Routes */}
|
||||
{(agent.routeIds?.length ?? 0) > 0 && (
|
||||
<>
|
||||
<SectionHeader>Routes</SectionHeader>
|
||||
<div className={styles.routeBadges}>
|
||||
{(agent.routeIds || []).map((r: string) => (
|
||||
<Badge key={r} label={r} color="auto" />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Charts grid — 3x2 */}
|
||||
<div className={styles.chartsGrid}>
|
||||
<div className={styles.chartCard}>
|
||||
<div className={styles.chartHeader}>
|
||||
<div className={styles.chartTitle}>CPU Usage</div>
|
||||
<div className={styles.chartMeta}>{cpuPct != null ? `${(cpuPct * 100).toFixed(0)}% current` : ''}</div>
|
||||
<span className={styles.chartTitle}>CPU Usage</span>
|
||||
<span className={styles.chartMeta}>
|
||||
{cpuDisplay != null ? `${cpuDisplay}% current` : ''}
|
||||
</span>
|
||||
</div>
|
||||
{cpuSeries
|
||||
? <AreaChart series={cpuSeries} yLabel="%" height={200} />
|
||||
: <EmptyState title="No data" description="No CPU metrics available" />}
|
||||
{cpuSeries ? (
|
||||
<AreaChart
|
||||
series={cpuSeries}
|
||||
height={160}
|
||||
yLabel="%"
|
||||
threshold={{ value: 85, label: 'Alert' }}
|
||||
/>
|
||||
) : (
|
||||
<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 className={styles.chartMeta}>{heapUsed != null && heapMax != null ? `${(heapUsed / (1024 * 1024)).toFixed(0)} MB / ${(heapMax / (1024 * 1024)).toFixed(0)} MB` : ''}</div>
|
||||
<span className={styles.chartTitle}>Memory (Heap)</span>
|
||||
<span className={styles.chartMeta}>
|
||||
{heapUsedMB != null && heapMaxMB != null
|
||||
? `${heapUsedMB} MB / ${heapMaxMB} MB`
|
||||
: ''}
|
||||
</span>
|
||||
</div>
|
||||
{heapSeries
|
||||
? <AreaChart series={heapSeries} yLabel="MB" height={200} />
|
||||
: <EmptyState title="No data" description="No heap metrics available" />}
|
||||
{heapSeries ? (
|
||||
<AreaChart series={heapSeries} height={160} yLabel="MB" />
|
||||
) : (
|
||||
<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 className={styles.chartMeta}>{agent?.tps != null ? `${agent.tps.toFixed(1)} msg/s` : ''}</div>
|
||||
<span className={styles.chartTitle}>Throughput</span>
|
||||
<span className={styles.chartMeta}>
|
||||
{agent?.tps != null ? `${agent.tps.toFixed(1)} msg/s` : ''}
|
||||
</span>
|
||||
</div>
|
||||
{throughputSeries
|
||||
? <AreaChart series={throughputSeries} yLabel="msg/s" height={200} />
|
||||
: <EmptyState title="No data" description="No throughput data in range" />}
|
||||
{throughputSeries ? (
|
||||
<LineChart series={throughputSeries} height={160} yLabel="msg/s" />
|
||||
) : (
|
||||
<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 className={styles.chartMeta}>{agent?.errorRate != null ? `${(agent.errorRate * 100).toFixed(1)}%` : ''}</div>
|
||||
<span className={styles.chartTitle}>Error Rate</span>
|
||||
<span className={styles.chartMeta}>
|
||||
{agent?.errorRate != null ? `${(agent.errorRate * 100).toFixed(1)}%` : ''}
|
||||
</span>
|
||||
</div>
|
||||
{errorSeries
|
||||
? <LineChart series={errorSeries} yLabel="%" height={200} />
|
||||
: <EmptyState title="No data" description="No error data in range" />}
|
||||
{errorSeries ? (
|
||||
<LineChart series={errorSeries} height={160} yLabel="err/h" />
|
||||
) : (
|
||||
<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>
|
||||
{threadSeries && <div className={styles.chartMeta}>{threadSeries[0].data[threadSeries[0].data.length - 1]?.y.toFixed(0)} active</div>}
|
||||
<span className={styles.chartTitle}>Thread Count</span>
|
||||
<span className={styles.chartMeta}>
|
||||
{threadSeries
|
||||
? `${threadSeries[0].data[threadSeries[0].data.length - 1]?.y.toFixed(0)} active`
|
||||
: ''}
|
||||
</span>
|
||||
</div>
|
||||
{threadSeries
|
||||
? <LineChart series={threadSeries} yLabel="threads" height={200} />
|
||||
: <EmptyState title="No data" description="No thread metrics available" />}
|
||||
{threadSeries ? (
|
||||
<LineChart series={threadSeries} height={160} yLabel="threads" />
|
||||
) : (
|
||||
<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>
|
||||
<span className={styles.chartTitle}>GC Pauses</span>
|
||||
<span className={styles.chartMeta} />
|
||||
</div>
|
||||
{gcSeries
|
||||
? <BarChart series={gcSeries} yLabel="ms" height={200} />
|
||||
: <EmptyState title="No data" description="No GC metrics available" />}
|
||||
{gcSeries ? (
|
||||
<BarChart series={gcSeries} height={160} yLabel="ms" />
|
||||
) : (
|
||||
<EmptyState title="No data" description="No GC metrics available" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.bottomSection}>
|
||||
<EmptyState title="Application Log" description="Application log streaming is not yet available" />
|
||||
|
||||
<div className={styles.eventCard}>
|
||||
<div className={styles.eventCardHeader}>
|
||||
<span>Timeline</span>
|
||||
<span className={styles.eventCount}>{feedEvents.length} events</span>
|
||||
{/* Log + Timeline side by side */}
|
||||
<div className={styles.bottomRow}>
|
||||
<div className={styles.logCard}>
|
||||
<div className={styles.logHeader}>
|
||||
<SectionHeader>Application Log</SectionHeader>
|
||||
<Tabs tabs={LOG_TABS} active={logFilter} onChange={setLogFilter} />
|
||||
</div>
|
||||
{feedEvents.length > 0
|
||||
? <EventFeed events={feedEvents} maxItems={50} />
|
||||
: <div className={styles.emptyEvents}>No events in the selected time range.</div>}
|
||||
{filteredLogs.length > 0 ? (
|
||||
<LogViewer entries={filteredLogs} maxHeight={360} />
|
||||
) : (
|
||||
<div className={styles.logEmpty}>
|
||||
Application log streaming is not yet available.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.timelineCard}>
|
||||
<div className={styles.timelineHeader}>
|
||||
<span className={styles.chartTitle}>Timeline</span>
|
||||
<span className={styles.chartMeta}>{feedEvents.length} events</span>
|
||||
</div>
|
||||
{feedEvents.length > 0 ? (
|
||||
<EventFeed events={feedEvents} maxItems={50} />
|
||||
) : (
|
||||
<div className={styles.logEmpty}>No events in the selected time range.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatUptime(seconds?: number): string {
|
||||
if (!seconds) return '—';
|
||||
if (!seconds) return '\u2014';
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const mins = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
Reference in New Issue
Block a user