import * as React from 'react'; import { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { Copy, Check, Trash2, Plus } from 'lucide-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Switch } from '@/components/ui/switch'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogPortal, AlertDialogOverlay, } from '@/components/ui/alert-dialog'; import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; import { backendClient } from '@/app/infra/http'; import { PanelToolbar } from '../settings-dialog/panel-layout'; interface ApiKey { id: number; name: string; key: string; description: string; created_at: string; } interface Webhook { id: number; name: string; url: string; description: string; enabled: boolean; created_at: string; } interface ApiIntegrationPanelProps { // True when this panel is the active section and the dialog is open. active: boolean; } export default function ApiIntegrationPanel({ active, }: ApiIntegrationPanelProps) { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState('apikeys'); const [apiKeys, setApiKeys] = useState([]); const [webhooks, setWebhooks] = useState([]); const [loading, setLoading] = useState(false); const [showCreateDialog, setShowCreateDialog] = useState(false); const [newKeyName, setNewKeyName] = useState(''); const [newKeyDescription, setNewKeyDescription] = useState(''); const [createdKey, setCreatedKey] = useState(null); const [deleteKeyId, setDeleteKeyId] = useState(null); // Webhook state const [showCreateWebhookDialog, setShowCreateWebhookDialog] = useState(false); const [newWebhookName, setNewWebhookName] = useState(''); const [newWebhookUrl, setNewWebhookUrl] = useState(''); const [newWebhookDescription, setNewWebhookDescription] = useState(''); const [newWebhookEnabled, setNewWebhookEnabled] = useState(true); const [deleteWebhookId, setDeleteWebhookId] = useState(null); const copiedTimerRef = useRef | undefined>( undefined, ); const [copiedKey, setCopiedKey] = useState(null); // 清理 body 样式,防止嵌套对话框关闭后页面无法交互 useEffect(() => { if (!deleteKeyId && !deleteWebhookId) { const cleanup = () => { document.body.style.removeProperty('pointer-events'); }; cleanup(); const timer = setTimeout(cleanup, 100); return () => clearTimeout(timer); } }, [deleteKeyId, deleteWebhookId]); useEffect(() => { if (active) { loadApiKeys(); loadWebhooks(); } }, [active]); const loadApiKeys = async () => { setLoading(true); try { const response = (await backendClient.get('/api/v1/apikeys')) as { keys: ApiKey[]; }; setApiKeys(response.keys || []); } catch (error) { toast.error(`Failed to load API keys: ${error}`); } finally { setLoading(false); } }; const handleCreateApiKey = async () => { if (!newKeyName.trim()) { toast.error(t('common.apiKeyNameRequired')); return; } try { const response = (await backendClient.post('/api/v1/apikeys', { name: newKeyName, description: newKeyDescription, })) as { key: ApiKey }; setCreatedKey(response.key); toast.success(t('common.apiKeyCreated')); setNewKeyName(''); setNewKeyDescription(''); setShowCreateDialog(false); loadApiKeys(); } catch (error) { toast.error(`Failed to create API key: ${error}`); } }; const handleDeleteApiKey = async (keyId: number) => { try { await backendClient.delete(`/api/v1/apikeys/${keyId}`); toast.success(t('common.apiKeyDeleted')); loadApiKeys(); setDeleteKeyId(null); } catch (error) { toast.error(`Failed to delete API key: ${error}`); } }; const copyToClipboard = (text: string) => { const el = document.createElement('span'); el.textContent = text; el.style.cssText = 'position:fixed;opacity:0;pointer-events:none;white-space:pre;'; document.body.appendChild(el); const range = document.createRange(); range.selectNodeContents(el); const sel = window.getSelection(); sel?.removeAllRanges(); sel?.addRange(range); document.execCommand('copy'); sel?.removeAllRanges(); document.body.removeChild(el); }; const handleCopyKey = (key: string) => { try { copyToClipboard(key); } catch {} clearTimeout(copiedTimerRef.current); setCopiedKey(key); copiedTimerRef.current = setTimeout(() => setCopiedKey(null), 2000); }; const maskApiKey = (key: string) => { if (key.length <= 8) return key; return `${key.substring(0, 8)}...${key.substring(key.length - 4)}`; }; // Webhook methods const loadWebhooks = async () => { setLoading(true); try { const response = (await backendClient.get('/api/v1/webhooks')) as { webhooks: Webhook[]; }; setWebhooks(response.webhooks || []); } catch (error) { toast.error(`Failed to load webhooks: ${error}`); } finally { setLoading(false); } }; const handleCreateWebhook = async () => { if (!newWebhookName.trim()) { toast.error(t('common.webhookNameRequired')); return; } if (!newWebhookUrl.trim()) { toast.error(t('common.webhookUrlRequired')); return; } try { await backendClient.post('/api/v1/webhooks', { name: newWebhookName, url: newWebhookUrl, description: newWebhookDescription, enabled: newWebhookEnabled, }); toast.success(t('common.webhookCreated')); setNewWebhookName(''); setNewWebhookUrl(''); setNewWebhookDescription(''); setNewWebhookEnabled(true); setShowCreateWebhookDialog(false); loadWebhooks(); } catch (error) { toast.error(`Failed to create webhook: ${error}`); } }; const handleDeleteWebhook = async (webhookId: number) => { try { await backendClient.delete(`/api/v1/webhooks/${webhookId}`); toast.success(t('common.webhookDeleted')); loadWebhooks(); setDeleteWebhookId(null); } catch (error) { toast.error(`Failed to delete webhook: ${error}`); } }; const handleToggleWebhook = async (webhook: Webhook) => { try { await backendClient.put(`/api/v1/webhooks/${webhook.id}`, { enabled: !webhook.enabled, }); loadWebhooks(); } catch (error) { toast.error(`Failed to update webhook: ${error}`); } }; return ( <> {t('common.apiKeys')} {t('common.webhooks')} {activeTab === 'apikeys' ? ( ) : ( )} {/* API Keys Tab */}

{t('common.apiKeyHint')}

{loading ? (
{t('common.loading')}
) : apiKeys.length === 0 ? (
{t('common.noApiKeys')}
) : (
{t('common.name')} {t('common.apiKeyValue')} {t('common.actions')} {apiKeys.map((item) => (
{item.name}
{item.description && (
{item.description}
)}
{maskApiKey(item.key)}
))}
)}
{/* Webhooks Tab */}

{t('common.webhookHint')}

{loading ? (
{t('common.loading')}
) : webhooks.length === 0 ? (
{t('common.noWebhooks')}
) : (
{t('common.name')} {t('common.webhookUrl')} {t('common.webhookEnabled')} {t('common.actions')} {webhooks.map((webhook) => (
{webhook.name}
{webhook.description && (
{webhook.description}
)}
{webhook.url}
handleToggleWebhook(webhook)} />
))}
)}
{/* Create API Key Dialog */} {t('common.createApiKey')}
setNewKeyName(e.target.value)} placeholder={t('common.name')} className="mt-1" />
setNewKeyDescription(e.target.value)} placeholder={t('common.description')} className="mt-1" />
{/* Show Created Key Dialog */} setCreatedKey(null)}> {t('common.apiKeyCreated')} {t('common.apiKeyCreatedMessage')}
{/* Create Webhook Dialog */} {t('common.createWebhook')}
setNewWebhookName(e.target.value)} placeholder={t('common.webhookName')} className="mt-1" />
setNewWebhookUrl(e.target.value)} placeholder="https://example.com/webhook" className="mt-1" />
setNewWebhookDescription(e.target.value)} placeholder={t('common.description')} className="mt-1" />
{/* Delete Confirmation Dialog */} setDeleteKeyId(null)} /> setDeleteKeyId(null)} > {t('common.confirmDelete')} {t('common.apiKeyDeleteConfirm')} setDeleteKeyId(null)}> {t('common.cancel')} deleteKeyId && handleDeleteApiKey(deleteKeyId)} > {t('common.delete')} {/* Delete Webhook Confirmation Dialog */} setDeleteWebhookId(null)} /> setDeleteWebhookId(null)} > {t('common.confirmDelete')} {t('common.webhookDeleteConfirm')} setDeleteWebhookId(null)}> {t('common.cancel')} deleteWebhookId && handleDeleteWebhook(deleteWebhookId) } > {t('common.delete')} ); }