mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 03:00:49 +08:00
feat: initialize new React application structure with TypeScript, ESLint, and Prettier configurations, while removing legacy files and adding new components and pages for enhanced functionality
This commit is contained in:
217
src/pages/ApiKeysPage.tsx
Normal file
217
src/pages/ApiKeysPage.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
||||
import { apiKeysApi } from '@/services/api';
|
||||
import { maskApiKey } from '@/utils/format';
|
||||
|
||||
export function ApiKeysPage() {
|
||||
const { t } = useTranslation();
|
||||
const { showNotification } = useNotificationStore();
|
||||
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
||||
|
||||
const config = useConfigStore((state) => state.config);
|
||||
const fetchConfig = useConfigStore((state) => state.fetchConfig);
|
||||
const updateConfigValue = useConfigStore((state) => state.updateConfigValue);
|
||||
const clearCache = useConfigStore((state) => state.clearCache);
|
||||
|
||||
const [apiKeys, setApiKeys] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deletingIndex, setDeletingIndex] = useState<number | null>(null);
|
||||
|
||||
const disableControls = useMemo(() => connectionStatus !== 'connected', [connectionStatus]);
|
||||
|
||||
const loadApiKeys = useCallback(
|
||||
async (force = false) => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const result = (await fetchConfig('api-keys', force)) as string[] | undefined;
|
||||
const list = Array.isArray(result) ? result : [];
|
||||
setApiKeys(list);
|
||||
} catch (err: any) {
|
||||
setError(err?.message || t('notification.refresh_failed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchConfig, t]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadApiKeys(true);
|
||||
}, [loadApiKeys]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Array.isArray(config?.apiKeys)) {
|
||||
setApiKeys(config.apiKeys);
|
||||
}
|
||||
}, [config?.apiKeys]);
|
||||
|
||||
const openAddModal = () => {
|
||||
setEditingIndex(null);
|
||||
setInputValue('');
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const openEditModal = (index: number) => {
|
||||
setEditingIndex(index);
|
||||
setInputValue(apiKeys[index] ?? '');
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalOpen(false);
|
||||
setInputValue('');
|
||||
setEditingIndex(null);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const trimmed = inputValue.trim();
|
||||
if (!trimmed) {
|
||||
showNotification(`${t('notification.please_enter')} ${t('notification.api_key')}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const isEdit = editingIndex !== null;
|
||||
const nextKeys = isEdit
|
||||
? apiKeys.map((key, idx) => (idx === editingIndex ? trimmed : key))
|
||||
: [...apiKeys, trimmed];
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
if (isEdit && editingIndex !== null) {
|
||||
await apiKeysApi.update(editingIndex, trimmed);
|
||||
showNotification(t('notification.api_key_updated'), 'success');
|
||||
} else {
|
||||
await apiKeysApi.replace(nextKeys);
|
||||
showNotification(t('notification.api_key_added'), 'success');
|
||||
}
|
||||
|
||||
setApiKeys(nextKeys);
|
||||
updateConfigValue('api-keys', nextKeys);
|
||||
clearCache('api-keys');
|
||||
closeModal();
|
||||
} catch (err: any) {
|
||||
showNotification(`${t('notification.update_failed')}: ${err?.message || ''}`, 'error');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (index: number) => {
|
||||
if (!window.confirm(t('api_keys.delete_confirm'))) return;
|
||||
setDeletingIndex(index);
|
||||
try {
|
||||
await apiKeysApi.delete(index);
|
||||
const nextKeys = apiKeys.filter((_, idx) => idx !== index);
|
||||
setApiKeys(nextKeys);
|
||||
updateConfigValue('api-keys', nextKeys);
|
||||
clearCache('api-keys');
|
||||
showNotification(t('notification.api_key_deleted'), 'success');
|
||||
} catch (err: any) {
|
||||
showNotification(`${t('notification.delete_failed')}: ${err?.message || ''}`, 'error');
|
||||
} finally {
|
||||
setDeletingIndex(null);
|
||||
}
|
||||
};
|
||||
|
||||
const actionButtons = (
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button variant="secondary" size="sm" onClick={() => loadApiKeys(true)} disabled={loading}>
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<Button size="sm" onClick={openAddModal} disabled={disableControls}>
|
||||
{t('api_keys.add_button')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card title={t('api_keys.proxy_auth_title')} extra={actionButtons}>
|
||||
{error && <div className="error-box">{error}</div>}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex-center" style={{ padding: '24px 0' }}>
|
||||
<LoadingSpinner size={28} />
|
||||
</div>
|
||||
) : apiKeys.length === 0 ? (
|
||||
<EmptyState
|
||||
title={t('api_keys.empty_title')}
|
||||
description={t('api_keys.empty_desc')}
|
||||
action={
|
||||
<Button onClick={openAddModal} disabled={disableControls}>
|
||||
{t('api_keys.add_button')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="item-list">
|
||||
{apiKeys.map((key, index) => (
|
||||
<div key={index} className="item-row">
|
||||
<div className="item-meta">
|
||||
<div className="pill">#{index + 1}</div>
|
||||
<div className="item-title">{t('api_keys.item_title')}</div>
|
||||
<div className="item-subtitle">{maskApiKey(String(key || ''))}</div>
|
||||
</div>
|
||||
<div className="item-actions">
|
||||
<Button variant="secondary" size="sm" onClick={() => openEditModal(index)} disabled={disableControls}>
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(index)}
|
||||
disabled={disableControls || deletingIndex === index}
|
||||
loading={deletingIndex === index}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
open={modalOpen}
|
||||
onClose={closeModal}
|
||||
title={editingIndex !== null ? t('api_keys.edit_modal_title') : t('api_keys.add_modal_title')}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="secondary" onClick={closeModal} disabled={saving}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleSave} loading={saving}>
|
||||
{editingIndex !== null ? t('common.update') : t('common.add')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
label={
|
||||
editingIndex !== null ? t('api_keys.edit_modal_key_label') : t('api_keys.add_modal_key_label')
|
||||
}
|
||||
placeholder={
|
||||
editingIndex !== null
|
||||
? t('api_keys.edit_modal_key_label')
|
||||
: t('api_keys.add_modal_key_placeholder')
|
||||
}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
disabled={saving}
|
||||
/>
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user