|
|
| import React, { useState } from 'react'; |
| import { Edit2, Trash2, Power, Globe } from 'lucide-react'; |
| import { useTranslation } from 'react-i18next'; |
| import { ProxyEntry } from '../../../types/config'; |
| import ProxyEditModal from './ProxyEditModal'; |
| import { Account } from '../../../types/account'; |
|
|
| interface ProxyListProps { |
| proxies: ProxyEntry[]; |
| onUpdate: (proxies: ProxyEntry[]) => void; |
| accountBindings: Record<string, string>; |
| accounts: Account[]; |
| selectedIds: Set<string>; |
| onSelectionChange: (ids: Set<string>) => void; |
| isTesting?: boolean; |
| } |
|
|
| export default function ProxyList({ proxies, onUpdate, accountBindings, accounts, selectedIds, onSelectionChange, isTesting }: ProxyListProps) { |
| const { t } = useTranslation(); |
| const [editingProxy, setEditingProxy] = useState<ProxyEntry | undefined>(undefined); |
| const [isEditModalOpen, setIsEditModalOpen] = useState(false); |
|
|
| const handleEdit = (proxy: ProxyEntry) => { |
| setEditingProxy(proxy); |
| setIsEditModalOpen(true); |
| }; |
|
|
| const handleDelete = (id: string) => { |
| if (confirm(t('settings.proxy_pool.confirm_delete', 'Are you sure you want to delete this proxy?'))) { |
| onUpdate(proxies.filter(p => p.id !== id)); |
| } |
| }; |
|
|
| const handleSaveProxy = (entry: ProxyEntry) => { |
| if (editingProxy) { |
| onUpdate(proxies.map(p => p.id === entry.id ? entry : p)); |
| } |
| setEditingProxy(undefined); |
| }; |
|
|
| const handleToggleEnabled = (id: string) => { |
| onUpdate(proxies.map(p => p.id === id ? { ...p, enabled: !p.enabled } : p)); |
| }; |
|
|
| const sortedProxies = [...proxies].sort((a, b) => a.priority - b.priority); |
|
|
| const handleSelectAll = (e: React.ChangeEvent<HTMLInputElement>) => { |
| if (e.target.checked) { |
| onSelectionChange(new Set(proxies.map(p => p.id))); |
| } else { |
| onSelectionChange(new Set()); |
| } |
| }; |
|
|
| const handleSelectOne = (id: string) => { |
| const newSelected = new Set(selectedIds); |
| if (newSelected.has(id)) { |
| newSelected.delete(id); |
| } else { |
| newSelected.add(id); |
| } |
| onSelectionChange(newSelected); |
| }; |
|
|
| const isAllSelected = proxies.length > 0 && selectedIds.size === proxies.length; |
| const isSomeSelected = selectedIds.size > 0 && selectedIds.size < proxies.length; |
|
|
| |
| const getBoundAccounts = (proxyId: string) => { |
| const boundAccountIds = Object.entries(accountBindings) |
| .filter(([_, boundProxyId]) => boundProxyId === proxyId) |
| .map(([accountId]) => accountId); |
|
|
| return boundAccountIds.map(id => accounts.find(a => a.id === id)).filter(Boolean) as Account[]; |
| }; |
|
|
| return ( |
| <div className="overflow-hidden"> |
| <table className="min-w-full divide-y divide-gray-100 dark:divide-gray-800"> |
| <thead className="bg-gray-50/50 dark:bg-gray-900/50"> |
| <tr> |
| <th scope="col" className="px-4 py-3 text-left w-10 pl-6"> |
| <input |
| type="checkbox" |
| checked={isAllSelected} |
| ref={input => { |
| if (input) input.indeterminate = isSomeSelected; |
| }} |
| onChange={handleSelectAll} |
| className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" |
| /> |
| </th> |
| <th scope="col" className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-widest min-w-[60px]"> |
| {t('settings.proxy_pool.column_priority', 'PRI')} |
| </th> |
| <th scope="col" className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-widest"> |
| {t('settings.proxy_pool.column_status', 'Status')} |
| </th> |
| <th scope="col" className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-widest w-1/3"> |
| {t('settings.proxy_pool.column_details', 'Proxy Details')} |
| </th> |
| <th scope="col" className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-widest"> |
| {t('settings.proxy_pool.column_bindings', 'Bindings')} |
| </th> |
| <th scope="col" className="px-4 py-3 text-right text-[10px] font-bold text-gray-400 uppercase tracking-widest pr-6"> |
| {t('common.actions', 'Actions')} |
| </th> |
| </tr> |
| </thead> |
| <tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-50 dark:divide-gray-800"> |
| {sortedProxies.length === 0 ? ( |
| <td colSpan={6} className="px-6 py-12 text-center text-sm text-gray-500 dark:text-gray-400"> |
| <span className="opacity-50">{t('settings.proxy_pool.empty', 'No proxies available.')}</span> |
| </td> |
| ) : ( |
| sortedProxies.map((proxy) => { |
| const boundAccounts = getBoundAccounts(proxy.id); |
| |
| return ( |
| <tr |
| key={proxy.id} |
| className={`group hover:bg-gray-50 dark:hover:bg-gray-800/20 transition-colors ${!proxy.enabled ? 'bg-gray-50/30 dark:bg-gray-900/50' : ''} ${selectedIds.has(proxy.id) ? 'bg-blue-50/50 dark:bg-blue-900/10' : ''}`} |
| > |
| <td className="px-4 py-3 whitespace-nowrap pl-6"> |
| <input |
| type="checkbox" |
| checked={selectedIds.has(proxy.id)} |
| onChange={() => handleSelectOne(proxy.id)} |
| className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" |
| /> |
| </td> |
| <td className="px-4 py-3 whitespace-nowrap"> |
| <div className={`flex items-center justify-center w-6 h-6 rounded-full border text-[10px] font-black transition-all shadow-inner ${proxy.enabled |
| ? 'bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300' |
| : 'bg-gray-50 dark:bg-gray-900 border-gray-100 dark:border-gray-800 text-gray-400 dark:text-gray-600 opacity-50' |
| }`}> |
| {proxy.priority} |
| </div> |
| </td> |
| <td className="px-4 py-3 whitespace-nowrap"> |
| <div className="flex items-center gap-2"> |
| <div className="relative group"> |
| <button |
| onClick={() => !isTesting && handleToggleEnabled(proxy.id)} |
| className={`relative p-1 rounded-lg transition-all ${!proxy.enabled ? 'opacity-40 hover:opacity-100' : ''}`} |
| > |
| <div className={`w-2 h-2 rounded-full transition-all duration-500 shadow-lg ${!proxy.enabled |
| ? 'bg-gray-400' |
| : proxy.latency !== undefined && proxy.latency !== null |
| ? 'bg-emerald-500 shadow-emerald-500/50' |
| : proxy.is_healthy |
| ? 'bg-emerald-500 shadow-emerald-500/50' |
| : isTesting && !proxy.latency |
| ? 'bg-blue-400 animate-pulse' |
| : 'bg-rose-500 shadow-rose-500/50' |
| }`}></div> |
| </button> |
| </div> |
| |
| {/* Status Pill Tag */} |
| <div className={`px-2 py-0.5 rounded-md text-[10px] font-black uppercase tracking-tighter border transition-all duration-300 ${!proxy.enabled |
| ? 'bg-gray-100 text-gray-400 border-gray-200 dark:bg-gray-800 dark:border-gray-700' |
| : proxy.latency !== undefined && proxy.latency !== null |
| ? 'bg-emerald-50 text-emerald-600 border-emerald-100 dark:bg-emerald-900/20 dark:text-emerald-400 dark:border-emerald-800/50 shadow-sm' |
| : isTesting |
| ? 'bg-blue-50 text-blue-600 border-blue-100 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800/50 animate-pulse' |
| : proxy.is_healthy |
| ? 'bg-emerald-50 text-emerald-600 border-emerald-100 dark:bg-emerald-900/20 dark:text-emerald-400 dark:border-emerald-800/50' |
| : 'bg-rose-50 text-rose-600 border-rose-100 dark:bg-rose-900/20 dark:text-rose-400 dark:border-rose-800/50 shadow-sm' |
| }`}> |
| {!proxy.enabled |
| ? t('settings.proxy_pool.status.inactive', 'Inactive') |
| : proxy.latency !== undefined && proxy.latency !== null |
| ? `${proxy.latency}ms` |
| : isTesting |
| ? t('settings.proxy_pool.status.checking', 'Checking') |
| : proxy.is_healthy |
| ? t('settings.proxy_pool.status.healthy', 'Healthy') |
| : t('settings.proxy_pool.status.timeout', 'Timeout') |
| } |
| </div> |
| </div> |
| </td> |
| <td className="px-4 py-3"> |
| <div className={`flex flex-col ${!proxy.enabled ? 'opacity-50 grayscale' : ''}`}> |
| <div className="flex items-center gap-2"> |
| <span className="font-semibold text-sm text-gray-900 dark:text-gray-100"> |
| {proxy.name} |
| </span> |
| {proxy.tags.map(tag => ( |
| <span key={tag} className="px-1.5 py-0.5 rounded text-[10px] bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400 font-medium tracking-wide"> |
| {tag} |
| </span> |
| ))} |
| </div> |
| <div className="flex items-center gap-1.5 text-xs text-gray-400 dark:text-gray-500 font-mono mt-0.5 max-w-[240px] truncate" title={proxy.url}> |
| <Globe size={10} /> |
| {proxy.url} |
| </div> |
| </div> |
| </td> |
| <td className="px-4 py-3 whitespace-nowrap"> |
| {boundAccounts.length > 0 ? ( |
| <div className="flex flex-wrap gap-1 max-w-[140px]" title={`Bound to:\n${boundAccounts.map(a => a.email).join('\n')}`}> |
| {boundAccounts.slice(0, 2).map(acc => ( |
| <div key={acc.id} className="px-1.5 py-0.5 rounded-full bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-100 dark:border-indigo-800/50 flex items-center gap-1"> |
| <div className="w-1.5 h-1.5 rounded-full bg-indigo-500 dark:bg-indigo-400"></div> |
| <span className="text-[10px] font-medium text-indigo-700 dark:text-indigo-300"> |
| {acc.email.split('@')[0].substring(0, 4)} |
| </span> |
| </div> |
| ))} |
| {boundAccounts.length > 2 && ( |
| <div className="px-1.5 py-0.5 rounded-full bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 flex items-center justify-center text-[10px] font-medium text-gray-500 dark:text-gray-400"> |
| +{boundAccounts.length - 2} |
| </div> |
| )} |
| </div> |
| ) : ( |
| <span className="text-xs text-gray-300 dark:text-gray-700 italic pl-1">{t('common.none', 'None')}</span> |
| )} |
| </td> |
| <td className="px-4 py-3 whitespace-nowrap text-right pr-6"> |
| <div className="flex items-center justify-end gap-1"> |
| <button |
| onClick={() => handleToggleEnabled(proxy.id)} |
| className={`p-1.5 transition-colors ${proxy.enabled |
| ? 'text-gray-400 hover:text-gray-700 dark:hover:text-gray-200' |
| : 'text-gray-300 hover:text-gray-500 dark:text-gray-600 dark:hover:text-gray-400' |
| }`} |
| title={proxy.enabled ? t('common.disable', 'Disable') : t('common.enable', 'Enable')} |
| > |
| <Power size={14} /> |
| </button> |
| <button |
| onClick={() => handleEdit(proxy)} |
| className="p-1.5 text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors" |
| title={t('common.edit', 'Edit')} |
| > |
| <Edit2 size={14} /> |
| </button> |
| <button |
| onClick={() => handleDelete(proxy.id)} |
| className="p-1.5 text-gray-400 hover:text-red-600 dark:hover:text-red-400 transition-colors" |
| title={t('common.delete', 'Delete')} |
| > |
| <Trash2 size={14} /> |
| </button> |
| </div> |
| </td> |
| </tr> |
| ) |
| }) |
| )} |
| </tbody> |
| </table> |
| |
| {isEditModalOpen && editingProxy && ( |
| <ProxyEditModal |
| isOpen={isEditModalOpen} |
| onClose={() => setIsEditModalOpen(false)} |
| onSave={handleSaveProxy} |
| initialData={editingProxy} |
| isEditing={true} |
| /> |
| )} |
| </div> |
| ); |
| } |
|
|