diff --git a/ui/src/pages/AgentHealth/AgentHealth.module.css b/ui/src/pages/AgentHealth/AgentHealth.module.css index 53fbc3ed..c8d06bcc 100644 --- a/ui/src/pages/AgentHealth/AgentHealth.module.css +++ b/ui/src/pages/AgentHealth/AgentHealth.module.css @@ -224,3 +224,27 @@ padding: 10px 16px; border-bottom: 1px solid var(--border-subtle); } + +.headerActions { + display: flex; + align-items: center; + gap: 6px; +} + +.sortBtn, +.refreshBtn { + background: none; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + color: var(--text-muted); + cursor: pointer; + font-size: 13px; + padding: 2px 6px; + line-height: 1; +} + +.sortBtn:hover, +.refreshBtn:hover { + color: var(--text-primary); + border-color: var(--amber); +} diff --git a/ui/src/pages/AgentHealth/AgentHealth.tsx b/ui/src/pages/AgentHealth/AgentHealth.tsx index adfc778c..2042d9aa 100644 --- a/ui/src/pages/AgentHealth/AgentHealth.tsx +++ b/ui/src/pages/AgentHealth/AgentHealth.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useRef, useEffect } from 'react'; +import { useState, useMemo } from 'react'; import { useParams, Link } from 'react-router'; import { StatCard, StatusDot, Badge, MonoText, ProgressBar, @@ -223,19 +223,8 @@ function AgentPerformanceContent({ agent }: { agent: AgentInstance }) { export default function AgentHealth() { const { appId } = useParams(); const { data: agents } = useAgents(undefined, appId); - const { data: events } = useAgentEvents(appId); - const timelineRef = useRef(null); - - // Override EventFeed's auto-scroll-to-bottom so newest (DESC) events stay visible at top - useEffect(() => { - const el = timelineRef.current; - if (!el) return; - const timer = requestAnimationFrame(() => { - const list = el.querySelector('[aria-label="Event feed"]') as HTMLElement | null; - if (list) list.scrollTop = 0; - }); - return () => cancelAnimationFrame(timer); - }, [events]); + const { data: events, refetch: refetchEvents } = useAgentEvents(appId); + const [eventSortAsc, setEventSortAsc] = useState(false); const [selectedInstance, setSelectedInstance] = useState(null); const [panelOpen, setPanelOpen] = useState(false); @@ -254,22 +243,22 @@ export default function AgentHealth() { const totalRoutes = agentList.reduce((s, a) => s + (a.totalRoutes ?? 0), 0); // Map events to FeedEvent - const feedEvents: FeedEvent[] = useMemo( - () => - (events ?? []).map((e: { id: number; agentId: string; eventType: string; detail: string; timestamp: string }) => ({ - 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.agentId}: ${e.eventType}${e.detail ? ' \u2014 ' + e.detail : ''}`, - timestamp: new Date(e.timestamp), - })), - [events], + const feedEvents: FeedEvent[] = useMemo(() => { + const mapped = (events ?? []).map((e: { id: number; agentId: string; eventType: string; detail: string; timestamp: string }) => ({ + 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.agentId}: ${e.eventType}${e.detail ? ' \u2014 ' + e.detail : ''}`, + timestamp: new Date(e.timestamp), + })); + return eventSortAsc ? mapped.toReversed() : mapped; + }, [events, eventSortAsc], ); // Column definitions for the instance DataTable @@ -512,10 +501,18 @@ export default function AgentHealth() { {/* EventFeed */} {feedEvents.length > 0 && ( -
+
Timeline - {feedEvents.length} events +
+ {feedEvents.length} events + + +
diff --git a/ui/src/pages/AgentInstance/AgentInstance.module.css b/ui/src/pages/AgentInstance/AgentInstance.module.css index 2027ec17..384fe706 100644 --- a/ui/src/pages/AgentInstance/AgentInstance.module.css +++ b/ui/src/pages/AgentInstance/AgentInstance.module.css @@ -145,6 +145,30 @@ border-bottom: 1px solid var(--border-subtle); } +.headerActions { + display: flex; + align-items: center; + gap: 6px; +} + +.sortBtn, +.refreshBtn { + background: none; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + color: var(--text-muted); + cursor: pointer; + font-size: 13px; + padding: 2px 6px; + line-height: 1; +} + +.sortBtn:hover, +.refreshBtn:hover { + color: var(--text-primary); + border-color: var(--amber); +} + .logToolbar { display: flex; align-items: center; diff --git a/ui/src/pages/AgentInstance/AgentInstance.tsx b/ui/src/pages/AgentInstance/AgentInstance.tsx index 338edbaa..09ff0946 100644 --- a/ui/src/pages/AgentInstance/AgentInstance.tsx +++ b/ui/src/pages/AgentInstance/AgentInstance.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState, useRef, useEffect } from 'react'; +import { useMemo, useState } from 'react'; import { useParams, Link } from 'react-router'; import { StatCard, StatusDot, Badge, LineChart, AreaChart, BarChart, @@ -33,26 +33,15 @@ export default function AgentInstance() { const { timeRange } = useGlobalFilters(); const [logSearch, setLogSearch] = useState(''); const [logLevels, setLogLevels] = useState>(new Set()); + const [logSortAsc, setLogSortAsc] = useState(false); + const [eventSortAsc, setEventSortAsc] = useState(false); const timeFrom = timeRange.start.toISOString(); const timeTo = timeRange.end.toISOString(); - const timelineRef = useRef(null); - const { data: agents, isLoading } = useAgents(undefined, appId); - const { data: events } = useAgentEvents(appId, instanceId); + const { data: events, refetch: refetchEvents } = useAgentEvents(appId, instanceId); const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, undefined, appId); - // Override EventFeed's auto-scroll-to-bottom so newest (DESC) events stay visible at top - useEffect(() => { - const el = timelineRef.current; - if (!el) return; - const timer = requestAnimationFrame(() => { - const list = el.querySelector('[aria-label="Event feed"]') as HTMLElement | null; - if (list) list.scrollTop = 0; - }); - return () => cancelAnimationFrame(timer); - }, [events]); - const agent = useMemo( () => (agents || []).find((a: any) => a.id === instanceId) as any, [agents, instanceId], @@ -87,25 +76,24 @@ export default function AgentInstance() { [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 ? ' \u2014 ' + e.detail : ''}`, - timestamp: new Date(e.timestamp), - })), - [events, instanceId], - ); + const feedEvents = useMemo(() => { + const mapped = (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), + })); + return eventSortAsc ? mapped.toReversed() : mapped; + }, [events, instanceId, eventSortAsc]); // JVM chart series helpers const cpuSeries = useMemo(() => { @@ -149,15 +137,15 @@ export default function AgentInstance() { ); // Application logs from OpenSearch - const { data: rawLogs } = useApplicationLogs(appId, instanceId); - const logEntries = useMemo( - () => (rawLogs || []).map((l) => ({ + const { data: rawLogs, refetch: refetchLogs } = useApplicationLogs(appId, instanceId); + const logEntries = useMemo(() => { + const mapped = (rawLogs || []).map((l) => ({ timestamp: l.timestamp ?? '', level: mapLogLevel(l.level), message: l.message ?? '', - })), - [rawLogs], - ); + })); + return logSortAsc ? mapped.toReversed() : mapped; + }, [rawLogs, logSortAsc]); const searchLower = logSearch.toLowerCase(); const filteredLogs = logEntries .filter((l) => logLevels.size === 0 || logLevels.has(l.level)) @@ -409,7 +397,15 @@ export default function AgentInstance() {
Application Log - {logEntries.length} entries +
+ {logEntries.length} entries + + +
@@ -448,10 +444,18 @@ export default function AgentInstance() { )}
-
+
Timeline - {feedEvents.length} events +
+ {feedEvents.length} events + + +
{feedEvents.length > 0 ? (