diff --git a/ui/src/app/api/jobs/[jobID]/delete/route.ts b/ui/src/app/api/jobs/[jobID]/delete/route.ts
new file mode 100644
index 00000000..618e33f4
--- /dev/null
+++ b/ui/src/app/api/jobs/[jobID]/delete/route.ts
@@ -0,0 +1,32 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { PrismaClient } from '@prisma/client';
+import { getTrainingFolder } from '@/server/settings';
+import path from 'path';
+import fs from 'fs';
+
+const prisma = new PrismaClient();
+
+export async function GET(request: NextRequest, { params }: { params: { jobID: string } }) {
+ const { jobID } = await params;
+
+ const job = await prisma.job.findUnique({
+ where: { id: jobID },
+ });
+
+ if (!job) {
+ return NextResponse.json({ error: 'Job not found' }, { status: 404 });
+ }
+
+ const trainingRoot = await getTrainingFolder();
+ const trainingFolder = path.join(trainingRoot, job.name);
+
+ if (fs.existsSync(trainingFolder)) {
+ fs.rmdirSync(trainingFolder, { recursive: true });
+ }
+
+ await prisma.job.delete({
+ where: { id: jobID },
+ });
+
+ return NextResponse.json(job);
+}
diff --git a/ui/src/components/JobsTable.tsx b/ui/src/components/JobsTable.tsx
index 7949e732..5f3b4dc6 100644
--- a/ui/src/components/JobsTable.tsx
+++ b/ui/src/components/JobsTable.tsx
@@ -2,6 +2,10 @@ import useJobsList from '@/hooks/useJobsList';
import Link from 'next/link';
import UniversalTable, { TableColumn } from '@/components/UniversalTable';
import { JobConfig } from '@/types';
+import { Eye, Trash2, Pen, Play, Pause } from 'lucide-react';
+import { Button } from '@headlessui/react';
+import { openConfirm } from '@/components/ConfirmModal';
+import { startJob, stopJob, deleteJob, getAvaliableJobActions } from '@/utils/jobs';
interface JobsTableProps {}
@@ -62,6 +66,78 @@ export default function JobsTable(props: JobsTableProps) {
key: 'info',
className: 'truncate max-w-xs',
},
+ {
+ title: 'Actions',
+ key: 'actions',
+ className: 'text-right',
+ render: row => {
+ const { canDelete, canEdit, canStop, canStart } = getAvaliableJobActions(row);
+ return (
+
+ {canStart && (
+
+ )}
+ {canStop && (
+
+ )}
+
+
+
+ {canEdit && (
+
+
+
+ )}
+ {canDelete && (
+
+ )}
+
+ );
+ },
+ },
];
return ;
diff --git a/ui/src/utils/jobs.ts b/ui/src/utils/jobs.ts
index 883953a4..1e30da1d 100644
--- a/ui/src/utils/jobs.ts
+++ b/ui/src/utils/jobs.ts
@@ -1,3 +1,6 @@
+import { JobConfig } from '@/types';
+import { Job } from '@prisma/client';
+
export const startJob = (jobID: string) => {
return new Promise((resolve, reject) => {
fetch(`/api/jobs/${jobID}/start`)
@@ -27,3 +30,36 @@ export const stopJob = (jobID: string) => {
});
});
};
+
+export const deleteJob = (jobID: string) => {
+ return new Promise((resolve, reject) => {
+ fetch(`/api/jobs/${jobID}/delete`)
+ .then(res => res.json())
+ .then(data => {
+ console.log('Job deleted:', data);
+ resolve();
+ })
+ .catch(error => {
+ console.error('Error deleting job:', error);
+ reject(error);
+ });
+ });
+};
+
+export const getJobConfig = (job: Job) => {
+ return JSON.parse(job.job_config) as JobConfig;
+};
+
+export const getAvaliableJobActions = (job: Job) => {
+ const jobConfig = getJobConfig(job);
+ const isStopping = job.stop && job.status === 'running';
+ const canDelete = ['completed', 'stopped', 'error'].includes(job.status) && !isStopping;
+ const canEdit = ['completed', 'stopped', 'error'].includes(job.status) && !isStopping;
+ const canStop = job.status === 'running' && !isStopping;
+ let canStart = ['stopped', 'error'].includes(job.status) && !isStopping;
+ // can resume if more steps were added
+ if (job.status === 'completed' && jobConfig.config.process[0].train.steps > job.step && !isStopping) {
+ canStart = true;
+ }
+ return { canDelete, canEdit, canStop, canStart };
+};