|
|
|
@ -12,7 +12,8 @@ import { |
|
|
|
Play, |
|
|
|
X, |
|
|
|
AlertTriangle, |
|
|
|
ExternalLink |
|
|
|
ExternalLink, |
|
|
|
Square |
|
|
|
} from 'lucide-react'; |
|
|
|
|
|
|
|
const HOURS_LIST = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0')); |
|
|
|
@ -25,6 +26,14 @@ function App() { |
|
|
|
const [logs, setLogs] = useState([]); |
|
|
|
const [health, setHealth] = useState({ database: 'loading', redis: 'loading' }); |
|
|
|
const [errorLogModalContent, setErrorLogModalContent] = useState(null); |
|
|
|
const [confirmModal, setConfirmModal] = useState({ |
|
|
|
isOpen: false, |
|
|
|
title: '', |
|
|
|
message: '', |
|
|
|
confirmText: 'حذف', |
|
|
|
confirmType: 'danger', |
|
|
|
onConfirm: () => {} |
|
|
|
}); |
|
|
|
|
|
|
|
// Loading states |
|
|
|
const [loadingCrawlers, setLoadingCrawlers] = useState(true); |
|
|
|
@ -271,15 +280,23 @@ function App() { |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const deleteCrawler = async (id) => { |
|
|
|
if (!confirm('آیا از حذف این کرالر اطمینان دارید؟')) return; |
|
|
|
try { |
|
|
|
await axios.delete(`/api/crawlers/${id}/`); |
|
|
|
showToast('کرالر با موفقیت حذف شد', 'success'); |
|
|
|
fetchCrawlers(); |
|
|
|
} catch (err) { |
|
|
|
showToast('خطا در حذف کرالر', 'danger'); |
|
|
|
} |
|
|
|
const deleteCrawler = (id) => { |
|
|
|
setConfirmModal({ |
|
|
|
isOpen: true, |
|
|
|
title: 'حذف کرالر', |
|
|
|
message: 'آیا از حذف این کرالر اطمینان دارید؟ تمام سوابق اجرا و آگهیهای مرتبط نیز حذف خواهند شد.', |
|
|
|
confirmText: 'حذف کرالر', |
|
|
|
confirmType: 'danger', |
|
|
|
onConfirm: async () => { |
|
|
|
try { |
|
|
|
await axios.delete(`/api/crawlers/${id}/`); |
|
|
|
showToast('کرالر با موفقیت حذف شد', 'success'); |
|
|
|
fetchCrawlers(); |
|
|
|
} catch (err) { |
|
|
|
showToast('خطا در حذف کرالر', 'danger'); |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
const triggerCrawler = async (id) => { |
|
|
|
@ -294,26 +311,61 @@ function App() { |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const deleteAd = async (id) => { |
|
|
|
if (!confirm('آیا از حذف این آگهی اطمینان دارید؟')) return; |
|
|
|
try { |
|
|
|
await axios.delete(`/api/ads/${id}/`); |
|
|
|
showToast('آگهی با موفقیت حذف شد', 'success'); |
|
|
|
setAds(prev => prev.filter(item => item.id !== id)); |
|
|
|
} catch (err) { |
|
|
|
showToast('خطا در حذف آگهی', 'danger'); |
|
|
|
} |
|
|
|
const deleteAd = (id) => { |
|
|
|
setConfirmModal({ |
|
|
|
isOpen: true, |
|
|
|
title: 'حذف آگهی از فید', |
|
|
|
message: 'آیا از حذف این آگهی از فید اطمینان دارید؟ این عملیات قابل بازگشت نیست.', |
|
|
|
confirmText: 'حذف آگهی', |
|
|
|
confirmType: 'danger', |
|
|
|
onConfirm: async () => { |
|
|
|
try { |
|
|
|
await axios.delete(`/api/ads/${id}/`); |
|
|
|
showToast('آگهی با موفقیت حذف شد', 'success'); |
|
|
|
setAds(prev => prev.filter(item => item.id !== id)); |
|
|
|
} catch (err) { |
|
|
|
showToast('خطا در حذف آگهی', 'danger'); |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
const deleteRun = async (id) => { |
|
|
|
if (!confirm('آیا از حذف این سابقه اجرا اطمینان دارید؟')) return; |
|
|
|
try { |
|
|
|
await axios.delete(`/api/crawlers/runs/${id}/`); |
|
|
|
showToast('سابقه اجرا با موفقیت حذف شد', 'success'); |
|
|
|
setLogs(prev => prev.filter(item => item.id !== id)); |
|
|
|
} catch (err) { |
|
|
|
showToast('خطا در حذف سابقه اجرا', 'danger'); |
|
|
|
} |
|
|
|
const deleteRun = (id) => { |
|
|
|
setConfirmModal({ |
|
|
|
isOpen: true, |
|
|
|
title: 'حذف سابقه اجرا', |
|
|
|
message: 'آیا از حذف این رکورد سابقه اجرا اطمینان دارید؟', |
|
|
|
confirmText: 'حذف رکورد', |
|
|
|
confirmType: 'danger', |
|
|
|
onConfirm: async () => { |
|
|
|
try { |
|
|
|
await axios.delete(`/api/crawlers/runs/${id}/`); |
|
|
|
showToast('سابقه اجرا با موفقیت حذف شد', 'success'); |
|
|
|
setLogs(prev => prev.filter(item => item.id !== id)); |
|
|
|
} catch (err) { |
|
|
|
showToast('خطا در حذف سابقه اجرا', 'danger'); |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
const stopRun = (id) => { |
|
|
|
setConfirmModal({ |
|
|
|
isOpen: true, |
|
|
|
title: 'متوقفسازی اجرای کرالر', |
|
|
|
message: 'آیا از متوقفسازی این اجرای فعال اطمینان دارید؟', |
|
|
|
confirmText: 'توقف اجرا', |
|
|
|
confirmType: 'warning', |
|
|
|
onConfirm: async () => { |
|
|
|
try { |
|
|
|
await axios.post(`/api/crawlers/runs/${id}/stop/`); |
|
|
|
showToast('اجرای کرالر متوقف شد', 'info'); |
|
|
|
fetchLogs(); |
|
|
|
} catch (err) { |
|
|
|
showToast('خطا در متوقفسازی اجرا', 'danger'); |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
@ -677,13 +729,24 @@ function App() { |
|
|
|
{run.error_log || '-'} |
|
|
|
</td> |
|
|
|
<td className="px-5 py-4 border-b border-white/5 text-center"> |
|
|
|
<button |
|
|
|
onClick={() => deleteRun(run.id)} |
|
|
|
className="inline-flex items-center justify-center p-1.5 rounded-lg bg-rose-500/10 border border-rose-500/20 text-rose-400 hover:bg-rose-500/20 transition-all duration-200" |
|
|
|
title="حذف سابقه اجرا" |
|
|
|
> |
|
|
|
<Trash2 className="w-4 h-4" /> |
|
|
|
</button> |
|
|
|
<div className="flex items-center justify-center gap-2"> |
|
|
|
{run.status === 'RUNNING' && ( |
|
|
|
<button |
|
|
|
onClick={() => stopRun(run.id)} |
|
|
|
className="inline-flex items-center justify-center p-1.5 rounded-lg bg-amber-500/10 border border-amber-500/20 text-amber-400 hover:bg-amber-500/20 transition-all duration-200" |
|
|
|
title="توقف اجرا" |
|
|
|
> |
|
|
|
<Square className="w-4 h-4 fill-current" /> |
|
|
|
</button> |
|
|
|
)} |
|
|
|
<button |
|
|
|
onClick={() => deleteRun(run.id)} |
|
|
|
className="inline-flex items-center justify-center p-1.5 rounded-lg bg-rose-500/10 border border-rose-500/20 text-rose-400 hover:bg-rose-500/20 transition-all duration-200" |
|
|
|
title="حذف سابقه اجرا" |
|
|
|
> |
|
|
|
<Trash2 className="w-4 h-4" /> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</td> |
|
|
|
</tr> |
|
|
|
))} |
|
|
|
@ -882,6 +945,44 @@ function App() { |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
{/* Custom Confirmation Modal */} |
|
|
|
{confirmModal.isOpen && ( |
|
|
|
<div className="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50 transition-opacity duration-200"> |
|
|
|
<div className="bg-slate-900 border border-white/5 rounded-2xl w-[480px] max-w-[95%] shadow-[0_20px_50px_rgba(0,0,0,0.6)] flex flex-col scale-100 transition-transform duration-200 overflow-hidden"> |
|
|
|
<div className="flex items-center justify-between p-5 border-b border-white/5"> |
|
|
|
<div className="flex items-center gap-3"> |
|
|
|
<div className={`p-2 rounded-xl border ${confirmModal.confirmType === 'danger' ? 'bg-rose-500/10 border-rose-500/20 text-rose-400' : 'bg-amber-500/10 border-amber-500/20 text-amber-400'}`}> |
|
|
|
<AlertTriangle className="w-5 h-5" /> |
|
|
|
</div> |
|
|
|
<h3 className="text-base font-bold text-white">{confirmModal.title}</h3> |
|
|
|
</div> |
|
|
|
<button className="bg-transparent border-none text-gray-400 cursor-pointer flex items-center hover:text-white" onClick={() => setConfirmModal(prev => ({ ...prev, isOpen: false }))}> |
|
|
|
<X className="w-5 h-5" /> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
<div className="p-6"> |
|
|
|
<p className="text-sm text-gray-300 leading-relaxed">{confirmModal.message}</p> |
|
|
|
</div> |
|
|
|
<div className="flex justify-end gap-3 p-5 border-t border-white/5 bg-slate-950/40"> |
|
|
|
<button |
|
|
|
className="inline-flex items-center justify-center border border-white/5 rounded-lg px-4 py-2 text-sm font-medium cursor-pointer transition-all duration-200 bg-white/5 text-white hover:bg-white/10" |
|
|
|
onClick={() => setConfirmModal(prev => ({ ...prev, isOpen: false }))} |
|
|
|
> |
|
|
|
انصراف |
|
|
|
</button> |
|
|
|
<button |
|
|
|
className={`inline-flex items-center justify-center border border-transparent rounded-lg px-4 py-2 text-sm font-medium cursor-pointer transition-all duration-200 text-white shadow-lg ${confirmModal.confirmType === 'danger' ? 'bg-rose-600 hover:bg-rose-700 shadow-rose-600/30' : 'bg-amber-600 hover:bg-amber-700 shadow-amber-600/30'}`} |
|
|
|
onClick={async () => { |
|
|
|
setConfirmModal(prev => ({ ...prev, isOpen: false })); |
|
|
|
await confirmModal.onConfirm(); |
|
|
|
}} |
|
|
|
> |
|
|
|
{confirmModal.confirmText} |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
|
|
|
|
</div> |
|
|
|
); |
|
|
|
|