2026-03-28 15:48:38 +01:00
|
|
|
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
2026-03-28 15:58:38 +01:00
|
|
|
import { useNavigate, useLocation, useParams } from 'react-router';
|
2026-03-29 13:08:58 +02:00
|
|
|
import { useGlobalFilters, useToast } from '@cameleer/design-system';
|
2026-03-28 14:22:34 +01:00
|
|
|
import { useExecutionDetail } from '../../api/queries/executions';
|
2026-03-28 13:57:13 +01:00
|
|
|
import { useDiagramByRoute } from '../../api/queries/diagrams';
|
|
|
|
|
import { useRouteCatalog } from '../../api/queries/catalog';
|
2026-03-29 13:08:58 +02:00
|
|
|
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands';
|
2026-04-02 19:08:00 +02:00
|
|
|
import type { TapDefinition, ConfigUpdateResponse } from '../../api/queries/commands';
|
2026-03-28 16:04:53 +01:00
|
|
|
import { useTracingStore } from '../../stores/tracing-store';
|
|
|
|
|
import type { NodeAction, NodeConfig } from '../../components/ProcessDiagram/types';
|
2026-03-29 13:08:58 +02:00
|
|
|
import { TapConfigModal } from '../../components/TapConfigModal';
|
2026-03-28 13:57:13 +01:00
|
|
|
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';
|
2026-03-28 15:20:17 +01:00
|
|
|
import type { SelectedExchange } from '../Dashboard/Dashboard';
|
2026-03-28 13:57:13 +01:00
|
|
|
|
|
|
|
|
export default function ExchangesPage() {
|
2026-03-28 15:48:38 +01:00
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const location = useLocation();
|
2026-03-31 15:26:36 +02:00
|
|
|
const { appId: scopedAppId, routeId: scopedRouteId, exchangeId: scopedExchangeId } =
|
|
|
|
|
useParams<{ appId?: string; routeId?: string; exchangeId?: string }>();
|
2026-03-28 15:48:38 +01:00
|
|
|
|
|
|
|
|
// Restore selection from browser history state (enables Back/Forward)
|
|
|
|
|
const stateSelected = (location.state as any)?.selectedExchange as SelectedExchange | undefined;
|
|
|
|
|
|
2026-03-31 15:26:36 +02:00
|
|
|
// Derive selection from URL params when no state-based selection exists (Cmd-K, bookmarks)
|
|
|
|
|
const urlDerivedExchange: SelectedExchange | null =
|
|
|
|
|
(scopedExchangeId && scopedAppId && scopedRouteId)
|
fix: update frontend field names for identity rename (applicationId, instanceId)
The backend identity rename (applicationName → applicationId,
agentId → instanceId) was not reflected in the frontend. This caused
drilldown to fail (detail.applicationName was undefined, disabling
the diagram fetch) and various display issues.
Updated schema.d.ts, ExchangeHeader, ExecutionDiagram, Dashboard,
AgentHealth, AgentInstance, LayoutShell, LogTab, InfoTab, DetailPanel,
ExchangesPage, and tracing-store.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:22:16 +02:00
|
|
|
? { executionId: scopedExchangeId, applicationId: scopedAppId, routeId: scopedRouteId }
|
2026-03-31 15:26:36 +02:00
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
const [selected, setSelectedInternal] = useState<SelectedExchange | null>(stateSelected ?? urlDerivedExchange);
|
|
|
|
|
|
|
|
|
|
// Sync selection from history state or URL params on navigation changes
|
2026-03-28 15:48:38 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
const restored = (location.state as any)?.selectedExchange as SelectedExchange | undefined;
|
2026-03-31 15:26:36 +02:00
|
|
|
if (restored) {
|
|
|
|
|
setSelectedInternal(restored);
|
|
|
|
|
} else if (scopedExchangeId && scopedAppId && scopedRouteId) {
|
|
|
|
|
setSelectedInternal({
|
|
|
|
|
executionId: scopedExchangeId,
|
fix: update frontend field names for identity rename (applicationId, instanceId)
The backend identity rename (applicationName → applicationId,
agentId → instanceId) was not reflected in the frontend. This caused
drilldown to fail (detail.applicationName was undefined, disabling
the diagram fetch) and various display issues.
Updated schema.d.ts, ExchangeHeader, ExecutionDiagram, Dashboard,
AgentHealth, AgentInstance, LayoutShell, LogTab, InfoTab, DetailPanel,
ExchangesPage, and tracing-store.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:22:16 +02:00
|
|
|
applicationId: scopedAppId,
|
2026-03-31 15:26:36 +02:00
|
|
|
routeId: scopedRouteId,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
setSelectedInternal(null);
|
|
|
|
|
}
|
|
|
|
|
}, [location.state, scopedExchangeId, scopedAppId, scopedRouteId]);
|
2026-03-28 15:48:38 +01:00
|
|
|
|
2026-03-28 14:29:19 +01:00
|
|
|
const [splitPercent, setSplitPercent] = useState(50);
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
2026-03-28 15:48:38 +01:00
|
|
|
// Select an exchange: push a history entry so Back restores the previous state
|
2026-03-28 15:20:17 +01:00
|
|
|
const handleExchangeSelect = useCallback((exchange: SelectedExchange) => {
|
2026-03-28 15:48:38 +01:00
|
|
|
setSelectedInternal(exchange);
|
|
|
|
|
navigate(location.pathname + location.search, {
|
|
|
|
|
state: { ...location.state, selectedExchange: exchange },
|
|
|
|
|
});
|
|
|
|
|
}, [navigate, location.pathname, location.search, location.state]);
|
2026-03-28 15:20:17 +01:00
|
|
|
|
2026-03-28 15:48:38 +01:00
|
|
|
// Select a correlated exchange: push another history entry
|
fix: update frontend field names for identity rename (applicationId, instanceId)
The backend identity rename (applicationName → applicationId,
agentId → instanceId) was not reflected in the frontend. This caused
drilldown to fail (detail.applicationName was undefined, disabling
the diagram fetch) and various display issues.
Updated schema.d.ts, ExchangeHeader, ExecutionDiagram, Dashboard,
AgentHealth, AgentInstance, LayoutShell, LogTab, InfoTab, DetailPanel,
ExchangesPage, and tracing-store.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:22:16 +02:00
|
|
|
const handleCorrelatedSelect = useCallback((executionId: string, applicationId: string, routeId: string) => {
|
|
|
|
|
const exchange = { executionId, applicationId, routeId };
|
2026-03-28 15:48:38 +01:00
|
|
|
setSelectedInternal(exchange);
|
|
|
|
|
navigate(location.pathname + location.search, {
|
|
|
|
|
state: { ...location.state, selectedExchange: exchange },
|
|
|
|
|
});
|
|
|
|
|
}, [navigate, location.pathname, location.search, location.state]);
|
2026-03-28 15:26:01 +01:00
|
|
|
|
2026-03-31 15:26:36 +02:00
|
|
|
// Clear selection: navigate up to route level when URL has exchangeId
|
2026-03-28 15:42:45 +01:00
|
|
|
const handleClearSelection = useCallback(() => {
|
2026-03-28 15:48:38 +01:00
|
|
|
setSelectedInternal(null);
|
2026-03-31 15:26:36 +02:00
|
|
|
if (scopedExchangeId && scopedAppId && scopedRouteId) {
|
|
|
|
|
navigate(`/exchanges/${scopedAppId}/${scopedRouteId}`, {
|
|
|
|
|
state: { ...location.state, selectedExchange: undefined },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [scopedExchangeId, scopedAppId, scopedRouteId, navigate, location.state]);
|
2026-03-28 15:42:45 +01:00
|
|
|
|
2026-03-28 14:29:19 +01:00
|
|
|
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);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-03-28 15:58:38 +01:00
|
|
|
// Show split view when a route is scoped (sidebar) or an exchange is selected
|
|
|
|
|
const showSplit = !!selected || !!scopedRouteId;
|
|
|
|
|
|
|
|
|
|
if (!showSplit) {
|
2026-03-28 15:20:17 +01:00
|
|
|
return <Dashboard onExchangeSelect={handleExchangeSelect} />;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 15:58:38 +01:00
|
|
|
// Determine what the right panel shows
|
fix: update frontend field names for identity rename (applicationId, instanceId)
The backend identity rename (applicationName → applicationId,
agentId → instanceId) was not reflected in the frontend. This caused
drilldown to fail (detail.applicationName was undefined, disabling
the diagram fetch) and various display issues.
Updated schema.d.ts, ExchangeHeader, ExecutionDiagram, Dashboard,
AgentHealth, AgentInstance, LayoutShell, LogTab, InfoTab, DetailPanel,
ExchangesPage, and tracing-store.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:22:16 +02:00
|
|
|
const panelAppId = selected?.applicationId ?? scopedAppId!;
|
2026-03-28 15:58:38 +01:00
|
|
|
const panelRouteId = selected?.routeId ?? scopedRouteId!;
|
|
|
|
|
const panelExchangeId = selected?.executionId ?? undefined;
|
|
|
|
|
|
2026-03-28 13:57:13 +01:00
|
|
|
return (
|
2026-03-28 14:29:19 +01:00
|
|
|
<div ref={containerRef} className={styles.splitView}>
|
|
|
|
|
<div className={styles.leftPanel} style={{ width: `${splitPercent}%` }}>
|
2026-03-28 15:20:17 +01:00
|
|
|
<Dashboard onExchangeSelect={handleExchangeSelect} />
|
2026-03-28 14:22:34 +01:00
|
|
|
</div>
|
2026-03-28 14:29:19 +01:00
|
|
|
<div className={styles.splitter} onPointerDown={handleSplitterDown} />
|
|
|
|
|
<div className={styles.rightPanel} style={{ width: `${100 - splitPercent}%` }}>
|
2026-03-28 15:20:17 +01:00
|
|
|
<DiagramPanel
|
2026-03-28 15:58:38 +01:00
|
|
|
appId={panelAppId}
|
|
|
|
|
routeId={panelRouteId}
|
|
|
|
|
exchangeId={panelExchangeId}
|
2026-03-28 15:26:01 +01:00
|
|
|
onCorrelatedSelect={handleCorrelatedSelect}
|
2026-03-28 15:42:45 +01:00
|
|
|
onClearSelection={handleClearSelection}
|
2026-03-28 15:20:17 +01:00
|
|
|
/>
|
2026-03-28 14:22:34 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-28 13:57:13 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 15:20:17 +01:00
|
|
|
// ─── Right panel: diagram + execution overlay ───────────────────────────────
|
2026-03-28 13:57:13 +01:00
|
|
|
|
2026-03-28 14:22:34 +01:00
|
|
|
interface DiagramPanelProps {
|
2026-03-28 13:57:13 +01:00
|
|
|
appId: string;
|
|
|
|
|
routeId: string;
|
2026-03-28 15:58:38 +01:00
|
|
|
exchangeId?: string;
|
fix: update frontend field names for identity rename (applicationId, instanceId)
The backend identity rename (applicationName → applicationId,
agentId → instanceId) was not reflected in the frontend. This caused
drilldown to fail (detail.applicationName was undefined, disabling
the diagram fetch) and various display issues.
Updated schema.d.ts, ExchangeHeader, ExecutionDiagram, Dashboard,
AgentHealth, AgentInstance, LayoutShell, LogTab, InfoTab, DetailPanel,
ExchangesPage, and tracing-store.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:22:16 +02:00
|
|
|
onCorrelatedSelect: (executionId: string, applicationId: string, routeId: string) => void;
|
2026-03-28 15:42:45 +01:00
|
|
|
onClearSelection: () => void;
|
2026-03-28 13:57:13 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-28 15:42:45 +01:00
|
|
|
function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearSelection }: DiagramPanelProps) {
|
2026-03-28 13:57:13 +01:00
|
|
|
const { timeRange } = useGlobalFilters();
|
|
|
|
|
const timeFrom = timeRange.start.toISOString();
|
|
|
|
|
const timeTo = timeRange.end.toISOString();
|
|
|
|
|
|
2026-03-28 15:58:38 +01:00
|
|
|
const { data: detail } = useExecutionDetail(exchangeId ?? null);
|
|
|
|
|
const diagramQuery = useDiagramByRoute(appId, routeId);
|
2026-03-28 13:57:13 +01:00
|
|
|
|
|
|
|
|
const { data: catalog } = useRouteCatalog(timeFrom, timeTo);
|
|
|
|
|
const knownRouteIds = useMemo(() => {
|
|
|
|
|
const ids = new Set<string>();
|
|
|
|
|
if (catalog) {
|
|
|
|
|
for (const app of catalog as any[]) {
|
|
|
|
|
for (const r of app.routes || []) {
|
|
|
|
|
ids.add(r.routeId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ids;
|
|
|
|
|
}, [catalog]);
|
|
|
|
|
|
2026-03-28 18:31:08 +01:00
|
|
|
// Build endpoint URI → routeId map for cross-route drill-down
|
|
|
|
|
const endpointRouteMap = useMemo(() => {
|
|
|
|
|
const map = new Map<string, string>();
|
|
|
|
|
if (catalog) {
|
|
|
|
|
for (const app of catalog as any[]) {
|
|
|
|
|
for (const r of app.routes || []) {
|
|
|
|
|
if (r.fromEndpointUri) {
|
|
|
|
|
map.set(r.fromEndpointUri, r.routeId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return map;
|
|
|
|
|
}, [catalog]);
|
|
|
|
|
|
2026-03-30 18:08:40 +02:00
|
|
|
// Build nodeConfigs from app config (for TRACE/TAP badges)
|
2026-03-28 16:04:53 +01:00
|
|
|
const { data: appConfig } = useApplicationConfig(appId);
|
|
|
|
|
const nodeConfigs = useMemo(() => {
|
|
|
|
|
const map = new Map<string, NodeConfig>();
|
2026-03-30 18:08:40 +02:00
|
|
|
if (appConfig?.tracedProcessors) {
|
|
|
|
|
for (const pid of Object.keys(appConfig.tracedProcessors)) {
|
2026-03-28 16:04:53 +01:00
|
|
|
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;
|
2026-03-30 18:08:40 +02:00
|
|
|
}, [appConfig]);
|
2026-03-28 16:04:53 +01:00
|
|
|
|
2026-03-29 13:08:58 +02:00
|
|
|
// Processor options for tap modal dropdown
|
|
|
|
|
const processorOptions = useMemo(() => {
|
|
|
|
|
const nodes = diagramQuery.data?.nodes;
|
|
|
|
|
if (!nodes) return [];
|
|
|
|
|
return (nodes as Array<{ id?: string; label?: string }>)
|
|
|
|
|
.filter(n => n.id)
|
|
|
|
|
.map(n => ({ value: n.id!, label: n.label || n.id! }));
|
|
|
|
|
}, [diagramQuery.data]);
|
|
|
|
|
|
|
|
|
|
// Tap modal state
|
|
|
|
|
const [tapModalOpen, setTapModalOpen] = useState(false);
|
|
|
|
|
const [tapModalTarget, setTapModalTarget] = useState<string | undefined>();
|
|
|
|
|
const [editingTap, setEditingTap] = useState<TapDefinition | null>(null);
|
|
|
|
|
|
|
|
|
|
const updateConfig = useUpdateApplicationConfig();
|
|
|
|
|
const { toast } = useToast();
|
|
|
|
|
|
|
|
|
|
const handleTapSave = useCallback((updatedConfig: typeof appConfig) => {
|
|
|
|
|
if (!updatedConfig) return;
|
|
|
|
|
updateConfig.mutate(updatedConfig, {
|
2026-04-02 19:08:00 +02:00
|
|
|
onSuccess: (saved: ConfigUpdateResponse) => {
|
|
|
|
|
if (saved.pushResult.success) {
|
|
|
|
|
toast({ title: 'Tap configuration saved', description: `Pushed to ${saved.pushResult.total}/${saved.pushResult.total} agents (v${saved.config.version})`, variant: 'success' });
|
|
|
|
|
} else {
|
|
|
|
|
const failed = [...saved.pushResult.responses.filter(r => r.status !== 'SUCCESS').map(r => r.agentId), ...saved.pushResult.timedOut];
|
|
|
|
|
toast({ title: 'Tap configuration saved — partial push failure', description: `${saved.pushResult.responded}/${saved.pushResult.total} responded. Failed: ${failed.join(', ')}`, variant: 'warning', duration: 86_400_000 });
|
|
|
|
|
}
|
2026-03-29 13:08:58 +02:00
|
|
|
},
|
|
|
|
|
onError: () => {
|
2026-04-02 19:08:00 +02:00
|
|
|
toast({ title: 'Tap update failed', description: 'Could not save configuration', variant: 'error', duration: 86_400_000 });
|
2026-03-29 13:08:58 +02:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}, [updateConfig, toast]);
|
|
|
|
|
|
|
|
|
|
const handleTapDelete = useCallback((tap: TapDefinition) => {
|
|
|
|
|
if (!appConfig) return;
|
|
|
|
|
const taps = appConfig.taps.filter(t => t.tapId !== tap.tapId);
|
|
|
|
|
updateConfig.mutate({ ...appConfig, taps }, {
|
2026-04-02 19:08:00 +02:00
|
|
|
onSuccess: (saved: ConfigUpdateResponse) => {
|
|
|
|
|
if (saved.pushResult.success) {
|
|
|
|
|
toast({ title: 'Tap deleted', description: `${tap.attributeName} removed — pushed to ${saved.pushResult.total}/${saved.pushResult.total} agents (v${saved.config.version})`, variant: 'success' });
|
|
|
|
|
} else {
|
|
|
|
|
const failed = [...saved.pushResult.responses.filter(r => r.status !== 'SUCCESS').map(r => r.agentId), ...saved.pushResult.timedOut];
|
|
|
|
|
toast({ title: 'Tap deleted — partial push failure', description: `${saved.pushResult.responded}/${saved.pushResult.total} responded. Failed: ${failed.join(', ')}`, variant: 'warning', duration: 86_400_000 });
|
|
|
|
|
}
|
2026-03-29 13:08:58 +02:00
|
|
|
},
|
|
|
|
|
onError: () => {
|
2026-04-02 19:08:00 +02:00
|
|
|
toast({ title: 'Tap delete failed', description: 'Could not save configuration', variant: 'error', duration: 86_400_000 });
|
2026-03-29 13:08:58 +02:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}, [appConfig, updateConfig, toast]);
|
|
|
|
|
|
2026-03-28 14:37:58 +01:00
|
|
|
const handleNodeAction = useCallback((nodeId: string, action: NodeAction) => {
|
|
|
|
|
if (action === 'configure-tap') {
|
2026-03-29 13:08:58 +02:00
|
|
|
if (!appConfig) return;
|
|
|
|
|
// Check if there's an existing tap for this processor
|
|
|
|
|
const existing = appConfig.taps?.find(t => t.processorId === nodeId) ?? null;
|
|
|
|
|
setEditingTap(existing);
|
|
|
|
|
setTapModalTarget(nodeId);
|
|
|
|
|
setTapModalOpen(true);
|
|
|
|
|
} else if (action === 'toggle-trace') {
|
|
|
|
|
if (!appConfig) return;
|
|
|
|
|
const newMap = useTracingStore.getState().toggleProcessor(appId, nodeId);
|
|
|
|
|
const enabled = nodeId in newMap;
|
|
|
|
|
const tracedProcessors: Record<string, string> = {};
|
|
|
|
|
for (const [k, v] of Object.entries(newMap)) tracedProcessors[k] = v;
|
|
|
|
|
updateConfig.mutate({
|
|
|
|
|
...appConfig,
|
|
|
|
|
tracedProcessors,
|
|
|
|
|
}, {
|
2026-04-02 19:08:00 +02:00
|
|
|
onSuccess: (saved: ConfigUpdateResponse) => {
|
|
|
|
|
if (saved.pushResult.success) {
|
|
|
|
|
toast({ title: `Tracing ${enabled ? 'enabled' : 'disabled'}`, description: `${nodeId} — pushed to ${saved.pushResult.total}/${saved.pushResult.total} agents (v${saved.config.version})`, variant: 'success' });
|
|
|
|
|
} else {
|
|
|
|
|
const failed = [...saved.pushResult.responses.filter(r => r.status !== 'SUCCESS').map(r => r.agentId), ...saved.pushResult.timedOut];
|
|
|
|
|
toast({ title: `Tracing update — partial push failure`, description: `${saved.pushResult.responded}/${saved.pushResult.total} responded. Failed: ${failed.join(', ')}`, variant: 'warning', duration: 86_400_000 });
|
|
|
|
|
}
|
2026-03-29 13:08:58 +02:00
|
|
|
},
|
|
|
|
|
onError: () => {
|
|
|
|
|
useTracingStore.getState().toggleProcessor(appId, nodeId);
|
2026-04-02 19:08:00 +02:00
|
|
|
toast({ title: 'Tracing update failed', description: 'Could not save configuration', variant: 'error', duration: 86_400_000 });
|
2026-03-29 13:08:58 +02:00
|
|
|
},
|
|
|
|
|
});
|
2026-03-28 14:37:58 +01:00
|
|
|
}
|
2026-03-29 13:08:58 +02:00
|
|
|
}, [appId, appConfig, updateConfig, toast]);
|
|
|
|
|
|
|
|
|
|
const tapModal = appConfig && (
|
|
|
|
|
<TapConfigModal
|
|
|
|
|
open={tapModalOpen}
|
|
|
|
|
onClose={() => setTapModalOpen(false)}
|
|
|
|
|
tap={editingTap}
|
|
|
|
|
processorOptions={processorOptions}
|
|
|
|
|
defaultProcessorId={tapModalTarget}
|
|
|
|
|
application={appId}
|
|
|
|
|
config={appConfig}
|
|
|
|
|
onSave={handleTapSave}
|
|
|
|
|
onDelete={handleTapDelete}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2026-03-28 14:37:58 +01:00
|
|
|
|
2026-03-28 15:58:38 +01:00
|
|
|
// Exchange selected: show header + execution diagram
|
|
|
|
|
if (exchangeId && detail) {
|
2026-03-28 14:22:34 +01:00
|
|
|
return (
|
|
|
|
|
<>
|
2026-03-28 15:42:45 +01:00
|
|
|
<ExchangeHeader detail={detail} onCorrelatedSelect={onCorrelatedSelect} onClearSelection={onClearSelection} />
|
2026-03-28 14:22:34 +01:00
|
|
|
<ExecutionDiagram
|
|
|
|
|
executionId={exchangeId}
|
|
|
|
|
executionDetail={detail}
|
|
|
|
|
knownRouteIds={knownRouteIds}
|
2026-03-28 18:31:08 +01:00
|
|
|
endpointRouteMap={endpointRouteMap}
|
2026-03-28 14:37:58 +01:00
|
|
|
onNodeAction={handleNodeAction}
|
2026-03-28 16:04:53 +01:00
|
|
|
nodeConfigs={nodeConfigs}
|
2026-03-28 14:22:34 +01:00
|
|
|
/>
|
2026-03-29 13:08:58 +02:00
|
|
|
{tapModal}
|
2026-03-28 14:22:34 +01:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-03-28 13:57:13 +01:00
|
|
|
|
2026-03-28 15:58:38 +01:00
|
|
|
// No exchange selected: show topology-only diagram
|
|
|
|
|
if (diagramQuery.data) {
|
|
|
|
|
return (
|
2026-03-29 13:08:58 +02:00
|
|
|
<>
|
|
|
|
|
<ProcessDiagram
|
|
|
|
|
application={appId}
|
|
|
|
|
routeId={routeId}
|
|
|
|
|
diagramLayout={diagramQuery.data}
|
|
|
|
|
knownRouteIds={knownRouteIds}
|
|
|
|
|
endpointRouteMap={endpointRouteMap}
|
|
|
|
|
onNodeAction={handleNodeAction}
|
|
|
|
|
nodeConfigs={nodeConfigs}
|
|
|
|
|
/>
|
|
|
|
|
{tapModal}
|
|
|
|
|
</>
|
2026-03-28 15:58:38 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 14:22:34 +01:00
|
|
|
return (
|
|
|
|
|
<div className={styles.emptyRight}>
|
2026-03-28 15:58:38 +01:00
|
|
|
Loading diagram...
|
2026-03-28 13:57:13 +01:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|