diff --git a/src/components/layout/activity-panel.tsx b/src/components/layout/activity-panel.tsx index 37a9d6d05..a63982398 100644 --- a/src/components/layout/activity-panel.tsx +++ b/src/components/layout/activity-panel.tsx @@ -3,7 +3,7 @@ import { ChevronUp, ChevronDown, Loader2, CheckCircle2, AlertCircle, FileText, Users, Lightbulb, BookOpen, GitMerge, BarChart3, HelpCircle, Layout, RotateCcw, X, Clock, TrendingUp, Target, Pause, Play, - ArrowUp, ArrowDown, + ArrowUp, ArrowDown, Trash2, } from "lucide-react" import { useTranslation } from "react-i18next" import { useAppDialog } from "@/stores/app-dialog-store" @@ -20,6 +20,10 @@ import { cancelTask, cancelTasks, cancelAllTasks, + removeTask, + removeTasks, + hasActiveRun, + retainQueuedSelection, movePendingTask, pauseProcessing, resumeProcessing, @@ -154,6 +158,26 @@ export function ActivityPanel() { }) }, [project, selectedTaskIds]) + const syncAfterRemoval = useCallback(() => { + const remaining = [...getQueue()] + setSelectedTaskIds((current) => retainQueuedSelection(current, remaining)) + setQueueTasks(remaining) + }, []) + + const handleRemoveSelected = useCallback(async () => { + if (!project) return + const removable = queueTasks.filter( + (task) => selectedTaskIds.has(task.id) && isRemovable(task), + ) + if (removable.length === 0) return + if (!(await appDialog.confirm({ + message: t("activity.removeSelectedConfirm", { count: removable.length }), + variant: "destructive", + }))) return + await removeTasks(removable.map((task) => task.id)) + syncAfterRemoval() + }, [appDialog, project, queueTasks, selectedTaskIds, syncAfterRemoval, t]) + const handleMoveTask = useCallback((taskId: string, direction: "up" | "down") => { void movePendingTask(taskId, direction).then(() => setQueueTasks([...getQueue()])) }, []) @@ -172,6 +196,11 @@ export function ActivityPanel() { cancelTask(taskId) }, [project]) + const handleIngestRemove = useCallback((taskId: string) => { + if (!project) return + void removeTask(taskId).then(syncAfterRemoval) + }, [project, syncAfterRemoval]) + const handleCancelAll = useCallback(async () => { if (!project) return const activeCount = queueSummary.pending + queueSummary.processing @@ -346,6 +375,12 @@ export function ActivityPanel() { > {t("activity.cancelSelected")} + )} @@ -435,7 +470,7 @@ export function ActivityPanel() { {/* Queue tasks */} {visibleQueueTasks.map((task) => ( - + ))} {orderedQueueTasks.length > visibleQueueTasks.length && (
@@ -471,12 +506,19 @@ export function ActivityPanel() { ) } -function QueueRow({ task, selected, onSelect, onRetry, onCancel, onMove }: { +/** Stopped, and its run has already released the worker slot. */ +function isRemovable(task: IngestTask): boolean { + return (task.status === "failed" || task.status === "cancelled") && + !hasActiveRun(task.id) +} + +function QueueRow({ task, selected, onSelect, onRetry, onCancel, onRemove, onMove }: { task: IngestTask selected: boolean onSelect: (id: string) => void onRetry: (id: string) => void onCancel: (id: string) => void + onRemove: (id: string) => void onMove: (id: string, direction: "up" | "down") => void }) { const { t } = useTranslation() @@ -517,6 +559,15 @@ function QueueRow({ task, selected, onSelect, onRetry, onCancel, onMove }: { )} + {isRemovable(task) && ( + + )} {task.status === "pending" && ( <>