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;
|
font-size: 13px;
|
||||||
padding: 8px 0;
|
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,
|
Spinner,
|
||||||
MonoText,
|
MonoText,
|
||||||
Sparkline,
|
Sparkline,
|
||||||
|
Toggle,
|
||||||
} from '@cameleer/design-system';
|
} from '@cameleer/design-system';
|
||||||
import type { KpiItem, Column } from '@cameleer/design-system';
|
import type { KpiItem, Column } from '@cameleer/design-system';
|
||||||
import { useGlobalFilters } 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 { useDiagramByRoute } from '../../api/queries/diagrams';
|
||||||
import { useProcessorMetrics } from '../../api/queries/processor-metrics';
|
import { useProcessorMetrics } from '../../api/queries/processor-metrics';
|
||||||
import { useStatsTimeseries, useSearchExecutions, useExecutionStats } from '../../api/queries/executions';
|
import { useStatsTimeseries, useSearchExecutions, useExecutionStats } from '../../api/queries/executions';
|
||||||
|
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands';
|
||||||
import type { ExecutionSummary, AppCatalogEntry, RouteSummary } from '../../api/types';
|
import type { ExecutionSummary, AppCatalogEntry, RouteSummary } from '../../api/types';
|
||||||
import { mapDiagramToRouteNodes, toFlowSegments } from '../../utils/diagram-mapping';
|
import { mapDiagramToRouteNodes, toFlowSegments } from '../../utils/diagram-mapping';
|
||||||
import styles from './RouteDetail.module.css';
|
import styles from './RouteDetail.module.css';
|
||||||
@@ -294,6 +296,18 @@ export default function RouteDetail() {
|
|||||||
limit: 200,
|
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 ───────────────────────────────────────────────────────────
|
// ── Derived data ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const appEntry: AppCatalogEntry | undefined = useMemo(() =>
|
const appEntry: AppCatalogEntry | undefined = useMemo(() =>
|
||||||
@@ -408,11 +422,29 @@ export default function RouteDetail() {
|
|||||||
.sort((a, b) => b.count - a.count);
|
.sort((a, b) => b.count - a.count);
|
||||||
}, [errorResult]);
|
}, [errorResult]);
|
||||||
|
|
||||||
// KPI items
|
// Route taps — cross-reference config taps with diagram processor IDs
|
||||||
const kpiItems = useMemo(() =>
|
const routeTaps = useMemo(() => {
|
||||||
buildDetailKpiItems(stats, throughputSparkline, errorSparkline, latencySparkline),
|
if (!config.data?.taps || !diagram) return [];
|
||||||
[stats, throughputSparkline, errorSparkline, latencySparkline],
|
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(() => {
|
||||||
|
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), []);
|
const processorColumns = useMemo(() => makeProcessorColumns(styles), []);
|
||||||
|
|
||||||
@@ -420,6 +452,7 @@ export default function RouteDetail() {
|
|||||||
{ label: 'Performance', value: 'performance' },
|
{ label: 'Performance', value: 'performance' },
|
||||||
{ label: 'Recent Executions', value: 'executions', count: exchangeRows.length },
|
{ label: 'Recent Executions', value: 'executions', count: exchangeRows.length },
|
||||||
{ label: 'Error Patterns', value: 'errors', count: errorPatterns.length },
|
{ label: 'Error Patterns', value: 'errors', count: errorPatterns.length },
|
||||||
|
{ label: 'Taps', value: 'taps', count: routeTaps.length },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ── Render ─────────────────────────────────────────────────────────────────
|
// ── Render ─────────────────────────────────────────────────────────────────
|
||||||
@@ -439,6 +472,10 @@ export default function RouteDetail() {
|
|||||||
<Badge label={appId ?? ''} color="auto" />
|
<Badge label={appId ?? ''} color="auto" />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.headerRight}>
|
<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.headerStat}>
|
||||||
<div className={styles.headerStatLabel}>Exchanges</div>
|
<div className={styles.headerStatLabel}>Exchanges</div>
|
||||||
<div className={styles.headerStatValue}>{exchangeCount.toLocaleString()}</div>
|
<div className={styles.headerStatValue}>{exchangeCount.toLocaleString()}</div>
|
||||||
@@ -592,6 +629,16 @@ export default function RouteDetail() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user