diff --git a/ui/src/api/queries/commands.ts b/ui/src/api/queries/commands.ts index dc5d2eb9..cefa610f 100644 --- a/ui/src/api/queries/commands.ts +++ b/ui/src/api/queries/commands.ts @@ -31,6 +31,9 @@ export interface ApplicationConfig { tapVersion: number routeRecording: Record compressSuccess: boolean + sensitiveKeys?: string[] + globalSensitiveKeys?: string[] + mergedSensitiveKeys?: string[] } /** Authenticated fetch using the JWT from auth store. Paths are relative to apiBaseUrl. */ @@ -58,8 +61,13 @@ export function useApplicationConfig(application: string | undefined) { queryKey: ['applicationConfig', application], queryFn: async () => { const res = await authFetch(`/config/${application}`) - if (!res.ok) throw new Error('Failed to fetch config') - return res.json() as Promise + if (!res.ok) throw new Error(`Failed to fetch config: ${res.status}`) + const data = await res.json() + // Server returns AppConfigResponse: { config, globalSensitiveKeys, mergedSensitiveKeys } + const cfg = data.config ?? data + cfg.globalSensitiveKeys = data.globalSensitiveKeys ?? null + cfg.mergedSensitiveKeys = data.mergedSensitiveKeys ?? null + return cfg as ApplicationConfig }, enabled: !!application, }) diff --git a/ui/src/pages/Admin/AppConfigDetailPage.module.css b/ui/src/pages/Admin/AppConfigDetailPage.module.css index b5a2bde9..01e550fd 100644 --- a/ui/src/pages/Admin/AppConfigDetailPage.module.css +++ b/ui/src/pages/Admin/AppConfigDetailPage.module.css @@ -100,3 +100,23 @@ color: var(--text-primary); margin-bottom: 16px; } + +.sensitiveKeysRow { + display: flex; + flex-direction: column; + gap: var(--space-xs); +} + +.pillList { + display: flex; + flex-wrap: wrap; + gap: var(--space-xs); + min-height: 28px; + align-items: center; +} + +.sensitiveKeyInput { + display: flex; + gap: var(--space-sm); + max-width: 400px; +} diff --git a/ui/src/pages/Admin/AppConfigDetailPage.tsx b/ui/src/pages/Admin/AppConfigDetailPage.tsx index b6c814a1..d552c6c9 100644 --- a/ui/src/pages/Admin/AppConfigDetailPage.tsx +++ b/ui/src/pages/Admin/AppConfigDetailPage.tsx @@ -2,7 +2,7 @@ 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, + Button, SectionHeader, MonoText, Badge, DataTable, Spinner, Toggle, Select, Label, Tag, useToast, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands'; @@ -85,6 +85,8 @@ export default function AppConfigDetailPage() { const [form, setForm] = useState | null>(null); const [tracedDraft, setTracedDraft] = useState>({}); const [routeRecordingDraft, setRouteRecordingDraft] = useState>({}); + const [sensitiveKeysDraft, setSensitiveKeysDraft] = useState([]); + const [sensitiveKeyInput, setSensitiveKeyInput] = useState(''); // Find routes for this application from the catalog const appRoutes: CatalogRoute[] = useMemo(() => { @@ -106,6 +108,7 @@ export default function AppConfigDetailPage() { }); setTracedDraft({ ...config.tracedProcessors }); setRouteRecordingDraft({ ...config.routeRecording }); + setSensitiveKeysDraft([...(config.sensitiveKeys ?? [])]); } }, [config]); @@ -122,6 +125,7 @@ export default function AppConfigDetailPage() { }); setTracedDraft({ ...config.tracedProcessors }); setRouteRecordingDraft({ ...config.routeRecording }); + setSensitiveKeysDraft([...(config.sensitiveKeys ?? [])]); setEditing(true); } @@ -148,6 +152,7 @@ export default function AppConfigDetailPage() { ...form, tracedProcessors: tracedDraft, routeRecording: routeRecordingDraft, + sensitiveKeys: sensitiveKeysDraft.length > 0 ? sensitiveKeysDraft : undefined, } as ApplicationConfig; updateConfig.mutate({ config: updated, environment: selectedEnv }, { onSuccess: (saved: ConfigUpdateResponse) => { @@ -468,6 +473,76 @@ export default function AppConfigDetailPage() { )} + + {/* ── Sensitive Keys ──────────────────────────────────────────────────── */} +
+ Sensitive Keys + {config.globalSensitiveKeys && config.globalSensitiveKeys.length > 0 ? ( + + {config.globalSensitiveKeys.length} global (enforced) · {(editing ? sensitiveKeysDraft : config.sensitiveKeys ?? []).length} app-specific + + ) : ( + + No global sensitive keys configured. Agents use their built-in defaults. + + )} + + {/* Global keys — read-only pills */} + {config.globalSensitiveKeys && config.globalSensitiveKeys.length > 0 && ( +
+ +
+ {config.globalSensitiveKeys.map((key) => ( + + ))} +
+
+ )} + + {/* Per-app keys — editable */} +
+ +
+ {(editing ? sensitiveKeysDraft : config.sensitiveKeys ?? []).map((key, i) => ( + editing ? ( + setSensitiveKeysDraft((prev) => prev.filter((_, idx) => idx !== i))} /> + ) : ( + + ) + ))} + {(editing ? sensitiveKeysDraft : config.sensitiveKeys ?? []).length === 0 && ( + None + )} +
+ {editing && ( +
+ setSensitiveKeyInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + const trimmed = sensitiveKeyInput.trim(); + if (trimmed && !sensitiveKeysDraft.some((k) => k.toLowerCase() === trimmed.toLowerCase())) { + setSensitiveKeysDraft((prev) => [...prev, trimmed]); + setSensitiveKeyInput(''); + } + } + }} + placeholder="Add key or glob pattern" + /> +
+ )} +
+ + {config.globalSensitiveKeys && config.globalSensitiveKeys.length > 0 && ( + + Global keys are enforced by your administrator and cannot be removed per-app. + + )} +
); }