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")
|
logger.info(f"Reset {count} failed tasks for retry")
|
||||||
return count
|
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,
|
get_recent_activity,
|
||||||
retry_failed_task,
|
retry_failed_task,
|
||||||
retry_all_failed_tasks,
|
retry_all_failed_tasks,
|
||||||
|
cancel_cache_task,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -924,3 +925,26 @@ async def pypi_cache_retry_all(
|
|||||||
"""
|
"""
|
||||||
count = retry_all_failed_tasks(db)
|
count = retry_all_failed_tasks(db)
|
||||||
return {"message": f"Queued {count} tasks for retry", "count": count}
|
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);
|
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);
|
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 {
|
.btn-sm {
|
||||||
padding: 0.25rem 0.75rem;
|
padding: 0.25rem 0.75rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
getPyPICacheActiveTasks,
|
getPyPICacheActiveTasks,
|
||||||
retryPyPICacheTask,
|
retryPyPICacheTask,
|
||||||
retryAllPyPICacheTasks,
|
retryAllPyPICacheTasks,
|
||||||
|
cancelPyPICacheTask,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import { PyPICacheStatus, PyPICacheTask, PyPICacheActiveTask } from '../types';
|
import { PyPICacheStatus, PyPICacheTask, PyPICacheActiveTask } from '../types';
|
||||||
import './AdminJobsPage.css';
|
import './AdminJobsPage.css';
|
||||||
@@ -25,6 +26,7 @@ function AdminJobsPage() {
|
|||||||
// Action states
|
// Action states
|
||||||
const [retryingPackage, setRetryingPackage] = useState<string | null>(null);
|
const [retryingPackage, setRetryingPackage] = useState<string | null>(null);
|
||||||
const [retryingAll, setRetryingAll] = useState(false);
|
const [retryingAll, setRetryingAll] = useState(false);
|
||||||
|
const [cancelingPackage, setCancelingPackage] = useState<string | null>(null);
|
||||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
// Auto-refresh
|
// 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) {
|
if (authLoading) {
|
||||||
return <div className="admin-jobs-page">Loading...</div>;
|
return <div className="admin-jobs-page">Loading...</div>;
|
||||||
}
|
}
|
||||||
@@ -217,18 +232,10 @@ function AdminJobsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Unified Jobs Section */}
|
{/* Jobs Section */}
|
||||||
<section className="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
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={handleRetryAll}
|
onClick={handleRetryAll}
|
||||||
@@ -236,8 +243,8 @@ function AdminJobsPage() {
|
|||||||
>
|
>
|
||||||
{retryingAll ? 'Retrying...' : `Retry All Failed (${failedTasks.length})`}
|
{retryingAll ? 'Retrying...' : `Retry All Failed (${failedTasks.length})`}
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{loadingStatus && !cacheStatus ? (
|
{loadingStatus && !cacheStatus ? (
|
||||||
<p className="loading-text">Loading job status...</p>
|
<p className="loading-text">Loading job status...</p>
|
||||||
@@ -249,9 +256,9 @@ function AdminJobsPage() {
|
|||||||
<table className="jobs-table">
|
<table className="jobs-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Package</th>
|
<th>Package</th>
|
||||||
<th>Status</th>
|
|
||||||
<th>Depth</th>
|
<th>Depth</th>
|
||||||
<th>Attempts</th>
|
<th>Attempts</th>
|
||||||
<th>Details</th>
|
<th>Details</th>
|
||||||
@@ -262,13 +269,6 @@ function AdminJobsPage() {
|
|||||||
{allJobs.map((job) => {
|
{allJobs.map((job) => {
|
||||||
return (
|
return (
|
||||||
<tr key={job.id} className={`job-row ${job.status}`}>
|
<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">
|
<td className="status-cell">
|
||||||
{job.status === 'in_progress' ? (
|
{job.status === 'in_progress' ? (
|
||||||
<span className="status-badge working">Working</span>
|
<span className="status-badge working">Working</span>
|
||||||
@@ -278,6 +278,13 @@ function AdminJobsPage() {
|
|||||||
<span className="status-badge pending">Pending</span>
|
<span className="status-badge pending">Pending</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</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.depth}</td>
|
||||||
<td>{job.attempts}</td>
|
<td>{job.attempts}</td>
|
||||||
<td className="details-cell">
|
<td className="details-cell">
|
||||||
@@ -305,6 +312,15 @@ function AdminJobsPage() {
|
|||||||
{retryingPackage === job.package ? '...' : 'Retry'}
|
{retryingPackage === job.package ? '...' : 'Retry'}
|
||||||
</button>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user