feat: add processor tracing toggle to exchange detail views
Wire getActions on ProcessorTimeline and RouteFlow to send SET_TRACED_PROCESSORS commands to all agents of the same application. Tracing state managed via Zustand store with optimistic UI and rollback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
21
ui/src/api/queries/commands.ts
Normal file
21
ui/src/api/queries/commands.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query'
|
||||||
|
import { api } from '../client'
|
||||||
|
|
||||||
|
interface SendGroupCommandParams {
|
||||||
|
group: string
|
||||||
|
type: string
|
||||||
|
payload: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSendGroupCommand() {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ group, type, payload }: SendGroupCommandParams) => {
|
||||||
|
const { data, error } = await api.POST('/agents/groups/{group}/commands', {
|
||||||
|
params: { path: { group } },
|
||||||
|
body: { type, payload } as any,
|
||||||
|
})
|
||||||
|
if (error) throw new Error('Failed to send command')
|
||||||
|
return data!
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useState, useMemo } from 'react'
|
import { useState, useMemo, useCallback } from 'react'
|
||||||
import { useParams, useNavigate } from 'react-router'
|
import { useParams, useNavigate } from 'react-router'
|
||||||
import {
|
import {
|
||||||
Badge, StatusDot, MonoText, CodeBlock, InfoCallout,
|
Badge, StatusDot, MonoText, CodeBlock, InfoCallout,
|
||||||
ProcessorTimeline, Breadcrumb, Spinner, RouteFlow,
|
ProcessorTimeline, Breadcrumb, Spinner, RouteFlow, useToast,
|
||||||
} from '@cameleer/design-system'
|
} from '@cameleer/design-system'
|
||||||
import type { ProcessorStep, RouteNode } from '@cameleer/design-system'
|
import type { ProcessorStep, RouteNode } from '@cameleer/design-system'
|
||||||
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions'
|
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions'
|
||||||
import { useCorrelationChain } from '../../api/queries/correlation'
|
import { useCorrelationChain } from '../../api/queries/correlation'
|
||||||
import { useDiagramLayout } from '../../api/queries/diagrams'
|
import { useDiagramLayout } from '../../api/queries/diagrams'
|
||||||
import { mapDiagramToRouteNodes } from '../../utils/diagram-mapping'
|
import { mapDiagramToRouteNodes } from '../../utils/diagram-mapping'
|
||||||
|
import { useTracingStore } from '../../stores/tracing-store'
|
||||||
|
import { useSendGroupCommand } from '../../api/queries/commands'
|
||||||
import styles from './ExchangeDetail.module.css'
|
import styles from './ExchangeDetail.module.css'
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
@@ -130,6 +132,59 @@ export default function ExchangeDetail() {
|
|||||||
}))
|
}))
|
||||||
}, [diagram, processors, procList])
|
}, [diagram, processors, procList])
|
||||||
|
|
||||||
|
// ProcessorId lookup: timeline index → processorId
|
||||||
|
const processorIds: string[] = useMemo(() => {
|
||||||
|
const ids: string[] = []
|
||||||
|
function walk(node: any) {
|
||||||
|
ids.push(node.processorId || '')
|
||||||
|
if (node.children) node.children.forEach(walk)
|
||||||
|
}
|
||||||
|
procList.forEach(walk)
|
||||||
|
return ids
|
||||||
|
}, [procList])
|
||||||
|
|
||||||
|
// ProcessorId lookup: flow node index → processorId (diagram order)
|
||||||
|
const flowProcessorIds: string[] = useMemo(() => {
|
||||||
|
if (!diagram?.nodes) return processorIds
|
||||||
|
const flatProcs: Array<{ diagramNodeId?: string; processorId?: string }> = []
|
||||||
|
function flatten(nodes: any[]) {
|
||||||
|
for (const n of nodes) {
|
||||||
|
flatProcs.push(n)
|
||||||
|
if (n.children) flatten(n.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flatten(procList)
|
||||||
|
const lookup = new Map(flatProcs
|
||||||
|
.filter(p => p.diagramNodeId && p.processorId)
|
||||||
|
.map(p => [p.diagramNodeId!, p.processorId!]))
|
||||||
|
return diagram.nodes.map(node => lookup.get(node.id ?? '') ?? '')
|
||||||
|
}, [diagram, procList, processorIds])
|
||||||
|
|
||||||
|
// ── Tracing toggle ──────────────────────────────────────────────────────
|
||||||
|
const { toast } = useToast()
|
||||||
|
const tracingStore = useTracingStore()
|
||||||
|
const sendCommand = useSendGroupCommand()
|
||||||
|
const appRoute = detail ? `${detail.applicationName}:${detail.routeId}` : ''
|
||||||
|
|
||||||
|
const handleToggleTracing = useCallback((processorId: string) => {
|
||||||
|
if (!processorId || !detail?.applicationName || !detail?.routeId) return
|
||||||
|
const newSet = tracingStore.toggleProcessor(appRoute, processorId)
|
||||||
|
sendCommand.mutate({
|
||||||
|
group: detail.applicationName,
|
||||||
|
type: 'set-traced-processors',
|
||||||
|
payload: { routeId: detail.routeId, processorIds: Array.from(newSet) },
|
||||||
|
}, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
const action = newSet.has(processorId) ? 'enabled' : 'disabled'
|
||||||
|
toast({ title: `Tracing ${action}`, description: `${processorId} — sent to ${data?.targetCount ?? 0} agent(s)`, variant: 'success' })
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
tracingStore.toggleProcessor(appRoute, processorId)
|
||||||
|
toast({ title: 'Command failed', description: 'Could not send tracing command', variant: 'error' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [detail, appRoute, tracingStore, sendCommand, toast])
|
||||||
|
|
||||||
// Correlation chain
|
// Correlation chain
|
||||||
const correlatedExchanges = useMemo(() => {
|
const correlatedExchanges = useMemo(() => {
|
||||||
if (!correlationData?.data || correlationData.data.length <= 1) return []
|
if (!correlationData?.data || correlationData.data.length <= 1) return []
|
||||||
@@ -287,6 +342,15 @@ export default function ExchangeDetail() {
|
|||||||
totalMs={detail.durationMs}
|
totalMs={detail.durationMs}
|
||||||
onProcessorClick={(_proc, index) => setSelectedProcessorIndex(index)}
|
onProcessorClick={(_proc, index) => setSelectedProcessorIndex(index)}
|
||||||
selectedIndex={activeIndex}
|
selectedIndex={activeIndex}
|
||||||
|
getActions={(_proc, index) => {
|
||||||
|
const pid = processorIds[index]
|
||||||
|
if (!pid || !detail?.applicationName) return []
|
||||||
|
return [{
|
||||||
|
label: tracingStore.isTraced(appRoute, pid) ? 'Disable Tracing' : 'Enable Tracing',
|
||||||
|
onClick: () => handleToggleTracing(pid),
|
||||||
|
disabled: sendCommand.isPending,
|
||||||
|
}]
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<InfoCallout>No processor data available</InfoCallout>
|
<InfoCallout>No processor data available</InfoCallout>
|
||||||
@@ -297,6 +361,15 @@ export default function ExchangeDetail() {
|
|||||||
nodes={routeNodes}
|
nodes={routeNodes}
|
||||||
onNodeClick={(_node, index) => setSelectedProcessorIndex(index)}
|
onNodeClick={(_node, index) => setSelectedProcessorIndex(index)}
|
||||||
selectedIndex={activeIndex}
|
selectedIndex={activeIndex}
|
||||||
|
getActions={(_node, index) => {
|
||||||
|
const pid = flowProcessorIds[index]
|
||||||
|
if (!pid || !detail?.applicationName) return []
|
||||||
|
return [{
|
||||||
|
label: tracingStore.isTraced(appRoute, pid) ? 'Disable Tracing' : 'Enable Tracing',
|
||||||
|
onClick: () => handleToggleTracing(pid),
|
||||||
|
disabled: sendCommand.isPending,
|
||||||
|
}]
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
|||||||
24
ui/src/stores/tracing-store.ts
Normal file
24
ui/src/stores/tracing-store.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
interface TracingState {
|
||||||
|
tracedProcessors: Record<string, Set<string>>
|
||||||
|
isTraced: (appRoute: string, processorId: string) => boolean
|
||||||
|
toggleProcessor: (appRoute: string, processorId: string) => Set<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTracingStore = create<TracingState>((set, get) => ({
|
||||||
|
tracedProcessors: {},
|
||||||
|
|
||||||
|
isTraced: (appRoute, processorId) =>
|
||||||
|
get().tracedProcessors[appRoute]?.has(processorId) ?? false,
|
||||||
|
|
||||||
|
toggleProcessor: (appRoute, processorId) => {
|
||||||
|
const current = new Set(get().tracedProcessors[appRoute] ?? [])
|
||||||
|
if (current.has(processorId)) current.delete(processorId)
|
||||||
|
else current.add(processorId)
|
||||||
|
set((state) => ({
|
||||||
|
tracedProcessors: { ...state.tracedProcessors, [appRoute]: current },
|
||||||
|
}))
|
||||||
|
return current
|
||||||
|
},
|
||||||
|
}))
|
||||||
Reference in New Issue
Block a user