Allow multi selecting jobs and deleting them in one go

This commit is contained in:
Jaret Burkett
2026-06-12 08:01:56 -06:00
parent 55ce6570f2
commit 9cf34f945c
2 changed files with 126 additions and 2 deletions

View File

@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import useJobsList from '@/hooks/useJobsList';
import Link from 'next/link';
import UniversalTable, { TableColumn } from '@/components/UniversalTable';
@@ -10,6 +10,9 @@ import classNames from 'classnames';
import { startQueue, stopQueue } from '@/utils/queue';
import { CgSpinner } from 'react-icons/cg';
import useGPUInfo from '@/hooks/useGPUInfo';
import { openConfirm } from '@/components/ConfirmModal';
import { deleteJob, stopJob } from '@/utils/jobs';
import { Trash2 } from 'lucide-react';
interface JobsTableProps {
autoStartQueue?: boolean;
@@ -21,13 +24,103 @@ export default function JobsTable({ onlyActive = false, job_type = null }: JobsT
const { jobs, status, refreshJobs } = useJobsList({ onlyActive, reloadInterval: 5000, job_type });
const { queues, status: queueStatus, refreshQueues } = useQueueList();
const { gpuList, isGPUInfoLoaded } = useGPUInfo();
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [deleteProgress, setDeleteProgress] = useState<{ done: number; total: number } | null>(null);
const refresh = () => {
refreshJobs();
refreshQueues();
};
const isDeleting = deleteProgress !== null;
const allSelected = jobs.length > 0 && jobs.every(job => selectedIds.has(job.id));
const toggleRow = (id: string) => {
setSelectedIds(prev => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
const toggleAll = () => {
setSelectedIds(allSelected ? new Set() : new Set(jobs.map(job => job.id)));
};
const onMassDelete = () => {
const jobsToDelete = jobs.filter(job => selectedIds.has(job.id));
if (jobsToDelete.length === 0) return;
const runningCount = jobsToDelete.filter(job => job.status === 'running').length;
let message = `Are you sure you want to delete ${jobsToDelete.length} job${
jobsToDelete.length === 1 ? '' : 's'
}? This will also permanently remove them from your disk.`;
if (runningCount > 0) {
message += ` WARNING: ${runningCount} of them ${
runningCount === 1 ? 'is' : 'are'
} currently running and will be stopped first.`;
}
openConfirm({
title: 'Delete Jobs',
message: message,
type: 'warning',
confirmText: 'Delete',
onConfirm: async () => {
setDeleteProgress({ done: 0, total: jobsToDelete.length });
for (let i = 0; i < jobsToDelete.length; i++) {
const job = jobsToDelete[i];
try {
if (job.status === 'running') {
try {
await stopJob(job.id);
} catch (e) {
console.error('Error stopping job before deleting:', e);
}
}
await deleteJob(job.id);
setSelectedIds(prev => {
const next = new Set(prev);
next.delete(job.id);
return next;
});
} catch (e) {
console.error('Error deleting job:', job.name, e);
}
setDeleteProgress({ done: i + 1, total: jobsToDelete.length });
refreshJobs();
}
setDeleteProgress(null);
refresh();
},
});
};
const columns: TableColumn[] = [
{
title: (
<input
type="checkbox"
checked={allSelected}
onChange={toggleAll}
disabled={isDeleting}
className="cursor-pointer accent-blue-500"
/>
),
key: 'select',
className: 'w-8',
render: row => (
<input
type="checkbox"
checked={selectedIds.has(row.id)}
onChange={() => toggleRow(row.id)}
disabled={isDeleting}
className="cursor-pointer accent-blue-500"
/>
),
},
{
title: 'Name',
key: 'name',
@@ -151,6 +244,37 @@ export default function JobsTable({ onlyActive = false, job_type = null }: JobsT
return (
<div>
{(selectedIds.size > 0 || isDeleting) && (
<div className="fixed top-16 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 px-4 py-2 bg-gray-800 rounded-lg border border-gray-700 shadow-lg">
{isDeleting ? (
<>
<CgSpinner className="inline animate-spin text-red-400" />
<span className="text-sm text-gray-300">
Deleting {deleteProgress.done} / {deleteProgress.total}...
</span>
</>
) : (
<>
<span className="text-sm text-gray-300 flex-1">
{selectedIds.size} job{selectedIds.size === 1 ? '' : 's'} selected
</span>
<button
onClick={() => setSelectedIds(new Set())}
className="text-xs text-gray-300 bg-gray-700 hover:bg-gray-600 px-2 py-1 rounded"
>
Clear
</button>
<button
onClick={onMassDelete}
className="text-xs text-white bg-red-600 hover:bg-red-700 px-2 py-1 rounded flex items-center gap-1"
>
<Trash2 className="w-3.5 h-3.5" />
Delete Selected
</button>
</>
)}
</div>
)}
{Object.keys(jobsDict)
.sort()
.filter(key => key !== 'Idle')

View File

@@ -2,7 +2,7 @@ import Loading from './Loading';
import classNames from 'classnames';
export interface TableColumn {
title: string;
title: React.ReactNode;
key: string;
render?: (row: any) => React.ReactNode;
className?: string;