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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user