From 00c9a0006e9201d0add468ad4efab1077a5b7ec4 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 12 Apr 2026 21:59:38 +0200 Subject: [PATCH] feat: rework runtime charts and fix time range propagation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runtime page (AgentInstance): - Rearrange charts: CPU, Memory, GC (top); Threads, Chunks Exported, Chunks Dropped (bottom). Removes throughput/error charts (belong on Dashboard, not Runtime). - Pass global time range (from/to) to useAgentMetrics — charts now respect the time filter instead of always showing last 60 minutes. - Bottom row (logs + timeline) fills remaining vertical space. Dashboard L3: - Processor metrics section fills remaining vertical space. - Chart x-axis uses timestamps instead of bucket indices. Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/api/queries/agent-metrics.ts | 12 +- .../AgentInstance/AgentInstance.module.css | 5 +- ui/src/pages/AgentInstance/AgentInstance.tsx | 111 ++++++++---------- ui/src/pages/DashboardTab/DashboardL3.tsx | 2 +- .../DashboardTab/DashboardTab.module.css | 13 +- 5 files changed, 75 insertions(+), 68 deletions(-) diff --git a/ui/src/api/queries/agent-metrics.ts b/ui/src/api/queries/agent-metrics.ts index b105015f..9970c943 100644 --- a/ui/src/api/queries/agent-metrics.ts +++ b/ui/src/api/queries/agent-metrics.ts @@ -3,16 +3,24 @@ import { config } from '../../config'; import { useAuthStore } from '../../auth/auth-store'; import { useRefreshInterval } from './use-refresh-interval'; -export function useAgentMetrics(agentId: string | null, names: string[], buckets = 60) { +export function useAgentMetrics( + agentId: string | null, + names: string[], + buckets = 60, + from?: string, + to?: string, +) { const refetchInterval = useRefreshInterval(30_000); return useQuery({ - queryKey: ['agent-metrics', agentId, names.join(','), buckets], + queryKey: ['agent-metrics', agentId, names.join(','), buckets, from, to], queryFn: async () => { const token = useAuthStore.getState().accessToken; const params = new URLSearchParams({ names: names.join(','), buckets: String(buckets), }); + if (from) params.set('from', from); + if (to) params.set('to', to); const res = await fetch(`${config.apiBaseUrl}/agents/${agentId}/metrics?${params}`, { headers: { Authorization: `Bearer ${token}`, diff --git a/ui/src/pages/AgentInstance/AgentInstance.module.css b/ui/src/pages/AgentInstance/AgentInstance.module.css index e437f2b1..ffd4b200 100644 --- a/ui/src/pages/AgentInstance/AgentInstance.module.css +++ b/ui/src/pages/AgentInstance/AgentInstance.module.css @@ -105,11 +105,13 @@ font-family: var(--font-mono); } -/* Log + Timeline side by side */ +/* Log + Timeline side by side — fill remaining vertical space */ .bottomRow { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; + flex: 1; + min-height: 300px; } /* Timeline card — card styling via sectionStyles.section */ @@ -117,7 +119,6 @@ overflow: hidden; display: flex; flex-direction: column; - max-height: 420px; } .timelineHeader { diff --git a/ui/src/pages/AgentInstance/AgentInstance.tsx b/ui/src/pages/AgentInstance/AgentInstance.tsx index 3522b3ea..eb0a008a 100644 --- a/ui/src/pages/AgentInstance/AgentInstance.tsx +++ b/ui/src/pages/AgentInstance/AgentInstance.tsx @@ -13,7 +13,6 @@ 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 { useStatsTimeseries } from '../../api/queries/executions'; import { useAgentMetrics } from '../../api/queries/agent-metrics'; import { formatUptime, mapLogLevel, eventSeverity, eventIcon } from '../../utils/agent-utils'; import { useEnvironmentStore } from '../../api/environment-store'; @@ -47,7 +46,6 @@ export default function AgentInstance() { const selectedEnv = useEnvironmentStore((s) => s.environment); const { data: agents, isLoading } = useAgents(undefined, appId, selectedEnv); const { data: events } = useAgentEvents(appId, instanceId, 50, eventRefreshTo, selectedEnv); - const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, undefined, appId, selectedEnv); const agent = useMemo( () => (agents || []).find((a: any) => a.instanceId === instanceId) as any, @@ -59,33 +57,26 @@ export default function AgentInstance() { agent?.instanceId || null, ['process.cpu.usage.value', 'jvm.memory.used.value', 'jvm.memory.max.value'], 1, + timeFrom, timeTo, ); const cpuPct = latestMetrics?.metrics?.['process.cpu.usage.value']?.[0]?.value; const heapUsed = latestMetrics?.metrics?.['jvm.memory.used.value']?.[0]?.value; const heapMax = latestMetrics?.metrics?.['jvm.memory.max.value']?.[0]?.value; const memPct = heapMax ? (heapUsed! / heapMax) * 100 : undefined; - // Chart metrics (60 buckets) + // Chart metrics (60 buckets, time-range-aware) const { data: jvmMetrics } = useAgentMetrics( agent?.instanceId || null, ['process.cpu.usage.value', 'jvm.memory.used.value', 'jvm.memory.max.value', 'jvm.threads.live.value', 'jvm.gc.pause.total_time'], - 60, + 60, timeFrom, timeTo, ); - const chartData = useMemo(() => { - const buckets: any[] = timeseries?.buckets || []; - // Compute bucket duration in seconds from consecutive timestamps (for msg/s conversion) - const bucketSecs = - buckets.length >= 2 - ? (new Date(buckets[1].timestamp).getTime() - new Date(buckets[0].timestamp).getTime()) / 1000 - : 60; - return buckets.map((b: any) => ({ - time: b.timestamp, - throughput: bucketSecs > 0 ? b.totalCount / bucketSecs : b.totalCount, - latency: b.avgDurationMs, - errorPct: b.totalCount > 0 ? (b.failedCount / b.totalCount) * 100 : 0, - })); - }, [timeseries]); + // Agent pipeline metrics (60 buckets, time-range-aware) + const { data: agentMetrics } = useAgentMetrics( + agent?.instanceId || null, + ['cameleer.chunks.exported.count', 'cameleer.chunks.dropped.count'], + 60, timeFrom, timeTo, + ); const feedEvents = useMemo(() => { const mapped = (events || []) @@ -127,15 +118,17 @@ export default function AgentInstance() { return pts.map((p: any) => ({ time: p.time, gc: p.value })); }, [jvmMetrics]); - const throughputData = useMemo(() => { - if (!chartData.length) return []; - return chartData.map((d: any) => ({ time: d.time, throughput: d.throughput })); - }, [chartData]); + const chunksExportedData = useMemo(() => { + const pts = agentMetrics?.metrics?.['cameleer.chunks.exported.count']; + if (!pts?.length) return []; + return pts.map((p: any) => ({ time: p.time, exported: p.value })); + }, [agentMetrics]); - const errorData = useMemo(() => { - if (!chartData.length) return []; - return chartData.map((d: any) => ({ time: d.time, errorPct: d.errorPct })); - }, [chartData]); + const chunksDroppedData = useMemo(() => { + const pts = agentMetrics?.metrics?.['cameleer.chunks.dropped.count']; + if (!pts?.length) return []; + return pts.map((p: any) => ({ time: p.time, dropped: p.value })); + }, [agentMetrics]); // Application logs const { data: rawLogs } = useApplicationLogs(appId, instanceId, { toOverride: logRefreshTo, source: logSource || undefined }); @@ -350,37 +343,17 @@ export default function AgentInstance() {
- Throughput - - {agent?.tps != null ? `${agent.tps.toFixed(1)} msg/s` : ''} - + GC Pauses +
- {throughputData.length ? ( - - + {gcData.length ? ( + + ) : ( - - )} -
- -
-
- Error Rate - - {agent?.errorRate != null ? `${(agent.errorRate * 100).toFixed(1)}%` : ''} - -
- {errorData.length ? ( - - - - ) : ( - + )}
@@ -406,17 +379,33 @@ export default function AgentInstance() {
- GC Pauses + Chunks Exported
- {gcData.length ? ( - - + {chunksExportedData.length ? ( + + ) : ( - + + )} +
+ +
+
+ Chunks Dropped + +
+ {chunksDroppedData.length ? ( + + + + ) : ( + )}
diff --git a/ui/src/pages/DashboardTab/DashboardL3.tsx b/ui/src/pages/DashboardTab/DashboardL3.tsx index 8af26a58..9bccb7e3 100644 --- a/ui/src/pages/DashboardTab/DashboardL3.tsx +++ b/ui/src/pages/DashboardTab/DashboardL3.tsx @@ -383,7 +383,7 @@ export default function DashboardL3() { )} {/* Processor Metrics — toggle between diagram and table */} -
+
Processor Metrics
diff --git a/ui/src/pages/DashboardTab/DashboardTab.module.css b/ui/src/pages/DashboardTab/DashboardTab.module.css index ce114578..c4f2c05c 100644 --- a/ui/src/pages/DashboardTab/DashboardTab.module.css +++ b/ui/src/pages/DashboardTab/DashboardTab.module.css @@ -42,9 +42,18 @@ text-decoration: underline; } -/* Diagram container — height override; card styling via tableStyles.tableSection */ +/* Processor section — fills remaining vertical space */ +.processorSection { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +/* Diagram container — fills remaining space inside processor section */ .diagramHeight { - height: 280px; + flex: 1; + min-height: 280px; } /* Chart fill */