feat(ui): add draggable splitter between search results and diagram panel
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
.splitView {
|
.splitView {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -8,14 +7,30 @@
|
|||||||
.leftPanel {
|
.leftPanel {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
height: 100%;
|
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 {
|
.rightPanel {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emptyRight {
|
.emptyRight {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useMemo, useCallback } from 'react';
|
import { useState, useMemo, useCallback, useRef } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { useGlobalFilters } from '@cameleer/design-system';
|
import { useGlobalFilters } from '@cameleer/design-system';
|
||||||
import { useExecutionDetail } from '../../api/queries/executions';
|
import { useExecutionDetail } from '../../api/queries/executions';
|
||||||
@@ -22,14 +22,48 @@ export default function ExchangesPage() {
|
|||||||
return <Dashboard />;
|
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 (
|
return (
|
||||||
<div className={styles.splitView}>
|
<div ref={containerRef} className={styles.splitView}>
|
||||||
<div className={styles.leftPanel}>
|
<div className={styles.leftPanel} style={{ width: `${splitPercent}%` }}>
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.rightPanel}>
|
<div className={styles.splitter} onPointerDown={handleSplitterDown} />
|
||||||
<DiagramPanel appId={appId!} routeId={routeId} exchangeId={exchangeId} />
|
<div className={styles.rightPanel} style={{ width: `${100 - splitPercent}%` }}>
|
||||||
|
<DiagramPanel appId={appId} routeId={routeId} exchangeId={exchangeId} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -48,13 +82,9 @@ function DiagramPanel({ appId, routeId, exchangeId }: DiagramPanelProps) {
|
|||||||
const timeFrom = timeRange.start.toISOString();
|
const timeFrom = timeRange.start.toISOString();
|
||||||
const timeTo = timeRange.end.toISOString();
|
const timeTo = timeRange.end.toISOString();
|
||||||
|
|
||||||
// Fetch execution detail if an exchange is selected
|
|
||||||
const { data: detail } = useExecutionDetail(exchangeId ?? null);
|
const { data: detail } = useExecutionDetail(exchangeId ?? null);
|
||||||
|
|
||||||
// Fetch diagram for topology-only view
|
|
||||||
const diagramQuery = useDiagramByRoute(appId, routeId);
|
const diagramQuery = useDiagramByRoute(appId, routeId);
|
||||||
|
|
||||||
// Known route IDs for drill-down
|
|
||||||
const { data: catalog } = useRouteCatalog(timeFrom, timeTo);
|
const { data: catalog } = useRouteCatalog(timeFrom, timeTo);
|
||||||
const knownRouteIds = useMemo(() => {
|
const knownRouteIds = useMemo(() => {
|
||||||
const ids = new Set<string>();
|
const ids = new Set<string>();
|
||||||
@@ -68,7 +98,6 @@ function DiagramPanel({ appId, routeId, exchangeId }: DiagramPanelProps) {
|
|||||||
return ids;
|
return ids;
|
||||||
}, [catalog]);
|
}, [catalog]);
|
||||||
|
|
||||||
// If exchange selected: show header + ExecutionDiagram
|
|
||||||
if (exchangeId && detail) {
|
if (exchangeId && detail) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -82,7 +111,6 @@ function DiagramPanel({ appId, routeId, exchangeId }: DiagramPanelProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No exchange: show topology-only ProcessDiagram
|
|
||||||
if (diagramQuery.data) {
|
if (diagramQuery.data) {
|
||||||
return (
|
return (
|
||||||
<ProcessDiagram
|
<ProcessDiagram
|
||||||
|
|||||||
Reference in New Issue
Block a user