feat: add per-app sensitive keys section to AppConfigDetailPage

Adds sensitiveKeys/globalSensitiveKeys/mergedSensitiveKeys fields to
ApplicationConfig, unwraps the new AppConfigResponse envelope in
useApplicationConfig, and renders an editable Sensitive Keys section
with read-only global pills and add/remove app-specific key tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-14 18:26:05 +02:00
parent 96780db9ad
commit 7b73b5c9c5
3 changed files with 106 additions and 3 deletions

View File

@@ -31,6 +31,9 @@ export interface ApplicationConfig {
tapVersion: number
routeRecording: Record<string, boolean>
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<ApplicationConfig>
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,
})

View File

@@ -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;
}

View File

@@ -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<Partial<ApplicationConfig> | null>(null);
const [tracedDraft, setTracedDraft] = useState<Record<string, string>>({});
const [routeRecordingDraft, setRouteRecordingDraft] = useState<Record<string, boolean>>({});
const [sensitiveKeysDraft, setSensitiveKeysDraft] = useState<string[]>([]);
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() {
</span>
)}
</div>
{/* ── Sensitive Keys ──────────────────────────────────────────────────── */}
<div className={sectionStyles.section}>
<SectionHeader>Sensitive Keys</SectionHeader>
{config.globalSensitiveKeys && config.globalSensitiveKeys.length > 0 ? (
<span className={styles.sectionSummary}>
{config.globalSensitiveKeys.length} global (enforced) &middot; {(editing ? sensitiveKeysDraft : config.sensitiveKeys ?? []).length} app-specific
</span>
) : (
<span className={styles.sectionSummary}>
No global sensitive keys configured. Agents use their built-in defaults.
</span>
)}
{/* Global keys — read-only pills */}
{config.globalSensitiveKeys && config.globalSensitiveKeys.length > 0 && (
<div className={styles.sensitiveKeysRow}>
<Label>Global (enforced)</Label>
<div className={styles.pillList}>
{config.globalSensitiveKeys.map((key) => (
<Badge key={key} label={key} color="auto" variant="filled" />
))}
</div>
</div>
)}
{/* Per-app keys — editable */}
<div className={styles.sensitiveKeysRow}>
<Label>Application-specific</Label>
<div className={styles.pillList}>
{(editing ? sensitiveKeysDraft : config.sensitiveKeys ?? []).map((key, i) => (
editing ? (
<Tag key={`${key}-${i}`} label={key} onRemove={() => setSensitiveKeysDraft((prev) => prev.filter((_, idx) => idx !== i))} />
) : (
<Badge key={key} label={key} color="primary" variant="filled" />
)
))}
{(editing ? sensitiveKeysDraft : config.sensitiveKeys ?? []).length === 0 && (
<span className={styles.hint}>None</span>
)}
</div>
{editing && (
<div className={styles.sensitiveKeyInput}>
<input
className={styles.numberInput}
style={{ flex: 1 }}
value={sensitiveKeyInput}
onChange={(e) => 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"
/>
</div>
)}
</div>
{config.globalSensitiveKeys && config.globalSensitiveKeys.length > 0 && (
<span className={styles.hint}>
Global keys are enforced by your administrator and cannot be removed per-app.
</span>
)}
</div>
</div>
);
}