feat: add type, status, and library filters to jobs list
Filter jobs by type, status, or library with dropdowns above the table. Shows filtered count and a clear button when filters are active. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,9 +46,19 @@ function formatDuration(start: string, end: string | null): string {
|
|||||||
export function JobsList({ initialJobs, libraries, highlightJobId }: JobsListProps) {
|
export function JobsList({ initialJobs, libraries, highlightJobId }: JobsListProps) {
|
||||||
const { t, locale } = useTranslation();
|
const { t, locale } = useTranslation();
|
||||||
const [jobs, setJobs] = useState(initialJobs);
|
const [jobs, setJobs] = useState(initialJobs);
|
||||||
|
const [filterType, setFilterType] = useState<string>("");
|
||||||
|
const [filterStatus, setFilterStatus] = useState<string>("");
|
||||||
|
const [filterLibrary, setFilterLibrary] = useState<string>("");
|
||||||
|
|
||||||
|
const filteredJobs = jobs.filter((job) => {
|
||||||
|
if (filterType && job.type !== filterType) return false;
|
||||||
|
if (filterStatus && job.status !== filterStatus) return false;
|
||||||
|
if (filterLibrary && job.library_id !== filterLibrary) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const initialPage = highlightJobId
|
const initialPage = highlightJobId
|
||||||
? Math.ceil((initialJobs.findIndex(j => j.id === highlightJobId) + 1) / PAGE_SIZE) || 1
|
? Math.ceil((filteredJobs.findIndex(j => j.id === highlightJobId) + 1) / PAGE_SIZE) || 1
|
||||||
: 1;
|
: 1;
|
||||||
const [currentPage, setCurrentPage] = useState(initialPage);
|
const [currentPage, setCurrentPage] = useState(initialPage);
|
||||||
|
|
||||||
@@ -65,9 +75,14 @@ export function JobsList({ initialJobs, libraries, highlightJobId }: JobsListPro
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const totalPages = Math.ceil(jobs.length / PAGE_SIZE);
|
// Derive unique types, statuses, libraries for filter options
|
||||||
|
const jobTypes = [...new Set(jobs.map(j => j.type))].sort();
|
||||||
|
const jobStatuses = [...new Set(jobs.map(j => j.status))].sort();
|
||||||
|
const jobLibraryIds = [...new Set(jobs.map(j => j.library_id).filter(Boolean))] as string[];
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(filteredJobs.length / PAGE_SIZE);
|
||||||
const pageStart = (currentPage - 1) * PAGE_SIZE;
|
const pageStart = (currentPage - 1) * PAGE_SIZE;
|
||||||
const visibleJobs = jobs.slice(pageStart, pageStart + PAGE_SIZE);
|
const visibleJobs = filteredJobs.slice(pageStart, pageStart + PAGE_SIZE);
|
||||||
|
|
||||||
// Refresh jobs list via SSE
|
// Refresh jobs list via SSE
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -128,6 +143,49 @@ export function JobsList({ initialJobs, libraries, highlightJobId }: JobsListPro
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-card rounded-xl shadow-sm border border-border/60 overflow-hidden">
|
<div className="bg-card rounded-xl shadow-sm border border-border/60 overflow-hidden">
|
||||||
|
<div className="flex items-center gap-3 px-4 py-3 border-b border-border/60 bg-muted/30 flex-wrap">
|
||||||
|
<select
|
||||||
|
value={filterType}
|
||||||
|
onChange={(e) => { setFilterType(e.target.value); setCurrentPage(1); }}
|
||||||
|
className="text-xs border border-border rounded-md px-2.5 py-1.5 bg-background focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
>
|
||||||
|
<option value="">{t("jobsList.allTypes")}</option>
|
||||||
|
{jobTypes.map((type) => (
|
||||||
|
<option key={type} value={type}>{t(`jobType.${type}` as never) || type}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
value={filterStatus}
|
||||||
|
onChange={(e) => { setFilterStatus(e.target.value); setCurrentPage(1); }}
|
||||||
|
className="text-xs border border-border rounded-md px-2.5 py-1.5 bg-background focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
>
|
||||||
|
<option value="">{t("jobsList.allStatuses")}</option>
|
||||||
|
{jobStatuses.map((status) => (
|
||||||
|
<option key={status} value={status}>{status}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
value={filterLibrary}
|
||||||
|
onChange={(e) => { setFilterLibrary(e.target.value); setCurrentPage(1); }}
|
||||||
|
className="text-xs border border-border rounded-md px-2.5 py-1.5 bg-background focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
>
|
||||||
|
<option value="">{t("jobsList.allLibraries")}</option>
|
||||||
|
{jobLibraryIds.map((id) => (
|
||||||
|
<option key={id} value={id}>{libraries.get(id) || id}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{(filterType || filterStatus || filterLibrary) && (
|
||||||
|
<button
|
||||||
|
onClick={() => { setFilterType(""); setFilterStatus(""); setFilterLibrary(""); setCurrentPage(1); }}
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
{t("common.clear")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<span className="text-xs text-muted-foreground ml-auto">
|
||||||
|
{filteredJobs.length} / {jobs.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -163,8 +221,8 @@ export function JobsList({ initialJobs, libraries, highlightJobId }: JobsListPro
|
|||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{t("pagination.range", {
|
{t("pagination.range", {
|
||||||
start: pageStart + 1,
|
start: pageStart + 1,
|
||||||
end: Math.min(pageStart + PAGE_SIZE, jobs.length),
|
end: Math.min(pageStart + PAGE_SIZE, filteredJobs.length),
|
||||||
total: jobs.length,
|
total: filteredJobs.length,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
|||||||
@@ -282,6 +282,9 @@ const en: Record<TranslationKey, string> = {
|
|||||||
"jobsList.duration": "Duration",
|
"jobsList.duration": "Duration",
|
||||||
"jobsList.created": "Created",
|
"jobsList.created": "Created",
|
||||||
"jobsList.actions": "Actions",
|
"jobsList.actions": "Actions",
|
||||||
|
"jobsList.allTypes": "All types",
|
||||||
|
"jobsList.allStatuses": "All statuses",
|
||||||
|
"jobsList.allLibraries": "All libraries",
|
||||||
|
|
||||||
// Job row
|
// Job row
|
||||||
"jobRow.showProgress": "Show progress",
|
"jobRow.showProgress": "Show progress",
|
||||||
|
|||||||
@@ -280,6 +280,9 @@ const fr = {
|
|||||||
"jobsList.duration": "Durée",
|
"jobsList.duration": "Durée",
|
||||||
"jobsList.created": "Créé",
|
"jobsList.created": "Créé",
|
||||||
"jobsList.actions": "Actions",
|
"jobsList.actions": "Actions",
|
||||||
|
"jobsList.allTypes": "Tous les types",
|
||||||
|
"jobsList.allStatuses": "Tous les statuts",
|
||||||
|
"jobsList.allLibraries": "Toutes les bibliothèques",
|
||||||
|
|
||||||
// Job row
|
// Job row
|
||||||
"jobRow.showProgress": "Afficher la progression",
|
"jobRow.showProgress": "Afficher la progression",
|
||||||
|
|||||||
Reference in New Issue
Block a user