feat: integrate ExecutionDiagram into ExchangeDetail flow view
Replace the RouteFlow-based flow view with the new ExecutionDiagram component which provides execution overlay, iteration stepping, and an integrated detail panel. The gantt view and all other page sections remain unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -265,6 +265,17 @@
|
|||||||
padding: 12px 16px;
|
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)
|
DETAIL SPLIT (IN / OUT panels)
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|||||||
@@ -2,19 +2,21 @@ import { useState, useMemo, useCallback, useEffect } from 'react'
|
|||||||
import { useParams, useNavigate } from 'react-router'
|
import { useParams, useNavigate } from 'react-router'
|
||||||
import {
|
import {
|
||||||
Badge, StatusDot, MonoText, CodeBlock, InfoCallout,
|
Badge, StatusDot, MonoText, CodeBlock, InfoCallout,
|
||||||
ProcessorTimeline, Spinner, RouteFlow, useToast,
|
ProcessorTimeline, Spinner, useToast,
|
||||||
LogViewer, ButtonGroup, SectionHeader, useBreadcrumb,
|
LogViewer, ButtonGroup, SectionHeader, useBreadcrumb,
|
||||||
Modal, Tabs, Button, Select, Input, Textarea,
|
Modal, Tabs, Button, Select, Input, Textarea,
|
||||||
|
useGlobalFilters,
|
||||||
} from '@cameleer/design-system'
|
} from '@cameleer/design-system'
|
||||||
import type { ProcessorStep, RouteNode, NodeBadge, LogEntry, ButtonGroupItem } from '@cameleer/design-system'
|
import type { ProcessorStep, NodeBadge, LogEntry, ButtonGroupItem } from '@cameleer/design-system'
|
||||||
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions'
|
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions'
|
||||||
import { useCorrelationChain } from '../../api/queries/correlation'
|
import { useCorrelationChain } from '../../api/queries/correlation'
|
||||||
import { useDiagramLayout } from '../../api/queries/diagrams'
|
|
||||||
import { buildFlowSegments, toFlowSegments } from '../../utils/diagram-mapping'
|
|
||||||
import { useTracingStore } from '../../stores/tracing-store'
|
import { useTracingStore } from '../../stores/tracing-store'
|
||||||
import { useApplicationConfig, useUpdateApplicationConfig, useReplayExchange } from '../../api/queries/commands'
|
import { useApplicationConfig, useUpdateApplicationConfig, useReplayExchange } from '../../api/queries/commands'
|
||||||
import { useAgents } from '../../api/queries/agents'
|
import { useAgents } from '../../api/queries/agents'
|
||||||
import { useApplicationLogs } from '../../api/queries/logs'
|
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'
|
import styles from './ExchangeDetail.module.css'
|
||||||
|
|
||||||
const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [
|
const LOG_LEVEL_ITEMS: ButtonGroupItem[] = [
|
||||||
@@ -88,7 +90,6 @@ export default function ExchangeDetail() {
|
|||||||
|
|
||||||
const { data: detail, isLoading } = useExecutionDetail(id ?? null)
|
const { data: detail, isLoading } = useExecutionDetail(id ?? null)
|
||||||
const { data: correlationData } = useCorrelationChain(detail?.correlationId ?? null)
|
const { data: correlationData } = useCorrelationChain(detail?.correlationId ?? null)
|
||||||
const { data: diagram } = useDiagramLayout(detail?.diagramContentHash ?? null)
|
|
||||||
|
|
||||||
const [timelineView, setTimelineView] = useState<'gantt' | 'flow'>('gantt')
|
const [timelineView, setTimelineView] = useState<'gantt' | 'flow'>('gantt')
|
||||||
const [logSearch, setLogSearch] = useState('')
|
const [logSearch, setLogSearch] = useState('')
|
||||||
@@ -170,33 +171,6 @@ export default function ExchangeDetail() {
|
|||||||
const inputBody = snapshot?.inputBody ?? null
|
const inputBody = snapshot?.inputBody ?? null
|
||||||
const outputBody = snapshot?.outputBody ?? null
|
const outputBody = snapshot?.outputBody ?? null
|
||||||
|
|
||||||
// Build RouteFlow nodes from diagram + execution data, split into flow segments
|
|
||||||
const { routeFlows, flowNodeIds } = useMemo(() => {
|
|
||||||
if (diagram?.nodes) {
|
|
||||||
const { flows, nodeIds } = buildFlowSegments(diagram.nodes, procList)
|
|
||||||
// Apply badges to each node across all flows
|
|
||||||
let idx = 0
|
|
||||||
const badgedFlows = flows.map(flow => ({
|
|
||||||
...flow,
|
|
||||||
nodes: flow.nodes.map(node => ({
|
|
||||||
...node,
|
|
||||||
badges: badgesFor(nodeIds[idx++]),
|
|
||||||
})),
|
|
||||||
}))
|
|
||||||
return { routeFlows: badgedFlows, flowNodeIds: nodeIds }
|
|
||||||
}
|
|
||||||
// Fallback: build from processor list (no diagram available)
|
|
||||||
const nodes = processors.map((p) => ({
|
|
||||||
name: p.name,
|
|
||||||
type: 'process' as RouteNode['type'],
|
|
||||||
durationMs: p.durationMs,
|
|
||||||
status: p.status,
|
|
||||||
badges: badgesFor(p.name),
|
|
||||||
}))
|
|
||||||
const { flows } = toFlowSegments(nodes)
|
|
||||||
return { routeFlows: flows, flowNodeIds: [] as string[] }
|
|
||||||
}, [diagram, processors, procList, tracedMap])
|
|
||||||
|
|
||||||
// ProcessorId lookup: timeline index → processorId
|
// ProcessorId lookup: timeline index → processorId
|
||||||
const processorIds: string[] = useMemo(() => {
|
const processorIds: string[] = useMemo(() => {
|
||||||
const ids: string[] = []
|
const ids: string[] = []
|
||||||
@@ -208,12 +182,6 @@ export default function ExchangeDetail() {
|
|||||||
return ids
|
return ids
|
||||||
}, [procList])
|
}, [procList])
|
||||||
|
|
||||||
// Map flow display index → processor tree index (for snapshot API)
|
|
||||||
// flowNodeIds already contains processor IDs in flow-order
|
|
||||||
const flowToTreeIndex = useMemo(() =>
|
|
||||||
flowNodeIds.map(pid => pid ? processorIds.indexOf(pid) : -1),
|
|
||||||
[flowNodeIds, processorIds],
|
|
||||||
)
|
|
||||||
|
|
||||||
// ── Tracing toggle ──────────────────────────────────────────────────────
|
// ── Tracing toggle ──────────────────────────────────────────────────────
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
@@ -248,6 +216,36 @@ export default function ExchangeDetail() {
|
|||||||
})
|
})
|
||||||
}, [detail, app, appConfig, tracingStore, updateConfig, toast])
|
}, [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<string>()
|
||||||
|
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<string, NodeConfig>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}, [handleToggleTracing])
|
||||||
|
|
||||||
// ── Replay ─────────────────────────────────────────────────────────────
|
// ── Replay ─────────────────────────────────────────────────────────────
|
||||||
const { data: liveAgents } = useAgents('LIVE', detail?.applicationName)
|
const { data: liveAgents } = useAgents('LIVE', detail?.applicationName)
|
||||||
const replay = useReplayExchange()
|
const replay = useReplayExchange()
|
||||||
@@ -461,9 +459,9 @@ export default function ExchangeDetail() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{timelineView === 'gantt' && (
|
||||||
<div className={styles.timelineBody}>
|
<div className={styles.timelineBody}>
|
||||||
{timelineView === 'gantt' ? (
|
{processors.length > 0 ? (
|
||||||
processors.length > 0 ? (
|
|
||||||
<ProcessorTimeline
|
<ProcessorTimeline
|
||||||
processors={processors}
|
processors={processors}
|
||||||
totalMs={detail.durationMs}
|
totalMs={detail.durationMs}
|
||||||
@@ -481,33 +479,23 @@ export default function ExchangeDetail() {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<InfoCallout>No processor data available</InfoCallout>
|
<InfoCallout>No processor data available</InfoCallout>
|
||||||
)
|
|
||||||
) : (
|
|
||||||
routeFlows.length > 0 ? (
|
|
||||||
<RouteFlow
|
|
||||||
flows={routeFlows}
|
|
||||||
onNodeClick={(_node, index) => {
|
|
||||||
const treeIdx = flowToTreeIndex[index]
|
|
||||||
if (treeIdx >= 0) setSelectedProcessorIndex(treeIdx)
|
|
||||||
}}
|
|
||||||
selectedIndex={flowToTreeIndex.indexOf(activeIndex)}
|
|
||||||
getActions={(_node, index) => {
|
|
||||||
const pid = flowNodeIds[index] ?? ''
|
|
||||||
if (!pid || !detail?.applicationName) return []
|
|
||||||
return [{
|
|
||||||
label: tracingStore.isTraced(app, pid) ? 'Disable Tracing' : 'Enable Tracing',
|
|
||||||
onClick: () => handleToggleTracing(pid),
|
|
||||||
disabled: updateConfig.isPending,
|
|
||||||
}]
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Spinner />
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{timelineView === 'flow' && detail && (
|
||||||
|
<div className={styles.executionDiagramContainer}>
|
||||||
|
<ExecutionDiagram
|
||||||
|
executionId={id!}
|
||||||
|
executionDetail={detail}
|
||||||
|
knownRouteIds={knownRouteIds}
|
||||||
|
onNodeAction={handleNodeAction}
|
||||||
|
nodeConfigs={nodeConfigs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Exchange-level body (start/end of route) */}
|
{/* Exchange-level body (start/end of route) */}
|
||||||
{detail && (detail.inputBody || detail.outputBody) && (
|
{detail && (detail.inputBody || detail.outputBody) && (
|
||||||
<div className={styles.detailSplit}>
|
<div className={styles.detailSplit}>
|
||||||
|
|||||||
Reference in New Issue
Block a user