fix(ui): exchange selection uses state, not URL navigation
Row click no longer navigates to /exchanges/:app/:route/:id which was changing the search scope. Instead, Dashboard calls onExchangeSelect callback and ExchangesPage manages the selected exchange as local state. The search criteria and scope are preserved when selecting an exchange. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -176,7 +176,17 @@ function buildBaseColumns(): Column<Row>[] {
|
||||
|
||||
// ─── Dashboard component ─────────────────────────────────────────────────────
|
||||
|
||||
export default function Dashboard() {
|
||||
export interface SelectedExchange {
|
||||
executionId: string;
|
||||
applicationName: string;
|
||||
routeId: string;
|
||||
}
|
||||
|
||||
interface DashboardProps {
|
||||
onExchangeSelect?: (exchange: SelectedExchange) => void;
|
||||
}
|
||||
|
||||
export default function Dashboard({ onExchangeSelect }: DashboardProps = {}) {
|
||||
const { appId, routeId } = useParams<{ appId: string; routeId: string }>()
|
||||
const navigate = useNavigate()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
@@ -228,8 +238,13 @@ export default function Dashboard() {
|
||||
|
||||
function handleRowClick(row: Row) {
|
||||
setSelectedId(row.id)
|
||||
// Navigate to the split view with diagram
|
||||
navigate(`/exchanges/${row.applicationName}/${row.routeId}/${row.executionId}`)
|
||||
if (onExchangeSelect) {
|
||||
onExchangeSelect({
|
||||
executionId: row.executionId,
|
||||
applicationName: row.applicationName ?? '',
|
||||
routeId: row.routeId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handleRowAccent(row: Row): 'error' | 'warning' | undefined {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useGlobalFilters } from '@cameleer/design-system';
|
||||
import { useExecutionDetail } from '../../api/queries/executions';
|
||||
import { useDiagramByRoute } from '../../api/queries/diagrams';
|
||||
@@ -10,35 +10,18 @@ import { ExecutionDiagram } from '../../components/ExecutionDiagram/ExecutionDia
|
||||
import { ProcessDiagram } from '../../components/ProcessDiagram';
|
||||
import styles from './ExchangesPage.module.css';
|
||||
|
||||
// The full-width Dashboard table — used at every scope level
|
||||
import Dashboard from '../Dashboard/Dashboard';
|
||||
import type { SelectedExchange } from '../Dashboard/Dashboard';
|
||||
|
||||
export default function ExchangesPage() {
|
||||
const { appId, routeId, exchangeId } = useParams<{
|
||||
appId?: string; routeId?: string; exchangeId?: string;
|
||||
}>();
|
||||
|
||||
// No route scoped: full-width Dashboard
|
||||
if (!routeId) {
|
||||
return <Dashboard />;
|
||||
}
|
||||
|
||||
// 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 [selected, setSelected] = useState<SelectedExchange | null>(null);
|
||||
const [splitPercent, setSplitPercent] = useState(50);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleExchangeSelect = useCallback((exchange: SelectedExchange) => {
|
||||
setSelected(exchange);
|
||||
}, []);
|
||||
|
||||
const handleSplitterDown = useCallback((e: React.PointerEvent) => {
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
const container = containerRef.current;
|
||||
@@ -57,25 +40,35 @@ function SplitExchangeView({ appId, routeId, exchangeId }: SplitExchangeViewProp
|
||||
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 />
|
||||
<Dashboard onExchangeSelect={handleExchangeSelect} />
|
||||
</div>
|
||||
<div className={styles.splitter} onPointerDown={handleSplitterDown} />
|
||||
<div className={styles.rightPanel} style={{ width: `${100 - splitPercent}%` }}>
|
||||
<DiagramPanel appId={appId} routeId={routeId} exchangeId={exchangeId} />
|
||||
<DiagramPanel
|
||||
appId={selected.applicationName}
|
||||
routeId={selected.routeId}
|
||||
exchangeId={selected.executionId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Right panel: diagram + optional execution overlay ──────────────────────
|
||||
// ─── Right panel: diagram + execution overlay ───────────────────────────────
|
||||
|
||||
interface DiagramPanelProps {
|
||||
appId: string;
|
||||
routeId: string;
|
||||
exchangeId?: string;
|
||||
exchangeId: string;
|
||||
}
|
||||
|
||||
function DiagramPanel({ appId, routeId, exchangeId }: DiagramPanelProps) {
|
||||
@@ -84,8 +77,7 @@ function DiagramPanel({ appId, routeId, exchangeId }: DiagramPanelProps) {
|
||||
const timeFrom = timeRange.start.toISOString();
|
||||
const timeTo = timeRange.end.toISOString();
|
||||
|
||||
const { data: detail } = useExecutionDetail(exchangeId ?? null);
|
||||
const diagramQuery = useDiagramByRoute(appId, routeId);
|
||||
const { data: detail } = useExecutionDetail(exchangeId);
|
||||
|
||||
const { data: catalog } = useRouteCatalog(timeFrom, timeTo);
|
||||
const knownRouteIds = useMemo(() => {
|
||||
@@ -106,7 +98,7 @@ function DiagramPanel({ appId, routeId, exchangeId }: DiagramPanelProps) {
|
||||
}
|
||||
}, [appId, navigate]);
|
||||
|
||||
if (exchangeId && detail) {
|
||||
if (detail) {
|
||||
return (
|
||||
<>
|
||||
<ExchangeHeader detail={detail} />
|
||||
@@ -120,21 +112,9 @@ function DiagramPanel({ appId, routeId, exchangeId }: DiagramPanelProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (diagramQuery.data) {
|
||||
return (
|
||||
<ProcessDiagram
|
||||
application={appId}
|
||||
routeId={routeId}
|
||||
diagramLayout={diagramQuery.data}
|
||||
knownRouteIds={knownRouteIds}
|
||||
onNodeAction={handleNodeAction}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.emptyRight}>
|
||||
Loading diagram...
|
||||
Loading execution...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user