feat(ui): add recording toggle, active taps KPI, and taps tab to RouteDetail

- Add Toggle for route recording on/off in the route header
- Fetch application config to determine recording state and route taps
- Add Active Taps KPI card showing enabled/total tap counts
- Add Taps tab to the tabbed section with placeholder content

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-26 18:44:06 +01:00
parent 47ff122c48
commit 807e191397
2 changed files with 91 additions and 4 deletions

View File

@@ -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;
}

View File

@@ -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() {
<Badge label={appId ?? ''} color="auto" />
</div>
<div className={styles.headerRight}>
<div className={styles.recordingPill}>
<span className={styles.recordingLabel}>Recording</span>
<Toggle checked={isRecording} onChange={toggleRecording} />
</div>
<div className={styles.headerStat}>
<div className={styles.headerStatLabel}>Exchanges</div>
<div className={styles.headerStatValue}>{exchangeCount.toLocaleString()}</div>
@@ -592,6 +629,16 @@ export default function RouteDetail() {
)}
</div>
)}
{activeTab === 'taps' && (
<div className={styles.tapsSection}>
<div className={styles.emptyState}>
{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)`}
</div>
</div>
)}
</div>
</div>
);