Browse Source

feat(frontend): add custom confirmation modal and stop running jobs action in run history

master
PouyaKhajavi 1 day ago
parent
commit
5307fa459f
  1. 115
      frontend/src/App.jsx

115
frontend/src/App.jsx

@ -12,7 +12,8 @@ import {
Play, Play,
X, X,
AlertTriangle, AlertTriangle,
ExternalLink
ExternalLink,
Square
} from 'lucide-react'; } from 'lucide-react';
const HOURS_LIST = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0')); const HOURS_LIST = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
@ -25,6 +26,14 @@ function App() {
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const [health, setHealth] = useState({ database: 'loading', redis: 'loading' }); const [health, setHealth] = useState({ database: 'loading', redis: 'loading' });
const [errorLogModalContent, setErrorLogModalContent] = useState(null); const [errorLogModalContent, setErrorLogModalContent] = useState(null);
const [confirmModal, setConfirmModal] = useState({
isOpen: false,
title: '',
message: '',
confirmText: 'حذف',
confirmType: 'danger',
onConfirm: () => {}
});
// Loading states // Loading states
const [loadingCrawlers, setLoadingCrawlers] = useState(true); const [loadingCrawlers, setLoadingCrawlers] = useState(true);
@ -271,8 +280,14 @@ function App() {
} }
}; };
const deleteCrawler = async (id) => {
if (!confirm('آیا از حذف این کرالر اطمینان دارید؟')) return;
const deleteCrawler = (id) => {
setConfirmModal({
isOpen: true,
title: 'حذف کرالر',
message: 'آیا از حذف این کرالر اطمینان دارید؟ تمام سوابق اجرا و آگهی‌های مرتبط نیز حذف خواهند شد.',
confirmText: 'حذف کرالر',
confirmType: 'danger',
onConfirm: async () => {
try { try {
await axios.delete(`/api/crawlers/${id}/`); await axios.delete(`/api/crawlers/${id}/`);
showToast('کرالر با موفقیت حذف شد', 'success'); showToast('کرالر با موفقیت حذف شد', 'success');
@ -280,6 +295,8 @@ function App() {
} catch (err) { } catch (err) {
showToast('خطا در حذف کرالر', 'danger'); showToast('خطا در حذف کرالر', 'danger');
} }
}
});
}; };
const triggerCrawler = async (id) => { const triggerCrawler = async (id) => {
@ -294,8 +311,14 @@ function App() {
} }
}; };
const deleteAd = async (id) => {
if (!confirm('آیا از حذف این آگهی اطمینان دارید؟')) return;
const deleteAd = (id) => {
setConfirmModal({
isOpen: true,
title: 'حذف آگهی از فید',
message: 'آیا از حذف این آگهی از فید اطمینان دارید؟ این عملیات قابل بازگشت نیست.',
confirmText: 'حذف آگهی',
confirmType: 'danger',
onConfirm: async () => {
try { try {
await axios.delete(`/api/ads/${id}/`); await axios.delete(`/api/ads/${id}/`);
showToast('آگهی با موفقیت حذف شد', 'success'); showToast('آگهی با موفقیت حذف شد', 'success');
@ -303,10 +326,18 @@ function App() {
} catch (err) { } catch (err) {
showToast('خطا در حذف آگهی', 'danger'); showToast('خطا در حذف آگهی', 'danger');
} }
}
});
}; };
const deleteRun = async (id) => {
if (!confirm('آیا از حذف این سابقه اجرا اطمینان دارید؟')) return;
const deleteRun = (id) => {
setConfirmModal({
isOpen: true,
title: 'حذف سابقه اجرا',
message: 'آیا از حذف این رکورد سابقه اجرا اطمینان دارید؟',
confirmText: 'حذف رکورد',
confirmType: 'danger',
onConfirm: async () => {
try { try {
await axios.delete(`/api/crawlers/runs/${id}/`); await axios.delete(`/api/crawlers/runs/${id}/`);
showToast('سابقه اجرا با موفقیت حذف شد', 'success'); showToast('سابقه اجرا با موفقیت حذف شد', 'success');
@ -314,6 +345,27 @@ function App() {
} catch (err) { } catch (err) {
showToast('خطا در حذف سابقه اجرا', 'danger'); 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,6 +729,16 @@ function App() {
{run.error_log || '-'} {run.error_log || '-'}
</td> </td>
<td className="px-5 py-4 border-b border-white/5 text-center"> <td className="px-5 py-4 border-b border-white/5 text-center">
<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 <button
onClick={() => deleteRun(run.id)} 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" 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"
@ -684,6 +746,7 @@ function App() {
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
</div>
</td> </td>
</tr> </tr>
))} ))}
@ -882,6 +945,44 @@ function App() {
</div> </div>
</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> </div>
); );

Loading…
Cancel
Save