feat: agent row click navigates to detail page instead of slide-in
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 55s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 40s

Replace DetailPanel overlay with direct navigation to
/runtime/:appId/:instanceId on row click. Removes the slide-in panel,
AgentOverviewContent, and AgentPerformanceContent helper components.
The full AgentInstance page already provides all the same data plus
more (charts, routes, logs).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-30 16:28:12 +02:00
parent d8a21f0724
commit 77e87504d6

View File

@@ -2,15 +2,14 @@ import { useState, useMemo, useCallback } from 'react';
import { useParams, useNavigate } from 'react-router'; import { useParams, useNavigate } from 'react-router';
import { ExternalLink, RefreshCw, Pencil } from 'lucide-react'; import { ExternalLink, RefreshCw, Pencil } from 'lucide-react';
import { import {
StatCard, StatusDot, Badge, MonoText, ProgressBar, StatCard, StatusDot, Badge, MonoText,
GroupCard, DataTable, LineChart, EventFeed, DetailPanel, GroupCard, DataTable, EventFeed,
LogViewer, ButtonGroup, SectionHeader, Toggle, useToast, LogViewer, ButtonGroup, SectionHeader, Toggle, useToast,
} from '@cameleer/design-system'; } from '@cameleer/design-system';
import type { Column, FeedEvent, LogEntry, ButtonGroupItem } from '@cameleer/design-system'; import type { Column, FeedEvent, LogEntry, ButtonGroupItem } from '@cameleer/design-system';
import styles from './AgentHealth.module.css'; import styles from './AgentHealth.module.css';
import { useAgents, useAgentEvents } from '../../api/queries/agents'; import { useAgents, useAgentEvents } from '../../api/queries/agents';
import { useApplicationLogs } from '../../api/queries/logs'; import { useApplicationLogs } from '../../api/queries/logs';
import { useAgentMetrics } from '../../api/queries/agent-metrics';
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands'; import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands';
import type { AgentInstance } from '../../api/types'; import type { AgentInstance } from '../../api/types';
@@ -96,132 +95,6 @@ function appHealth(group: AppGroup): 'success' | 'warning' | 'error' {
// ── Detail sub-components ──────────────────────────────────────────────────── // ── Detail sub-components ────────────────────────────────────────────────────
function AgentOverviewContent({ agent }: { agent: AgentInstance }) {
const { data: memMetrics } = useAgentMetrics(
agent.id,
['jvm.memory.heap.used', 'jvm.memory.heap.max'],
1,
);
const { data: cpuMetrics } = useAgentMetrics(agent.id, ['jvm.cpu.process'], 1);
const cpuValue = cpuMetrics?.metrics?.['jvm.cpu.process']?.[0]?.value;
const heapUsed = memMetrics?.metrics?.['jvm.memory.heap.used']?.[0]?.value;
const heapMax = memMetrics?.metrics?.['jvm.memory.heap.max']?.[0]?.value;
const heapPercent =
heapUsed != null && heapMax != null && heapMax > 0
? Math.round((heapUsed / heapMax) * 100)
: undefined;
const cpuPercent = cpuValue != null ? Math.round(cpuValue * 100) : undefined;
const ns = normalizeStatus(agent.status);
return (
<div className={styles.detailContent}>
<div className={styles.detailRow}>
<span className={styles.detailLabel}>Status</span>
<Badge label={agent.status} color={statusColor(ns)} variant="filled" />
</div>
<div className={styles.detailRow}>
<span className={styles.detailLabel}>Application</span>
<MonoText size="xs">{agent.application}</MonoText>
</div>
<div className={styles.detailRow}>
<span className={styles.detailLabel}>Uptime</span>
<MonoText size="xs">{formatUptime(agent.uptimeSeconds)}</MonoText>
</div>
<div className={styles.detailRow}>
<span className={styles.detailLabel}>Last Seen</span>
<MonoText size="xs">{timeAgo(agent.lastHeartbeat)}</MonoText>
</div>
<div className={styles.detailRow}>
<span className={styles.detailLabel}>Throughput</span>
<MonoText size="xs">{agent.tps != null ? `${agent.tps.toFixed(1)}/s` : '\u2014'}</MonoText>
</div>
<div className={styles.detailRow}>
<span className={styles.detailLabel}>Errors</span>
<MonoText size="xs" className={agent.errorRate ? styles.instanceError : undefined}>
{formatErrorRate(agent.errorRate)}
</MonoText>
</div>
<div className={styles.detailRow}>
<span className={styles.detailLabel}>Routes</span>
<span>{agent.activeRoutes ?? 0}/{agent.totalRoutes ?? 0} active</span>
</div>
<div className={styles.detailRow}>
<span className={styles.detailLabel}>Heap Memory</span>
{heapPercent != null ? (
<div className={styles.detailProgress}>
<ProgressBar
value={heapPercent}
variant={heapPercent > 85 ? 'error' : heapPercent > 70 ? 'warning' : 'success'}
size="sm"
/>
<MonoText size="xs">{heapPercent}%</MonoText>
</div>
) : (
<MonoText size="xs">N/A</MonoText>
)}
</div>
<div className={styles.detailRow}>
<span className={styles.detailLabel}>CPU</span>
{cpuPercent != null ? (
<div className={styles.detailProgress}>
<ProgressBar
value={cpuPercent}
variant={cpuPercent > 80 ? 'error' : cpuPercent > 60 ? 'warning' : 'success'}
size="sm"
/>
<MonoText size="xs">{cpuPercent}%</MonoText>
</div>
) : (
<MonoText size="xs">N/A</MonoText>
)}
</div>
</div>
);
}
function AgentPerformanceContent({ agent }: { agent: AgentInstance }) {
const { data: tpsMetrics } = useAgentMetrics(agent.id, ['cameleer.tps'], 60);
const { data: errMetrics } = useAgentMetrics(agent.id, ['cameleer.error.rate'], 60);
const tpsSeries = useMemo(() => {
const raw = tpsMetrics?.metrics?.['cameleer.tps'] ?? [];
return [{ label: 'TPS', data: raw.map((p) => ({ x: new Date(p.time), y: p.value })) }];
}, [tpsMetrics]);
const errSeries = useMemo(() => {
const raw = errMetrics?.metrics?.['cameleer.error.rate'] ?? [];
return [{
label: 'Error Rate',
data: raw.map((p) => ({ x: new Date(p.time), y: p.value * 100 })),
color: 'var(--error)',
}];
}, [errMetrics]);
return (
<div className={styles.detailContent}>
<div className={styles.chartPanel}>
<div className={styles.chartTitle}>Throughput (msg/s)</div>
{tpsSeries[0].data.length > 0 ? (
<LineChart series={tpsSeries} height={160} yLabel="msg/s" />
) : (
<div className={styles.emptyChart}>No data available</div>
)}
</div>
<div className={styles.chartPanel}>
<div className={styles.chartTitle}>Error Rate (%)</div>
{errSeries[0].data.length > 0 ? (
<LineChart series={errSeries} height={160} yLabel="%" />
) : (
<div className={styles.emptyChart}>No data available</div>
)}
</div>
</div>
);
}
const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [ const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [
{ value: 'error', label: 'Error', color: 'var(--error)' }, { value: 'error', label: 'Error', color: 'var(--error)' },
{ value: 'warn', label: 'Warn', color: 'var(--warning)' }, { value: 'warn', label: 'Warn', color: 'var(--warning)' },
@@ -301,9 +174,6 @@ export default function AgentHealth() {
.filter((l) => logLevels.size === 0 || logLevels.has(l.level)) .filter((l) => logLevels.size === 0 || logLevels.has(l.level))
.filter((l) => !logSearchLower || l.message.toLowerCase().includes(logSearchLower)); .filter((l) => !logSearchLower || l.message.toLowerCase().includes(logSearchLower));
const [selectedInstance, setSelectedInstance] = useState<AgentInstance | null>(null);
const [panelOpen, setPanelOpen] = useState(false);
const agentList = agents ?? []; const agentList = agents ?? [];
const groups = useMemo(() => groupByApp(agentList), [agentList]); const groups = useMemo(() => groupByApp(agentList), [agentList]);
@@ -428,26 +298,9 @@ export default function AgentHealth() {
); );
function handleInstanceClick(inst: AgentInstance) { function handleInstanceClick(inst: AgentInstance) {
setSelectedInstance(inst); navigate(`/runtime/${inst.application}/${inst.id}`);
setPanelOpen(true);
} }
// Detail panel tabs
const detailTabs = selectedInstance
? [
{
label: 'Overview',
value: 'overview',
content: <AgentOverviewContent agent={selectedInstance} />,
},
{
label: 'Performance',
value: 'performance',
content: <AgentPerformanceContent agent={selectedInstance} />,
},
]
: [];
const isFullWidth = !!appId; const isFullWidth = !!appId;
return ( return (
@@ -677,7 +530,6 @@ export default function AgentHealth() {
columns={instanceColumns} columns={instanceColumns}
data={group.instances} data={group.instances}
onRowClick={handleInstanceClick} onRowClick={handleInstanceClick}
selectedId={panelOpen ? selectedInstance?.id : undefined}
pageSize={50} pageSize={50}
flush flush
/> />
@@ -758,15 +610,6 @@ export default function AgentHealth() {
</div> </div>
</div> </div>
{/* Detail panel — auto-portals to AppShell level via design system */}
{selectedInstance && (
<DetailPanel
open={panelOpen}
onClose={() => { setPanelOpen(false); setSelectedInstance(null); }}
title={selectedInstance.name ?? selectedInstance.id}
tabs={detailTabs}
/>
)}
</div> </div>
); );
} }