fix(ui): exchange selection uses state, not URL navigation
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 55s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s

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:
hsiegeln
2026-03-28 15:20:17 +01:00
parent 2f2e503447
commit 29f4be542b
2 changed files with 42 additions and 47 deletions

View File

@@ -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 {

View File

@@ -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>
);
}