Add cancel job button and improve jobs table UI
- Remove "All Jobs" title
- Move Status column to front of table
- Add Cancel button for in-progress jobs
- Add cancel endpoint: POST /pypi/cache/cancel/{package_name}
- Add btn-danger CSS styling
This commit is contained in:
@@ -699,3 +699,37 @@ def retry_all_failed_tasks(db: Session) -> int:
|
||||
|
||||
logger.info(f"Reset {count} failed tasks for retry")
|
||||
return count
|
||||
|
||||
|
||||
def cancel_cache_task(db: Session, package_name: str) -> Optional[PyPICacheTask]:
|
||||
"""
|
||||
Cancel an in-progress or pending cache task.
|
||||
|
||||
Args:
|
||||
db: Database session.
|
||||
package_name: The package name to cancel.
|
||||
|
||||
Returns:
|
||||
The cancelled task, or None if not found.
|
||||
"""
|
||||
normalized = re.sub(r"[-_.]+", "-", package_name).lower()
|
||||
|
||||
task = (
|
||||
db.query(PyPICacheTask)
|
||||
.filter(
|
||||
PyPICacheTask.package_name == normalized,
|
||||
PyPICacheTask.status.in_(["pending", "in_progress"]),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not task:
|
||||
return None
|
||||
|
||||
task.status = "failed"
|
||||
task.completed_at = datetime.utcnow()
|
||||
task.error_message = "Cancelled by admin"
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Cancelled cache task: {normalized}")
|
||||
return task
|
||||
|
||||
@@ -34,6 +34,7 @@ from .pypi_cache_worker import (
|
||||
get_recent_activity,
|
||||
retry_failed_task,
|
||||
retry_all_failed_tasks,
|
||||
cancel_cache_task,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -924,3 +925,26 @@ async def pypi_cache_retry_all(
|
||||
"""
|
||||
count = retry_all_failed_tasks(db)
|
||||
return {"message": f"Queued {count} tasks for retry", "count": count}
|
||||
|
||||
|
||||
@router.post("/cache/cancel/{package_name}")
|
||||
async def pypi_cache_cancel(
|
||||
package_name: str,
|
||||
db: Session = Depends(get_db),
|
||||
_current_user: User = Depends(require_admin),
|
||||
):
|
||||
"""
|
||||
Cancel an in-progress or pending cache task.
|
||||
|
||||
Args:
|
||||
package_name: The package name to cancel.
|
||||
|
||||
Requires admin privileges.
|
||||
"""
|
||||
task = cancel_cache_task(db, package_name)
|
||||
if not task:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No active cache task found for package '{package_name}'"
|
||||
)
|
||||
return {"message": f"Cancelled task for {task.package_name}", "task_id": str(task.id)}
|
||||
|
||||
@@ -794,3 +794,11 @@ export async function retryAllPyPICacheTasks(): Promise<PyPICacheRetryResponse>
|
||||
});
|
||||
return handleResponse<PyPICacheRetryResponse>(response);
|
||||
}
|
||||
|
||||
export async function cancelPyPICacheTask(packageName: string): Promise<PyPICacheRetryResponse> {
|
||||
const response = await fetch(`/pypi/cache/cancel/${encodeURIComponent(packageName)}`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
return handleResponse<PyPICacheRetryResponse>(response);
|
||||
}
|
||||
|
||||
@@ -429,6 +429,17 @@
|
||||
border-color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #ef4444;
|
||||
border-color: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #dc2626;
|
||||
border-color: #dc2626;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getPyPICacheActiveTasks,
|
||||
retryPyPICacheTask,
|
||||
retryAllPyPICacheTasks,
|
||||
cancelPyPICacheTask,
|
||||
} from '../api';
|
||||
import { PyPICacheStatus, PyPICacheTask, PyPICacheActiveTask } from '../types';
|
||||
import './AdminJobsPage.css';
|
||||
@@ -25,6 +26,7 @@ function AdminJobsPage() {
|
||||
// Action states
|
||||
const [retryingPackage, setRetryingPackage] = useState<string | null>(null);
|
||||
const [retryingAll, setRetryingAll] = useState(false);
|
||||
const [cancelingPackage, setCancelingPackage] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
|
||||
// Auto-refresh
|
||||
@@ -110,6 +112,19 @@ function AdminJobsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancelPackage(packageName: string) {
|
||||
setCancelingPackage(packageName);
|
||||
try {
|
||||
const result = await cancelPyPICacheTask(packageName);
|
||||
setSuccessMessage(result.message);
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
setStatusError(err instanceof Error ? err.message : 'Failed to cancel');
|
||||
} finally {
|
||||
setCancelingPackage(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (authLoading) {
|
||||
return <div className="admin-jobs-page">Loading...</div>;
|
||||
}
|
||||
@@ -217,18 +232,10 @@ function AdminJobsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Unified Jobs Section */}
|
||||
{/* Jobs Section */}
|
||||
<section className="jobs-section">
|
||||
<div className="section-header">
|
||||
<h2>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
||||
</svg>
|
||||
All Jobs
|
||||
</h2>
|
||||
{failedTasks.length > 0 && (
|
||||
{failedTasks.length > 0 && (
|
||||
<div className="section-header">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleRetryAll}
|
||||
@@ -236,8 +243,8 @@ function AdminJobsPage() {
|
||||
>
|
||||
{retryingAll ? 'Retrying...' : `Retry All Failed (${failedTasks.length})`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadingStatus && !cacheStatus ? (
|
||||
<p className="loading-text">Loading job status...</p>
|
||||
@@ -249,9 +256,9 @@ function AdminJobsPage() {
|
||||
<table className="jobs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Type</th>
|
||||
<th>Package</th>
|
||||
<th>Status</th>
|
||||
<th>Depth</th>
|
||||
<th>Attempts</th>
|
||||
<th>Details</th>
|
||||
@@ -262,13 +269,6 @@ function AdminJobsPage() {
|
||||
{allJobs.map((job) => {
|
||||
return (
|
||||
<tr key={job.id} className={`job-row ${job.status}`}>
|
||||
<td>
|
||||
<span className="type-badge pypi">PyPI</span>
|
||||
</td>
|
||||
<td className="package-name">
|
||||
{job.status === 'in_progress' && <span className="spinner"></span>}
|
||||
{job.package}
|
||||
</td>
|
||||
<td className="status-cell">
|
||||
{job.status === 'in_progress' ? (
|
||||
<span className="status-badge working">Working</span>
|
||||
@@ -278,6 +278,13 @@ function AdminJobsPage() {
|
||||
<span className="status-badge pending">Pending</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<span className="type-badge pypi">PyPI</span>
|
||||
</td>
|
||||
<td className="package-name">
|
||||
{job.status === 'in_progress' && <span className="spinner"></span>}
|
||||
{job.package}
|
||||
</td>
|
||||
<td>{job.depth}</td>
|
||||
<td>{job.attempts}</td>
|
||||
<td className="details-cell">
|
||||
@@ -305,6 +312,15 @@ function AdminJobsPage() {
|
||||
{retryingPackage === job.package ? '...' : 'Retry'}
|
||||
</button>
|
||||
)}
|
||||
{job.status === 'in_progress' && (
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => handleCancelPackage(job.package)}
|
||||
disabled={cancelingPackage === job.package}
|
||||
>
|
||||
{cancelingPackage === job.package ? '...' : 'Cancel'}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user