From 92edef92e6c804ba91c65df04143b6497d6f4221 Mon Sep 17 00:00:00 2001 From: Mondo Diaz Date: Mon, 2 Feb 2026 14:34:48 -0600 Subject: [PATCH] Redesign jobs dashboard with unified table and progress bar - Add overall progress bar showing completed/active/failed counts - Unify all job types into single table with Type column - Simplify status to Working/Pending/Failed badges - Remove NPM "Coming Soon" section - Add get_recent_activity() function for future activity feed - Fix dark mode CSS using CSS variables --- backend/app/pypi_cache_worker.py | 34 ++ backend/app/pypi_proxy.py | 1 + frontend/src/pages/AdminJobsPage.css | 445 +++++++++++++++------------ frontend/src/pages/AdminJobsPage.tsx | 296 +++++++++--------- 4 files changed, 422 insertions(+), 354 deletions(-) diff --git a/backend/app/pypi_cache_worker.py b/backend/app/pypi_cache_worker.py index 416b102..180c0e2 100644 --- a/backend/app/pypi_cache_worker.py +++ b/backend/app/pypi_cache_worker.py @@ -600,6 +600,40 @@ def get_active_tasks(db: Session, limit: int = 50) -> List[dict]: ] +def get_recent_activity(db: Session, limit: int = 20) -> List[dict]: + """ + Get recent task completions and failures for activity feed. + + Args: + db: Database session. + limit: Maximum number of items to return. + + Returns: + List of recent activity items sorted by time descending. + """ + # Get recently completed and failed tasks + tasks = ( + db.query(PyPICacheTask) + .filter(PyPICacheTask.status.in_(["completed", "failed"])) + .filter(PyPICacheTask.completed_at != None) + .order_by(PyPICacheTask.completed_at.desc()) + .limit(limit) + .all() + ) + + return [ + { + "id": str(task.id), + "package": task.package_name, + "status": task.status, + "type": "pypi", + "error": task.error_message if task.status == "failed" else None, + "completed_at": task.completed_at.isoformat() if task.completed_at else None, + } + for task in tasks + ] + + def retry_failed_task(db: Session, package_name: str) -> Optional[PyPICacheTask]: """ Reset a failed task to retry. diff --git a/backend/app/pypi_proxy.py b/backend/app/pypi_proxy.py index a6cc35c..d62338c 100644 --- a/backend/app/pypi_proxy.py +++ b/backend/app/pypi_proxy.py @@ -29,6 +29,7 @@ from .pypi_cache_worker import ( get_cache_status, get_failed_tasks, get_active_tasks, + get_recent_activity, retry_failed_task, retry_all_failed_tasks, ) diff --git a/frontend/src/pages/AdminJobsPage.css b/frontend/src/pages/AdminJobsPage.css index 92cf924..d2706e2 100644 --- a/frontend/src/pages/AdminJobsPage.css +++ b/frontend/src/pages/AdminJobsPage.css @@ -54,19 +54,19 @@ /* Messages */ .success-message { padding: 0.75rem 1rem; - background-color: #d4edda; - border: 1px solid #c3e6cb; + background-color: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.3); border-radius: 4px; - color: #155724; + color: #10b981; margin-bottom: 1rem; } .error-message { padding: 0.75rem 1rem; - background-color: #f8d7da; - border: 1px solid #f5c6cb; + background-color: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 4px; - color: #721c24; + color: #ef4444; margin-bottom: 1rem; } @@ -77,10 +77,10 @@ } .success-text { - color: #2e7d32; + color: #10b981; padding: 1rem; text-align: center; - background: #e8f5e9; + background: rgba(16, 185, 129, 0.1); border-radius: 4px; margin-top: 1rem; } @@ -92,6 +92,69 @@ text-align: center; } +/* Overall Progress Bar */ +.overall-progress { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1rem 1.5rem; + margin-bottom: 1.5rem; +} + +.progress-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.progress-label { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 500; + color: var(--text-primary); +} + +.progress-stats { + font-size: 0.9rem; + color: var(--text-secondary); +} + +.progress-bar-container { + height: 8px; + background: var(--bg-tertiary); + border-radius: 4px; + overflow: hidden; + position: relative; +} + +.progress-bar-fill { + height: 100%; + background: linear-gradient(90deg, #10b981, #34d399); + border-radius: 4px; + transition: width 0.3s ease; +} + +.progress-bar-active { + position: absolute; + top: 0; + right: 0; + width: 30%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.5), transparent); + animation: progress-sweep 1.5s ease-in-out infinite; +} + +@keyframes progress-sweep { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(400%); + } +} + /* Jobs Section */ .jobs-section { background: var(--bg-secondary); @@ -101,10 +164,6 @@ margin-bottom: 1.5rem; } -.jobs-section.coming-soon { - opacity: 0.7; -} - .section-header { display: flex; justify-content: space-between; @@ -112,51 +171,6 @@ margin-bottom: 1rem; } -/* Status Cards */ -.status-cards { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 1rem; - margin-bottom: 1rem; -} - -.status-card { - background: var(--bg-primary); - border-radius: 8px; - padding: 1rem; - text-align: center; - border: 1px solid var(--border-color); -} - -.status-card .status-value { - font-size: 2rem; - font-weight: 600; - line-height: 1.2; -} - -.status-card .status-label { - font-size: 0.85rem; - color: var(--text-secondary); - text-transform: uppercase; - margin-top: 0.25rem; -} - -.status-card.pending .status-value { - color: #f59e0b; -} - -.status-card.in-progress .status-value { - color: #3b82f6; -} - -.status-card.completed .status-value { - color: #10b981; -} - -.status-card.failed .status-value { - color: #ef4444; -} - /* Jobs Table */ .jobs-table { width: 100%; @@ -189,41 +203,190 @@ font-family: monospace; font-weight: 500; color: var(--text-primary); + display: flex; + align-items: center; + gap: 0.5rem; } -.jobs-table .error-cell { - max-width: 300px; +/* Job Row States */ +.job-row.in_progress { + background-color: rgba(59, 130, 246, 0.05); +} + +.job-row.failed { + background-color: rgba(239, 68, 68, 0.05); +} + +.job-row.stale { + background-color: rgba(245, 158, 11, 0.1); +} + +.job-row:hover { + background-color: var(--bg-tertiary); +} + +/* Type Badge */ +.type-badge { + display: inline-block; + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; +} + +.type-badge.pypi { + background-color: rgba(59, 130, 246, 0.15); + color: var(--color-primary, #3b82f6); +} + +/* Status Cell */ +.status-cell { + min-width: 120px; +} + +.status-with-progress { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.mini-progress-bar { + width: 80px; + height: 4px; + background: var(--bg-tertiary); + border-radius: 2px; + overflow: hidden; + position: relative; +} + +.mini-progress-fill { + position: absolute; + top: 0; + left: 0; + width: 40%; + height: 100%; + background: var(--color-primary, #3b82f6); + border-radius: 2px; + animation: mini-progress 1s ease-in-out infinite; +} + +@keyframes mini-progress { + 0%, 100% { + left: 0; + width: 40%; + } + 50% { + left: 60%; + width: 40%; + } +} + +/* Status Badges */ +.status-badge { + display: inline-block; + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; +} + +.status-badge.running, +.status-badge.working { + background-color: rgba(59, 130, 246, 0.15); + color: var(--color-primary, #3b82f6); +} + +.status-badge.failed { + background-color: rgba(239, 68, 68, 0.15); + color: #ef4444; +} + +.status-badge.pending { + background-color: rgba(245, 158, 11, 0.15); + color: #f59e0b; +} + +.status-badge.stale { + background-color: rgba(245, 158, 11, 0.15); + color: #f59e0b; +} + +/* Spinner */ +.spinner { + display: inline-block; + width: 12px; + height: 12px; + border: 2px solid var(--border-color); + border-top-color: var(--color-primary, #3b82f6); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Pulse Indicator */ +.pulse-indicator { + width: 8px; + height: 8px; + background-color: var(--color-primary, #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); + } +} + +/* Details Cell */ +.details-cell { + max-width: 200px; +} + +.error-text { + color: #ef4444; + font-size: 0.85rem; + display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - color: #c62828; - font-size: 0.9rem; } -.jobs-table .timestamp { +.version-text { + font-family: monospace; + font-size: 0.85rem; + color: var(--text-secondary); +} + +.elapsed-time { + font-size: 0.75rem; + color: var(--text-secondary); + font-family: monospace; +} + +.timestamp { font-size: 0.85rem; color: var(--text-secondary); white-space: nowrap; } -.jobs-table .actions-cell { +.actions-cell { white-space: nowrap; text-align: right; } -/* Badges */ -.coming-soon-badge { - display: inline-block; - padding: 0.2rem 0.5rem; - border-radius: 4px; - font-size: 0.7rem; - font-weight: 500; - background-color: #e0e0e0; - color: #616161; - margin-left: 0.75rem; - text-transform: uppercase; -} - /* Buttons */ .btn { padding: 0.5rem 1rem; @@ -271,128 +434,8 @@ 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: var(--color-primary, #3b82f6); - margin-top: 1rem; -} - -.pulse-indicator { - width: 8px; - height: 8px; - background-color: var(--color-primary, #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 var(--color-primary, #3b82f6); - border-radius: 4px; -} - -.jobs-table.active-table th { - background: var(--bg-tertiary); - color: var(--color-primary, #3b82f6); -} - -.jobs-table .active-row { - background-color: var(--bg-secondary); -} - -.jobs-table .active-row:hover { - background-color: var(--bg-tertiary); -} - -.jobs-table .version-constraint { - font-family: monospace; - font-size: 0.85rem; - color: var(--text-secondary); -} - -/* Spinner for active tasks */ -.spinner { - display: inline-block; - width: 12px; - height: 12px; - border: 2px solid var(--border-color); - border-top-color: var(--color-primary, #3b82f6); - border-radius: 50%; - animation: spin 1s linear infinite; - margin-right: 0.5rem; - vertical-align: middle; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -/* Elapsed time */ -.elapsed-time { - font-family: monospace; - font-size: 0.9rem; - color: var(--text-secondary); -} - -.elapsed-time.stale { - color: #f59e0b; - font-weight: 500; -} - -/* Status badges */ -.status-badge { - display: inline-block; - padding: 0.2rem 0.5rem; - border-radius: 4px; - font-size: 0.75rem; - font-weight: 500; - text-transform: uppercase; -} - -.status-badge.running { - background-color: rgba(59, 130, 246, 0.15); - color: var(--color-primary, #3b82f6); -} - -.status-badge.stale { - background-color: rgba(245, 158, 11, 0.15); - color: #f59e0b; -} - -/* Stale row highlighting */ -.jobs-table .stale-row { - background-color: rgba(245, 158, 11, 0.1); -} - -.jobs-table .stale-row:hover { - background-color: rgba(245, 158, 11, 0.15); -} - /* Responsive */ @media (max-width: 768px) { - .status-cards { - grid-template-columns: repeat(2, 1fr); - } - .page-header { flex-direction: column; align-items: flex-start; @@ -403,7 +446,13 @@ font-size: 0.85rem; } - .jobs-table .error-cell { - max-width: 150px; + .details-cell { + max-width: 100px; + } + + .progress-header { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; } } diff --git a/frontend/src/pages/AdminJobsPage.tsx b/frontend/src/pages/AdminJobsPage.tsx index 34a1607..85372f8 100644 --- a/frontend/src/pages/AdminJobsPage.tsx +++ b/frontend/src/pages/AdminJobsPage.tsx @@ -126,6 +126,49 @@ function AdminJobsPage() { ? cacheStatus.pending + cacheStatus.in_progress + cacheStatus.completed + cacheStatus.failed : 0; + const completedPercent = totalJobs > 0 + ? Math.round((cacheStatus?.completed ?? 0) / totalJobs * 100) + : 0; + + // Combine all jobs into unified list with type column + type UnifiedJob = { + id: string; + type: 'pypi'; + package: string; + status: 'pending' | 'in_progress' | 'completed' | 'failed'; + depth: number; + attempts: number; + error?: string | null; + started_at?: string | null; + failed_at?: string | null; + version_constraint?: string | null; + }; + + const allJobs: UnifiedJob[] = [ + // Active tasks first + ...activeTasks.map((t): UnifiedJob => ({ + id: t.id, + type: 'pypi', + package: t.package, + status: 'in_progress', + depth: t.depth, + attempts: t.attempts, + started_at: t.started_at, + version_constraint: t.version_constraint, + })), + // Then failed tasks + ...failedTasks.map((t): UnifiedJob => ({ + id: t.id, + type: 'pypi', + package: t.package, + status: 'failed', + depth: t.depth, + attempts: t.attempts, + error: t.error, + failed_at: t.failed_at, + })), + ]; + return (
@@ -148,7 +191,33 @@ function AdminJobsPage() { {successMessage &&
{successMessage}
} {statusError &&
{statusError}
} - {/* PyPI Cache Jobs Section */} + {/* Overall Progress Bar */} + {totalJobs > 0 && ( +
+
+ + + Overall Progress + + + {cacheStatus?.completed ?? 0} / {totalJobs} completed + {(cacheStatus?.in_progress ?? 0) > 0 && ` · ${cacheStatus?.in_progress} active`} + {(cacheStatus?.failed ?? 0) > 0 && ` · ${cacheStatus?.failed} failed`} + +
+
+
+ {(cacheStatus?.in_progress ?? 0) > 0 && ( +
+ )} +
+
+ )} + + {/* Unified Jobs Section */}

@@ -157,7 +226,7 @@ function AdminJobsPage() { - PyPI Cache Jobs + All Jobs

{failedTasks.length > 0 && (
- - {/* Placeholder for future job types */} -
-
-

- - - - - - NPM Cache Jobs - Coming Soon -

-
-

NPM proxy support is planned for a future release.

-
); }