diff --git a/ui/src/pages/Exchanges/ExchangesPage.module.css b/ui/src/pages/Exchanges/ExchangesPage.module.css new file mode 100644 index 00000000..138d8833 --- /dev/null +++ b/ui/src/pages/Exchanges/ExchangesPage.module.css @@ -0,0 +1,22 @@ +.threeColumn { + display: grid; + grid-template-columns: 280px 1fr; + height: 100%; + overflow: hidden; +} + +.rightPanel { + display: flex; + flex-direction: column; + overflow: hidden; + height: 100%; +} + +.emptyRight { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-muted); + font-size: 0.875rem; +} diff --git a/ui/src/pages/Exchanges/ExchangesPage.tsx b/ui/src/pages/Exchanges/ExchangesPage.tsx new file mode 100644 index 00000000..94153434 --- /dev/null +++ b/ui/src/pages/Exchanges/ExchangesPage.tsx @@ -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 ; + } + + // Route is scoped: render 3-column layout + return ( + + ); +} + +// ─── 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(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(); + 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 ( + + + + + {selectedExchangeId && detail ? ( + <> + + + > + ) : ( + diagramQuery.data ? ( + + ) : ( + + Select an exchange to view execution details + + ) + )} + + + ); +}