From 8227cabd17780db98b656ab95cdf1e10e92ca86e Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Mon, 15 Dec 2025 16:35:18 +0000 Subject: [PATCH] batch processing --- orchestrator/package-lock.json | 44 ++++++ orchestrator/package.json | 1 + .../src/client/components/JobCard.tsx | 6 +- .../src/client/components/JobList.tsx | 136 +++++++++++++++--- .../src/client/components/JobTable.tsx | 45 +++++- orchestrator/src/components/ui/checkbox.tsx | 27 ++++ 6 files changed, 236 insertions(+), 23 deletions(-) create mode 100644 orchestrator/src/components/ui/checkbox.tsx diff --git a/orchestrator/package-lock.json b/orchestrator/package-lock.json index 4e2171b..bc7de2f 100644 --- a/orchestrator/package-lock.json +++ b/orchestrator/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-separator": "^1.1.8", @@ -1392,6 +1393,35 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -2068,6 +2098,20 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", diff --git a/orchestrator/package.json b/orchestrator/package.json index 7184a32..a0d7e35 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -18,6 +18,7 @@ "pipeline:run": "tsx src/server/pipeline/run.ts" }, "dependencies": { + "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-progress": "^1.1.8", diff --git a/orchestrator/src/client/components/JobCard.tsx b/orchestrator/src/client/components/JobCard.tsx index 21aa1e1..9642363 100644 --- a/orchestrator/src/client/components/JobCard.tsx +++ b/orchestrator/src/client/components/JobCard.tsx @@ -25,9 +25,9 @@ import { StatusBadge } from "./StatusBadge"; interface JobCardProps { job: Job; - onApply: (id: string) => void; - onReject: (id: string) => void; - onProcess: (id: string) => void; + onApply: (id: string) => void | Promise; + onReject: (id: string) => void | Promise; + onProcess: (id: string) => void | Promise; isProcessing: boolean; } diff --git a/orchestrator/src/client/components/JobList.tsx b/orchestrator/src/client/components/JobList.tsx index 34366bb..e288713 100644 --- a/orchestrator/src/client/components/JobList.tsx +++ b/orchestrator/src/client/components/JobList.tsx @@ -4,6 +4,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { ArrowUpDown, LayoutGrid, Search, Table2 } from "lucide-react"; +import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; @@ -26,9 +27,9 @@ import { JobTable, type JobSort } from "./JobTable"; interface JobListProps { jobs: Job[]; - onApply: (id: string) => void; - onReject: (id: string) => void; - onProcess: (id: string) => void; + onApply: (id: string) => void | Promise; + onReject: (id: string) => void | Promise; + onProcess: (id: string) => void | Promise; processingJobId: string | null; } @@ -179,6 +180,8 @@ export const JobList: React.FC = ({ const [activeTab, setActiveTab] = useState("ready"); const [searchQuery, setSearchQuery] = useState(""); const [sort, setSort] = useState(DEFAULT_SORT); + const [selectedJobIds, setSelectedJobIds] = useState>(() => new Set()); + const [batchAction, setBatchAction] = useState(null); const [viewMode, setViewMode] = useState(() => { try { const raw = localStorage.getItem(JOB_LIST_VIEW_STORAGE_KEY); @@ -197,6 +200,10 @@ export const JobList: React.FC = ({ } }, [viewMode]); + useEffect(() => { + setSelectedJobIds(new Set()); + }, [activeTab, viewMode]); + const counts = useMemo(() => { const byTab: Record = { ready: 0, @@ -242,12 +249,63 @@ export const JobList: React.FC = ({ return map; }, [jobsForTab, searchQuery, sort]); + const activeTabJobs = visibleJobsForTab.get(activeTab) ?? []; + + useEffect(() => { + setSelectedJobIds((current) => { + const visibleIds = new Set(activeTabJobs.map((job) => job.id)); + const next = new Set(); + for (const id of current) { + if (visibleIds.has(id)) next.add(id); + } + return next.size === current.size ? current : next; + }); + }, [activeTabJobs]); + const activeResultsCount = visibleJobsForTab.get(activeTab)?.length ?? 0; const hasActiveFilters = searchQuery.trim().length > 0 || sort.key !== DEFAULT_SORT.key || sort.direction !== DEFAULT_SORT.direction; + const selectedJobs = useMemo(() => { + if (selectedJobIds.size === 0) return []; + return activeTabJobs.filter((job) => selectedJobIds.has(job.id)); + }, [activeTabJobs, selectedJobIds]); + + const selectedCount = selectedJobIds.size; + + const runBatch = async (action: "process" | "reject" | "apply") => { + if (selectedJobs.length === 0) return; + + const eligible = selectedJobs.filter((job) => { + if (action === "process") return job.status === "discovered"; + if (action === "apply") return job.status === "ready"; + return job.status === "discovered" || job.status === "ready"; + }); + + const skipped = selectedJobs.length - eligible.length; + if (eligible.length === 0) { + toast.message("No eligible jobs selected"); + return; + } + + setBatchAction(action); + try { + for (const job of eligible) { + if (action === "process") await Promise.resolve(onProcess(job.id)); + if (action === "apply") await Promise.resolve(onApply(job.id)); + if (action === "reject") await Promise.resolve(onReject(job.id)); + } + + setSelectedJobIds(new Set()); + const actionLabel = action === "process" ? "Processed" : action === "apply" ? "Applied" : "Skipped"; + toast.success(`${actionLabel} ${eligible.length} jobs`, skipped > 0 ? { description: `Skipped ${skipped} ineligible.` } : undefined); + } finally { + setBatchAction(null); + } + }; + return ( = ({ ) : ( <> {viewMode === "table" ? ( - - - - - +
+ {tab.id === activeTab && selectedCount > 0 && ( +
+
+ {selectedCount}{" "} + selected +
+
+ + + + +
+
+ )} + + + + + + +
) : (
{filteredJobs.map((job) => ( diff --git a/orchestrator/src/client/components/JobTable.tsx b/orchestrator/src/client/components/JobTable.tsx index 4d81f2d..a924a0f 100644 --- a/orchestrator/src/client/components/JobTable.tsx +++ b/orchestrator/src/client/components/JobTable.tsx @@ -17,6 +17,7 @@ import { import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { DropdownMenu, DropdownMenuContent, @@ -49,9 +50,11 @@ export interface JobTableProps { jobs: Job[]; sort: JobSort; onSortChange: (sort: JobSort) => void; - onApply: (id: string) => void; - onReject: (id: string) => void; - onProcess: (id: string) => void; + selectedJobIds: Set; + onSelectedJobIdsChange: (ids: Set) => void; + onApply: (id: string) => void | Promise; + onReject: (id: string) => void | Promise; + onProcess: (id: string) => void | Promise; processingJobId: string | null; } @@ -123,15 +126,36 @@ export const JobTable: React.FC = ({ jobs, sort, onSortChange, + selectedJobIds, + onSelectedJobIdsChange, onApply, onReject, onProcess, processingJobId, }) => { + const selectedCount = jobs.reduce((count, job) => count + (selectedJobIds.has(job.id) ? 1 : 0), 0); + const allSelected = jobs.length > 0 && selectedCount === jobs.length; + const someSelected = selectedCount > 0 && selectedCount < jobs.length; + return ( + + { + const next = new Set(selectedJobIds); + if (checked) { + for (const job of jobs) next.add(job.id); + } else { + for (const job of jobs) next.delete(job.id); + } + onSelectedJobIdsChange(next); + }} + /> + @@ -178,9 +202,22 @@ export const JobTable: React.FC = ({ const canProcess = job.status === "discovered"; const canReject = ["discovered", "ready"].includes(job.status); const isProcessing = processingJobId === job.id; + const isSelected = selectedJobIds.has(job.id); return ( - + + + { + const next = new Set(selectedJobIds); + if (checked) next.add(job.id); + else next.delete(job.id); + onSelectedJobIdsChange(next); + }} + /> +