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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user