feat(jobs): paginate jobs list
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Eye, Plus, RotateCcw } from "lucide-react";
|
||||
import { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { ChevronLeft, ChevronRight, Eye, Plus, RotateCcw } from "lucide-react";
|
||||
import { FormEvent, useCallback, useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -21,6 +21,12 @@ const jobTypeOptions: SelectOption[] = [
|
||||
{ value: "demo.flaky", label: "Flaky retry" }
|
||||
];
|
||||
|
||||
const pageSizeOptions: SelectOption[] = [
|
||||
{ value: "10", label: "10 / page" },
|
||||
{ value: "25", label: "25 / page" },
|
||||
{ value: "50", label: "50 / page" }
|
||||
];
|
||||
|
||||
function demoPayload(type: string) {
|
||||
if (type === "demo.slow") return { sleep_seconds: 8 };
|
||||
if (type === "demo.timeout") return { sleep_seconds: 45 };
|
||||
@@ -31,9 +37,12 @@ function demoPayload(type: string) {
|
||||
|
||||
export function JobsPage() {
|
||||
const [jobs, setJobs] = useState<Job[]>([]);
|
||||
const [totalJobs, setTotalJobs] = useState(0);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [statusFilter, setStatusFilter] = useState<JobStatus | "all">("all");
|
||||
const [typeFilter, setTypeFilter] = useState("");
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [jobType, setJobType] = useState("demo.success");
|
||||
const [payload, setPayload] = useState(JSON.stringify(demoPayload("demo.success"), null, 2));
|
||||
const [priority, setPriority] = useState(50);
|
||||
@@ -42,8 +51,15 @@ export function JobsPage() {
|
||||
const [availableAt, setAvailableAt] = useState("");
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setJobs(await api.listJobs());
|
||||
}, []);
|
||||
const page = await api.listJobsPage({
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
status: statusFilter === "all" ? undefined : statusFilter,
|
||||
type: typeFilter
|
||||
});
|
||||
setJobs(page.results);
|
||||
setTotalJobs(page.count);
|
||||
}, [currentPage, pageSize, statusFilter, typeFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh().catch((caught) => toast.error(caught instanceof Error ? caught.message : String(caught)));
|
||||
@@ -51,13 +67,17 @@ export function JobsPage() {
|
||||
return () => window.clearInterval(id);
|
||||
}, [refresh]);
|
||||
|
||||
const filteredJobs = useMemo(() => {
|
||||
return jobs.filter((job) => {
|
||||
if (statusFilter !== "all" && job.status !== statusFilter) return false;
|
||||
if (typeFilter && !job.type.toLowerCase().includes(typeFilter.toLowerCase())) return false;
|
||||
return true;
|
||||
});
|
||||
}, [jobs, statusFilter, typeFilter]);
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [pageSize, statusFilter, typeFilter]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(totalJobs / pageSize));
|
||||
const firstItem = totalJobs ? (currentPage - 1) * pageSize + 1 : 0;
|
||||
const lastItem = Math.min(totalJobs, currentPage * pageSize);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPage > totalPages) setCurrentPage(totalPages);
|
||||
}, [currentPage, totalPages]);
|
||||
|
||||
function setType(nextType: string) {
|
||||
setJobType(nextType);
|
||||
@@ -142,6 +162,40 @@ export function JobsPage() {
|
||||
<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)} />
|
||||
</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">
|
||||
<table>
|
||||
<thead>
|
||||
@@ -156,7 +210,7 @@ export function JobsPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredJobs.map((job) => (
|
||||
{jobs.map((job) => (
|
||||
<tr key={job.id}>
|
||||
<td>
|
||||
<StatusBadge status={job.status} />
|
||||
@@ -181,7 +235,25 @@ export function JobsPage() {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{!filteredJobs.length && <EmptyState title="No jobs match the current filter" />}
|
||||
{!jobs.length && <EmptyState title="No jobs match the current filter" />}
|
||||
</div>
|
||||
<div className="pagination-bar">
|
||||
<span>
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
<div className="pagination-controls">
|
||||
<button className="secondary-button" disabled={currentPage <= 1} type="button" onClick={() => setCurrentPage((page) => Math.max(1, page - 1))}>
|
||||
<ChevronLeft size={16} /> Previous
|
||||
</button>
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={currentPage >= totalPages}
|
||||
type="button"
|
||||
onClick={() => setCurrentPage((page) => Math.min(totalPages, page + 1))}
|
||||
>
|
||||
Next <ChevronRight size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user