From 38b76513c7e36b33c3a08a5f12f6dcd67fd5624f Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 2 Apr 2026 22:56:49 +0200 Subject: [PATCH] feat: route control buttons reflect current route state Buttons are disabled based on route state: Started disables Start/Resume, Stopped disables Stop/Suspend/Resume, Suspended disables Start/Suspend. State looked up from catalog API. Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/pages/Exchanges/ExchangeHeader.tsx | 20 +++++++++++++++++++- ui/src/pages/Exchanges/RouteControlBar.tsx | 12 ++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/ui/src/pages/Exchanges/ExchangeHeader.tsx b/ui/src/pages/Exchanges/ExchangeHeader.tsx index 236c08a5..5d41a454 100644 --- a/ui/src/pages/Exchanges/ExchangeHeader.tsx +++ b/ui/src/pages/Exchanges/ExchangeHeader.tsx @@ -1,9 +1,10 @@ import { useMemo } from 'react'; import { useNavigate } from 'react-router'; import { GitBranch, Server, RotateCcw, FileText } from 'lucide-react'; -import { StatusDot, MonoText, Badge } from '@cameleer/design-system'; +import { StatusDot, MonoText, Badge, useGlobalFilters } from '@cameleer/design-system'; import { useCorrelationChain } from '../../api/queries/correlation'; import { useAgents } from '../../api/queries/agents'; +import { useRouteCatalog } from '../../api/queries/catalog'; import { useAuthStore } from '../../auth/auth-store'; import type { ExecutionDetail } from '../../components/ExecutionDiagram/types'; import { attributeBadgeColor } from '../../utils/attribute-color'; @@ -44,11 +45,27 @@ function formatDuration(ms: number): string { export function ExchangeHeader({ detail, onCorrelatedSelect, onClearSelection }: ExchangeHeaderProps) { const navigate = useNavigate(); + const { timeRange } = useGlobalFilters(); const { data: chainResult } = useCorrelationChain(detail.correlationId ?? null); const chain = chainResult?.data; const showChain = chain && chain.length > 1; const attrs = Object.entries(detail.attributes ?? {}); + // Look up route state from catalog + const { data: catalog } = useRouteCatalog(timeRange.start.toISOString(), timeRange.end.toISOString()); + const routeState = useMemo(() => { + if (!catalog) return undefined; + for (const app of catalog as any[]) { + if (app.appId !== detail.applicationId) continue; + for (const route of app.routes || []) { + if (route.routeId === detail.routeId) { + return (route.routeState ?? 'started') as 'started' | 'stopped' | 'suspended'; + } + } + } + return undefined; + }, [catalog, detail.applicationId, detail.routeId]); + // Look up agent state for icon coloring + route control capability const { data: agents } = useAgents(undefined, detail.applicationId); const { agentState, hasRouteControl, hasReplay } = useMemo(() => { @@ -114,6 +131,7 @@ export function ExchangeHeader({ detail, onCorrelatedSelect, onClearSelection }: > = { + started: new Set(['start', 'resume']), + stopped: new Set(['stop', 'suspend', 'resume']), + suspended: new Set(['start', 'suspend']), +}; + +export function RouteControlBar({ application, routeId, routeState, hasRouteControl, hasReplay, agentId, exchangeId, inputHeaders, inputBody }: RouteControlBarProps) { const { toast } = useToast(); const sendRouteCommand = useSendRouteCommand(); const replayExchange = useReplayExchange(); @@ -34,6 +41,7 @@ export function RouteControlBar({ application, routeId, hasRouteControl, hasRepl const [pendingConfirm, setPendingConfirm] = useState(null); const busy = sendingAction !== null; + const disabledActions = ACTION_DISABLED[routeState ?? 'started'] ?? new Set(); const handleRouteClick = useCallback((action: RouteAction) => { if (action === 'stop' || action === 'suspend') { @@ -98,7 +106,7 @@ export function RouteControlBar({ application, routeId, hasRouteControl, hasRepl