feat: route control buttons reflect current route state
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m8s
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / docker (push) Has been cancelled

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) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-02 22:56:49 +02:00
parent 2265ebf801
commit 38b76513c7
2 changed files with 29 additions and 3 deletions

View File

@@ -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 }:
<RouteControlBar
application={detail.applicationId}
routeId={detail.routeId}
routeState={routeState}
hasRouteControl={hasRouteControl}
hasReplay={hasReplay}
agentId={detail.instanceId}

View File

@@ -8,6 +8,7 @@ import styles from './RouteControlBar.module.css';
interface RouteControlBarProps {
application: string;
routeId: string;
routeState?: 'started' | 'stopped' | 'suspended';
hasRouteControl: boolean;
hasReplay: boolean;
agentId?: string;
@@ -25,7 +26,13 @@ const ROUTE_ACTIONS: { action: RouteAction; label: string; icon: typeof Play; co
{ action: 'resume', label: 'Resume', icon: PlayCircle, colorClass: styles.success },
];
export function RouteControlBar({ application, routeId, hasRouteControl, hasReplay, agentId, exchangeId, inputHeaders, inputBody }: RouteControlBarProps) {
const ACTION_DISABLED: Record<string, Set<RouteAction>> = {
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<RouteAction | null>(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
<button
key={action}
className={`${styles.segment} ${colorClass}`}
disabled={busy}
disabled={busy || disabledActions.has(action)}
onClick={() => handleRouteClick(action)}
title={`${label} route ${routeId}`}
>