import { useState, useEffect, useCallback } from 'react'; import { Plus, Pencil, Trash2, RefreshCw, Key, Shield, Globe, Search, Lock, Eye, EyeOff, Copy, Server, Container, Database, FileText, Cloud, Clock, AlertTriangle, CheckCircle2, XCircle, Zap, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'; import { toast } from 'sonner'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { credentialApi } from '@/services/api'; // ────── Types ────── interface Credential { id: number; name: string; variable_name: string; secret_type: SecretType; summary: string; tags: string[]; description: string; expires_at: string | null; is_active: boolean; env_prefix: string; created_at: string; updated_at: string; } type SecretType = 'ssh' | 'docker' | 'tls' | 'aksk' | 'database' | 'api' | 'generic'; const TYPE_CONFIG: Record = { ssh: { label: 'SSH 密钥', icon: Server, color: 'text-amber-400', desc: 'SSH 私钥/公钥/密码' }, docker: { label: 'Docker 凭证', icon: Container, color: 'text-cyan-400', desc: 'Docker Registry 登录凭据' }, tls: { label: 'TLS 证书', icon: Shield, color: 'text-green-400', desc: 'TLS/SSL 证书与私钥' }, aksk: { label: 'AKSK 密钥', icon: Cloud, color: 'text-purple-400', desc: '云服务 AccessKey/SecretKey' }, database: { label: '数据库凭证', icon: Database, color: 'text-blue-400', desc: '数据库连接信息' }, api: { label: 'API 密钥', icon: Globe, color: 'text-orange-400', desc: 'API Token/Key + 端点' }, generic: { label: '通用凭证', icon: FileText, color: 'text-gray-400', desc: '自定义 Key/Value 键值对' }, }; const TYPE_BADGE_CLASS: Record = { ssh: 'bg-amber-500/20 text-amber-400 border-amber-500/30', docker: 'bg-cyan-500/20 text-cyan-400 border-cyan-500/30', tls: 'bg-green-500/20 text-green-400 border-green-500/30', aksk: 'bg-purple-500/20 text-purple-400 border-purple-500/30', database: 'bg-blue-500/20 text-blue-400 border-blue-500/30', api: 'bg-orange-500/20 text-orange-400 border-orange-500/30', generic: 'bg-gray-500/20 text-gray-400 border-gray-500/30', }; // ────── Detail Dialog ────── function CredDetailDialog({ open, onClose, credId }: { open: boolean; onClose: () => void; credId: number | null }) { const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(false); const [showMap, setShowMap] = useState>({}); useEffect(() => { if (!credId || !open) return; setLoading(true); credentialApi.get(credId).then(( res: any) => { setDetail(res); }).catch(() => toast.error('加载详情失败')).finally(() => setLoading(false)); }, [credId, open]); if (!open || !credId) return null; const toggleShow = (key: string) => setShowMap(prev => ({ ...prev, [key]: !prev[key] })); const copyValue = (val: string) => { navigator.clipboard.writeText(val); toast.success('已复制'); }; const st = detail?.secret_type as SecretType; const cfg = TYPE_CONFIG[st]; const content = detail?.content || {}; const Field = ({ label, value, sensitive, fieldKey }: { label: string; value: any; sensitive?: boolean; fieldKey?: string }) => { if (value === undefined || value === null || value === '') return null; const isLong = typeof value === 'string' && value.length > 80; const show = fieldKey ? showMap[fieldKey] : false; return (
{label}
{isLong ? (