diff --git a/ui/src/pages/Admin/AppConfigPage.module.css b/ui/src/pages/Admin/AppConfigPage.module.css index 7f0ab0f1..d0485d5a 100644 --- a/ui/src/pages/Admin/AppConfigPage.module.css +++ b/ui/src/pages/Admin/AppConfigPage.module.css @@ -16,6 +16,34 @@ opacity: 1; } +.editBtn { + background: transparent; + border: none; + color: var(--text-faint); + opacity: 0.75; + cursor: pointer; + font-size: 13px; + padding: 2px 4px; + border-radius: var(--radius-sm); + line-height: 1; + display: inline-flex; +} + +.editBtn:hover { + color: var(--text-primary); + opacity: 1; +} + +.editBtn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +.editActions { + display: inline-flex; + gap: 2px; +} + .inlineSelect { padding: 3px 8px; border: 1px solid var(--border-subtle); diff --git a/ui/src/pages/Admin/AppConfigPage.tsx b/ui/src/pages/Admin/AppConfigPage.tsx index 70284410..9fda46a4 100644 --- a/ui/src/pages/Admin/AppConfigPage.tsx +++ b/ui/src/pages/Admin/AppConfigPage.tsx @@ -1,4 +1,4 @@ -import { useMemo, useCallback } from 'react'; +import { useState, useMemo, useCallback } from 'react'; import { useNavigate } from 'react-router'; import { DataTable, Badge, MonoText, useToast } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; @@ -20,7 +20,9 @@ function timeAgo(iso?: string): string { return `${Math.floor(hours / 24)}d ago`; } -function logLevelColor(level?: string): string { +type BadgeColor = 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto'; + +function logLevelColor(level?: string): BadgeColor { switch (level?.toUpperCase()) { case 'ERROR': return 'error'; case 'WARN': return 'warning'; @@ -29,23 +31,59 @@ function logLevelColor(level?: string): string { } } +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'; + } +} + export default function AppConfigPage() { const navigate = useNavigate(); const { toast } = useToast(); const { data: configs } = useAllApplicationConfigs(); const updateConfig = useUpdateApplicationConfig(); + const [editingApp, setEditingApp] = useState(null); + const [draft, setDraft] = useState>({}); - const handleChange = useCallback((config: ApplicationConfig, field: string, value: string | boolean) => { - const updated = { ...config, [field]: value }; + const startEditing = useCallback((row: ConfigRow) => { + setEditingApp(row.application); + setDraft({ + logForwardingLevel: row.logForwardingLevel ?? 'INFO', + engineLevel: row.engineLevel ?? 'REGULAR', + payloadCaptureMode: row.payloadCaptureMode ?? 'NONE', + metricsEnabled: row.metricsEnabled, + }); + }, []); + + const cancelEditing = useCallback(() => { + setEditingApp(null); + setDraft({}); + }, []); + + const saveEditing = useCallback((row: ConfigRow) => { + const updated = { ...row, ...draft }; updateConfig.mutate(updated, { onSuccess: (saved) => { - toast({ title: 'Config updated', description: `${config.application}: ${field} \u2192 ${value} (v${saved.version})`, variant: 'success' }); + setEditingApp(null); + setDraft({}); + toast({ title: 'Config updated', description: `${row.application} (v${saved.version})`, variant: 'success' }); }, onError: () => { - toast({ title: 'Config update failed', description: config.application, variant: 'error' }); + toast({ title: 'Config update failed', description: row.application, variant: 'error' }); }, }); - }, [updateConfig, toast]); + }, [draft, updateConfig, toast]); const columns: Column[] = useMemo(() => [ { @@ -65,6 +103,41 @@ export default function AppConfigPage() { ), }, + { + key: '_edit', + header: '', + width: '36px', + render: (_val, row) => { + const isEditing = editingApp === row.application; + return isEditing ? ( + + + + + ) : ( + + ); + }, + }, { key: 'application', header: 'Application', @@ -74,69 +147,88 @@ export default function AppConfigPage() { { key: 'logForwardingLevel', header: 'Log Level', - render: (_val, row) => ( - - ), + render: (_val, row) => { + const val = row.logForwardingLevel ?? 'INFO'; + if (editingApp === row.application) { + return ( + + ); + } + return ; + }, }, { key: 'engineLevel', header: 'Engine Level', - render: (_val, row) => ( - - ), + render: (_val, row) => { + const val = row.engineLevel ?? 'REGULAR'; + if (editingApp === row.application) { + return ( + + ); + } + return ; + }, }, { key: 'payloadCaptureMode', header: 'Payload Capture', - render: (_val, row) => ( - - ), + render: (_val, row) => { + const val = row.payloadCaptureMode ?? 'NONE'; + if (editingApp === row.application) { + return ( + + ); + } + return ; + }, }, { key: 'metricsEnabled', header: 'Metrics', width: '80px', - render: (_val, row) => ( - - ), + render: (_val, row) => { + if (editingApp === row.application) { + return ( + + ); + } + return ; + }, }, { key: 'tracedProcessors', @@ -160,7 +252,7 @@ export default function AppConfigPage() { header: 'Updated', render: (_val, row) => {timeAgo(row.updatedAt)}, }, - ], [navigate, handleChange, updateConfig.isPending]); + ], [navigate, editingApp, draft, startEditing, cancelEditing, saveEditing, updateConfig.isPending]); return (
diff --git a/ui/src/pages/AgentHealth/AgentHealth.module.css b/ui/src/pages/AgentHealth/AgentHealth.module.css index 98efca28..5adb55b3 100644 --- a/ui/src/pages/AgentHealth/AgentHealth.module.css +++ b/ui/src/pages/AgentHealth/AgentHealth.module.css @@ -94,6 +94,31 @@ cursor: pointer; } +.configEditBtn { + background: transparent; + border: none; + color: var(--text-faint); + opacity: 0.75; + cursor: pointer; + font-size: 14px; + padding: 4px 6px; + border-radius: var(--radius-sm); + line-height: 1; + display: inline-flex; + align-self: flex-end; + margin-left: auto; +} + +.configEditBtn:hover { + color: var(--text-primary); + opacity: 1; +} + +.configEditBtn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + /* Section header */ .sectionTitle { font-size: 13px; diff --git a/ui/src/pages/AgentHealth/AgentHealth.tsx b/ui/src/pages/AgentHealth/AgentHealth.tsx index 2623b737..7ce80dc7 100644 --- a/ui/src/pages/AgentHealth/AgentHealth.tsx +++ b/ui/src/pages/AgentHealth/AgentHealth.tsx @@ -247,18 +247,34 @@ export default function AgentHealth() { const { data: appConfig } = useApplicationConfig(appId); const updateConfig = useUpdateApplicationConfig(); - const handleConfigChange = useCallback((field: string, value: string | boolean) => { + const [configEditing, setConfigEditing] = useState(false); + const [configDraft, setConfigDraft] = useState>({}); + + const startConfigEdit = useCallback(() => { if (!appConfig) return; - const updated = { ...appConfig, [field]: value }; + setConfigDraft({ + logForwardingLevel: appConfig.logForwardingLevel ?? 'INFO', + engineLevel: appConfig.engineLevel ?? 'REGULAR', + payloadCaptureMode: appConfig.payloadCaptureMode ?? 'NONE', + metricsEnabled: appConfig.metricsEnabled, + }); + setConfigEditing(true); + }, [appConfig]); + + const saveConfigEdit = useCallback(() => { + if (!appConfig) return; + const updated = { ...appConfig, ...configDraft }; updateConfig.mutate(updated, { onSuccess: (saved) => { - toast({ title: 'Config updated', description: `${field} → ${value} (v${saved.version})`, variant: 'success' }); + setConfigEditing(false); + setConfigDraft({}); + toast({ title: 'Config updated', description: `${appId} (v${saved.version})`, variant: 'success' }); }, onError: () => { toast({ title: 'Config update failed', variant: 'error' }); }, }); - }, [appConfig, updateConfig, toast]); + }, [appConfig, configDraft, updateConfig, toast, appId]); const [eventSortAsc, setEventSortAsc] = useState(false); const [eventRefreshTo, setEventRefreshTo] = useState(); const { data: events } = useAgentEvents(appId, undefined, 50, eventRefreshTo); @@ -509,60 +525,81 @@ export default function AgentHealth() { {/* Application config bar */} {appId && appConfig && (
-
- Log Level - -
-
- Engine Level - -
-
- Payload Capture - -
-
- Metrics - -
+ {configEditing ? ( + <> +
+ Log Level + +
+
+ Engine Level + +
+
+ Payload Capture + +
+
+ Metrics + +
+ + + + ) : ( + <> +
+ Log Level + +
+
+ Engine Level + +
+
+ Payload Capture + +
+
+ Metrics + +
+ + + )}
)}