From 64b677696e165b558994c2516f58e7736c6104d0 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:48:14 +0100 Subject: [PATCH] feat(ui): restructure AppConfigDetailPage into 3 sections Merge Logging + Observability into unified "Settings" section with flex-wrap badge grid including new compressSuccess toggle. Merge Traced Processors with Taps into "Traces & Taps" section showing capture mode and tap badges per processor. Add "Route Recording" section with per-route toggles sourced from route catalog. All new fields (compressSuccess, routeRecording) included in form state and save payload. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Admin/AppConfigDetailPage.module.css | 20 +- ui/src/pages/Admin/AppConfigDetailPage.tsx | 337 +++++++++++++----- 2 files changed, 263 insertions(+), 94 deletions(-) diff --git a/ui/src/pages/Admin/AppConfigDetailPage.module.css b/ui/src/pages/Admin/AppConfigDetailPage.module.css index 0c7696da..392aebe6 100644 --- a/ui/src/pages/Admin/AppConfigDetailPage.module.css +++ b/ui/src/pages/Admin/AppConfigDetailPage.module.css @@ -1,5 +1,5 @@ .page { - max-width: 640px; + max-width: 720px; margin: 0 auto; } @@ -115,11 +115,23 @@ padding: 16px 20px; } +.sectionSummary { + font-size: 12px; + color: var(--text-muted); +} + +.settingsGrid { + display: flex; + flex-wrap: wrap; + gap: 16px 24px; +} + .field { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; + min-width: 140px; } .label { @@ -158,6 +170,12 @@ cursor: pointer; } +.tapBadges { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + .hint { font-size: 11px; color: var(--text-muted); diff --git a/ui/src/pages/Admin/AppConfigDetailPage.tsx b/ui/src/pages/Admin/AppConfigDetailPage.tsx index 833c364d..ff483a90 100644 --- a/ui/src/pages/Admin/AppConfigDetailPage.tsx +++ b/ui/src/pages/Admin/AppConfigDetailPage.tsx @@ -1,15 +1,29 @@ import { useEffect, useState, useMemo } from 'react'; import { useParams, useNavigate } from 'react-router'; import { - Button, SectionHeader, MonoText, Badge, DataTable, Spinner, useToast, + Button, SectionHeader, MonoText, Badge, DataTable, Spinner, Toggle, useToast, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands'; -import type { ApplicationConfig } from '../../api/queries/commands'; +import type { ApplicationConfig, TapDefinition } from '../../api/queries/commands'; +import { useRouteCatalog } from '../../api/queries/catalog'; +import type { AppCatalogEntry, RouteSummary } from '../../api/types'; import styles from './AppConfigDetailPage.module.css'; type BadgeColor = 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto'; -interface TracedRow { id: string; processorId: string; captureMode: string } + +interface TracedTapRow { + id: string; + processorId: string; + captureMode: string | null; + taps: TapDefinition[]; +} + +interface RouteRecordingRow { + id: string; + routeId: string; + recording: boolean; +} function formatTimestamp(iso?: string): string { if (!iso) return '\u2014'; @@ -59,22 +73,32 @@ export default function AppConfigDetailPage() { const { toast } = useToast(); const { data: config, isLoading } = useApplicationConfig(appId); const updateConfig = useUpdateApplicationConfig(); + const { data: catalog } = useRouteCatalog(); const [editing, setEditing] = useState(false); const [form, setForm] = useState | null>(null); const [tracedDraft, setTracedDraft] = useState>({}); + const [routeRecordingDraft, setRouteRecordingDraft] = useState>({}); + + // Find routes for this application from the catalog + const appRoutes: RouteSummary[] = useMemo(() => { + if (!catalog || !appId) return []; + const entry = (catalog as AppCatalogEntry[]).find((e) => e.appId === appId); + return entry?.routes ?? []; + }, [catalog, appId]); useEffect(() => { if (config) { - // Reset form when server data arrives (after save or initial load) setForm({ logForwardingLevel: config.logForwardingLevel ?? 'INFO', engineLevel: config.engineLevel ?? 'REGULAR', payloadCaptureMode: config.payloadCaptureMode ?? 'NONE', metricsEnabled: config.metricsEnabled, samplingRate: config.samplingRate, + compressSuccess: config.compressSuccess, }); setTracedDraft({ ...config.tracedProcessors }); + setRouteRecordingDraft({ ...config.routeRecording }); } }, [config]); @@ -86,8 +110,10 @@ export default function AppConfigDetailPage() { payloadCaptureMode: config.payloadCaptureMode ?? 'NONE', metricsEnabled: config.metricsEnabled, samplingRate: config.samplingRate, + compressSuccess: config.compressSuccess, }); setTracedDraft({ ...config.tracedProcessors }); + setRouteRecordingDraft({ ...config.routeRecording }); setEditing(true); } @@ -110,9 +136,18 @@ export default function AppConfigDetailPage() { }); } + function updateRouteRecording(routeId: string, recording: boolean) { + setRouteRecordingDraft((prev) => ({ ...prev, [routeId]: recording })); + } + function handleSave() { if (!config || !form) return; - const updated = { ...config, ...form, tracedProcessors: tracedDraft }; + const updated: ApplicationConfig = { + ...config, + ...form, + tracedProcessors: tracedDraft, + routeRecording: routeRecordingDraft, + } as ApplicationConfig; updateConfig.mutate(updated, { onSuccess: (saved) => { setEditing(false); @@ -124,24 +159,53 @@ export default function AppConfigDetailPage() { }); } - const tracedRows: TracedRow[] = useMemo(() => { + // ── Traces & Taps merged rows ────────────────────────────────────────────── + + const tracedTapRows: TracedTapRow[] = useMemo(() => { + const traced = editing ? tracedDraft : (config?.tracedProcessors ?? {}); + const taps = config?.taps ?? []; + + // Collect all unique processor IDs + const processorIds = new Set([ + ...Object.keys(traced), + ...taps.map((t) => t.processorId), + ]); + + return Array.from(processorIds).sort().map((pid) => ({ + id: pid, + processorId: pid, + captureMode: traced[pid] ?? null, + taps: taps.filter((t) => t.processorId === pid), + })); + }, [editing, tracedDraft, config?.tracedProcessors, config?.taps]); + + const tracedCount = useMemo(() => { const source = editing ? tracedDraft : (config?.tracedProcessors ?? {}); - return Object.entries(source).map( - ([pid, mode]) => ({ id: pid, processorId: pid, captureMode: mode }), - ); + return Object.keys(source).length; }, [editing, tracedDraft, config?.tracedProcessors]); - const tracedColumns: Column[] = useMemo(() => [ - { key: 'processorId', header: 'Processor ID', render: (_v, row) => {row.processorId} }, + const tapCount = config?.taps?.length ?? 0; + + const tracedTapColumns: Column[] = useMemo(() => [ + { + key: 'processorId', + header: 'Processor', + render: (_v, row) => {row.processorId}, + }, { key: 'captureMode', - header: 'Capture Mode', + header: 'Capture', render: (_v, row) => { + if (row.captureMode === null) { + return ; + } if (editing) { return ( - updateField('logForwardingLevel', e.target.value)}> - - - - - - ) : ( - - )} - Minimum log level forwarded from agents to the server - - - -
- Observability -
- - {editing ? ( - - ) : ( - - )} -
-
- - {editing ? ( - - ) : ( - - )} -
-
- - {editing ? ( - - ) : ( - - )} -
-
- - {editing ? ( - <> + Settings +
+
+ + {editing ? ( + + ) : ( + + )} +
+
+ + {editing ? ( + + ) : ( + + )} +
+
+ + {editing ? ( + + ) : ( + + )} +
+
+ + {editing ? ( + + ) : ( + + )} +
+
+ + {editing ? ( updateField('samplingRate', parseFloat(e.target.value) || 0)} /> - 0.0 = sample nothing, 1.0 = capture all executions - - ) : ( - {form.samplingRate} - )} + ) : ( + {form.samplingRate} + )} +
+
+ + {editing ? ( + + ) : ( + + )} +
+ {/* ── Traces & Taps ─────────────────────────────────────────────── */}
- Traced Processors ({tracedRows.length}) - {tracedRows.length > 0 ? ( - columns={tracedColumns} data={tracedRows} pageSize={20} /> + Traces & Taps + + {tracedCount} traced · {tapCount} taps · manage taps on route pages + + {tracedTapRows.length > 0 ? ( + columns={tracedTapColumns} data={tracedTapRows} pageSize={20} /> ) : ( - No processors are individually traced. - {!editing && ' Enable tracing per-processor on the exchange detail page.'} + No processors are individually traced and no taps are defined. + {!editing && ' Enable tracing per-processor on the exchange detail page, or add taps on route pages.'} + + )} +
+ + {/* ── Route Recording ───────────────────────────────────────────── */} +
+ Route Recording + + {recordingCount} of {routeRecordingRows.length} routes recording + + {routeRecordingRows.length > 0 ? ( + columns={routeRecordingColumns} data={routeRecordingRows} pageSize={20} /> + ) : ( + + No routes found for this application. Routes appear once agents report data. )}