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:
Mondo Diaz
2026-02-02 15:18:59 -06:00
parent 36cf288526
commit 0a6dad9af0
5 changed files with 114 additions and 21 deletions

View File

@@ -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

View File

@@ -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)}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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>
);