2026-03-28 15:48:38 +01:00
|
|
|
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
|
|
|
|
import { useNavigate, useLocation } from 'react-router';
|
2026-03-28 13:57:13 +01:00
|
|
|
import { useGlobalFilters } 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-28 14:37:58 +01:00
|
|
|
import type { NodeAction } from '../../components/ProcessDiagram/types';
|
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();
|
|
|
|
|
|
|
|
|
|
// Restore selection from browser history state (enables Back/Forward)
|
|
|
|
|
const stateSelected = (location.state as any)?.selectedExchange as SelectedExchange | undefined;
|
|
|
|
|
const [selected, setSelectedInternal] = useState<SelectedExchange | null>(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]);
|
|
|
|
|
|
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
|
2026-03-28 15:26:01 +01:00
|
|
|
const handleCorrelatedSelect = useCallback((executionId: string, applicationName: string, routeId: string) => {
|
2026-03-28 15:48:38 +01:00
|
|
|
const exchange = { executionId, applicationName, routeId };
|
|
|
|
|
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-28 15:48:38 +01:00
|
|
|
// Clear selection: push a history entry without selection (so Back returns to selected state)
|
2026-03-28 15:42:45 +01:00
|
|
|
const handleClearSelection = useCallback(() => {
|
2026-03-28 15:48:38 +01:00
|
|
|
setSelectedInternal(null);
|
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:20:17 +01:00
|
|
|
// No exchange selected: full-width Dashboard
|
|
|
|
|
if (!selected) {
|
|
|
|
|
return <Dashboard onExchangeSelect={handleExchangeSelect} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Exchange selected: resizable split — Dashboard on left, diagram on right
|
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
|
|
|
|
|
appId={selected.applicationName}
|
|
|
|
|
routeId={selected.routeId}
|
|
|
|
|
exchangeId={selected.executionId}
|
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:20:17 +01:00
|
|
|
exchangeId: string;
|
2026-03-28 15:26:01 +01:00
|
|
|
onCorrelatedSelect: (executionId: string, applicationName: 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 14:37:58 +01:00
|
|
|
const navigate = useNavigate();
|
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:20:17 +01:00
|
|
|
const { data: detail } = useExecutionDetail(exchangeId);
|
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 14:37:58 +01:00
|
|
|
const handleNodeAction = useCallback((nodeId: string, action: NodeAction) => {
|
|
|
|
|
if (action === 'configure-tap') {
|
|
|
|
|
navigate(`/admin/appconfig?app=${encodeURIComponent(appId)}&processor=${encodeURIComponent(nodeId)}`);
|
|
|
|
|
}
|
|
|
|
|
}, [appId, navigate]);
|
|
|
|
|
|
2026-03-28 15:20:17 +01:00
|
|
|
if (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 14:37:58 +01:00
|
|
|
onNodeAction={handleNodeAction}
|
2026-03-28 14:22:34 +01:00
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-03-28 13:57:13 +01:00
|
|
|
|
2026-03-28 14:22:34 +01:00
|
|
|
return (
|
|
|
|
|
<div className={styles.emptyRight}>
|
2026-03-28 15:20:17 +01:00
|
|
|
Loading execution...
|
2026-03-28 13:57:13 +01:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|