Add Active Workers table to Background Jobs dashboard
Shows currently processing cache tasks in a dynamic table with: - Package name and version constraint being cached - Recursion depth and attempt number - Start timestamp - Pulsing indicator to show live activity Backend changes: - Add get_active_tasks() function to pypi_cache_worker.py - Add GET /pypi/cache/active endpoint to pypi_proxy.py Frontend changes: - Add PyPICacheActiveTask type - Add getPyPICacheActiveTasks() API function - Add Active Workers section with animated table - Auto-refreshes every 5 seconds with existing data
This commit is contained in:
@@ -754,6 +754,7 @@ export async function testUpstreamSource(id: string): Promise<UpstreamSourceTest
|
||||
import {
|
||||
PyPICacheStatus,
|
||||
PyPICacheTask,
|
||||
PyPICacheActiveTask,
|
||||
PyPICacheRetryResponse,
|
||||
} from './types';
|
||||
|
||||
@@ -771,6 +772,13 @@ export async function getPyPICacheFailedTasks(limit: number = 50): Promise<PyPIC
|
||||
return handleResponse<PyPICacheTask[]>(response);
|
||||
}
|
||||
|
||||
export async function getPyPICacheActiveTasks(limit: number = 50): Promise<PyPICacheActiveTask[]> {
|
||||
const response = await fetch(`/pypi/cache/active?limit=${limit}`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
return handleResponse<PyPICacheActiveTask[]>(response);
|
||||
}
|
||||
|
||||
export async function retryPyPICacheTask(packageName: string): Promise<PyPICacheRetryResponse> {
|
||||
const response = await fetch(`/pypi/cache/retry/${encodeURIComponent(packageName)}`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -271,6 +271,62 @@
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Active Workers Section */
|
||||
.active-workers-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.active-workers-section h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #3b82f6;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.pulse-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.jobs-table.active-table {
|
||||
border: 1px solid #3b82f6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.jobs-table.active-table th {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.jobs-table .active-row {
|
||||
background-color: #f0f9ff;
|
||||
}
|
||||
|
||||
.jobs-table .active-row:hover {
|
||||
background-color: #e0f2fe;
|
||||
}
|
||||
|
||||
.jobs-table .version-constraint {
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.status-cards {
|
||||
|
||||
@@ -4,10 +4,11 @@ import { useAuth } from '../contexts/AuthContext';
|
||||
import {
|
||||
getPyPICacheStatus,
|
||||
getPyPICacheFailedTasks,
|
||||
getPyPICacheActiveTasks,
|
||||
retryPyPICacheTask,
|
||||
retryAllPyPICacheTasks,
|
||||
} from '../api';
|
||||
import { PyPICacheStatus, PyPICacheTask } from '../types';
|
||||
import { PyPICacheStatus, PyPICacheTask, PyPICacheActiveTask } from '../types';
|
||||
import './AdminJobsPage.css';
|
||||
|
||||
function AdminJobsPage() {
|
||||
@@ -17,6 +18,7 @@ function AdminJobsPage() {
|
||||
// PyPI cache status
|
||||
const [cacheStatus, setCacheStatus] = useState<PyPICacheStatus | null>(null);
|
||||
const [failedTasks, setFailedTasks] = useState<PyPICacheTask[]>([]);
|
||||
const [activeTasks, setActiveTasks] = useState<PyPICacheActiveTask[]>([]);
|
||||
const [loadingStatus, setLoadingStatus] = useState(true);
|
||||
const [statusError, setStatusError] = useState<string | null>(null);
|
||||
|
||||
@@ -39,12 +41,14 @@ function AdminJobsPage() {
|
||||
|
||||
setStatusError(null);
|
||||
try {
|
||||
const [status, failed] = await Promise.all([
|
||||
const [status, failed, active] = await Promise.all([
|
||||
getPyPICacheStatus(),
|
||||
getPyPICacheFailedTasks(100),
|
||||
getPyPICacheActiveTasks(50),
|
||||
]);
|
||||
setCacheStatus(status);
|
||||
setFailedTasks(failed);
|
||||
setActiveTasks(active);
|
||||
} catch (err) {
|
||||
setStatusError(err instanceof Error ? err.message : 'Failed to load status');
|
||||
} finally {
|
||||
@@ -192,52 +196,101 @@ function AdminJobsPage() {
|
||||
|
||||
{totalJobs === 0 ? (
|
||||
<p className="empty-message">No cache jobs yet. Jobs are created when packages are downloaded through the PyPI proxy.</p>
|
||||
) : failedTasks.length === 0 && cacheStatus?.pending === 0 && cacheStatus?.in_progress === 0 ? (
|
||||
<p className="success-text">All jobs completed successfully.</p>
|
||||
) : failedTasks.length > 0 ? (
|
||||
<>
|
||||
<h3>Failed Tasks</h3>
|
||||
<table className="jobs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package</th>
|
||||
<th>Error</th>
|
||||
<th>Attempts</th>
|
||||
<th>Depth</th>
|
||||
<th>Failed At</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{failedTasks.map((task) => (
|
||||
<tr key={task.id}>
|
||||
<td className="package-name">{task.package}</td>
|
||||
<td className="error-cell" title={task.error || ''}>
|
||||
{task.error || 'Unknown error'}
|
||||
</td>
|
||||
<td>{task.attempts}</td>
|
||||
<td>{task.depth}</td>
|
||||
<td className="timestamp">
|
||||
{task.failed_at
|
||||
? new Date(task.failed_at).toLocaleString()
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="actions-cell">
|
||||
<button
|
||||
className="btn btn-sm btn-secondary"
|
||||
onClick={() => handleRetryPackage(task.package)}
|
||||
disabled={retryingPackage === task.package}
|
||||
>
|
||||
{retryingPackage === task.package ? 'Retrying...' : 'Retry'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
) : (
|
||||
<p className="empty-message">Jobs are processing. No failures yet.</p>
|
||||
<>
|
||||
{/* Active Workers Table */}
|
||||
{activeTasks.length > 0 && (
|
||||
<div className="active-workers-section">
|
||||
<h3>
|
||||
<span className="pulse-indicator"></span>
|
||||
Active Workers ({activeTasks.length})
|
||||
</h3>
|
||||
<table className="jobs-table active-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package</th>
|
||||
<th>Version</th>
|
||||
<th>Depth</th>
|
||||
<th>Attempt</th>
|
||||
<th>Started</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{activeTasks.map((task) => (
|
||||
<tr key={task.id} className="active-row">
|
||||
<td className="package-name">{task.package}</td>
|
||||
<td className="version-constraint">
|
||||
{task.version_constraint || '*'}
|
||||
</td>
|
||||
<td>{task.depth}</td>
|
||||
<td>{task.attempts + 1}</td>
|
||||
<td className="timestamp">
|
||||
{task.started_at
|
||||
? new Date(task.started_at).toLocaleTimeString()
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Failed Tasks Table */}
|
||||
{failedTasks.length > 0 && (
|
||||
<>
|
||||
<h3>Failed Tasks</h3>
|
||||
<table className="jobs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package</th>
|
||||
<th>Error</th>
|
||||
<th>Attempts</th>
|
||||
<th>Depth</th>
|
||||
<th>Failed At</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{failedTasks.map((task) => (
|
||||
<tr key={task.id}>
|
||||
<td className="package-name">{task.package}</td>
|
||||
<td className="error-cell" title={task.error || ''}>
|
||||
{task.error || 'Unknown error'}
|
||||
</td>
|
||||
<td>{task.attempts}</td>
|
||||
<td>{task.depth}</td>
|
||||
<td className="timestamp">
|
||||
{task.failed_at
|
||||
? new Date(task.failed_at).toLocaleString()
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="actions-cell">
|
||||
<button
|
||||
className="btn btn-sm btn-secondary"
|
||||
onClick={() => handleRetryPackage(task.package)}
|
||||
disabled={retryingPackage === task.package}
|
||||
>
|
||||
{retryingPackage === task.package ? 'Retrying...' : 'Retry'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Success message when nothing is pending/in-progress/failed */}
|
||||
{failedTasks.length === 0 && activeTasks.length === 0 && cacheStatus?.pending === 0 && (
|
||||
<p className="success-text">All jobs completed successfully.</p>
|
||||
)}
|
||||
|
||||
{/* In progress message */}
|
||||
{activeTasks.length === 0 && failedTasks.length === 0 && (cacheStatus?.pending ?? 0) > 0 && (
|
||||
<p className="empty-message">Jobs queued for processing...</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -575,6 +575,15 @@ export interface PyPICacheTask {
|
||||
failed_at: string | null;
|
||||
}
|
||||
|
||||
export interface PyPICacheActiveTask {
|
||||
id: string;
|
||||
package: string;
|
||||
version_constraint: string | null;
|
||||
depth: number;
|
||||
attempts: number;
|
||||
started_at: string | null;
|
||||
}
|
||||
|
||||
export interface PyPICacheRetryResponse {
|
||||
message: string;
|
||||
task_id?: string;
|
||||
|
||||
Reference in New Issue
Block a user