feat: redesign exchange detail page with interactive processor inspector
All checks were successful
Build & Publish / publish (push) Successful in 44s

- Rewrite ExchangeDetail with split Message IN/OUT panels that update
  on processor click, error panel for failed processors, and
  Timeline/Flow toggle for the processor visualization
- Add correlation chain in header with status-colored clickable nodes
  sorted by start time, labeled "Correlated Exchanges"
- Add Exchange ID column and inspect button (↗) to Dashboard table
- Add "Open full details" link in the exchange slide-in panel
- Add selectedIndex prop to ProcessorTimeline and RouteFlow for
  highlighting the active processor
- Add onNodeClick + selectedIndex to RouteFlow for interactive use
- Add correlationGroup field to exchange mock data
- Fix sidebar section toggle indentation alignment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-19 14:15:28 +01:00
parent 9c9063dc1b
commit 932dc9dcbd
10 changed files with 725 additions and 251 deletions

View File

@@ -223,3 +223,43 @@
font-family: var(--font-mono);
word-break: break-word;
}
/* Inspect exchange icon in table */
.inspectLink {
background: transparent;
border: none;
color: var(--text-faint);
opacity: 0.75;
cursor: pointer;
font-size: 13px;
padding: 2px 4px;
border-radius: var(--radius-sm);
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
transition: color 0.15s, opacity 0.15s;
}
.inspectLink:hover {
color: var(--text-primary);
opacity: 1;
}
/* Open full details link in panel */
.openDetailLink {
background: transparent;
border: none;
color: var(--amber);
cursor: pointer;
font-size: 12px;
padding: 0;
font-family: var(--font-body);
transition: color 0.1s;
}
.openDetailLink:hover {
color: var(--amber-deep);
text-decoration: underline;
text-underline-offset: 2px;
}

View File

@@ -1,5 +1,5 @@
import { useState, useMemo } from 'react'
import { useParams } from 'react-router-dom'
import { useParams, useNavigate } from 'react-router-dom'
import styles from './Dashboard.module.css'
// Layout
@@ -68,8 +68,8 @@ function statusLabel(status: Exchange['status']): string {
}
}
// ─── Table columns ────────────────────────────────────────────────────────────
const COLUMNS: Column<Exchange>[] = [
// ─── Table columns (base, without navigate action) ──────────────────────────
const BASE_COLUMNS: Column<Exchange>[] = [
{
key: 'status',
header: 'Status',
@@ -97,6 +97,14 @@ const COLUMNS: Column<Exchange>[] = [
<span className={styles.appName}>{ROUTE_TO_APP.get(row.route) ?? row.routeGroup}</span>
),
},
{
key: 'id',
header: 'Exchange ID',
sortable: true,
render: (_, row) => (
<MonoText size="xs">{row.id}</MonoText>
),
},
{
key: 'timestamp',
header: 'Started',
@@ -145,10 +153,34 @@ const SHORTCUTS = [
// ─── Dashboard component ──────────────────────────────────────────────────────
export function Dashboard() {
const { id: appId, routeId } = useParams<{ id: string; routeId: string }>()
const navigate = useNavigate()
const [selectedId, setSelectedId] = useState<string | undefined>()
const [panelOpen, setPanelOpen] = useState(false)
const [selectedExchange, setSelectedExchange] = useState<Exchange | null>(null)
// Build columns with inspect action as second column
const COLUMNS: Column<Exchange>[] = useMemo(() => {
const inspectCol: Column<Exchange> = {
key: 'correlationId' as keyof Exchange,
header: '',
width: '36px',
render: (_, row) => (
<button
className={styles.inspectLink}
title="Inspect exchange"
onClick={(e) => {
e.stopPropagation()
navigate(`/exchanges/${row.id}`)
}}
>
&#x2197;
</button>
),
}
const [statusCol, ...rest] = BASE_COLUMNS
return [statusCol, inspectCol, ...rest]
}, [navigate])
const { isInTimeRange, statusFilters } = useGlobalFilters()
// Build set of route IDs belonging to the selected app (if any)
@@ -232,6 +264,16 @@ export function Dashboard() {
onClose={() => setPanelOpen(false)}
title={`${selectedExchange.orderId}${selectedExchange.route}`}
>
{/* Link to full detail page */}
<div className={styles.panelSection}>
<button
className={styles.openDetailLink}
onClick={() => navigate(`/exchanges/${selectedExchange.id}`)}
>
Open full details &#x2192;
</button>
</div>
{/* Overview */}
<div className={styles.panelSection}>
<div className={styles.panelSectionTitle}>Overview</div>