import { useEffect, useState, useMemo } from 'react'; import { useParams, useNavigate } from 'react-router'; import { ArrowLeft, Pencil, X } from 'lucide-react'; import { Button, SectionHeader, MonoText, Badge, DataTable, Spinner, Toggle, Select, Label, useToast, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands'; import type { ApplicationConfig, TapDefinition, ConfigUpdateResponse } from '../../api/queries/commands'; import { useEnvironmentStore } from '../../api/environment-store'; import { useCatalog } from '../../api/queries/catalog'; import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog'; import { applyTracedProcessorUpdate, applyRouteRecordingUpdate } from '../../utils/config-draft-utils'; import styles from './AppConfigDetailPage.module.css'; import sectionStyles from '../../styles/section-card.module.css'; type BadgeColor = 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto'; 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'; return new Date(iso).toLocaleString('en-GB', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', }); } function logLevelColor(level?: string): BadgeColor { switch (level?.toUpperCase()) { case 'ERROR': return 'error'; case 'WARN': return 'warning'; case 'DEBUG': return 'running'; case 'TRACE': return 'auto'; default: return 'success'; } } function engineLevelColor(level?: string): BadgeColor { switch (level?.toUpperCase()) { case 'NONE': return 'error'; case 'MINIMAL': return 'warning'; case 'COMPLETE': return 'running'; default: return 'success'; } } function payloadColor(mode?: string): BadgeColor { switch (mode?.toUpperCase()) { case 'INPUT': case 'OUTPUT': return 'warning'; case 'BOTH': return 'running'; default: return 'auto'; } } function captureColor(mode: string): BadgeColor { switch (mode?.toUpperCase()) { case 'INPUT': case 'OUTPUT': return 'warning'; case 'BOTH': return 'running'; default: return 'auto'; } } export default function AppConfigDetailPage() { const { appId } = useParams<{ appId: string }>(); const navigate = useNavigate(); const { toast } = useToast(); const selectedEnv = useEnvironmentStore((s) => s.environment); const { data: config, isLoading } = useApplicationConfig(appId); const updateConfig = useUpdateApplicationConfig(); const { data: catalog } = useCatalog(); 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: CatalogRoute[] = useMemo(() => { if (!catalog || !appId) return []; const entry = (catalog as CatalogApp[]).find((e) => e.slug === appId); return entry?.routes ?? []; }, [catalog, appId]); useEffect(() => { if (config) { setForm({ applicationLogLevel: config.applicationLogLevel ?? 'INFO', agentLogLevel: config.agentLogLevel ?? 'INFO', engineLevel: config.engineLevel ?? 'REGULAR', payloadCaptureMode: config.payloadCaptureMode ?? 'BOTH', metricsEnabled: config.metricsEnabled, samplingRate: config.samplingRate, compressSuccess: config.compressSuccess, }); setTracedDraft({ ...config.tracedProcessors }); setRouteRecordingDraft({ ...config.routeRecording }); } }, [config]); function startEditing() { if (!config) return; setForm({ applicationLogLevel: config.applicationLogLevel ?? 'INFO', agentLogLevel: config.agentLogLevel ?? 'INFO', engineLevel: config.engineLevel ?? 'REGULAR', payloadCaptureMode: config.payloadCaptureMode ?? 'BOTH', metricsEnabled: config.metricsEnabled, samplingRate: config.samplingRate, compressSuccess: config.compressSuccess, }); setTracedDraft({ ...config.tracedProcessors }); setRouteRecordingDraft({ ...config.routeRecording }); setEditing(true); } function cancelEditing() { setEditing(false); } function updateField(key: K, value: ApplicationConfig[K]) { setForm((prev) => prev ? { ...prev, [key]: value } : prev); } function updateTracedProcessor(processorId: string, mode: string) { setTracedDraft((prev) => applyTracedProcessorUpdate(prev, processorId, mode)); } function updateRouteRecording(routeId: string, recording: boolean) { setRouteRecordingDraft((prev) => applyRouteRecordingUpdate(prev, routeId, recording)); } function handleSave() { if (!config || !form) return; const updated: ApplicationConfig = { ...config, ...form, tracedProcessors: tracedDraft, routeRecording: routeRecordingDraft, } as ApplicationConfig; updateConfig.mutate({ config: updated, environment: selectedEnv }, { onSuccess: (saved: ConfigUpdateResponse) => { setEditing(false); if (saved.pushResult.success) { toast({ title: 'Config saved', description: `${appId} updated to v${saved.config.version} — pushed to ${saved.pushResult.total}/${saved.pushResult.total} agents`, variant: 'success' }); } else { const failed = [...saved.pushResult.responses.filter(r => r.status !== 'SUCCESS').map(r => r.agentId), ...saved.pushResult.timedOut]; toast({ title: 'Config saved — partial push failure', description: `${saved.pushResult.responded}/${saved.pushResult.total} responded. Failed: ${failed.join(', ')}`, variant: 'warning', duration: 86_400_000 }); } }, onError: () => { toast({ title: 'Failed to save configuration', description: 'Could not update configuration', variant: 'error', duration: 86_400_000 }); }, }); } // ── 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.keys(source).length; }, [editing, tracedDraft, config?.tracedProcessors]); const tapCount = config?.taps?.length ?? 0; const tracedTapColumns: Column[] = useMemo(() => [ { key: 'processorId', header: 'Processor', render: (_v, row) => {row.processorId}, }, { key: 'captureMode', header: 'Capture', render: (_v, row) => { if (row.captureMode === null) { return ; } if (editing) { return ( updateField('applicationLogLevel', e.target.value)} options={[ { value: 'ERROR', label: 'ERROR' }, { value: 'WARN', label: 'WARN' }, { value: 'INFO', label: 'INFO' }, { value: 'DEBUG', label: 'DEBUG' }, { value: 'TRACE', label: 'TRACE' }, ]} /> ) : ( )}
{editing ? ( updateField('engineLevel', e.target.value)} options={[ { value: 'NONE', label: 'None' }, { value: 'MINIMAL', label: 'Minimal' }, { value: 'REGULAR', label: 'Regular' }, { value: 'COMPLETE', label: 'Complete' }, ]} /> ) : ( )}
{editing ? ( updateField('samplingRate', parseFloat(e.target.value) || 0)} /> ) : ( {form.samplingRate} )}
{editing ? ( updateField('compressSuccess', (e.target as HTMLInputElement).checked)} label={form.compressSuccess ? 'On' : 'Off'} /> ) : ( )}
{/* ── Traces & Taps ─────────────────────────────────────────────── */}
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 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. )}
); }