- Remove icons from all page title sections in tab bars - Fix double padding on mobile for Features page - Fix drag state not resetting when Apply/Cancel pressed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
189 lines
6.0 KiB
TypeScript
189 lines
6.0 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useTranslation } from '../contexts/LanguageContext';
|
|
import { useSidebar } from '../contexts/SidebarContext';
|
|
import { apiKeysAPI } from '../api/client';
|
|
import type { ApiKeyItem } from '../api/client';
|
|
import '../styles/APIKeys.css';
|
|
|
|
export default function APIKeys() {
|
|
const { t } = useTranslation();
|
|
const { toggleMobileMenu } = useSidebar();
|
|
|
|
const [items, setItems] = useState<ApiKeyItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState('');
|
|
|
|
const [name, setName] = useState('');
|
|
const [createdKey, setCreatedKey] = useState<string | null>(null);
|
|
|
|
const load = async () => {
|
|
setLoading(true);
|
|
setError('');
|
|
try {
|
|
const data = await apiKeysAPI.list();
|
|
setItems(data.items || []);
|
|
} catch (err: any) {
|
|
setError(err?.response?.data?.detail || t.common.error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
load();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
const create = async () => {
|
|
setBusy(true);
|
|
setError('');
|
|
setCreatedKey(null);
|
|
try {
|
|
const created = await apiKeysAPI.create({ name: name.trim() });
|
|
setCreatedKey(created.key);
|
|
setName('');
|
|
await load();
|
|
} catch (err: any) {
|
|
setError(err?.response?.data?.detail || t.common.error);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
const revoke = async (id: string) => {
|
|
setBusy(true);
|
|
setError('');
|
|
try {
|
|
await apiKeysAPI.revoke(id);
|
|
await load();
|
|
} catch (err: any) {
|
|
setError(err?.response?.data?.detail || t.common.error);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
const deleteKey = async (id: string) => {
|
|
setBusy(true);
|
|
setError('');
|
|
try {
|
|
await apiKeysAPI.delete(id);
|
|
await load();
|
|
} catch (err: any) {
|
|
setError(err?.response?.data?.detail || t.common.error);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
const copy = async () => {
|
|
if (!createdKey) return;
|
|
try {
|
|
await navigator.clipboard.writeText(createdKey);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
};
|
|
|
|
return (
|
|
<main className="main-content api-keys-root">
|
|
<div className="page-tabs-container">
|
|
<div className="page-tabs-slider">
|
|
<button className="mobile-menu-btn" onClick={toggleMobileMenu} aria-label={t.theme.toggleMenu}>
|
|
<span className="material-symbols-outlined">menu</span>
|
|
</button>
|
|
<div className="page-title-section">
|
|
<span className="page-title-text">{t.apiKeysPage.title}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="page-content">
|
|
{error && <div className="error-message">{error}</div>}
|
|
|
|
<div className="api-keys-section">
|
|
<h3 className="section-title">{t.apiKeysPage.createTitle}</h3>
|
|
<p className="api-keys-desc">{t.apiKeysPage.createDesc}</p>
|
|
|
|
<div className="api-keys-create-row">
|
|
<input
|
|
className="api-keys-input"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder={t.apiKeysPage.namePlaceholder}
|
|
disabled={busy}
|
|
/>
|
|
<button className="btn-primary" onClick={create} disabled={busy || name.trim().length < 1}>
|
|
{t.apiKeysPage.createButton}
|
|
</button>
|
|
</div>
|
|
|
|
{createdKey && (
|
|
<div className="api-keys-created">
|
|
<div className="api-keys-created-header">
|
|
<span className="badge badge-accent">{t.apiKeysPage.showOnce}</span>
|
|
<button className="btn-link" onClick={copy}>{t.apiKeysPage.copy}</button>
|
|
</div>
|
|
<code className="api-keys-created-key">{createdKey}</code>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="api-keys-section">
|
|
<h3 className="section-title">{t.apiKeysPage.listTitle}</h3>
|
|
|
|
{loading ? (
|
|
<div className="loading">{t.common.loading}</div>
|
|
) : items.length === 0 ? (
|
|
<div className="api-keys-empty">{t.apiKeysPage.empty}</div>
|
|
) : (
|
|
<div className="api-keys-table-card">
|
|
<table className="api-keys-table">
|
|
<thead>
|
|
<tr>
|
|
<th>{t.apiKeysPage.name}</th>
|
|
<th>{t.apiKeysPage.prefix}</th>
|
|
<th>{t.apiKeysPage.status}</th>
|
|
<th>{t.apiKeysPage.lastUsed}</th>
|
|
<th>{t.apiKeysPage.usage}</th>
|
|
<th>{t.apiKeysPage.actions}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{items.map((k) => (
|
|
<tr key={k.id}>
|
|
<td>{k.name}</td>
|
|
<td className="mono">{k.key_prefix}</td>
|
|
<td>
|
|
<span className={`badge ${k.is_active ? 'badge-success' : 'badge-muted'}`}>
|
|
{k.is_active ? t.settings.enabled : t.settings.disabled}
|
|
</span>
|
|
</td>
|
|
<td className="mono">{k.last_used_at ? new Date(k.last_used_at).toLocaleString() : '—'}</td>
|
|
<td className="mono">{k.usage_count}</td>
|
|
<td className="api-keys-actions">
|
|
{k.is_active ? (
|
|
<button className="btn-link" onClick={() => revoke(k.id)} disabled={busy}>
|
|
{t.apiKeysPage.revoke}
|
|
</button>
|
|
) : (
|
|
<span className="api-keys-muted">{t.apiKeysPage.revoked}</span>
|
|
)}
|
|
<button className="btn-link danger" onClick={() => deleteKey(k.id)} disabled={busy}>
|
|
{t.common.delete}
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|