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:
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) · {(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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user