From 9f281c3354f33b88ac618a3370786e92e5715742 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 28 Mar 2026 16:14:15 +0100 Subject: [PATCH] chore(ui): remove dead code from navigation redesign Deleted: - ScopeTrail component (replaced by inline breadcrumb in TopBar) - ExchangeList component (replaced by Dashboard DataTable) - ExchangeDetail page (replaced by inline split view) Removed from Dashboard: - flattenProcessors() function (unused after detail panel removal) - 11 dead CSS classes (panelSection, overviewGrid, errorBlock, inspectLink, openDetailLink, filterBar, etc.) Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/components/ScopeTrail.module.css | 40 - ui/src/components/ScopeTrail.tsx | 38 - ui/src/pages/Dashboard/Dashboard.module.css | 128 --- ui/src/pages/Dashboard/Dashboard.tsx | 18 - .../ExchangeDetail/ExchangeDetail.module.css | 685 --------------- .../pages/ExchangeDetail/ExchangeDetail.tsx | 819 ------------------ .../pages/Exchanges/ExchangeList.module.css | 74 -- ui/src/pages/Exchanges/ExchangeList.tsx | 56 -- 8 files changed, 1858 deletions(-) delete mode 100644 ui/src/components/ScopeTrail.module.css delete mode 100644 ui/src/components/ScopeTrail.tsx delete mode 100644 ui/src/pages/ExchangeDetail/ExchangeDetail.module.css delete mode 100644 ui/src/pages/ExchangeDetail/ExchangeDetail.tsx delete mode 100644 ui/src/pages/Exchanges/ExchangeList.module.css delete mode 100644 ui/src/pages/Exchanges/ExchangeList.tsx diff --git a/ui/src/components/ScopeTrail.module.css b/ui/src/components/ScopeTrail.module.css deleted file mode 100644 index 844c4b5b..00000000 --- a/ui/src/components/ScopeTrail.module.css +++ /dev/null @@ -1,40 +0,0 @@ -.trail { - display: flex; - align-items: center; - gap: 0; - font-size: 0.8125rem; - color: var(--text-muted); - min-height: 1.5rem; -} - -.segment { - display: inline-flex; - align-items: center; -} - -.link { - color: var(--text-secondary); - text-decoration: none; - cursor: pointer; - background: none; - border: none; - padding: 0; - font: inherit; - font-size: 0.8125rem; -} - -.link:hover { - color: var(--amber); - text-decoration: underline; -} - -.separator { - margin: 0 0.375rem; - color: var(--text-muted); - user-select: none; -} - -.current { - color: var(--text-primary); - font-weight: 500; -} diff --git a/ui/src/components/ScopeTrail.tsx b/ui/src/components/ScopeTrail.tsx deleted file mode 100644 index fa733661..00000000 --- a/ui/src/components/ScopeTrail.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { Scope } from '../hooks/useScope'; -import styles from './ScopeTrail.module.css'; - -interface ScopeTrailProps { - scope: Scope; - onNavigate: (path: string) => void; -} - -export function ScopeTrail({ scope, onNavigate }: ScopeTrailProps) { - const segments: { label: string; path: string }[] = [ - { label: 'All Applications', path: `/${scope.tab}` }, - ]; - - if (scope.appId) { - segments.push({ label: scope.appId, path: `/${scope.tab}/${scope.appId}` }); - } - - if (scope.routeId) { - segments.push({ label: scope.routeId, path: `/${scope.tab}/${scope.appId}/${scope.routeId}` }); - } - - return ( - - ); -} diff --git a/ui/src/pages/Dashboard/Dashboard.module.css b/ui/src/pages/Dashboard/Dashboard.module.css index 3b0b640c..98aa5e49 100644 --- a/ui/src/pages/Dashboard/Dashboard.module.css +++ b/ui/src/pages/Dashboard/Dashboard.module.css @@ -11,11 +11,6 @@ /* Table section — stretches to fill and scrolls internally */ -/* Filter bar spacing */ -.filterBar { - margin-bottom: 16px; -} - .tableSection { display: flex; flex-direction: column; @@ -166,112 +161,6 @@ margin-top: 3px; } -/* Detail panel sections */ -.panelSection { - padding-bottom: 16px; - margin-bottom: 16px; - border-bottom: 1px solid var(--border-subtle); -} - -.panelSection:last-child { - border-bottom: none; - margin-bottom: 0; - padding-bottom: 0; -} - -.panelSectionTitle { - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - color: var(--text-muted); - margin-bottom: 10px; - display: flex; - align-items: center; - gap: 8px; -} - -.panelSectionMeta { - margin-left: auto; - font-family: var(--font-mono); - font-size: 10px; - font-weight: 500; - text-transform: none; - letter-spacing: 0; - color: var(--text-faint); -} - -/* Overview grid */ -.overviewGrid { - display: flex; - flex-direction: column; - gap: 8px; -} - -.overviewRow { - display: flex; - align-items: flex-start; - gap: 12px; -} - -.overviewLabel { - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.6px; - color: var(--text-muted); - width: 90px; - flex-shrink: 0; - padding-top: 2px; -} - -/* Error block */ -.errorBlock { - background: var(--error-bg); - border: 1px solid var(--error-border); - border-radius: var(--radius-sm); - padding: 10px 12px; -} - -.errorClass { - font-family: var(--font-mono); - font-size: 10px; - font-weight: 600; - color: var(--error); - margin-bottom: 4px; -} - -.errorMessage { - font-size: 11px; - color: var(--text-secondary); - line-height: 1.5; - 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; - text-decoration: none; -} - -.inspectLink:hover { - color: var(--text-primary); - opacity: 1; -} - /* Attributes cell in table */ .attrCell { display: flex; @@ -288,20 +177,3 @@ color: var(--text-muted); } -/* 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; -} diff --git a/ui/src/pages/Dashboard/Dashboard.tsx b/ui/src/pages/Dashboard/Dashboard.tsx index d02caa2d..e196b6c1 100644 --- a/ui/src/pages/Dashboard/Dashboard.tsx +++ b/ui/src/pages/Dashboard/Dashboard.tsx @@ -66,24 +66,6 @@ function durationClass(ms: number, status: string): string { return styles.durBreach } -function flattenProcessors(nodes: any[]): any[] { - const result: any[] = [] - let offset = 0 - function walk(node: any) { - result.push({ - name: node.processorId || node.processorType, - type: node.processorType, - durationMs: node.durationMs ?? 0, - status: node.status === 'COMPLETED' ? 'ok' : node.status === 'FAILED' ? 'fail' : 'ok', - startMs: offset, - }) - offset += node.durationMs ?? 0 - if (node.children) node.children.forEach(walk) - } - nodes.forEach(walk) - return result -} - // ─── Table columns (base, without inspect action) ──────────────────────────── function buildBaseColumns(): Column[] { diff --git a/ui/src/pages/ExchangeDetail/ExchangeDetail.module.css b/ui/src/pages/ExchangeDetail/ExchangeDetail.module.css deleted file mode 100644 index 2d18e19f..00000000 --- a/ui/src/pages/ExchangeDetail/ExchangeDetail.module.css +++ /dev/null @@ -1,685 +0,0 @@ -/* Scrollable content area */ -.content { - flex: 1; - overflow-y: auto; - padding: 20px 24px 40px; - min-width: 0; - background: var(--bg-body); -} - -.loadingContainer { - display: flex; - justify-content: center; - padding: 4rem; -} - -/* ========================================================================== - EXCHANGE HEADER CARD - ========================================================================== */ -.exchangeHeader { - background: var(--bg-surface); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-card); - padding: 16px 20px; - margin-bottom: 14px; -} - -.headerRow { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; -} - -.headerLeft { - display: flex; - align-items: flex-start; - gap: 12px; - flex: 1; -} - -.exchangeId { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 6px; -} - -.exchangeRoute { - font-size: 12px; - color: var(--text-secondary); - display: flex; - align-items: center; - gap: 6px; - flex-wrap: wrap; -} - -.routeLink { - color: var(--amber); - cursor: pointer; - text-decoration: underline; - text-underline-offset: 2px; -} - -.routeLink:hover { - color: var(--amber-deep); -} - -.headerDivider { - color: var(--text-faint); -} - -.headerRight { - display: flex; - gap: 20px; - flex-shrink: 0; -} - -.headerStat { - text-align: center; -} - -.headerStatLabel { - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.6px; - color: var(--text-muted); - margin-bottom: 2px; -} - -.headerStatValue { - font-size: 14px; - font-weight: 600; - font-family: var(--font-mono); - color: var(--text-primary); -} - -/* ========================================================================== - CORRELATION CHAIN - ========================================================================== */ -.correlationChain { - display: flex; - flex-direction: row; - align-items: center; - gap: 8px; - padding-top: 12px; - margin-top: 12px; - border-top: 1px solid var(--border-subtle); - flex-wrap: wrap; -} - -.chainLabel { - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - color: var(--text-muted); - margin-right: 4px; -} - -.chainNode { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 10px; - border-radius: var(--radius-sm); - border: 1px solid var(--border-subtle); - font-size: 11px; - font-family: var(--font-mono); - cursor: pointer; - background: var(--bg-surface); - color: var(--text-secondary); - transition: all 0.12s; -} - -.chainNode:hover { - border-color: var(--text-faint); - background: var(--bg-hover); -} - -.chainNodeCurrent { - background: var(--amber-bg); - border-color: var(--amber-light); - color: var(--amber-deep); - font-weight: 600; -} - -.chainNodeSuccess { - border-left: 3px solid var(--success); -} - -.chainNodeError { - border-left: 3px solid var(--error); -} - -.chainNodeRunning { - border-left: 3px solid var(--running); -} - -.chainNodeWarning { - border-left: 3px solid var(--warning); -} - -.chainMore { - color: var(--text-muted); - font-size: 11px; - font-style: italic; -} - -/* ========================================================================== - ATTRIBUTES STRIP - ========================================================================== */ -.attributesStrip { - display: flex; - gap: 8px; - flex-wrap: wrap; - align-items: center; - padding: 10px 14px; - background: var(--bg-surface); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-lg); - margin-bottom: 16px; -} - -.attributesLabel { - font-size: 11px; - color: var(--text-muted); - margin-right: 4px; -} - -/* ========================================================================== - TIMELINE SECTION - ========================================================================== */ -.timelineSection { - background: var(--bg-surface); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-card); - margin-bottom: 16px; - overflow: hidden; -} - -.timelineHeader { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 16px; - border-bottom: 1px solid var(--border-subtle); -} - -.timelineTitle { - font-size: 13px; - font-weight: 600; - color: var(--text-primary); - display: flex; - align-items: center; - gap: 8px; -} - -.procCount { - font-family: var(--font-mono); - font-size: 10px; - font-weight: 500; - padding: 1px 8px; - border-radius: 10px; - background: var(--bg-inset); - color: var(--text-muted); -} - -.timelineToggle { - display: inline-flex; - gap: 0; - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - overflow: hidden; -} - -.toggleBtn { - padding: 4px 12px; - font-size: 11px; - font-family: var(--font-body); - border: none; - background: transparent; - cursor: pointer; - color: var(--text-secondary); - transition: all 0.12s; -} - -.toggleBtn:hover { - background: var(--bg-hover); -} - -.toggleBtnActive { - background: var(--amber); - color: #fff; - font-weight: 600; -} - -.toggleBtnActive:hover { - background: var(--amber-deep); -} - -.timelineBody { - padding: 12px 16px; -} - -/* ========================================================================== - EXECUTION DIAGRAM CONTAINER (Flow view) - ========================================================================== */ -.executionDiagramContainer { - height: 600px; - border: 1px solid var(--border, #E4DFD8); - border-radius: var(--radius-md, 8px); - overflow: hidden; - margin-bottom: 16px; -} - -/* ========================================================================== - DETAIL SPLIT (IN / OUT panels) - ========================================================================== */ -.detailSplit { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 16px; - margin-bottom: 16px; -} - -.detailPanel { - background: var(--bg-surface); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-card); - overflow: hidden; -} - -.detailPanelError { - border-color: var(--error-border); -} - -.panelHeader { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 16px; - border-bottom: 1px solid var(--border-subtle); - background: var(--bg-raised); - gap: 8px; -} - -.detailPanelError .panelHeader { - background: var(--error-bg); - border-bottom-color: var(--error-border); -} - -.panelTitle { - font-size: 13px; - font-weight: 600; - color: var(--text-primary); - display: flex; - flex-direction: row; - align-items: center; - gap: 6px; -} - -.arrowIn { - color: var(--success); - font-weight: 700; -} - -.arrowOut { - color: var(--running); - font-weight: 700; -} - -.arrowError { - color: var(--error); - font-weight: 700; - font-size: 16px; -} - -.panelTag { - font-family: var(--font-mono); - font-size: 10px; - padding: 1px 6px; - border-radius: 8px; - background: var(--bg-inset); - color: var(--text-muted); - font-weight: 500; - white-space: nowrap; -} - -.panelBody { - padding: 16px; -} - -/* Headers section */ -.headersSection { - margin-bottom: 12px; -} - -.headerList { - display: flex; - flex-direction: column; - gap: 0; -} - -.headerKvRow { - display: grid; - grid-template-columns: 140px 1fr; - padding: 4px 0; - border-bottom: 1px solid var(--border-subtle); - font-size: 11px; -} - -.headerKvRow:last-child { - border-bottom: none; -} - -.headerKey { - font-family: var(--font-mono); - font-weight: 600; - color: var(--text-muted); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.headerValue { - font-family: var(--font-mono); - color: var(--text-primary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -/* Body section */ -.bodySection { - margin-top: 12px; -} - -.sectionLabel { - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.6px; - color: var(--text-muted); - margin-bottom: 6px; - display: flex; - align-items: center; - gap: 6px; -} - -.count { - font-family: var(--font-mono); - font-size: 10px; - padding: 0 5px; - border-radius: 8px; - background: var(--bg-inset); - color: var(--text-faint); -} - -/* Error panel styles */ -.errorBadgeRow { - display: flex; - gap: 8px; - margin-bottom: 8px; -} - -.errorHttpBadge { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: var(--radius-sm); - font-family: var(--font-mono); - font-size: 10px; - font-weight: 600; - background: var(--error-bg); - color: var(--error); - border: 1px solid var(--error-border); -} - -.errorMessageBox { - font-family: var(--font-mono); - font-size: 11px; - color: var(--text-secondary); - background: var(--error-bg); - padding: 10px 12px; - border-radius: var(--radius-sm); - border: 1px solid var(--error-border); - margin-bottom: 12px; - line-height: 1.5; - word-break: break-word; - white-space: pre-wrap; -} - -.errorDetailGrid { - display: grid; - grid-template-columns: 120px 1fr; - gap: 4px 12px; - font-size: 11px; -} - -.errorDetailLabel { - font-weight: 600; - color: var(--text-muted); - font-family: var(--font-mono); -} - -.errorDetailValue { - color: var(--text-primary); - font-family: var(--font-mono); - word-break: break-all; -} - -/* Snapshot loading */ -.snapshotLoading { - color: var(--text-muted); - font-size: 12px; - text-align: center; - padding: 20px; -} - -/* Exchange log section */ -.logSection { - margin-top: 20px; - background: var(--bg-surface); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-card); - overflow: hidden; - display: flex; - flex-direction: column; - max-height: 420px; -} - -.logSectionHeader { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 16px; - border-bottom: 1px solid var(--border-subtle); -} - -.logMeta { - font-size: 11px; - color: var(--text-muted); - font-family: var(--font-mono); -} - -.logToolbar { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - border-bottom: 1px solid var(--border-subtle); -} - -.logSearchWrap { - position: relative; - flex: 1; - min-width: 0; -} - -.logSearchInput { - width: 100%; - padding: 5px 28px 5px 10px; - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - background: var(--bg-body); - color: var(--text-primary); - font-size: 12px; - font-family: var(--font-body); - outline: none; -} - -.logSearchInput:focus { - border-color: var(--amber); -} - -.logSearchInput::placeholder { - color: var(--text-faint); -} - -.logSearchClear { - position: absolute; - right: 4px; - top: 50%; - transform: translateY(-50%); - background: none; - border: none; - color: var(--text-muted); - cursor: pointer; - font-size: 14px; - padding: 0 4px; - line-height: 1; -} - -.logClearFilters { - background: none; - border: none; - color: var(--text-muted); - font-size: 11px; - cursor: pointer; - padding: 2px 6px; - white-space: nowrap; -} - -.logClearFilters:hover { - color: var(--text-primary); -} - -.logEmpty { - padding: 24px; - text-align: center; - color: var(--text-faint); - font-size: 12px; -} - -/* ========================================================================== - REPLAY MODAL - ========================================================================== */ -.replayWarning { - background: var(--warning-bg, #3d3520); - border: 1px solid var(--warning-border, #6b5c2a); - border-radius: var(--radius-sm); - padding: 10px 14px; - font-size: 12px; - color: var(--warning, #e6b84f); - margin-bottom: 16px; - line-height: 1.5; -} - -.replayAgentRow { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 16px; -} - -.replayFieldLabel { - font-size: 12px; - font-weight: 600; - color: var(--text-secondary); - white-space: nowrap; -} - -.replayHeadersTable { - margin-top: 12px; -} - -.replayHeadersHead { - display: grid; - grid-template-columns: 1fr 1fr 28px; - gap: 8px; - padding-bottom: 6px; - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - color: var(--text-muted); -} - -.replayHeaderRow { - display: grid; - grid-template-columns: 1fr 1fr 28px; - gap: 8px; - margin-bottom: 6px; - align-items: center; -} - -.replayRemoveBtn { - background: none; - border: none; - color: var(--text-muted); - font-size: 16px; - cursor: pointer; - padding: 0; - line-height: 1; - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border-radius: var(--radius-sm); -} - -.replayRemoveBtn:hover { - color: var(--error); - background: var(--error-bg); -} - -.replayAddHeader { - background: none; - border: none; - color: var(--amber); - font-size: 12px; - cursor: pointer; - padding: 4px 0; - margin-top: 4px; -} - -.replayAddHeader:hover { - color: var(--amber-deep); - text-decoration: underline; -} - -.replayBodyArea { - margin-top: 12px; -} - -.replayBodyTextarea { - font-family: var(--font-mono); - font-size: 12px; - width: 100%; -} - -.replayFooter { - display: flex; - justify-content: flex-end; - gap: 8px; - margin-top: 16px; - padding-top: 12px; - border-top: 1px solid var(--border-subtle); -} diff --git a/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx b/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx deleted file mode 100644 index f58b5588..00000000 --- a/ui/src/pages/ExchangeDetail/ExchangeDetail.tsx +++ /dev/null @@ -1,819 +0,0 @@ -import { useState, useMemo, useCallback, useEffect } from 'react' -import { useParams, useNavigate } from 'react-router' -import { - Badge, StatusDot, MonoText, CodeBlock, InfoCallout, - ProcessorTimeline, Spinner, useToast, - LogViewer, ButtonGroup, SectionHeader, useBreadcrumb, - Modal, Tabs, Button, Select, Input, Textarea, - useGlobalFilters, -} from '@cameleer/design-system' -import type { ProcessorStep, NodeBadge, LogEntry, ButtonGroupItem } from '@cameleer/design-system' -import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions' -import { useCorrelationChain } from '../../api/queries/correlation' -import { useTracingStore } from '../../stores/tracing-store' -import { useApplicationConfig, useUpdateApplicationConfig, useReplayExchange } from '../../api/queries/commands' -import { useAgents } from '../../api/queries/agents' -import { useApplicationLogs } from '../../api/queries/logs' -import { useRouteCatalog } from '../../api/queries/catalog' -import { ExecutionDiagram } from '../../components/ExecutionDiagram' -import type { NodeAction, NodeConfig } from '../../components/ProcessDiagram/types' -import styles from './ExchangeDetail.module.css' - -const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [ - { value: 'error', label: 'Error', color: 'var(--error)' }, - { value: 'warn', label: 'Warn', color: 'var(--warning)' }, - { value: 'info', label: 'Info', color: 'var(--success)' }, - { value: 'debug', label: 'Debug', color: 'var(--running)' }, - { value: 'trace', label: 'Trace', color: 'var(--text-muted)' }, -] - -function mapLogLevel(level: string): LogEntry['level'] { - switch (level?.toUpperCase()) { - case 'ERROR': return 'error' - case 'WARN': case 'WARNING': return 'warn' - case 'DEBUG': return 'debug' - case 'TRACE': return 'trace' - default: return 'info' - } -} - -// ── Helpers ────────────────────────────────────────────────────────────────── -function formatDuration(ms: number): string { - if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s` - if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s` - return `${ms}ms` -} - -function backendStatusToVariant(status: string): 'success' | 'error' | 'running' | 'warning' { - switch (status.toUpperCase()) { - case 'COMPLETED': return 'success' - case 'FAILED': return 'error' - case 'RUNNING': return 'running' - default: return 'warning' - } -} - -function backendStatusToLabel(status: string): string { - return status.toUpperCase() -} - -function procStatusToStep(status: string): 'ok' | 'slow' | 'fail' { - const s = status.toUpperCase() - if (s === 'FAILED') return 'fail' - if (s === 'RUNNING') return 'slow' - return 'ok' -} - -function parseHeaders(raw: string | undefined | null): Record { - if (!raw) return {} - try { - const parsed = JSON.parse(raw) - if (typeof parsed === 'object' && parsed !== null) { - const result: Record = {} - for (const [k, v] of Object.entries(parsed)) { - result[k] = typeof v === 'string' ? v : JSON.stringify(v) - } - return result - } - } catch { /* ignore */ } - return {} -} - -function countProcessors(nodes: Array<{ children?: any[] }>): number { - return nodes.reduce((sum, n) => sum + 1 + countProcessors(n.children || []), 0) -} - -// ── ExchangeDetail ─────────────────────────────────────────────────────────── -export default function ExchangeDetail() { - const { id } = useParams<{ id: string }>() - const navigate = useNavigate() - - const { data: detail, isLoading } = useExecutionDetail(id ?? null) - const { data: correlationData } = useCorrelationChain(detail?.correlationId ?? null) - - const [timelineView, setTimelineView] = useState<'gantt' | 'flow'>('gantt') - const [logSearch, setLogSearch] = useState('') - const [logLevels, setLogLevels] = useState>(new Set()) - - // Replay modal state - const [replayOpen, setReplayOpen] = useState(false) - const [replayHeaders, setReplayHeaders] = useState>([]) - const [replayBody, setReplayBody] = useState('') - const [replayAgent, setReplayAgent] = useState('') - const [replayTab, setReplayTab] = useState('headers') - - const procList = detail - ? (detail.processors ?? []) - : [] - - // Subscribe to tracing state for badge rendering - const tracedMap = useTracingStore((s) => s.tracedProcessors[detail?.applicationName ?? '']) - - function badgesFor(processorId: string): NodeBadge[] | undefined { - if (!tracedMap || !(processorId in tracedMap)) return undefined - return [{ label: 'Traced', variant: 'info' }] - } - - // Flatten processor tree into ProcessorStep[] - const processors: ProcessorStep[] = useMemo(() => { - if (!procList.length) return [] - const result: ProcessorStep[] = [] - let offset = 0 - function walk(node: any) { - const pid = node.processorId || node.processorType - result.push({ - name: pid, - type: node.processorType, - durationMs: node.durationMs ?? 0, - status: procStatusToStep(node.status ?? ''), - startMs: offset, - badges: badgesFor(node.processorId || ''), - }) - offset += node.durationMs ?? 0 - if (node.children) node.children.forEach(walk) - } - procList.forEach(walk) - return result - }, [procList, tracedMap]) - - // Flatten processor tree into raw node objects (for attribute access) - const flatProcNodes = useMemo(() => { - const nodes: any[] = [] - function walk(node: any) { - nodes.push(node) - if (node.children) node.children.forEach(walk) - } - procList.forEach(walk) - return nodes - }, [procList]) - - // Default selected processor: first failed, or 0 - const defaultIndex = useMemo(() => { - if (!processors.length) return 0 - const failIdx = processors.findIndex((p) => p.status === 'fail') - return failIdx >= 0 ? failIdx : 0 - }, [processors]) - - const [selectedProcessorIndex, setSelectedProcessorIndex] = useState(null) - const activeIndex = selectedProcessorIndex ?? defaultIndex - - const { data: snapshot } = useProcessorSnapshot( - id ?? null, - procList.length > 0 ? activeIndex : null, - ) - - const selectedProc = processors[activeIndex] - const isSelectedFailed = selectedProc?.status === 'fail' - - // Parse snapshot data - const inputHeaders = parseHeaders(snapshot?.inputHeaders) - const outputHeaders = parseHeaders(snapshot?.outputHeaders) - const inputBody = snapshot?.inputBody ?? null - const outputBody = snapshot?.outputBody ?? null - - // ProcessorId lookup: timeline index → processorId - const processorIds: string[] = useMemo(() => { - const ids: string[] = [] - function walk(node: any) { - ids.push(node.processorId || '') - if (node.children) node.children.forEach(walk) - } - procList.forEach(walk) - return ids - }, [procList]) - - - // ── Tracing toggle ────────────────────────────────────────────────────── - const { toast } = useToast() - const tracingStore = useTracingStore() - const app = detail?.applicationName ?? '' - const { data: appConfig } = useApplicationConfig(app || undefined) - const updateConfig = useUpdateApplicationConfig() - - // Sync tracing store with server config - useEffect(() => { - if (appConfig?.tracedProcessors && app) { - tracingStore.syncFromServer(app, appConfig.tracedProcessors) - } - }, [appConfig, app]) - - const handleToggleTracing = useCallback((processorId: string) => { - if (!processorId || !detail?.applicationName || !appConfig) return - const newMap = tracingStore.toggleProcessor(app, processorId) - const updatedConfig = { - ...appConfig, - tracedProcessors: { ...newMap }, - } - updateConfig.mutate(updatedConfig, { - onSuccess: (saved) => { - const action = processorId in newMap ? 'enabled' : 'disabled' - toast({ title: `Tracing ${action}`, description: `${processorId} — config v${saved.version}`, variant: 'success' }) - }, - onError: () => { - tracingStore.toggleProcessor(app, processorId) - toast({ title: 'Config update failed', description: 'Could not save configuration', variant: 'error' }) - }, - }) - }, [detail, app, appConfig, tracingStore, updateConfig, toast]) - - // ── ExecutionDiagram support ────────────────────────────────────────── - const { timeRange } = useGlobalFilters() - const { data: catalog } = useRouteCatalog( - timeRange.start.toISOString(), - timeRange.end.toISOString(), - ) - - const knownRouteIds = useMemo(() => { - if (!catalog || !app) return new Set() - const appEntry = (catalog as Array<{ appId: string; routes?: Array<{ routeId: string }> }>) - .find(a => a.appId === app) - return new Set((appEntry?.routes ?? []).map(r => r.routeId)) - }, [catalog, app]) - - const nodeConfigs = useMemo(() => { - const map = new Map() - if (tracedMap) { - for (const pid of Object.keys(tracedMap)) { - map.set(pid, { traceEnabled: true }) - } - } - return map - }, [tracedMap]) - - const handleNodeAction = useCallback((nodeId: string, action: NodeAction) => { - if (action === 'toggle-trace') { - handleToggleTracing(nodeId) - } else if (action === 'configure-tap' && detail?.applicationName) { - navigate(`/admin/appconfig?app=${encodeURIComponent(detail.applicationName)}&processor=${encodeURIComponent(nodeId)}`) - } - }, [handleToggleTracing, detail?.applicationName, navigate]) - - // ── Replay ───────────────────────────────────────────────────────────── - const { data: liveAgents } = useAgents('LIVE', detail?.applicationName) - const replay = useReplayExchange() - - // Pre-populate replay form when modal opens - useEffect(() => { - if (!replayOpen || !detail) return - try { - const parsed = JSON.parse(detail.inputHeaders ?? '{}') - const entries = Object.entries(parsed).map(([k, v]) => ({ - key: k, - value: typeof v === 'string' ? v : JSON.stringify(v), - })) - setReplayHeaders(entries.length > 0 ? entries : [{ key: '', value: '' }]) - } catch { - setReplayHeaders([{ key: '', value: '' }]) - } - setReplayBody(detail.inputBody ?? '') - // Default to current agent if it is live - const agentIds = (liveAgents ?? []).map((a: any) => a.id) - setReplayAgent(agentIds.includes(detail.agentId) ? detail.agentId : (agentIds[0] ?? '')) - setReplayTab('headers') - }, [replayOpen]) - - function handleReplay() { - const headers: Record = {} - replayHeaders.forEach((h) => { if (h.key) headers[h.key] = h.value }) - replay.mutate( - { agentId: replayAgent, headers, body: replayBody }, - { - onSuccess: () => { toast({ title: 'Replay command sent', variant: 'success' }); setReplayOpen(false) }, - onError: (err) => { toast({ title: `Replay failed: ${err.message}`, variant: 'error' }) }, - }, - ) - } - - // Correlation chain - const correlatedExchanges = useMemo(() => { - if (!correlationData?.data || correlationData.data.length <= 1) return [] - return correlationData.data - }, [correlationData]) - - // Set semantic breadcrumb in TopBar when detail is loaded - const breadcrumbItems = useMemo(() => detail ? [ - { label: 'Applications', href: '/apps' }, - { label: detail.applicationName || 'App', href: `/apps/${detail.applicationName}` }, - { label: detail.routeId, href: `/apps/${detail.applicationName}/${detail.routeId}` }, - { label: detail.executionId || '' }, - ] : null, [detail?.applicationName, detail?.routeId, detail?.executionId]) - useBreadcrumb(breadcrumbItems) - - // Exchange logs from OpenSearch (filtered by exchangeId via MDC) - const { data: rawLogs } = useApplicationLogs( - detail?.applicationName, - undefined, - { exchangeId: detail?.exchangeId ?? undefined }, - ) - const logEntries = useMemo( - () => (rawLogs || []).map((l) => ({ - timestamp: l.timestamp ?? '', - level: mapLogLevel(l.level), - message: l.message ?? '', - })), - [rawLogs], - ) - const logSearchLower = logSearch.toLowerCase() - const filteredLogs = logEntries - .filter((l) => logLevels.size === 0 || logLevels.has(l.level)) - .filter((l) => !logSearchLower || l.message.toLowerCase().includes(logSearchLower)) - - // ── Loading state ──────────────────────────────────────────────────────── - if (isLoading) { - return ( -
- -
- ) - } - - // ── Not found state ────────────────────────────────────────────────────── - if (!detail) { - return ( -
- Exchange "{id}" not found. -
- ) - } - - const statusVariant = backendStatusToVariant(detail.status) - const statusLabel = backendStatusToLabel(detail.status) - - return ( -
- - {/* Exchange header card */} -
-
-
- -
-
- {detail.executionId} - -
-
- Route: navigate(`/apps/${detail.applicationName}/${detail.routeId}`)}>{detail.routeId} - {detail.applicationName && ( - <> - · - App: {detail.applicationName} - - )} - {detail.correlationId && ( - <> - · - Correlation: {detail.correlationId} - - )} -
-
-
-
-
-
Duration
-
{formatDuration(detail.durationMs)}
-
-
-
Agent
-
{detail.agentId}
-
-
-
Started
-
- {detail.startTime - ? new Date(detail.startTime).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) - : '\u2014'} -
-
-
-
Processors
-
{countProcessors(procList)}
-
- -
-
- - {/* Route-level Attributes */} - {detail.attributes && Object.keys(detail.attributes).length > 0 && ( -
- Attributes - {Object.entries(detail.attributes).map(([key, value]) => ( - - ))} -
- )} - - {/* Correlation Chain */} - {correlatedExchanges.length > 1 && ( -
- Correlated Exchanges - {correlatedExchanges.map((ce) => { - const isCurrent = ce.executionId === id - const variant = backendStatusToVariant(ce.status) - const statusCls = - variant === 'success' ? styles.chainNodeSuccess - : variant === 'error' ? styles.chainNodeError - : variant === 'running' ? styles.chainNodeRunning - : styles.chainNodeWarning - return ( - - ) - })} - {correlationData && correlationData.total > 20 && ( - +{correlationData.total - 20} more - )} -
- )} -
- - {/* Processor Timeline Section */} -
-
- - Processor Timeline - {processors.length} processors - -
- - -
-
- {timelineView === 'gantt' && ( -
- {processors.length > 0 ? ( - setSelectedProcessorIndex(index)} - selectedIndex={activeIndex} - getActions={(_proc, index) => { - const pid = processorIds[index] - if (!pid || !detail?.applicationName) return [] - return [{ - label: tracingStore.isTraced(app, pid) ? 'Disable Tracing' : 'Enable Tracing', - onClick: () => handleToggleTracing(pid), - disabled: updateConfig.isPending, - }] - }} - /> - ) : ( - No processor data available - )} -
- )} -
- - {timelineView === 'flow' && detail && ( -
- -
- )} - - {/* Exchange-level body (start/end of route) */} - {detail && (detail.inputBody || detail.outputBody) && ( -
-
-
- - Exchange Input - - at route entry -
-
- {detail.inputHeaders && ( -
-
Headers
-
- {Object.entries(parseHeaders(detail.inputHeaders)).map(([key, value]) => ( -
- {key} - {value} -
- ))} -
-
- )} -
-
Body
- -
-
-
-
-
- - Exchange Output - - at route exit -
-
- {detail.outputHeaders && ( -
-
Headers
-
- {Object.entries(parseHeaders(detail.outputHeaders)).map(([key, value]) => ( -
- {key} - {value} -
- ))} -
-
- )} -
-
Body
- -
-
-
-
- )} - - {/* Processor Attributes */} - {selectedProc && (() => { - const procNode = flatProcNodes[activeIndex] - return procNode?.attributes && Object.keys(procNode.attributes).length > 0 ? ( -
- Processor Attributes - {Object.entries(procNode.attributes).map(([key, value]) => ( - - ))} -
- ) : null - })()} - - {/* Processor Detail Panel (split IN / OUT) */} - {selectedProc && snapshot && ( -
- {/* Message IN */} -
-
- - Message IN - - at processor #{activeIndex + 1} entry -
-
- {Object.keys(inputHeaders).length > 0 && ( -
-
- Headers {Object.keys(inputHeaders).length} -
-
- {Object.entries(inputHeaders).map(([key, value]) => ( -
- {key} - {value} -
- ))} -
-
- )} -
-
Body
- -
-
-
- - {/* Message OUT or Error */} - {isSelectedFailed ? ( -
-
- - × Error at Processor #{activeIndex + 1} - - -
-
- {detail.errorMessage && ( -
{detail.errorMessage}
- )} -
- Processor - {selectedProc.name} - Duration - {formatDuration(selectedProc.durationMs)} - Status - {selectedProc.status.toUpperCase()} -
-
-
- ) : ( -
-
- - Message OUT - - after processor #{activeIndex + 1} -
-
- {Object.keys(outputHeaders).length > 0 && ( -
-
- Headers {Object.keys(outputHeaders).length} -
-
- {Object.entries(outputHeaders).map(([key, value]) => ( -
- {key} - {value} -
- ))} -
-
- )} -
-
Body
- -
-
-
- )} -
- )} - - {/* Snapshot loading indicator */} - {selectedProc && !snapshot && procList.length > 0 && ( -
- Loading exchange snapshot... -
- )} - - {/* Exchange Application Log */} - {detail && ( -
-
- Application Log - {logEntries.length} entries -
-
-
- setLogSearch(e.target.value)} - aria-label="Search logs" - /> - {logSearch && ( - - )} -
- - {logLevels.size > 0 && ( - - )} -
- {filteredLogs.length > 0 ? ( - - ) : ( -
- {logSearch || logLevels.size > 0 ? 'No matching log entries' : 'No logs captured for this exchange'} -
- )} -
- )} - - {/* Replay Modal */} - setReplayOpen(false)} title="Replay Exchange" size="lg"> -
- This will re-send the exchange to a live agent. The agent will process - it as a new exchange. Use with caution in production environments. -
- -
- Target Agent - { - const next = [...replayHeaders] - next[i] = { ...next[i], key: e.target.value } - setReplayHeaders(next) - }} - /> - { - const next = [...replayHeaders] - next[i] = { ...next[i], value: e.target.value } - setReplayHeaders(next) - }} - /> - -
- ))} - -
- )} - - {replayTab === 'body' && ( -
-