feat: add SensitiveKeysPage admin page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
39
ui/src/pages/Admin/SensitiveKeysPage.module.css
Normal file
39
ui/src/pages/Admin/SensitiveKeysPage.module.css
Normal file
@@ -0,0 +1,39 @@
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.infoBanner {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
background: var(--surface-secondary);
|
||||
padding: var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.pillList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-xs);
|
||||
min-height: 36px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inputRow {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inputRow input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
align-items: center;
|
||||
}
|
||||
116
ui/src/pages/Admin/SensitiveKeysPage.tsx
Normal file
116
ui/src/pages/Admin/SensitiveKeysPage.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Button, SectionHeader, Tag, Input, Toggle, Label, useToast } from '@cameleer/design-system';
|
||||
import { PageLoader } from '../../components/PageLoader';
|
||||
import { useSensitiveKeys, useUpdateSensitiveKeys } from '../../api/queries/admin/sensitive-keys';
|
||||
import styles from './SensitiveKeysPage.module.css';
|
||||
import sectionStyles from '../../styles/section-card.module.css';
|
||||
|
||||
export default function SensitiveKeysPage() {
|
||||
const { data, isLoading } = useSensitiveKeys();
|
||||
const updateKeys = useUpdateSensitiveKeys();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [draft, setDraft] = useState<string[]>([]);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [pushToAgents, setPushToAgents] = useState(false);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (data !== undefined && !initialized) {
|
||||
setDraft(data?.keys ?? []);
|
||||
setInitialized(true);
|
||||
}
|
||||
}, [data, initialized]);
|
||||
|
||||
const addKey = useCallback(() => {
|
||||
const trimmed = inputValue.trim();
|
||||
if (!trimmed) return;
|
||||
if (draft.some((k) => k.toLowerCase() === trimmed.toLowerCase())) {
|
||||
toast({ title: 'Duplicate key', description: `"${trimmed}" is already in the list`, variant: 'warning' });
|
||||
return;
|
||||
}
|
||||
setDraft((prev) => [...prev, trimmed]);
|
||||
setInputValue('');
|
||||
}, [inputValue, draft, toast]);
|
||||
|
||||
const removeKey = useCallback((index: number) => {
|
||||
setDraft((prev) => prev.filter((_, i) => i !== index));
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addKey();
|
||||
}
|
||||
}, [addKey]);
|
||||
|
||||
function handleSave() {
|
||||
updateKeys.mutate({ keys: draft, pushToAgents }, {
|
||||
onSuccess: (result) => {
|
||||
if (result.pushResult) {
|
||||
toast({
|
||||
title: 'Sensitive keys saved and pushed',
|
||||
description: `${result.pushResult.total} agent(s) notified, ${result.pushResult.responded} responded`,
|
||||
variant: result.pushResult.success ? 'success' : 'warning',
|
||||
});
|
||||
} else {
|
||||
toast({ title: 'Sensitive keys saved', variant: 'success' });
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: 'Failed to save sensitive keys', variant: 'error', duration: 86_400_000 });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (isLoading) return <PageLoader />;
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<SectionHeader>Sensitive Keys</SectionHeader>
|
||||
|
||||
<div className={styles.infoBanner}>
|
||||
Agents ship with built-in defaults (Authorization, Cookie, Set-Cookie, X-API-Key,
|
||||
X-Auth-Token, Proxy-Authorization). Configuring keys here replaces agent defaults for
|
||||
all applications. Leave unconfigured to use agent defaults.
|
||||
</div>
|
||||
|
||||
<div className={sectionStyles.section}>
|
||||
<Label>Global sensitive keys</Label>
|
||||
<div className={styles.pillList}>
|
||||
{draft.map((key, i) => (
|
||||
<Tag key={`${key}-${i}`} label={key} onRemove={() => removeKey(i)} />
|
||||
))}
|
||||
{draft.length === 0 && (
|
||||
<span style={{ color: 'var(--text-tertiary)', fontSize: 'var(--font-size-sm)' }}>
|
||||
No keys configured — agents use built-in defaults
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.inputRow}>
|
||||
<Input
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Add key or glob pattern (e.g. *password*)"
|
||||
/>
|
||||
<Button variant="secondary" size="sm" onClick={addKey} disabled={!inputValue.trim()}>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>
|
||||
<Toggle
|
||||
checked={pushToAgents}
|
||||
onChange={(e) => setPushToAgents((e.target as HTMLInputElement).checked)}
|
||||
label="Push to all connected agents immediately"
|
||||
/>
|
||||
<Button variant="primary" onClick={handleSave} loading={updateKeys.isPending}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user