diff --git a/frontend/src/pages/JobsPage.tsx b/frontend/src/pages/JobsPage.tsx index cc49702..1c30a9c 100644 --- a/frontend/src/pages/JobsPage.tsx +++ b/frontend/src/pages/JobsPage.tsx @@ -1,4 +1,4 @@ -import { ChevronLeft, ChevronRight, Eye, Plus, RotateCcw } from "lucide-react"; +import { Check, ChevronLeft, ChevronRight, Eye, Plus, RotateCcw, X } from "lucide-react"; import { FormEvent, useCallback, useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { toast } from "sonner"; @@ -35,6 +35,33 @@ function demoPayload(type: string) { return { sleep_seconds: 1 }; } +function attemptState(job: Job, attemptNumber: number) { + if (attemptNumber > job.attempts) return "empty"; + if (job.status === "succeeded") return attemptNumber === job.attempts ? "succeeded" : "failed"; + if (job.status === "running" && attemptNumber === job.attempts) return "running"; + return "failed"; +} + +function AttemptMeter({ job }: { job: Job }) { + return ( +
+ {Array.from({ length: job.max_attempts }, (_, index) => { + const attemptNumber = index + 1; + const state = attemptState(job, attemptNumber); + return ( + + {state === "succeeded" && } + {state === "failed" && } + + ); + })} + {/* + {job.attempts}/{job.max_attempts} + */} +
+ ); +} + export function JobsPage() { const [jobs, setJobs] = useState([]); const [totalJobs, setTotalJobs] = useState(0); @@ -162,40 +189,6 @@ export function JobsPage() { setStatusFilter(value as JobStatus | "all")} /> setTypeFilter(event.target.value)} /> -
- - Showing {firstItem}-{lastItem} of {totalJobs} - -
- setPageSize(Number(value))} /> - - - -
-
@@ -220,7 +213,7 @@ export function JobsPage() {
{job.priority} - {job.attempts}/{job.max_attempts} + @@ -239,9 +232,10 @@ export function JobsPage() {
- Page {currentPage} of {totalPages} + Showing {firstItem}-{lastItem} of {totalJobs}
+ setPageSize(Number(value))} /> diff --git a/frontend/src/styles.css b/frontend/src/styles.css index d19cb85..cf96f74 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1134,15 +1134,6 @@ label { padding-top: 12px; } -.pagination-bar.top { - border-bottom: 1px solid var(--border); - border-top: 0; - margin-bottom: 12px; - margin-top: 0; - padding-bottom: 12px; - padding-top: 0; -} - .pagination-controls { align-items: center; display: flex; @@ -1168,6 +1159,53 @@ label { width: 74px; } +.attempt-meter { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 5px; + min-width: 128px; +} + +.attempt-square { + align-items: center; + background: var(--panel-raised); + border: 1px solid var(--border); + border-radius: 5px; + color: var(--muted); + display: inline-flex; + height: 22px; + justify-content: center; + width: 22px; +} + +.attempt-square.succeeded { + background: var(--green-soft); + border-color: color-mix(in srgb, var(--green) 42%, var(--border)); + color: var(--green); +} + +.attempt-square.failed { + background: var(--red-soft); + border-color: color-mix(in srgb, var(--red) 42%, var(--border)); + color: var(--red); +} + +.attempt-square.running { + animation: attempt-running 960ms ease-in-out infinite alternate; + background: var(--orange-soft); + border-color: color-mix(in srgb, var(--orange) 42%, var(--border)); + color: var(--orange); +} + +.attempt-count { + color: var(--muted); + font-size: 12px; + font-weight: 900; + margin-left: 3px; + white-space: nowrap; +} + .event-feed-meta { align-items: center; display: flex; @@ -1746,6 +1784,16 @@ code { } } +@keyframes attempt-running { + from { + box-shadow: 0 0 0 0 color-mix(in srgb, var(--orange) 18%, transparent); + } + + to { + box-shadow: 0 0 0 4px color-mix(in srgb, var(--orange) 4%, transparent); + } +} + @media (prefers-reduced-motion: reduce) { *, *::before,