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