diff --git a/ui/src/pages/Exchanges/ExchangesPage.tsx b/ui/src/pages/Exchanges/ExchangesPage.tsx index dc6e6385..591d7f48 100644 --- a/ui/src/pages/Exchanges/ExchangesPage.tsx +++ b/ui/src/pages/Exchanges/ExchangesPage.tsx @@ -4,12 +4,15 @@ import { useGlobalFilters, useToast } from '@cameleer/design-system'; import { useExecutionDetail } from '../../api/queries/executions'; import { useDiagramByRoute } from '../../api/queries/diagrams'; import { useRouteCatalog } from '../../api/queries/catalog'; +import { useAgents } from '../../api/queries/agents'; import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands'; import type { TapDefinition, ConfigUpdateResponse } from '../../api/queries/commands'; +import { useAuthStore } from '../../auth/auth-store'; import { useTracingStore } from '../../stores/tracing-store'; import type { NodeAction, NodeConfig } from '../../components/ProcessDiagram/types'; import { TapConfigModal } from '../../components/TapConfigModal'; import { ExchangeHeader } from './ExchangeHeader'; +import { RouteControlBar } from './RouteControlBar'; import { ExecutionDiagram } from '../../components/ExecutionDiagram/ExecutionDiagram'; import { ProcessDiagram } from '../../components/ProcessDiagram'; import styles from './ExchangesPage.module.css'; @@ -148,6 +151,30 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS const diagramQuery = useDiagramByRoute(appId, routeId); const { data: catalog } = useRouteCatalog(timeFrom, timeTo); + + // Route state + capabilities for topology-only control bar + const { data: agents } = useAgents(undefined, appId); + const roles = useAuthStore((s) => s.roles); + const canControl = roles.some(r => r === 'OPERATOR' || r === 'ADMIN'); + const { hasRouteControl, hasReplay } = useMemo(() => { + if (!agents) return { hasRouteControl: false, hasReplay: false }; + const agentList = agents as any[]; + return { + hasRouteControl: agentList.some((a: any) => a.capabilities?.routeControl === true), + hasReplay: agentList.some((a: any) => a.capabilities?.replay === true), + }; + }, [agents]); + const routeState = useMemo(() => { + if (!catalog) return undefined; + for (const app of catalog as any[]) { + if (app.applicationId !== appId) continue; + for (const r of app.routes || []) { + if (r.routeId === routeId) return (r.routeState ?? 'started') as 'started' | 'stopped' | 'suspended'; + } + } + return undefined; + }, [catalog, appId, routeId]); + const knownRouteIds = useMemo(() => { const ids = new Set(); if (catalog) { @@ -313,10 +340,19 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS ); } - // No exchange selected: show topology-only diagram + // No exchange selected: show topology-only diagram with route control bar if (diagramQuery.data) { return ( <> + {canControl && (hasRouteControl || hasReplay) && ( + + )}