feat(frontend): show job attempt meter
This commit is contained in:
@@ -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 { FormEvent, useCallback, useEffect, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -35,6 +35,33 @@ function demoPayload(type: string) {
|
|||||||
return { sleep_seconds: 1 };
|
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 (
|
||||||
|
<div className="attempt-meter" aria-label={`${job.attempts} of ${job.max_attempts} attempts used`}>
|
||||||
|
{Array.from({ length: job.max_attempts }, (_, index) => {
|
||||||
|
const attemptNumber = index + 1;
|
||||||
|
const state = attemptState(job, attemptNumber);
|
||||||
|
return (
|
||||||
|
<span className={`attempt-square ${state}`} key={attemptNumber} title={`Attempt ${attemptNumber}: ${state}`}>
|
||||||
|
{state === "succeeded" && <Check size={12} strokeWidth={3} />}
|
||||||
|
{state === "failed" && <X size={12} strokeWidth={3} />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{/* <span className="attempt-count">
|
||||||
|
{job.attempts}/{job.max_attempts}
|
||||||
|
</span> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function JobsPage() {
|
export function JobsPage() {
|
||||||
const [jobs, setJobs] = useState<Job[]>([]);
|
const [jobs, setJobs] = useState<Job[]>([]);
|
||||||
const [totalJobs, setTotalJobs] = useState(0);
|
const [totalJobs, setTotalJobs] = useState(0);
|
||||||
@@ -162,40 +189,6 @@ export function JobsPage() {
|
|||||||
<SelectField value={statusFilter} options={statusOptions} onChange={(value) => setStatusFilter(value as JobStatus | "all")} />
|
<SelectField value={statusFilter} options={statusOptions} onChange={(value) => setStatusFilter(value as JobStatus | "all")} />
|
||||||
<input placeholder="Filter by type" value={typeFilter} onChange={(event) => setTypeFilter(event.target.value)} />
|
<input placeholder="Filter by type" value={typeFilter} onChange={(event) => setTypeFilter(event.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="pagination-bar top">
|
|
||||||
<span>
|
|
||||||
Showing {firstItem}-{lastItem} of {totalJobs}
|
|
||||||
</span>
|
|
||||||
<div className="pagination-controls">
|
|
||||||
<SelectField value={String(pageSize)} options={pageSizeOptions} onChange={(value) => setPageSize(Number(value))} />
|
|
||||||
<button className="icon-button" disabled={currentPage <= 1} type="button" aria-label="previous page" onClick={() => setCurrentPage((page) => Math.max(1, page - 1))}>
|
|
||||||
<ChevronLeft size={18} />
|
|
||||||
</button>
|
|
||||||
<label className="page-number-field">
|
|
||||||
Page
|
|
||||||
<input
|
|
||||||
max={totalPages}
|
|
||||||
min={1}
|
|
||||||
type="number"
|
|
||||||
value={currentPage}
|
|
||||||
onChange={(event) => {
|
|
||||||
const value = Number(event.target.value);
|
|
||||||
if (!Number.isNaN(value)) setCurrentPage(Math.min(totalPages, Math.max(1, value)));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span>of {totalPages}</span>
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
className="icon-button"
|
|
||||||
disabled={currentPage >= totalPages}
|
|
||||||
type="button"
|
|
||||||
aria-label="next page"
|
|
||||||
onClick={() => setCurrentPage((page) => Math.min(totalPages, page + 1))}
|
|
||||||
>
|
|
||||||
<ChevronRight size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="table-wrap">
|
<div className="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -220,7 +213,7 @@ export function JobsPage() {
|
|||||||
</td>
|
</td>
|
||||||
<td>{job.priority}</td>
|
<td>{job.priority}</td>
|
||||||
<td>
|
<td>
|
||||||
{job.attempts}/{job.max_attempts}
|
<AttemptMeter job={job} />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<DateTime value={job.available_at} />
|
<DateTime value={job.available_at} />
|
||||||
@@ -239,9 +232,10 @@ export function JobsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="pagination-bar">
|
<div className="pagination-bar">
|
||||||
<span>
|
<span>
|
||||||
Page {currentPage} of {totalPages}
|
Showing {firstItem}-{lastItem} of {totalJobs}
|
||||||
</span>
|
</span>
|
||||||
<div className="pagination-controls">
|
<div className="pagination-controls">
|
||||||
|
<SelectField value={String(pageSize)} options={pageSizeOptions} onChange={(value) => setPageSize(Number(value))} />
|
||||||
<button className="secondary-button" disabled={currentPage <= 1} type="button" onClick={() => setCurrentPage((page) => Math.max(1, page - 1))}>
|
<button className="secondary-button" disabled={currentPage <= 1} type="button" onClick={() => setCurrentPage((page) => Math.max(1, page - 1))}>
|
||||||
<ChevronLeft size={16} /> Previous
|
<ChevronLeft size={16} /> Previous
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1134,15 +1134,6 @@ label {
|
|||||||
padding-top: 12px;
|
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 {
|
.pagination-controls {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1168,6 +1159,53 @@ label {
|
|||||||
width: 74px;
|
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 {
|
.event-feed-meta {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
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) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
|
|||||||
Reference in New Issue
Block a user