diff --git a/ui/src/pages/Routes/RouteDetail.module.css b/ui/src/pages/Routes/RouteDetail.module.css index 943834de..692ce6f8 100644 --- a/ui/src/pages/Routes/RouteDetail.module.css +++ b/ui/src/pages/Routes/RouteDetail.module.css @@ -286,3 +286,43 @@ font-size: 13px; padding: 8px 0; } + +/* Recording pill */ +.recordingPill { + display: flex; + align-items: center; + gap: 8px; + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + padding: 6px 12px; +} + +.recordingLabel { + font-size: 11px; + color: var(--text-muted); +} + +/* Taps section */ +.tapsSection { + margin-top: 16px; +} + +.tapsHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; +} + +.tapsTitle { + font-size: 13px; + font-weight: 600; +} + +.emptyState { + padding: 40px; + text-align: center; + color: var(--text-muted); + font-size: 13px; +} diff --git a/ui/src/pages/Routes/RouteDetail.tsx b/ui/src/pages/Routes/RouteDetail.tsx index 5dcb116c..1c581f4c 100644 --- a/ui/src/pages/Routes/RouteDetail.tsx +++ b/ui/src/pages/Routes/RouteDetail.tsx @@ -13,6 +13,7 @@ import { Spinner, MonoText, Sparkline, + Toggle, } from '@cameleer/design-system'; import type { KpiItem, Column } from '@cameleer/design-system'; import { useGlobalFilters } from '@cameleer/design-system'; @@ -20,6 +21,7 @@ import { useRouteCatalog } from '../../api/queries/catalog'; import { useDiagramByRoute } from '../../api/queries/diagrams'; import { useProcessorMetrics } from '../../api/queries/processor-metrics'; import { useStatsTimeseries, useSearchExecutions, useExecutionStats } from '../../api/queries/executions'; +import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands'; import type { ExecutionSummary, AppCatalogEntry, RouteSummary } from '../../api/types'; import { mapDiagramToRouteNodes, toFlowSegments } from '../../utils/diagram-mapping'; import styles from './RouteDetail.module.css'; @@ -294,6 +296,18 @@ export default function RouteDetail() { limit: 200, }); + // ── Application config ────────────────────────────────────────────────────── + const config = useApplicationConfig(appId); + const updateConfig = useUpdateApplicationConfig(); + + const isRecording = config.data?.routeRecording?.[routeId!] !== false; + + function toggleRecording() { + if (!config.data) return; + const routeRecording = { ...config.data.routeRecording, [routeId!]: !isRecording }; + updateConfig.mutate({ ...config.data, routeRecording }); + } + // ── Derived data ─────────────────────────────────────────────────────────── const appEntry: AppCatalogEntry | undefined = useMemo(() => @@ -408,11 +422,29 @@ export default function RouteDetail() { .sort((a, b) => b.count - a.count); }, [errorResult]); + // Route taps — cross-reference config taps with diagram processor IDs + const routeTaps = useMemo(() => { + if (!config.data?.taps || !diagram) return []; + const routeProcessorIds = new Set( + (diagram.nodes || []).map((n: any) => n.id).filter(Boolean), + ); + return config.data.taps.filter(t => routeProcessorIds.has(t.processorId)); + }, [config.data?.taps, diagram]); + + const activeTapCount = routeTaps.filter(t => t.enabled).length; + // KPI items - const kpiItems = useMemo(() => - buildDetailKpiItems(stats, throughputSparkline, errorSparkline, latencySparkline), - [stats, throughputSparkline, errorSparkline, latencySparkline], - ); + const kpiItems = useMemo(() => { + const base = buildDetailKpiItems(stats, throughputSparkline, errorSparkline, latencySparkline); + base.push({ + label: 'Active Taps', + value: String(activeTapCount), + trend: { label: `${routeTaps.length} total`, variant: 'muted' as const }, + subtitle: `${activeTapCount} enabled / ${routeTaps.length} configured`, + borderColor: 'var(--running)', + }); + return base; + }, [stats, throughputSparkline, errorSparkline, latencySparkline, activeTapCount, routeTaps.length]); const processorColumns = useMemo(() => makeProcessorColumns(styles), []); @@ -420,6 +452,7 @@ export default function RouteDetail() { { label: 'Performance', value: 'performance' }, { label: 'Recent Executions', value: 'executions', count: exchangeRows.length }, { label: 'Error Patterns', value: 'errors', count: errorPatterns.length }, + { label: 'Taps', value: 'taps', count: routeTaps.length }, ]; // ── Render ───────────────────────────────────────────────────────────────── @@ -439,6 +472,10 @@ export default function RouteDetail() {
+
+ Recording + +
Exchanges
{exchangeCount.toLocaleString()}
@@ -592,6 +629,16 @@ export default function RouteDetail() { )}
)} + + {activeTab === 'taps' && ( +
+
+ {routeTaps.length === 0 + ? 'No taps configured for this route. Add a tap to extract business attributes from exchange data.' + : `${routeTaps.length} taps configured (${activeTapCount} active)`} +
+
+ )}
);