From ef9bc5a614c02df1672e7070607314e71a39f283 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 17 Apr 2026 12:55:39 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20AgentInstance=20=E2=80=94=20server-?= =?UTF-8?q?side=20multi-select=20filters=20+=20infinite=20scroll?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same pattern as AgentHealth, scoped to a single agent instance (passes agentId to both log and timeline streams). Co-Authored-By: Claude Sonnet 4.6 --- ui/src/pages/AgentInstance/AgentInstance.tsx | 120 +++++++++++++------ 1 file changed, 84 insertions(+), 36 deletions(-) diff --git a/ui/src/pages/AgentInstance/AgentInstance.tsx b/ui/src/pages/AgentInstance/AgentInstance.tsx index dbdcf857..e2c992bf 100644 --- a/ui/src/pages/AgentInstance/AgentInstance.tsx +++ b/ui/src/pages/AgentInstance/AgentInstance.tsx @@ -1,6 +1,9 @@ -import { useMemo, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; import { useParams } from 'react-router'; import { RefreshCw } from 'lucide-react'; +import { useInfiniteApplicationLogs } from '../../api/queries/logs'; +import { useInfiniteAgentEvents } from '../../api/queries/agents'; +import { InfiniteScrollArea } from '../../components/InfiniteScrollArea'; import { StatCard, StatusDot, Badge, ThemedChart, Line, Area, ReferenceLine, CHART_COLORS, EventFeed, Spinner, EmptyState, SectionHeader, MonoText, @@ -11,8 +14,7 @@ import styles from './AgentInstance.module.css'; import sectionStyles from '../../styles/section-card.module.css'; import logStyles from '../../styles/log-panel.module.css'; import chartCardStyles from '../../styles/chart-card.module.css'; -import { useAgents, useAgentEvents } from '../../api/queries/agents'; -import { useApplicationLogs } from '../../api/queries/logs'; +import { useAgents } from '../../api/queries/agents'; import { useAgentMetrics } from '../../api/queries/agent-metrics'; import { formatUptime, mapLogLevel, eventSeverity, eventIcon } from '../../utils/agent-utils'; import { useEnvironmentStore } from '../../api/environment-store'; @@ -36,17 +38,19 @@ export default function AgentInstance() { const { timeRange } = useGlobalFilters(); const [logSearch, setLogSearch] = useState(''); const [logLevels, setLogLevels] = useState>(new Set()); - const [logSource, setLogSource] = useState(''); // '' = all, 'app', 'agent' + const [logSources, setLogSources] = useState>(new Set()); const [logSortAsc, setLogSortAsc] = useState(false); const [eventSortAsc, setEventSortAsc] = useState(false); - const [logRefreshTo, setLogRefreshTo] = useState(); - const [eventRefreshTo, setEventRefreshTo] = useState(); + const [isLogAtTop, setIsLogAtTop] = useState(true); + const [isTimelineAtTop, setIsTimelineAtTop] = useState(true); + const logScrollRef = useRef(null); + const timelineScrollRef = useRef(null); const timeFrom = timeRange.start.toISOString(); const timeTo = timeRange.end.toISOString(); const selectedEnv = useEnvironmentStore((s) => s.environment); const { data: agents, isLoading } = useAgents(undefined, appId); - const { data: events } = useAgentEvents(appId, instanceId, 50, eventRefreshTo); + const eventStream = useInfiniteAgentEvents({ appId, agentId: instanceId, isAtTop: isTimelineAtTop }); const agent = useMemo( () => (agents || []).find((a: any) => a.instanceId === instanceId) as any, @@ -80,17 +84,17 @@ export default function AgentInstance() { ); const feedEvents = useMemo(() => { - const mapped = (events || []) - .filter((e: any) => !instanceId || e.instanceId === instanceId) - .map((e: any) => ({ - id: String(e.id), + const mapped = eventStream.items + .filter((e) => !instanceId || e.instanceId === instanceId) + .map((e) => ({ + id: `${e.timestamp}:${e.instanceId}:${e.eventType}`, severity: eventSeverity(e.eventType), icon: eventIcon(e.eventType), message: `${e.eventType}${e.detail ? ' \u2014 ' + e.detail : ''}`, timestamp: new Date(e.timestamp), })); return eventSortAsc ? mapped.toReversed() : mapped; - }, [events, instanceId, eventSortAsc]); + }, [eventStream.items, instanceId, eventSortAsc]); const formatTime = (t: string) => new Date(t).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); @@ -132,19 +136,26 @@ export default function AgentInstance() { }, [agentMetrics]); // Application logs - const { data: rawLogs } = useApplicationLogs(appId, instanceId, { toOverride: logRefreshTo, source: logSource || undefined }); + const logStream = useInfiniteApplicationLogs({ + application: appId, + agentId: instanceId, + sources: [...logSources], + levels: [...logLevels], + isAtTop: isLogAtTop, + }); const logEntries = useMemo(() => { - const mapped = (rawLogs || []).map((l) => ({ + const mapped = logStream.items.map((l) => ({ timestamp: l.timestamp ?? '', level: mapLogLevel(l.level), message: l.message ?? '', + source: l.source ?? undefined, })); return logSortAsc ? mapped.toReversed() : mapped; - }, [rawLogs, logSortAsc]); + }, [logStream.items, logSortAsc]); const searchLower = logSearch.toLowerCase(); - const filteredLogs = logEntries - .filter((l) => logLevels.size === 0 || logLevels.has(l.level)) - .filter((l) => !searchLower || l.message.toLowerCase().includes(searchLower)); + const filteredLogs = searchLower + ? logEntries.filter((l) => l.message.toLowerCase().includes(searchLower)) + : logEntries; if (isLoading) return ; @@ -396,11 +407,14 @@ export default function AgentInstance() {
Application Log
- {logEntries.length} entries + {logStream.items.length} entries -
@@ -428,9 +442,14 @@ export default function AgentInstance() {
setLogSource(v.size === 0 ? '' : [...v][0])} + value={logSources} + onChange={setLogSources} /> + {logSources.size > 0 && ( + + )} {logLevels.size > 0 && ( )} - {filteredLogs.length > 0 ? ( - - ) : ( -
- {logSearch || logLevels.size > 0 ? 'No matching log entries' : 'No log entries available'} -
- )} + 0} + maxHeight={360} + > + {filteredLogs.length > 0 ? ( + + ) : ( +
+ {logSearch || logLevels.size > 0 || logSources.size > 0 + ? 'No matching log entries' + : logStream.isLoading ? 'Loading logs…' : 'No log entries available'} +
+ )} +
Timeline
- {feedEvents.length} events + {eventStream.items.length} events -
- {feedEvents.length > 0 ? ( - - ) : ( -
No events in the selected time range.
- )} + 0} + maxHeight={360} + > + {feedEvents.length > 0 ? ( + + ) : ( +
+ {eventStream.isLoading ? 'Loading events…' : 'No events in the selected time range.'} +
+ )} +