import { useState, useMemo, useCallback, useRef, useEffect } from 'react'; import { useNavigate, useLocation, useParams } from 'react-router'; import { useGlobalFilters } from '@cameleer/design-system'; import { useExecutionDetail } from '../../api/queries/executions'; import { useDiagramByRoute } from '../../api/queries/diagrams'; import { useRouteCatalog } from '../../api/queries/catalog'; import { useApplicationConfig } from '../../api/queries/commands'; import { useTracingStore } from '../../stores/tracing-store'; import type { NodeAction, NodeConfig } from '../../components/ProcessDiagram/types'; import { ExchangeHeader } from './ExchangeHeader'; import { ExecutionDiagram } from '../../components/ExecutionDiagram/ExecutionDiagram'; import { ProcessDiagram } from '../../components/ProcessDiagram'; import styles from './ExchangesPage.module.css'; import Dashboard from '../Dashboard/Dashboard'; import type { SelectedExchange } from '../Dashboard/Dashboard'; export default function ExchangesPage() { const navigate = useNavigate(); const location = useLocation(); const { appId: scopedAppId, routeId: scopedRouteId } = useParams<{ appId?: string; routeId?: string }>(); // Restore selection from browser history state (enables Back/Forward) const stateSelected = (location.state as any)?.selectedExchange as SelectedExchange | undefined; const [selected, setSelectedInternal] = useState(stateSelected ?? null); // Sync from history state when the user navigates Back/Forward useEffect(() => { const restored = (location.state as any)?.selectedExchange as SelectedExchange | undefined; setSelectedInternal(restored ?? null); }, [location.state]); const [splitPercent, setSplitPercent] = useState(50); const containerRef = useRef(null); // Select an exchange: push a history entry so Back restores the previous state const handleExchangeSelect = useCallback((exchange: SelectedExchange) => { setSelectedInternal(exchange); navigate(location.pathname + location.search, { state: { ...location.state, selectedExchange: exchange }, }); }, [navigate, location.pathname, location.search, location.state]); // Select a correlated exchange: push another history entry const handleCorrelatedSelect = useCallback((executionId: string, applicationName: string, routeId: string) => { const exchange = { executionId, applicationName, routeId }; setSelectedInternal(exchange); navigate(location.pathname + location.search, { state: { ...location.state, selectedExchange: exchange }, }); }, [navigate, location.pathname, location.search, location.state]); // Clear selection: push a history entry without selection (so Back returns to selected state) const handleClearSelection = useCallback(() => { setSelectedInternal(null); }, []); const handleSplitterDown = useCallback((e: React.PointerEvent) => { e.currentTarget.setPointerCapture(e.pointerId); const container = containerRef.current; if (!container) return; const onMove = (me: PointerEvent) => { const rect = container.getBoundingClientRect(); const x = me.clientX - rect.left; const pct = Math.min(80, Math.max(20, (x / rect.width) * 100)); setSplitPercent(pct); }; const onUp = () => { document.removeEventListener('pointermove', onMove); document.removeEventListener('pointerup', onUp); }; document.addEventListener('pointermove', onMove); document.addEventListener('pointerup', onUp); }, []); // Show split view when a route is scoped (sidebar) or an exchange is selected const showSplit = !!selected || !!scopedRouteId; if (!showSplit) { return ; } // Determine what the right panel shows const panelAppId = selected?.applicationName ?? scopedAppId!; const panelRouteId = selected?.routeId ?? scopedRouteId!; const panelExchangeId = selected?.executionId ?? undefined; return (
); } // ─── Right panel: diagram + execution overlay ─────────────────────────────── interface DiagramPanelProps { appId: string; routeId: string; exchangeId?: string; onCorrelatedSelect: (executionId: string, applicationName: string, routeId: string) => void; onClearSelection: () => void; } function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearSelection }: DiagramPanelProps) { const navigate = useNavigate(); const { timeRange } = useGlobalFilters(); const timeFrom = timeRange.start.toISOString(); const timeTo = timeRange.end.toISOString(); const { data: detail } = useExecutionDetail(exchangeId ?? null); const diagramQuery = useDiagramByRoute(appId, routeId); const { data: catalog } = useRouteCatalog(timeFrom, timeTo); const knownRouteIds = useMemo(() => { const ids = new Set(); if (catalog) { for (const app of catalog as any[]) { for (const r of app.routes || []) { ids.add(r.routeId); } } } return ids; }, [catalog]); // Build nodeConfigs from tracing store + app config (for TRACE/TAP badges) const { data: appConfig } = useApplicationConfig(appId); const tracedMap = useTracingStore((s) => s.tracedProcessors[appId]); const nodeConfigs = useMemo(() => { const map = new Map(); if (tracedMap) { for (const pid of Object.keys(tracedMap)) { map.set(pid, { traceEnabled: true }); } } if (appConfig?.taps) { for (const tap of appConfig.taps) { if (tap.enabled) { const existing = map.get(tap.processorId); map.set(tap.processorId, { ...existing, tapExpression: tap.expression }); } } } return map; }, [tracedMap, appConfig]); const handleNodeAction = useCallback((nodeId: string, action: NodeAction) => { if (action === 'configure-tap') { navigate(`/admin/appconfig?app=${encodeURIComponent(appId)}&processor=${encodeURIComponent(nodeId)}`); } }, [appId, navigate]); // Exchange selected: show header + execution diagram if (exchangeId && detail) { return ( <> ); } // No exchange selected: show topology-only diagram if (diagramQuery.data) { return ( ); } return (
Loading diagram...
); }