import { useEffect, useState, useMemo } from 'react'; import { useParams, useNavigate } from 'react-router'; import { Button, SectionHeader, MonoText, Badge, DataTable, Spinner, 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 styles from './AppConfigDetailPage.module.css'; type BadgeColor = 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto'; interface TracedRow { id: string; processorId: string; captureMode: string } 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'; 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 { data: config, isLoading } = useApplicationConfig(appId); const updateConfig = useUpdateApplicationConfig(); const [editing, setEditing] = useState(false); const [form, setForm] = useState | null>(null); const [tracedDraft, setTracedDraft] = useState>({}); 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, }); setTracedDraft({ ...config.tracedProcessors }); } }, [config]); function startEditing() { if (!config) return; setForm({ logForwardingLevel: config.logForwardingLevel ?? 'INFO', engineLevel: config.engineLevel ?? 'REGULAR', payloadCaptureMode: config.payloadCaptureMode ?? 'NONE', metricsEnabled: config.metricsEnabled, samplingRate: config.samplingRate, }); setTracedDraft({ ...config.tracedProcessors }); 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) => { if (mode === 'REMOVE') { const next = { ...prev }; delete next[processorId]; return next; } return { ...prev, [processorId]: mode }; }); } function handleSave() { if (!config || !form) return; const updated = { ...config, ...form, tracedProcessors: tracedDraft }; updateConfig.mutate(updated, { onSuccess: (saved) => { setEditing(false); toast({ title: 'Config saved', description: `${appId} updated to v${saved.version}`, variant: 'success' }); }, onError: () => { toast({ title: 'Save failed', description: 'Could not update configuration', variant: 'error' }); }, }); } const tracedRows: TracedRow[] = useMemo(() => { const source = editing ? tracedDraft : (config?.tracedProcessors ?? {}); return Object.entries(source).map( ([pid, mode]) => ({ id: pid, processorId: pid, captureMode: mode }), ); }, [editing, tracedDraft, config?.tracedProcessors]); const tracedColumns: Column[] = useMemo(() => [ { key: 'processorId', header: 'Processor ID', render: (_v, row) => {row.processorId} }, { key: 'captureMode', header: 'Capture Mode', render: (_v, row) => { if (editing) { return ( ); } return ; }, }, ...(editing ? [{ key: '_remove' as const, header: '', width: '36px', render: (_v: unknown, row: TracedRow) => ( ), }] : []), ], [editing]); if (isLoading) { return
; } if (!config || !form) { return
No configuration found for "{appId}".
; } return (
{editing ? (
) : ( )}

{appId}

Version {config.version} {config.updatedAt && <> · Updated {formatTimestamp(config.updatedAt)}}
Logging
{editing ? ( ) : ( )} Minimum log level forwarded from agents to the server
Observability
{editing ? ( ) : ( )}
{editing ? ( ) : ( )}
{editing ? ( ) : ( )}
{editing ? ( <> updateField('samplingRate', parseFloat(e.target.value) || 0)} /> 0.0 = sample nothing, 1.0 = capture all executions ) : ( {form.samplingRate} )}
Traced Processors ({tracedRows.length}) {tracedRows.length > 0 ? ( columns={tracedColumns} data={tracedRows} pageSize={20} /> ) : ( No processors are individually traced. {!editing && ' Enable tracing per-processor on the exchange detail page.'} )}
); }