diff --git a/ui/src/pages/Exchanges/ExchangesPage.module.css b/ui/src/pages/Exchanges/ExchangesPage.module.css index c6b61565..3c545469 100644 --- a/ui/src/pages/Exchanges/ExchangesPage.module.css +++ b/ui/src/pages/Exchanges/ExchangesPage.module.css @@ -1,6 +1,5 @@ .splitView { - display: grid; - grid-template-columns: 1fr 1fr; + display: flex; height: 100%; overflow: hidden; } @@ -8,14 +7,30 @@ .leftPanel { overflow: auto; height: 100%; - border-right: 1px solid var(--border); + flex-shrink: 0; +} + +.splitter { + width: 5px; + flex-shrink: 0; + cursor: col-resize; + background: var(--border); + transition: background 0.15s; + touch-action: none; +} + +.splitter:hover, +.splitter:active { + background: var(--amber); } .rightPanel { + flex: 1; display: flex; flex-direction: column; overflow: hidden; height: 100%; + min-width: 0; } .emptyRight { diff --git a/ui/src/pages/Exchanges/ExchangesPage.tsx b/ui/src/pages/Exchanges/ExchangesPage.tsx index eb863470..3a09af0b 100644 --- a/ui/src/pages/Exchanges/ExchangesPage.tsx +++ b/ui/src/pages/Exchanges/ExchangesPage.tsx @@ -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 ; } - // Route scoped: 50:50 split — Dashboard table on left, diagram on right + // Route scoped: resizable split — Dashboard table on left, diagram on right + return ; +} + +// ─── 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(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 ( -
-
+
+
-
- +
+
+
); @@ -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(); @@ -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 (