feat(ui): add ExchangesPage with full-width and 3-column modes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-28 13:57:13 +01:00
parent fc27880d96
commit f2abe296ee
2 changed files with 135 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
import { useState, useMemo, useCallback } from 'react';
import { useParams } from 'react-router';
import { useGlobalFilters } from '@cameleer/design-system';
import { useSearchExecutions, useExecutionDetail } from '../../api/queries/executions';
import { useDiagramByRoute } from '../../api/queries/diagrams';
import { useRouteCatalog } from '../../api/queries/catalog';
import type { ExecutionSummary } from '../../api/types';
import { ExchangeList } from './ExchangeList';
import { ExchangeHeader } from './ExchangeHeader';
import { ExecutionDiagram } from '../../components/ExecutionDiagram/ExecutionDiagram';
import { ProcessDiagram } from '../../components/ProcessDiagram';
import styles from './ExchangesPage.module.css';
// Lazy-import the full-width Dashboard for the no-route-scope view
import Dashboard from '../Dashboard/Dashboard';
export default function ExchangesPage() {
const { appId, routeId, exchangeId } = useParams<{
appId?: string; routeId?: string; exchangeId?: string;
}>();
// If no route is scoped, render the existing full-width Dashboard table
if (!routeId) {
return <Dashboard />;
}
// Route is scoped: render 3-column layout
return (
<RouteExchangeView appId={appId!} routeId={routeId} initialExchangeId={exchangeId} />
);
}
// ─── 3-column view when route is scoped ─────────────────────────────────────
interface RouteExchangeViewProps {
appId: string;
routeId: string;
initialExchangeId?: string;
}
function RouteExchangeView({ appId, routeId, initialExchangeId }: RouteExchangeViewProps) {
const [selectedExchangeId, setSelectedExchangeId] = useState<string | undefined>(initialExchangeId);
const { timeRange } = useGlobalFilters();
const timeFrom = timeRange.start.toISOString();
const timeTo = timeRange.end.toISOString();
// Fetch exchanges for this route
const { data: searchResult } = useSearchExecutions(
{ timeFrom, timeTo, routeId, application: appId, sortField: 'startTime', sortDir: 'desc', offset: 0, limit: 50 },
true,
);
const exchanges: ExecutionSummary[] = searchResult?.data || [];
// Fetch execution detail for selected exchange
const { data: detail } = useExecutionDetail(selectedExchangeId ?? null);
// Fetch diagram for topology-only view (when no exchange selected)
const diagramQuery = useDiagramByRoute(appId, routeId);
// Known route IDs for drill-down resolution
const { data: catalog } = useRouteCatalog(timeFrom, timeTo);
const knownRouteIds = useMemo(() => {
const ids = new Set<string>();
if (catalog) {
for (const app of catalog as any[]) {
for (const r of app.routes || []) {
ids.add(r.routeId);
}
}
}
return ids;
}, [catalog]);
const handleExchangeSelect = useCallback((ex: ExecutionSummary) => {
setSelectedExchangeId(ex.executionId);
}, []);
return (
<div className={styles.threeColumn}>
<ExchangeList
exchanges={exchanges}
selectedId={selectedExchangeId}
onSelect={handleExchangeSelect}
/>
<div className={styles.rightPanel}>
{selectedExchangeId && detail ? (
<>
<ExchangeHeader detail={detail} />
<ExecutionDiagram
executionId={selectedExchangeId}
executionDetail={detail}
knownRouteIds={knownRouteIds}
/>
</>
) : (
diagramQuery.data ? (
<ProcessDiagram
application={appId}
routeId={routeId}
diagramLayout={diagramQuery.data}
knownRouteIds={knownRouteIds}
/>
) : (
<div className={styles.emptyRight}>
Select an exchange to view execution details
</div>
)
)}
</div>
</div>
);
}