Each exchange selection (from table or correlation chain) pushes a browser history entry with the selected exchange in location.state. When the user navigates away (to agent details, app scope, etc.) and presses Back, the previous history entry is restored and the split view with the diagram reappears exactly as they left it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
155 lines
5.8 KiB
TypeScript
155 lines
5.8 KiB
TypeScript
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
|
import { useNavigate, useLocation } 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 type { NodeAction } 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();
|
|
|
|
// 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]);
|
|
|
|
const [splitPercent, setSplitPercent] = useState(50);
|
|
const containerRef = useRef<HTMLDivElement>(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);
|
|
}, []);
|
|
|
|
// No exchange selected: full-width Dashboard
|
|
if (!selected) {
|
|
return <Dashboard onExchangeSelect={handleExchangeSelect} />;
|
|
}
|
|
|
|
// Exchange selected: resizable split — Dashboard on left, diagram on right
|
|
return (
|
|
<div ref={containerRef} className={styles.splitView}>
|
|
<div className={styles.leftPanel} style={{ width: `${splitPercent}%` }}>
|
|
<Dashboard onExchangeSelect={handleExchangeSelect} />
|
|
</div>
|
|
<div className={styles.splitter} onPointerDown={handleSplitterDown} />
|
|
<div className={styles.rightPanel} style={{ width: `${100 - splitPercent}%` }}>
|
|
<DiagramPanel
|
|
appId={selected.applicationName}
|
|
routeId={selected.routeId}
|
|
exchangeId={selected.executionId}
|
|
onCorrelatedSelect={handleCorrelatedSelect}
|
|
onClearSelection={handleClearSelection}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── 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);
|
|
|
|
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]);
|
|
|
|
const handleNodeAction = useCallback((nodeId: string, action: NodeAction) => {
|
|
if (action === 'configure-tap') {
|
|
navigate(`/admin/appconfig?app=${encodeURIComponent(appId)}&processor=${encodeURIComponent(nodeId)}`);
|
|
}
|
|
}, [appId, navigate]);
|
|
|
|
if (detail) {
|
|
return (
|
|
<>
|
|
<ExchangeHeader detail={detail} onCorrelatedSelect={onCorrelatedSelect} onClearSelection={onClearSelection} />
|
|
<ExecutionDiagram
|
|
executionId={exchangeId}
|
|
executionDetail={detail}
|
|
knownRouteIds={knownRouteIds}
|
|
onNodeAction={handleNodeAction}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={styles.emptyRight}>
|
|
Loading execution...
|
|
</div>
|
|
);
|
|
}
|