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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { GitBranch, Server, RotateCcw, FileText } from 'lucide-react';
|
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 { useCorrelationChain } from '../../api/queries/correlation';
|
||||||
import { useAgents } from '../../api/queries/agents';
|
import { useAgents } from '../../api/queries/agents';
|
||||||
|
import { useRouteCatalog } from '../../api/queries/catalog';
|
||||||
import { useAuthStore } from '../../auth/auth-store';
|
import { useAuthStore } from '../../auth/auth-store';
|
||||||
import type { ExecutionDetail } from '../../components/ExecutionDiagram/types';
|
import type { ExecutionDetail } from '../../components/ExecutionDiagram/types';
|
||||||
import { attributeBadgeColor } from '../../utils/attribute-color';
|
import { attributeBadgeColor } from '../../utils/attribute-color';
|
||||||
@@ -44,11 +45,27 @@ function formatDuration(ms: number): string {
|
|||||||
|
|
||||||
export function ExchangeHeader({ detail, onCorrelatedSelect, onClearSelection }: ExchangeHeaderProps) {
|
export function ExchangeHeader({ detail, onCorrelatedSelect, onClearSelection }: ExchangeHeaderProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { timeRange } = useGlobalFilters();
|
||||||
const { data: chainResult } = useCorrelationChain(detail.correlationId ?? null);
|
const { data: chainResult } = useCorrelationChain(detail.correlationId ?? null);
|
||||||
const chain = chainResult?.data;
|
const chain = chainResult?.data;
|
||||||
const showChain = chain && chain.length > 1;
|
const showChain = chain && chain.length > 1;
|
||||||
const attrs = Object.entries(detail.attributes ?? {});
|
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
|
// Look up agent state for icon coloring + route control capability
|
||||||
const { data: agents } = useAgents(undefined, detail.applicationId);
|
const { data: agents } = useAgents(undefined, detail.applicationId);
|
||||||
const { agentState, hasRouteControl, hasReplay } = useMemo(() => {
|
const { agentState, hasRouteControl, hasReplay } = useMemo(() => {
|
||||||
@@ -114,6 +131,7 @@ export function ExchangeHeader({ detail, onCorrelatedSelect, onClearSelection }:
|
|||||||
<RouteControlBar
|
<RouteControlBar
|
||||||
application={detail.applicationId}
|
application={detail.applicationId}
|
||||||
routeId={detail.routeId}
|
routeId={detail.routeId}
|
||||||
|
routeState={routeState}
|
||||||
hasRouteControl={hasRouteControl}
|
hasRouteControl={hasRouteControl}
|
||||||
hasReplay={hasReplay}
|
hasReplay={hasReplay}
|
||||||
agentId={detail.instanceId}
|
agentId={detail.instanceId}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import styles from './RouteControlBar.module.css';
|
|||||||
interface RouteControlBarProps {
|
interface RouteControlBarProps {
|
||||||
application: string;
|
application: string;
|
||||||
routeId: string;
|
routeId: string;
|
||||||
|
routeState?: 'started' | 'stopped' | 'suspended';
|
||||||
hasRouteControl: boolean;
|
hasRouteControl: boolean;
|
||||||
hasReplay: boolean;
|
hasReplay: boolean;
|
||||||
agentId?: string;
|
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 },
|
{ 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 { toast } = useToast();
|
||||||
const sendRouteCommand = useSendRouteCommand();
|
const sendRouteCommand = useSendRouteCommand();
|
||||||
const replayExchange = useReplayExchange();
|
const replayExchange = useReplayExchange();
|
||||||
@@ -34,6 +41,7 @@ export function RouteControlBar({ application, routeId, hasRouteControl, hasRepl
|
|||||||
const [pendingConfirm, setPendingConfirm] = useState<RouteAction | null>(null);
|
const [pendingConfirm, setPendingConfirm] = useState<RouteAction | null>(null);
|
||||||
|
|
||||||
const busy = sendingAction !== null;
|
const busy = sendingAction !== null;
|
||||||
|
const disabledActions = ACTION_DISABLED[routeState ?? 'started'] ?? new Set();
|
||||||
|
|
||||||
const handleRouteClick = useCallback((action: RouteAction) => {
|
const handleRouteClick = useCallback((action: RouteAction) => {
|
||||||
if (action === 'stop' || action === 'suspend') {
|
if (action === 'stop' || action === 'suspend') {
|
||||||
@@ -98,7 +106,7 @@ export function RouteControlBar({ application, routeId, hasRouteControl, hasRepl
|
|||||||
<button
|
<button
|
||||||
key={action}
|
key={action}
|
||||||
className={`${styles.segment} ${colorClass}`}
|
className={`${styles.segment} ${colorClass}`}
|
||||||
disabled={busy}
|
disabled={busy || disabledActions.has(action)}
|
||||||
onClick={() => handleRouteClick(action)}
|
onClick={() => handleRouteClick(action)}
|
||||||
title={`${label} route ${routeId}`}
|
title={`${label} route ${routeId}`}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user