ui(deploy): extract SensitiveKeysTab component

Pure presentational tab receiving SensitiveKeysFormState via value/onChange.
Calls useSensitiveKeys() internally to show global baseline (readonly).
Local useState for the new-key input buffer. Reuses skStyles from
SensitiveKeysPage.module.css for consistent pill/badge layout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-22 22:57:02 +02:00
parent bb06c4c689
commit f487e6caef

View File

@@ -0,0 +1,123 @@
import { useState } from 'react';
import { Badge, Button, Input, Tag } from '@cameleer/design-system';
import { Shield, Info } from 'lucide-react';
import { useSensitiveKeys } from '../../../../api/queries/admin/sensitive-keys';
import type { SensitiveKeysFormState } from '../hooks/useDeploymentPageState';
import skStyles from '../../../Admin/SensitiveKeysPage.module.css';
const AGENT_DEFAULTS = [
'Authorization',
'Cookie',
'Set-Cookie',
'X-API-Key',
'X-Auth-Token',
'Proxy-Authorization',
];
interface Props {
value: SensitiveKeysFormState;
onChange: (next: SensitiveKeysFormState) => void;
disabled?: boolean;
}
export function SensitiveKeysTab({ value, onChange, disabled }: Props) {
const [newKey, setNewKey] = useState('');
const { data: globalKeysConfig } = useSensitiveKeys();
const globalKeys = globalKeysConfig?.keys ?? [];
function addKey() {
const v = newKey.trim();
if (v && !value.sensitiveKeys.some((k) => k.toLowerCase() === v.toLowerCase())) {
onChange({ sensitiveKeys: [...value.sensitiveKeys, v] });
setNewKey('');
}
}
function removeKey(index: number) {
onChange({ sensitiveKeys: value.sensitiveKeys.filter((_, i) => i !== index) });
}
return (
<div>
<div className={skStyles.sectionTitle}>
<Shield size={14} />
<span>Agent built-in defaults</span>
</div>
<div className={skStyles.defaultsList}>
{AGENT_DEFAULTS.map((key) => (
<Badge key={key} label={key} variant="outlined" />
))}
</div>
{globalKeys.length > 0 && (
<>
<hr style={{ border: 'none', borderTop: '1px solid var(--border-subtle)', margin: '10px 0' }} />
<div className={skStyles.sectionTitle}>
<span>Global keys (enforced)</span>
<span className={skStyles.keyCount}>{globalKeys.length}</span>
</div>
<div className={skStyles.defaultsList}>
{globalKeys.map((key) => (
<Badge key={key} label={key} color="auto" variant="filled" />
))}
</div>
</>
)}
<hr style={{ border: 'none', borderTop: '1px solid var(--border-subtle)', margin: '10px 0' }} />
<div className={skStyles.sectionTitle}>
<span>Application-specific keys</span>
{value.sensitiveKeys.length > 0 && (
<span className={skStyles.keyCount}>{value.sensitiveKeys.length}</span>
)}
</div>
<div className={skStyles.pillList}>
{value.sensitiveKeys.map((k, i) => (
<Tag
key={`${k}-${i}`}
label={k}
onRemove={() => !disabled && removeKey(i)}
/>
))}
{value.sensitiveKeys.length === 0 && (
<span className={skStyles.emptyState}>
No app-specific keys agents use built-in defaults
{globalKeys.length > 0 ? ' and global keys' : ''}
</span>
)}
</div>
<div className={skStyles.inputRow}>
<Input
value={newKey}
onChange={(e) => setNewKey(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addKey();
}
}}
placeholder="Add key or glob pattern (e.g. *password*)"
disabled={disabled}
/>
<Button
variant="secondary"
size="sm"
disabled={disabled || !newKey.trim()}
onClick={addKey}
>
Add
</Button>
</div>
<div className={skStyles.hint}>
<Info size={12} />
<span>
The final masking configuration is: agent defaults + global keys + app-specific keys.
Supports exact header names and glob patterns.
</span>
</div>
</div>
);
}