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:
@@ -3,16 +3,24 @@ import { config } from '../../config';
|
|||||||
import { useAuthStore } from '../../auth/auth-store';
|
import { useAuthStore } from '../../auth/auth-store';
|
||||||
import { useRefreshInterval } from './use-refresh-interval';
|
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);
|
const refetchInterval = useRefreshInterval(30_000);
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['agent-metrics', agentId, names.join(','), buckets],
|
queryKey: ['agent-metrics', agentId, names.join(','), buckets, from, to],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const token = useAuthStore.getState().accessToken;
|
const token = useAuthStore.getState().accessToken;
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
names: names.join(','),
|
names: names.join(','),
|
||||||
buckets: String(buckets),
|
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}`, {
|
const res = await fetch(`${config.apiBaseUrl}/agents/${agentId}/metrics?${params}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
|
|||||||
@@ -105,11 +105,13 @@
|
|||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Log + Timeline side by side */
|
/* Log + Timeline side by side — fill remaining vertical space */
|
||||||
.bottomRow {
|
.bottomRow {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Timeline card — card styling via sectionStyles.section */
|
/* Timeline card — card styling via sectionStyles.section */
|
||||||
@@ -117,7 +119,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-height: 420px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.timelineHeader {
|
.timelineHeader {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import logStyles from '../../styles/log-panel.module.css';
|
|||||||
import chartCardStyles from '../../styles/chart-card.module.css';
|
import chartCardStyles from '../../styles/chart-card.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 { useStatsTimeseries } from '../../api/queries/executions';
|
|
||||||
import { useAgentMetrics } from '../../api/queries/agent-metrics';
|
import { useAgentMetrics } from '../../api/queries/agent-metrics';
|
||||||
import { formatUptime, mapLogLevel, eventSeverity, eventIcon } from '../../utils/agent-utils';
|
import { formatUptime, mapLogLevel, eventSeverity, eventIcon } from '../../utils/agent-utils';
|
||||||
import { useEnvironmentStore } from '../../api/environment-store';
|
import { useEnvironmentStore } from '../../api/environment-store';
|
||||||
@@ -47,7 +46,6 @@ export default function AgentInstance() {
|
|||||||
const selectedEnv = useEnvironmentStore((s) => s.environment);
|
const selectedEnv = useEnvironmentStore((s) => s.environment);
|
||||||
const { data: agents, isLoading } = useAgents(undefined, appId, selectedEnv);
|
const { data: agents, isLoading } = useAgents(undefined, appId, selectedEnv);
|
||||||
const { data: events } = useAgentEvents(appId, instanceId, 50, eventRefreshTo, selectedEnv);
|
const { data: events } = useAgentEvents(appId, instanceId, 50, eventRefreshTo, selectedEnv);
|
||||||
const { data: timeseries } = useStatsTimeseries(timeFrom, timeTo, undefined, appId, selectedEnv);
|
|
||||||
|
|
||||||
const agent = useMemo(
|
const agent = useMemo(
|
||||||
() => (agents || []).find((a: any) => a.instanceId === instanceId) as any,
|
() => (agents || []).find((a: any) => a.instanceId === instanceId) as any,
|
||||||
@@ -59,33 +57,26 @@ export default function AgentInstance() {
|
|||||||
agent?.instanceId || null,
|
agent?.instanceId || null,
|
||||||
['process.cpu.usage.value', 'jvm.memory.used.value', 'jvm.memory.max.value'],
|
['process.cpu.usage.value', 'jvm.memory.used.value', 'jvm.memory.max.value'],
|
||||||
1,
|
1,
|
||||||
|
timeFrom, timeTo,
|
||||||
);
|
);
|
||||||
const cpuPct = latestMetrics?.metrics?.['process.cpu.usage.value']?.[0]?.value;
|
const cpuPct = latestMetrics?.metrics?.['process.cpu.usage.value']?.[0]?.value;
|
||||||
const heapUsed = latestMetrics?.metrics?.['jvm.memory.used.value']?.[0]?.value;
|
const heapUsed = latestMetrics?.metrics?.['jvm.memory.used.value']?.[0]?.value;
|
||||||
const heapMax = latestMetrics?.metrics?.['jvm.memory.max.value']?.[0]?.value;
|
const heapMax = latestMetrics?.metrics?.['jvm.memory.max.value']?.[0]?.value;
|
||||||
const memPct = heapMax ? (heapUsed! / heapMax) * 100 : undefined;
|
const memPct = heapMax ? (heapUsed! / heapMax) * 100 : undefined;
|
||||||
|
|
||||||
// Chart metrics (60 buckets)
|
// Chart metrics (60 buckets, time-range-aware)
|
||||||
const { data: jvmMetrics } = useAgentMetrics(
|
const { data: jvmMetrics } = useAgentMetrics(
|
||||||
agent?.instanceId || null,
|
agent?.instanceId || null,
|
||||||
['process.cpu.usage.value', 'jvm.memory.used.value', 'jvm.memory.max.value', 'jvm.threads.live.value', 'jvm.gc.pause.total_time'],
|
['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(() => {
|
// Agent pipeline metrics (60 buckets, time-range-aware)
|
||||||
const buckets: any[] = timeseries?.buckets || [];
|
const { data: agentMetrics } = useAgentMetrics(
|
||||||
// Compute bucket duration in seconds from consecutive timestamps (for msg/s conversion)
|
agent?.instanceId || null,
|
||||||
const bucketSecs =
|
['cameleer.chunks.exported.count', 'cameleer.chunks.dropped.count'],
|
||||||
buckets.length >= 2
|
60, timeFrom, timeTo,
|
||||||
? (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]);
|
|
||||||
|
|
||||||
const feedEvents = useMemo<FeedEvent[]>(() => {
|
const feedEvents = useMemo<FeedEvent[]>(() => {
|
||||||
const mapped = (events || [])
|
const mapped = (events || [])
|
||||||
@@ -127,15 +118,17 @@ export default function AgentInstance() {
|
|||||||
return pts.map((p: any) => ({ time: p.time, gc: p.value }));
|
return pts.map((p: any) => ({ time: p.time, gc: p.value }));
|
||||||
}, [jvmMetrics]);
|
}, [jvmMetrics]);
|
||||||
|
|
||||||
const throughputData = useMemo(() => {
|
const chunksExportedData = useMemo(() => {
|
||||||
if (!chartData.length) return [];
|
const pts = agentMetrics?.metrics?.['cameleer.chunks.exported.count'];
|
||||||
return chartData.map((d: any) => ({ time: d.time, throughput: d.throughput }));
|
if (!pts?.length) return [];
|
||||||
}, [chartData]);
|
return pts.map((p: any) => ({ time: p.time, exported: p.value }));
|
||||||
|
}, [agentMetrics]);
|
||||||
|
|
||||||
const errorData = useMemo(() => {
|
const chunksDroppedData = useMemo(() => {
|
||||||
if (!chartData.length) return [];
|
const pts = agentMetrics?.metrics?.['cameleer.chunks.dropped.count'];
|
||||||
return chartData.map((d: any) => ({ time: d.time, errorPct: d.errorPct }));
|
if (!pts?.length) return [];
|
||||||
}, [chartData]);
|
return pts.map((p: any) => ({ time: p.time, dropped: p.value }));
|
||||||
|
}, [agentMetrics]);
|
||||||
|
|
||||||
// Application logs
|
// Application logs
|
||||||
const { data: rawLogs } = useApplicationLogs(appId, instanceId, { toOverride: logRefreshTo, source: logSource || undefined });
|
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={chartCardStyles.chartCard}>
|
||||||
<div className={styles.chartHeader}>
|
<div className={styles.chartHeader}>
|
||||||
<span className={styles.chartTitle}>Throughput</span>
|
<span className={styles.chartTitle}>GC Pauses</span>
|
||||||
<span className={styles.chartMeta}>
|
<span className={styles.chartMeta} />
|
||||||
{agent?.tps != null ? `${agent.tps.toFixed(1)} msg/s` : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{throughputData.length ? (
|
{gcData.length ? (
|
||||||
<ThemedChart data={throughputData} height={160} xDataKey="time"
|
<ThemedChart data={gcData} height={160} xDataKey="time"
|
||||||
xTickFormatter={formatTime} yLabel="msg/s">
|
xTickFormatter={formatTime} yLabel="ms">
|
||||||
<Line dataKey="throughput" name="msg/s" stroke={CHART_COLORS[0]}
|
<Area dataKey="gc" name="GC ms" stroke={CHART_COLORS[1]}
|
||||||
strokeWidth={2} dot={false} />
|
fill={CHART_COLORS[1]} fillOpacity={0.1} strokeWidth={2} dot={false} />
|
||||||
</ThemedChart>
|
</ThemedChart>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState title="No data" description="No throughput data in range" />
|
<EmptyState title="No data" description="No GC metrics available" />
|
||||||
)}
|
|
||||||
</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" />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -406,17 +379,33 @@ export default function AgentInstance() {
|
|||||||
|
|
||||||
<div className={chartCardStyles.chartCard}>
|
<div className={chartCardStyles.chartCard}>
|
||||||
<div className={styles.chartHeader}>
|
<div className={styles.chartHeader}>
|
||||||
<span className={styles.chartTitle}>GC Pauses</span>
|
<span className={styles.chartTitle}>Chunks Exported</span>
|
||||||
<span className={styles.chartMeta} />
|
<span className={styles.chartMeta} />
|
||||||
</div>
|
</div>
|
||||||
{gcData.length ? (
|
{chunksExportedData.length ? (
|
||||||
<ThemedChart data={gcData} height={160} xDataKey="time"
|
<ThemedChart data={chunksExportedData} height={160} xDataKey="time"
|
||||||
xTickFormatter={formatTime} yLabel="ms">
|
xTickFormatter={formatTime} yLabel="chunks">
|
||||||
<Area dataKey="gc" name="GC ms" stroke={CHART_COLORS[1]}
|
<Area dataKey="exported" name="Exported" stroke={CHART_COLORS[0]}
|
||||||
fill={CHART_COLORS[1]} fillOpacity={0.1} strokeWidth={2} dot={false} />
|
fill={CHART_COLORS[0]} fillOpacity={0.1} strokeWidth={2} dot={false} />
|
||||||
</ThemedChart>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ export default function DashboardL3() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Processor Metrics — toggle between diagram and table */}
|
{/* Processor Metrics — toggle between diagram and table */}
|
||||||
<div className={tableStyles.tableSection}>
|
<div className={`${tableStyles.tableSection} ${styles.processorSection}`}>
|
||||||
<div className={tableStyles.tableHeader}>
|
<div className={tableStyles.tableHeader}>
|
||||||
<span className={tableStyles.tableTitle}>Processor Metrics</span>
|
<span className={tableStyles.tableTitle}>Processor Metrics</span>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
|||||||
@@ -42,9 +42,18 @@
|
|||||||
text-decoration: underline;
|
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 {
|
.diagramHeight {
|
||||||
height: 280px;
|
flex: 1;
|
||||||
|
min-height: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chart fill */
|
/* Chart fill */
|
||||||
|
|||||||
Reference in New Issue
Block a user