diff --git a/ui/src/components/ContentTabs.module.css b/ui/src/components/ContentTabs.module.css index cd3c72d1..d7d3d07b 100644 --- a/ui/src/components/ContentTabs.module.css +++ b/ui/src/components/ContentTabs.module.css @@ -1,5 +1,5 @@ .wrapper { - padding: 0 1.5rem; - padding-top: 0.75rem; - padding-bottom: 0; + padding: 0.625rem 1.5rem 0; + border-bottom: 1px solid var(--border); + background: var(--surface); } diff --git a/ui/src/components/LayoutShell.tsx b/ui/src/components/LayoutShell.tsx index 10dcb64f..d5f626eb 100644 --- a/ui/src/components/LayoutShell.tsx +++ b/ui/src/components/LayoutShell.tsx @@ -229,7 +229,7 @@ function LayoutContent() { return ( +
} diff --git a/ui/src/pages/Exchanges/ExchangesPage.module.css b/ui/src/pages/Exchanges/ExchangesPage.module.css index 138d8833..c6b61565 100644 --- a/ui/src/pages/Exchanges/ExchangesPage.module.css +++ b/ui/src/pages/Exchanges/ExchangesPage.module.css @@ -1,10 +1,16 @@ -.threeColumn { +.splitView { display: grid; - grid-template-columns: 280px 1fr; + grid-template-columns: 1fr 1fr; height: 100%; overflow: hidden; } +.leftPanel { + overflow: auto; + height: 100%; + border-right: 1px solid var(--border); +} + .rightPanel { display: flex; flex-direction: column; diff --git a/ui/src/pages/Exchanges/ExchangesPage.tsx b/ui/src/pages/Exchanges/ExchangesPage.tsx index 94153434..eb863470 100644 --- a/ui/src/pages/Exchanges/ExchangesPage.tsx +++ b/ui/src/pages/Exchanges/ExchangesPage.tsx @@ -1,17 +1,15 @@ 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 { 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 +// The full-width Dashboard table — used at every scope level import Dashboard from '../Dashboard/Dashboard'; export default function ExchangesPage() { @@ -19,45 +17,44 @@ export default function ExchangesPage() { appId?: string; routeId?: string; exchangeId?: string; }>(); - // If no route is scoped, render the existing full-width Dashboard table + // No route scoped: full-width Dashboard if (!routeId) { return ; } - // Route is scoped: render 3-column layout + // Route scoped: 50:50 split — Dashboard table on left, diagram on right return ( - +
+
+ +
+
+ +
+
); } -// ─── 3-column view when route is scoped ───────────────────────────────────── +// ─── Right panel: diagram + optional execution overlay ────────────────────── -interface RouteExchangeViewProps { +interface DiagramPanelProps { appId: string; routeId: string; - initialExchangeId?: string; + exchangeId?: string; } -function RouteExchangeView({ appId, routeId, initialExchangeId }: RouteExchangeViewProps) { - const [selectedExchangeId, setSelectedExchangeId] = useState(initialExchangeId); +function DiagramPanel({ appId, routeId, exchangeId }: DiagramPanelProps) { 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 if an exchange is selected + const { data: detail } = useExecutionDetail(exchangeId ?? null); - // Fetch execution detail for selected exchange - const { data: detail } = useExecutionDetail(selectedExchangeId ?? null); - - // Fetch diagram for topology-only view (when no exchange selected) + // Fetch diagram for topology-only view const diagramQuery = useDiagramByRoute(appId, routeId); - // Known route IDs for drill-down resolution + // Known route IDs for drill-down const { data: catalog } = useRouteCatalog(timeFrom, timeTo); const knownRouteIds = useMemo(() => { const ids = new Set(); @@ -71,43 +68,35 @@ function RouteExchangeView({ appId, routeId, initialExchangeId }: RouteExchangeV return ids; }, [catalog]); - const handleExchangeSelect = useCallback((ex: ExecutionSummary) => { - setSelectedExchangeId(ex.executionId); - }, []); + // If exchange selected: show header + ExecutionDiagram + if (exchangeId && detail) { + return ( + <> + + + + ); + } + + // No exchange: show topology-only ProcessDiagram + if (diagramQuery.data) { + return ( + + ); + } return ( -
- - -
- {selectedExchangeId && detail ? ( - <> - - - - ) : ( - diagramQuery.data ? ( - - ) : ( -
- Select an exchange to view execution details -
- ) - )} -
+
+ Loading diagram...
); }