feat: trace data indicators, inline tap config, and detail tab gating
Trace data visibility: - ProcessorNode now includes hasTraceData flag computed from captured body/headers during tree conversion - ConfigBadge shows teal for tracing configured, green when data captured - Search results show green footprints icon for exchanges with trace data - New has_trace_data column on executions table (V11 migration with backfill) - OpenSearch documents and ExecutionSummary include the flag Inline tap configuration: - Extracted reusable TapConfigModal component from RouteDetail - Diagram context menu opens tap modal inline instead of navigating away - Toggle-trace action works immediately with toast feedback - Modal closes only on ESC, Cancel, Save, or Delete (not backdrop click) Detail panel tab gating: - Headers, Input, Output tabs disabled when no data is available - Works at both exchange and processor level - Falls back to Info tab when active tab becomes empty Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||
import { useNavigate, useLocation, useParams } from 'react-router';
|
||||
import { useGlobalFilters } from '@cameleer/design-system';
|
||||
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 { useApplicationConfig } from '../../api/queries/commands';
|
||||
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands';
|
||||
import type { TapDefinition } from '../../api/queries/commands';
|
||||
import { useTracingStore } from '../../stores/tracing-store';
|
||||
import type { NodeAction, NodeConfig } from '../../components/ProcessDiagram/types';
|
||||
import { TapConfigModal } from '../../components/TapConfigModal';
|
||||
import { ExchangeHeader } from './ExchangeHeader';
|
||||
import { ExecutionDiagram } from '../../components/ExecutionDiagram/ExecutionDiagram';
|
||||
import { ProcessDiagram } from '../../components/ProcessDiagram';
|
||||
@@ -115,7 +117,6 @@ interface DiagramPanelProps {
|
||||
}
|
||||
|
||||
function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearSelection }: DiagramPanelProps) {
|
||||
const navigate = useNavigate();
|
||||
const { timeRange } = useGlobalFilters();
|
||||
const timeFrom = timeRange.start.toISOString();
|
||||
const timeTo = timeRange.end.toISOString();
|
||||
@@ -172,11 +173,90 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
|
||||
return map;
|
||||
}, [tracedMap, appConfig]);
|
||||
|
||||
// Processor options for tap modal dropdown
|
||||
const processorOptions = useMemo(() => {
|
||||
const nodes = diagramQuery.data?.nodes;
|
||||
if (!nodes) return [];
|
||||
return (nodes as Array<{ id?: string; label?: string }>)
|
||||
.filter(n => n.id)
|
||||
.map(n => ({ value: n.id!, label: n.label || n.id! }));
|
||||
}, [diagramQuery.data]);
|
||||
|
||||
// Tap modal state
|
||||
const [tapModalOpen, setTapModalOpen] = useState(false);
|
||||
const [tapModalTarget, setTapModalTarget] = useState<string | undefined>();
|
||||
const [editingTap, setEditingTap] = useState<TapDefinition | null>(null);
|
||||
|
||||
const updateConfig = useUpdateApplicationConfig();
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleTapSave = useCallback((updatedConfig: typeof appConfig) => {
|
||||
if (!updatedConfig) return;
|
||||
updateConfig.mutate(updatedConfig, {
|
||||
onSuccess: (saved) => {
|
||||
toast({ title: 'Tap configuration saved', description: `Pushed to agents (v${saved.version})`, variant: 'success' });
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: 'Tap update failed', description: 'Could not save configuration', variant: 'error' });
|
||||
},
|
||||
});
|
||||
}, [updateConfig, toast]);
|
||||
|
||||
const handleTapDelete = useCallback((tap: TapDefinition) => {
|
||||
if (!appConfig) return;
|
||||
const taps = appConfig.taps.filter(t => t.tapId !== tap.tapId);
|
||||
updateConfig.mutate({ ...appConfig, taps }, {
|
||||
onSuccess: (saved) => {
|
||||
toast({ title: 'Tap deleted', description: `${tap.attributeName} removed (v${saved.version})`, variant: 'success' });
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: 'Tap delete failed', description: 'Could not save configuration', variant: 'error' });
|
||||
},
|
||||
});
|
||||
}, [appConfig, updateConfig, toast]);
|
||||
|
||||
const handleNodeAction = useCallback((nodeId: string, action: NodeAction) => {
|
||||
if (action === 'configure-tap') {
|
||||
navigate(`/admin/appconfig?app=${encodeURIComponent(appId)}&processor=${encodeURIComponent(nodeId)}`);
|
||||
if (!appConfig) return;
|
||||
// Check if there's an existing tap for this processor
|
||||
const existing = appConfig.taps?.find(t => t.processorId === nodeId) ?? null;
|
||||
setEditingTap(existing);
|
||||
setTapModalTarget(nodeId);
|
||||
setTapModalOpen(true);
|
||||
} else if (action === 'toggle-trace') {
|
||||
if (!appConfig) return;
|
||||
const newMap = useTracingStore.getState().toggleProcessor(appId, nodeId);
|
||||
const enabled = nodeId in newMap;
|
||||
const tracedProcessors: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(newMap)) tracedProcessors[k] = v;
|
||||
updateConfig.mutate({
|
||||
...appConfig,
|
||||
tracedProcessors,
|
||||
}, {
|
||||
onSuccess: (saved) => {
|
||||
toast({ title: `Tracing ${enabled ? 'enabled' : 'disabled'}`, description: `${nodeId} — pushed to agents (v${saved.version})`, variant: 'success' });
|
||||
},
|
||||
onError: () => {
|
||||
useTracingStore.getState().toggleProcessor(appId, nodeId);
|
||||
toast({ title: 'Tracing update failed', description: 'Could not save configuration', variant: 'error' });
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [appId, navigate]);
|
||||
}, [appId, appConfig, updateConfig, toast]);
|
||||
|
||||
const tapModal = appConfig && (
|
||||
<TapConfigModal
|
||||
open={tapModalOpen}
|
||||
onClose={() => setTapModalOpen(false)}
|
||||
tap={editingTap}
|
||||
processorOptions={processorOptions}
|
||||
defaultProcessorId={tapModalTarget}
|
||||
application={appId}
|
||||
config={appConfig}
|
||||
onSave={handleTapSave}
|
||||
onDelete={handleTapDelete}
|
||||
/>
|
||||
);
|
||||
|
||||
// Exchange selected: show header + execution diagram
|
||||
if (exchangeId && detail) {
|
||||
@@ -191,6 +271,7 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
|
||||
onNodeAction={handleNodeAction}
|
||||
nodeConfigs={nodeConfigs}
|
||||
/>
|
||||
{tapModal}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -198,15 +279,18 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
|
||||
// No exchange selected: show topology-only diagram
|
||||
if (diagramQuery.data) {
|
||||
return (
|
||||
<ProcessDiagram
|
||||
application={appId}
|
||||
routeId={routeId}
|
||||
diagramLayout={diagramQuery.data}
|
||||
knownRouteIds={knownRouteIds}
|
||||
endpointRouteMap={endpointRouteMap}
|
||||
onNodeAction={handleNodeAction}
|
||||
nodeConfigs={nodeConfigs}
|
||||
/>
|
||||
<>
|
||||
<ProcessDiagram
|
||||
application={appId}
|
||||
routeId={routeId}
|
||||
diagramLayout={diagramQuery.data}
|
||||
knownRouteIds={knownRouteIds}
|
||||
endpointRouteMap={endpointRouteMap}
|
||||
onNodeAction={handleNodeAction}
|
||||
nodeConfigs={nodeConfigs}
|
||||
/>
|
||||
{tapModal}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user