feat: combine process diagram and processor table into toggled card
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m25s
CI / docker (push) Successful in 1m9s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 40s

Dashboard L3 now shows a single Processor Metrics card with
Diagram/Table toggle buttons. The diagram shows native tooltips on
hover with full processor metrics (avg, p99, invocations, error rate,
% time).

Also fixes:
- Chart x-axis uses actual timestamps instead of bucket indices
- formatDurationShort uses locale formatting with max 3 decimals

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-12 21:40:43 +02:00
parent 66248f6b1c
commit 98ce7c2204
4 changed files with 60 additions and 28 deletions

View File

@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { useParams } from 'react-router';
import {
KpiStrip,
@@ -247,6 +247,7 @@ function buildKpiItems(
// ── Component ───────────────────────────────────────────────────────────────
export default function DashboardL3() {
const [processorView, setProcessorView] = useState<'diagram' | 'table'>('diagram');
const { appId, routeId } = useParams<{ appId: string; routeId: string }>();
const selectedEnv = useEnvironmentStore((s) => s.environment);
const { timeRange } = useGlobalFilters();
@@ -290,8 +291,8 @@ export default function DashboardL3() {
// ── Chart data ───────────────────────────────────────────────────────────
const chartData = useMemo(() =>
(timeseries?.buckets || []).map((b: any, i: number) => ({
idx: i,
(timeseries?.buckets || []).map((b: any) => ({
time: b.time,
throughput: b.totalCount,
p99: b.p99DurationMs,
errorRate: b.totalCount > 0 ? (b.failedCount / b.totalCount) * 100 : 0,
@@ -299,6 +300,9 @@ export default function DashboardL3() {
[timeseries],
);
const formatTime = (t: string) =>
new Date(t).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
// ── Processor table rows ────────────────────────────────────────────────
const processorRows: ProcessorRow[] = useMemo(() => {
if (!processorMetrics?.length) return [];
@@ -321,12 +325,15 @@ export default function DashboardL3() {
const totalAvg = processorMetrics.reduce(
(sum: number, m: any) => sum + m.avgDurationMs, 0,
);
const map = new Map<string, { avgDurationMs: number; p99DurationMs: number; pctOfRoute: number }>();
const map = new Map<string, { avgDurationMs: number; p99DurationMs: number; pctOfRoute: number; processorType?: string; totalCount?: number; errorRate?: number }>();
for (const m of processorMetrics) {
map.set(m.processorId, {
avgDurationMs: m.avgDurationMs,
p99DurationMs: m.p99DurationMs,
pctOfRoute: totalAvg > 0 ? (m.avgDurationMs / totalAvg) * 100 : 0,
processorType: m.processorType,
totalCount: m.totalCount,
errorRate: m.errorRate,
});
}
return map;
@@ -352,14 +359,14 @@ export default function DashboardL3() {
{(timeseries?.buckets?.length ?? 0) > 0 && (
<div className={styles.chartRow}>
<Card title="Throughput">
<ThemedChart data={chartData} height={200} xDataKey="idx" yLabel="msg/s">
<ThemedChart data={chartData} height={200} xDataKey="time" xTickFormatter={formatTime} yLabel="msg/s">
<Area dataKey="throughput" name="Throughput" stroke={CHART_COLORS[0]}
fill={CHART_COLORS[0]} fillOpacity={0.1} strokeWidth={2} dot={false} />
</ThemedChart>
</Card>
<Card title="Latency Percentiles">
<ThemedChart data={chartData} height={200} xDataKey="idx" yLabel="ms">
<ThemedChart data={chartData} height={200} xDataKey="time" xTickFormatter={formatTime} yLabel="ms">
<Line dataKey="p99" name="P99" stroke={CHART_COLORS[0]} strokeWidth={2} dot={false} />
<ReferenceLine y={slaThresholdMs} stroke="var(--error)" strokeDasharray="5 3"
label={{ value: `SLA ${slaThresholdMs}ms`, position: 'right', fill: 'var(--error)', fontSize: 9 }} />
@@ -367,7 +374,7 @@ export default function DashboardL3() {
</Card>
<Card title="Error Rate">
<ThemedChart data={chartData} height={200} xDataKey="idx" yLabel="%">
<ThemedChart data={chartData} height={200} xDataKey="time" xTickFormatter={formatTime} yLabel="%">
<Area dataKey="errorRate" name="Error Rate" stroke={CHART_COLORS[1]}
fill={CHART_COLORS[1]} fillOpacity={0.1} strokeWidth={2} dot={false} />
</ThemedChart>
@@ -375,33 +382,42 @@ export default function DashboardL3() {
</div>
)}
{/* Process Diagram with Latency Heatmap */}
{appId && routeId && (
<div className={`${tableStyles.tableSection} ${styles.diagramHeight}`}>
<ProcessDiagram
application={appId}
routeId={routeId}
diagramLayout={diagramLayout}
latencyHeatmap={latencyHeatmap}
/>
</div>
)}
{/* Processor Metrics Table */}
{/* Processor Metrics — toggle between diagram and table */}
<div className={tableStyles.tableSection}>
<div className={tableStyles.tableHeader}>
<span className={tableStyles.tableTitle}>Processor Metrics</span>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span className={tableStyles.tableMeta}>
{processorRows.length} processor{processorRows.length !== 1 ? 's' : ''}
</span>
<div className={styles.toggleRow}>
<button
className={`${styles.toggleBtn} ${processorView === 'diagram' ? styles.toggleActive : ''}`}
onClick={() => setProcessorView('diagram')}
>Diagram</button>
<button
className={`${styles.toggleBtn} ${processorView === 'table' ? styles.toggleActive : ''}`}
onClick={() => setProcessorView('table')}
>Table</button>
</div>
</div>
</div>
<DataTable
columns={PROCESSOR_COLUMNS}
data={processorRows}
sortable
/>
{processorView === 'diagram' && appId && routeId ? (
<div className={styles.diagramHeight}>
<ProcessDiagram
application={appId}
routeId={routeId}
diagramLayout={diagramLayout}
latencyHeatmap={latencyHeatmap}
/>
</div>
) : (
<DataTable
columns={PROCESSOR_COLUMNS}
data={processorRows}
sortable
/>
)}
</div>
{/* Top 5 Errors — hidden if empty */}