feat: agent row click navigates to detail page instead of slide-in
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:
@@ -2,15 +2,14 @@ import { useState, useMemo, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router';
|
||||
import { ExternalLink, RefreshCw, Pencil } from 'lucide-react';
|
||||
import {
|
||||
StatCard, StatusDot, Badge, MonoText, ProgressBar,
|
||||
GroupCard, DataTable, LineChart, EventFeed, DetailPanel,
|
||||
StatCard, StatusDot, Badge, MonoText,
|
||||
GroupCard, DataTable, EventFeed,
|
||||
LogViewer, ButtonGroup, SectionHeader, Toggle, useToast,
|
||||
} from '@cameleer/design-system';
|
||||
import type { Column, FeedEvent, LogEntry, ButtonGroupItem } from '@cameleer/design-system';
|
||||
import styles from './AgentHealth.module.css';
|
||||
import { useAgents, useAgentEvents } from '../../api/queries/agents';
|
||||
import { useApplicationLogs } from '../../api/queries/logs';
|
||||
import { useAgentMetrics } from '../../api/queries/agent-metrics';
|
||||
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands';
|
||||
import type { AgentInstance } from '../../api/types';
|
||||
|
||||
@@ -96,132 +95,6 @@ function appHealth(group: AppGroup): 'success' | 'warning' | 'error' {
|
||||
|
||||
// ── 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[] = [
|
||||
{ value: 'error', label: 'Error', color: 'var(--error)' },
|
||||
{ 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) => !logSearchLower || l.message.toLowerCase().includes(logSearchLower));
|
||||
|
||||
const [selectedInstance, setSelectedInstance] = useState<AgentInstance | null>(null);
|
||||
const [panelOpen, setPanelOpen] = useState(false);
|
||||
|
||||
const agentList = agents ?? [];
|
||||
|
||||
const groups = useMemo(() => groupByApp(agentList), [agentList]);
|
||||
@@ -428,26 +298,9 @@ export default function AgentHealth() {
|
||||
);
|
||||
|
||||
function handleInstanceClick(inst: AgentInstance) {
|
||||
setSelectedInstance(inst);
|
||||
setPanelOpen(true);
|
||||
navigate(`/runtime/${inst.application}/${inst.id}`);
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
return (
|
||||
@@ -677,7 +530,6 @@ export default function AgentHealth() {
|
||||
columns={instanceColumns}
|
||||
data={group.instances}
|
||||
onRowClick={handleInstanceClick}
|
||||
selectedId={panelOpen ? selectedInstance?.id : undefined}
|
||||
pageSize={50}
|
||||
flush
|
||||
/>
|
||||
@@ -758,15 +610,6 @@ export default function AgentHealth() {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user