feat(ui): add draggable splitter between search results and diagram panel
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m9s
CI / docker (push) Successful in 1m1s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s

This commit is contained in:
hsiegeln
2026-03-28 14:29:19 +01:00
parent 699ef86f8f
commit 91171590e6
2 changed files with 58 additions and 15 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useMemo, useCallback } from 'react';
import { useState, useMemo, useCallback, useRef } from 'react';
import { useParams } from 'react-router';
import { useGlobalFilters } from '@cameleer/design-system';
import { useExecutionDetail } from '../../api/queries/executions';
@@ -22,14 +22,48 @@ export default function ExchangesPage() {
return <Dashboard />;
}
// Route scoped: 50:50 split — Dashboard table on left, diagram on right
// Route scoped: resizable split — Dashboard table on left, diagram on right
return <SplitExchangeView appId={appId!} routeId={routeId} exchangeId={exchangeId} />;
}
// ─── Resizable split view ───────────────────────────────────────────────────
interface SplitExchangeViewProps {
appId: string;
routeId: string;
exchangeId?: string;
}
function SplitExchangeView({ appId, routeId, exchangeId }: SplitExchangeViewProps) {
const [splitPercent, setSplitPercent] = useState(50);
const containerRef = useRef<HTMLDivElement>(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);
}, []);
return (
<div className={styles.splitView}>
<div className={styles.leftPanel}>
<div ref={containerRef} className={styles.splitView}>
<div className={styles.leftPanel} style={{ width: `${splitPercent}%` }}>
<Dashboard />
</div>
<div className={styles.rightPanel}>
<DiagramPanel appId={appId!} routeId={routeId} exchangeId={exchangeId} />
<div className={styles.splitter} onPointerDown={handleSplitterDown} />
<div className={styles.rightPanel} style={{ width: `${100 - splitPercent}%` }}>
<DiagramPanel appId={appId} routeId={routeId} exchangeId={exchangeId} />
</div>
</div>
);
@@ -48,13 +82,9 @@ function DiagramPanel({ appId, routeId, exchangeId }: DiagramPanelProps) {
const timeFrom = timeRange.start.toISOString();
const timeTo = timeRange.end.toISOString();
// Fetch execution detail if an exchange is selected
const { data: detail } = useExecutionDetail(exchangeId ?? null);
// Fetch diagram for topology-only view
const diagramQuery = useDiagramByRoute(appId, routeId);
// Known route IDs for drill-down
const { data: catalog } = useRouteCatalog(timeFrom, timeTo);
const knownRouteIds = useMemo(() => {
const ids = new Set<string>();
@@ -68,7 +98,6 @@ function DiagramPanel({ appId, routeId, exchangeId }: DiagramPanelProps) {
return ids;
}, [catalog]);
// If exchange selected: show header + ExecutionDiagram
if (exchangeId && detail) {
return (
<>
@@ -82,7 +111,6 @@ function DiagramPanel({ appId, routeId, exchangeId }: DiagramPanelProps) {
);
}
// No exchange: show topology-only ProcessDiagram
if (diagramQuery.data) {
return (
<ProcessDiagram