diff --git a/ui/src/pages/AppsTab/AppsTab.module.css b/ui/src/pages/AppsTab/AppsTab.module.css index 55117502..33e48e55 100644 --- a/ui/src/pages/AppsTab/AppsTab.module.css +++ b/ui/src/pages/AppsTab/AppsTab.module.css @@ -380,3 +380,55 @@ opacity: 0.3; cursor: default; } + +/* Traces & Taps */ +.sectionSummary { + font-size: 12px; + color: var(--text-muted); + margin-bottom: 8px; + display: block; +} + +.hint { + color: var(--text-muted); + font-size: 12px; + font-style: italic; +} + +.routeLabel { + font-size: 12px; + color: var(--text-muted); +} + +.tapBadges { + display: flex; + gap: 4px; + flex-wrap: wrap; +} + +.tapBadgeLink { + background: none; + border: none; + padding: 0; + cursor: pointer; + border-radius: 4px; + transition: opacity 0.15s; +} + +.tapBadgeLink:hover { + opacity: 0.75; +} + +.removeBtn { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 16px; + padding: 0 4px; + line-height: 1; +} + +.removeBtn:hover { + color: var(--error); +} diff --git a/ui/src/pages/AppsTab/AppsTab.tsx b/ui/src/pages/AppsTab/AppsTab.tsx index a08e9973..637e949e 100644 --- a/ui/src/pages/AppsTab/AppsTab.tsx +++ b/ui/src/pages/AppsTab/AppsTab.tsx @@ -31,8 +31,10 @@ import { } from '../../api/queries/admin/apps'; import type { App, AppVersion, Deployment } from '../../api/queries/admin/apps'; import type { Environment } from '../../api/queries/admin/environments'; -import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands'; -import type { ApplicationConfig } from '../../api/queries/commands'; +import { useApplicationConfig, useUpdateApplicationConfig, useProcessorRouteMapping } 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 './AppsTab.module.css'; function formatBytes(bytes: number): string { @@ -456,13 +458,26 @@ function VersionRow({ version, environments, onDeploy }: { version: AppVersion; // CONFIGURATION SUB-TAB // ═══════════════════════════════════════════════════════════════════ +interface TracedTapRow { id: string; processorId: string; captureMode: string | null; taps: TapDefinition[]; } +interface RouteRecordingRow { id: string; routeId: string; recording: boolean; } + function ConfigSubTab({ app, environment }: { app: App; environment?: Environment }) { const { toast } = useToast(); + const navigate = useNavigate(); const { data: agentConfig } = useApplicationConfig(app.slug); const updateAgentConfig = useUpdateApplicationConfig(); const updateContainerConfig = useUpdateContainerConfig(); + const { data: catalog } = useRouteCatalog(); + const { data: processorToRoute = {} } = useProcessorRouteMapping(app.slug); const isProd = environment?.production ?? false; const [editing, setEditing] = useState(false); + const [configTab, setConfigTab] = useState<'agent' | 'infra'>('agent'); + + const appRoutes: RouteSummary[] = useMemo(() => { + if (!catalog) return []; + const entry = (catalog as AppCatalogEntry[]).find((e) => e.appId === app.slug); + return entry?.routes ?? []; + }, [catalog, app.slug]); // Agent config state const [engineLevel, setEngineLevel] = useState('REGULAR'); @@ -476,6 +491,9 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen const [samplingRate, setSamplingRate] = useState('1.0'); const [replayEnabled, setReplayEnabled] = useState(true); const [routeControlEnabled, setRouteControlEnabled] = useState(true); + const [compressSuccess, setCompressSuccess] = useState(false); + const [tracedDraft, setTracedDraft] = useState>({}); + const [routeRecordingDraft, setRouteRecordingDraft] = useState>({}); // Container config state const defaults = environment?.defaultContainerConfig ?? {}; @@ -493,12 +511,14 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen if (agentConfig) { setEngineLevel(agentConfig.engineLevel ?? 'REGULAR'); setPayloadCapture(agentConfig.payloadCaptureMode ?? 'BOTH'); - const raw = agentConfig.payloadCaptureMode !== undefined ? 4096 : 4096; // TODO: read from config when available setPayloadSize('4'); setPayloadUnit('KB'); setAppLogLevel(agentConfig.applicationLogLevel ?? 'INFO'); setAgentLogLevel(agentConfig.agentLogLevel ?? 'INFO'); setMetricsEnabled(agentConfig.metricsEnabled); setSamplingRate(String(agentConfig.samplingRate)); + setCompressSuccess(agentConfig.compressSuccess); + setTracedDraft({ ...agentConfig.tracedProcessors }); + setRouteRecordingDraft({ ...agentConfig.routeRecording }); } setMemoryLimit(String(merged.memoryLimitMb ?? 512)); setMemoryReserve(String(merged.memoryReserveMb ?? '')); @@ -516,11 +536,15 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen setEditing(false); } - function payloadSizeToBytes(): number { - const val = parseFloat(payloadSize) || 0; - if (payloadUnit === 'KB') return val * 1024; - if (payloadUnit === 'MB') return val * 1048576; - return val; + function updateTracedProcessor(processorId: string, mode: string) { + setTracedDraft((prev) => { + if (mode === 'REMOVE') { const next = { ...prev }; delete next[processorId]; return next; } + return { ...prev, [processorId]: mode }; + }); + } + + function updateRouteRecording(routeId: string, recording: boolean) { + setRouteRecordingDraft((prev) => ({ ...prev, [routeId]: recording })); } async function handleSave() { @@ -532,6 +556,9 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen engineLevel, payloadCaptureMode: payloadCapture, applicationLogLevel: appLogLevel, agentLogLevel, metricsEnabled, samplingRate: parseFloat(samplingRate) || 1.0, + compressSuccess, + tracedProcessors: tracedDraft, + routeRecording: routeRecordingDraft, }); } catch { toast({ title: 'Failed to save agent config', variant: 'error', duration: 86_400_000 }); return; } } @@ -557,6 +584,66 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen if (p && !ports.includes(p)) { setPorts([...ports, p]); setNewPort(''); } } + // Traces & Taps + const tracedTapRows: TracedTapRow[] = useMemo(() => { + const traced = editing ? tracedDraft : (agentConfig?.tracedProcessors ?? {}); + const taps = agentConfig?.taps ?? []; + const pids = new Set([...Object.keys(traced), ...taps.map(t => t.processorId)]); + return Array.from(pids).sort().map(pid => ({ id: pid, processorId: pid, captureMode: traced[pid] ?? null, taps: taps.filter(t => t.processorId === pid) })); + }, [editing, tracedDraft, agentConfig?.tracedProcessors, agentConfig?.taps]); + + const tracedCount = useMemo(() => Object.keys(editing ? tracedDraft : (agentConfig?.tracedProcessors ?? {})).length, [editing, tracedDraft, agentConfig?.tracedProcessors]); + const tapCount = agentConfig?.taps?.length ?? 0; + + const tracedTapColumns: Column[] = useMemo(() => [ + { key: 'route' as any, header: 'Route', render: (_v: unknown, row: TracedTapRow) => { + const routeId = processorToRoute[row.processorId]; + return routeId ? {routeId} : ; + }}, + { key: 'processorId', header: 'Processor', render: (_v: unknown, row: TracedTapRow) => {row.processorId} }, + { + key: 'captureMode', header: 'Capture', + render: (_v: unknown, row: TracedTapRow) => { + if (row.captureMode === null) return ; + if (editing) return ( + + ); + return ; + }, + }, + { + key: 'taps', header: 'Taps', + render: (_v: unknown, row: TracedTapRow) => row.taps.length === 0 + ? + :
{row.taps.map(t => ( + + ))}
, + }, + ...(editing ? [{ + key: '_remove' as const, header: '', width: '36px', + render: (_v: unknown, row: TracedTapRow) => row.captureMode === null ? null : ( + + ), + }] : []), + ], [editing, processorToRoute]); + + // Route Recording + const routeRecordingRows: RouteRecordingRow[] = useMemo(() => { + const rec = editing ? routeRecordingDraft : (agentConfig?.routeRecording ?? {}); + return appRoutes.map(r => ({ id: r.routeId, routeId: r.routeId, recording: rec[r.routeId] !== false })); + }, [editing, routeRecordingDraft, agentConfig?.routeRecording, appRoutes]); + + const recordingCount = routeRecordingRows.filter(r => r.recording).length; + + const routeRecordingColumns: Column[] = useMemo(() => [ + { key: 'routeId', header: 'Route', render: (_v: unknown, row: RouteRecordingRow) => {row.routeId} }, + { key: 'recording', header: 'Recording', width: '100px', render: (_v: unknown, row: RouteRecordingRow) => { if (editing) updateRouteRecording(row.routeId, !row.recording); }} disabled={!editing} /> }, + ], [editing, routeRecordingDraft]); + return ( <> {!editing && ( @@ -576,115 +663,148 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen )} - {/* Agent Observability */} - Agent Observability -
- Engine Level - setPayloadCapture(e.target.value)} - options={[{ value: 'NONE', label: 'NONE' }, { value: 'INPUT', label: 'INPUT' }, { value: 'OUTPUT', label: 'OUTPUT' }, { value: 'BOTH', label: 'BOTH' }]} /> - - Max Payload Size -
- setPayloadSize(e.target.value)} style={{ width: 70 }} /> - setAppLogLevel(e.target.value)} - options={['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'].map((l) => ({ value: l, label: l }))} /> - - Agent Log Level - setMetricsInterval(e.target.value)} style={{ width: 50 }} /> - s -
- - Sampling Rate - setSamplingRate(e.target.value)} style={{ width: 80 }} /> - - Replay -
- editing && setReplayEnabled(!replayEnabled)} disabled={!editing} /> - {replayEnabled ? 'Enabled' : 'Disabled'} -
- - Route Control -
- editing && setRouteControlEnabled(!routeControlEnabled)} disabled={!editing} /> - {routeControlEnabled ? 'Enabled' : 'Disabled'} -
+
+ +
- {/* Container Resources */} - Container Resources -
- Memory Limit -
- setMemoryLimit(e.target.value)} style={{ width: 80 }} /> - MB -
+ {configTab === 'agent' && ( + <> + {/* Observability Settings */} + Observability +
+ Engine Level + setMemoryReserve(e.target.value)} placeholder="---" style={{ width: 80 }} /> - MB + Payload Capture + setPayloadSize(e.target.value)} style={{ width: 70 }} /> + setAppLogLevel(e.target.value)} + options={['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'].map((l) => ({ value: l, label: l }))} /> + + Agent Log Level + setMetricsInterval(e.target.value)} style={{ width: 50 }} /> + s +
+ + Sampling Rate + setSamplingRate(e.target.value)} style={{ width: 80 }} /> + + Compress Success +
+ editing && setCompressSuccess(!compressSuccess)} disabled={!editing} /> + {compressSuccess ? 'Enabled' : 'Disabled'} +
+ + Replay +
+ editing && setReplayEnabled(!replayEnabled)} disabled={!editing} /> + {replayEnabled ? 'Enabled' : 'Disabled'} +
+ + Route Control +
+ editing && setRouteControlEnabled(!routeControlEnabled)} disabled={!editing} /> + {routeControlEnabled ? 'Enabled' : 'Disabled'} +
- {!isProd && Available in production environments only} -
- CPU Shares - setCpuShares(e.target.value)} style={{ width: 80 }} /> + {/* Traces & Taps */} + Traces & Taps + {tracedCount} traced · {tapCount} taps + {tracedTapRows.length > 0 + ? columns={tracedTapColumns} data={tracedTapRows} pageSize={20} flush /> + :

No processor traces or taps configured.

} - CPU Limit -
- setCpuLimit(e.target.value)} placeholder="e.g. 1.0" style={{ width: 80 }} /> - cores -
+ {/* Route Recording */} + Route Recording + {recordingCount} of {routeRecordingRows.length} routes recording + {routeRecordingRows.length > 0 + ? columns={routeRecordingColumns} data={routeRecordingRows} pageSize={20} flush /> + :

No routes found for this application.

} + + )} - Exposed Ports -
- {ports.map((p) => ( - - {p} - - + {configTab === 'infra' && ( + <> + {/* Container Resources */} + Container Resources +
+ Memory Limit +
+ setMemoryLimit(e.target.value)} style={{ width: 80 }} /> + MB +
+ + Memory Reserve +
+
+ setMemoryReserve(e.target.value)} placeholder="---" style={{ width: 80 }} /> + MB +
+ {!isProd && Available in production environments only} +
+ + CPU Shares + setCpuShares(e.target.value)} style={{ width: 80 }} /> + + CPU Limit +
+ setCpuLimit(e.target.value)} placeholder="e.g. 1.0" style={{ width: 80 }} /> + cores +
+ + Exposed Ports +
+ {ports.map((p) => ( + + {p} + + + ))} + setNewPort(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addPort(); } }} /> +
+
+ + {/* Environment Variables */} + Environment Variables + {envVars.map((v, i) => ( +
+ { + const next = [...envVars]; next[i] = { ...v, key: e.target.value }; setEnvVars(next); + }} className={styles.envVarKey} /> + { + const next = [...envVars]; next[i] = { ...v, value: e.target.value }; setEnvVars(next); + }} className={styles.envVarValue} /> + +
))} - setNewPort(e.target.value)} - onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addPort(); } }} /> -
- - - {/* Environment Variables */} - Environment Variables - {envVars.map((v, i) => ( -
- { - const next = [...envVars]; next[i] = { ...v, key: e.target.value }; setEnvVars(next); - }} className={styles.envVarKey} /> - { - const next = [...envVars]; next[i] = { ...v, value: e.target.value }; setEnvVars(next); - }} className={styles.envVarValue} /> - -
- ))} - {editing && ( - + {editing && ( + + )} + )} );