feat: rework runtime charts and fix time range propagation
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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<FeedEvent[]>(() => {
|
||||
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() {
|
||||
|
||||
<div className={chartCardStyles.chartCard}>
|
||||
<div className={styles.chartHeader}>
|
||||
<span className={styles.chartTitle}>Throughput</span>
|
||||
<span className={styles.chartMeta}>
|
||||
{agent?.tps != null ? `${agent.tps.toFixed(1)} msg/s` : ''}
|
||||
</span>
|
||||
<span className={styles.chartTitle}>GC Pauses</span>
|
||||
<span className={styles.chartMeta} />
|
||||
</div>
|
||||
{throughputData.length ? (
|
||||
<ThemedChart data={throughputData} height={160} xDataKey="time"
|
||||
xTickFormatter={formatTime} yLabel="msg/s">
|
||||
<Line dataKey="throughput" name="msg/s" stroke={CHART_COLORS[0]}
|
||||
strokeWidth={2} dot={false} />
|
||||
{gcData.length ? (
|
||||
<ThemedChart data={gcData} height={160} xDataKey="time"
|
||||
xTickFormatter={formatTime} yLabel="ms">
|
||||
<Area dataKey="gc" name="GC ms" stroke={CHART_COLORS[1]}
|
||||
fill={CHART_COLORS[1]} fillOpacity={0.1} strokeWidth={2} dot={false} />
|
||||
</ThemedChart>
|
||||
) : (
|
||||
<EmptyState title="No data" description="No throughput data in range" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={chartCardStyles.chartCard}>
|
||||
<div className={styles.chartHeader}>
|
||||
<span className={styles.chartTitle}>Error Rate</span>
|
||||
<span className={styles.chartMeta}>
|
||||
{agent?.errorRate != null ? `${(agent.errorRate * 100).toFixed(1)}%` : ''}
|
||||
</span>
|
||||
</div>
|
||||
{errorData.length ? (
|
||||
<ThemedChart data={errorData} height={160} xDataKey="time"
|
||||
xTickFormatter={formatTime} yLabel="%">
|
||||
<Line dataKey="errorPct" name="Error %" stroke={CHART_COLORS[0]}
|
||||
strokeWidth={2} dot={false} />
|
||||
</ThemedChart>
|
||||
) : (
|
||||
<EmptyState title="No data" description="No error data in range" />
|
||||
<EmptyState title="No data" description="No GC metrics available" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -406,17 +379,33 @@ export default function AgentInstance() {
|
||||
|
||||
<div className={chartCardStyles.chartCard}>
|
||||
<div className={styles.chartHeader}>
|
||||
<span className={styles.chartTitle}>GC Pauses</span>
|
||||
<span className={styles.chartTitle}>Chunks Exported</span>
|
||||
<span className={styles.chartMeta} />
|
||||
</div>
|
||||
{gcData.length ? (
|
||||
<ThemedChart data={gcData} height={160} xDataKey="time"
|
||||
xTickFormatter={formatTime} yLabel="ms">
|
||||
<Area dataKey="gc" name="GC ms" stroke={CHART_COLORS[1]}
|
||||
fill={CHART_COLORS[1]} fillOpacity={0.1} strokeWidth={2} dot={false} />
|
||||
{chunksExportedData.length ? (
|
||||
<ThemedChart data={chunksExportedData} height={160} xDataKey="time"
|
||||
xTickFormatter={formatTime} yLabel="chunks">
|
||||
<Area dataKey="exported" name="Exported" stroke={CHART_COLORS[0]}
|
||||
fill={CHART_COLORS[0]} fillOpacity={0.1} strokeWidth={2} dot={false} />
|
||||
</ThemedChart>
|
||||
) : (
|
||||
<EmptyState title="No data" description="No GC metrics available" />
|
||||
<EmptyState title="No data" description="No chunk export metrics available" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={chartCardStyles.chartCard}>
|
||||
<div className={styles.chartHeader}>
|
||||
<span className={styles.chartTitle}>Chunks Dropped</span>
|
||||
<span className={styles.chartMeta} />
|
||||
</div>
|
||||
{chunksDroppedData.length ? (
|
||||
<ThemedChart data={chunksDroppedData} height={160} xDataKey="time"
|
||||
xTickFormatter={formatTime} yLabel="chunks">
|
||||
<Area dataKey="dropped" name="Dropped" stroke="var(--error)"
|
||||
fill="var(--error)" fillOpacity={0.1} strokeWidth={2} dot={false} />
|
||||
</ThemedChart>
|
||||
) : (
|
||||
<EmptyState title="No data" description="No chunk drop metrics available" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user