diff --git a/ui/src/components/JobsTable.tsx b/ui/src/components/JobsTable.tsx
index 0986fcd1..7949e732 100644
--- a/ui/src/components/JobsTable.tsx
+++ b/ui/src/components/JobsTable.tsx
@@ -1,7 +1,7 @@
import useJobsList from '@/hooks/useJobsList';
-import Loading from './Loading';
-import { JobConfig } from '@/types';
import Link from 'next/link';
+import UniversalTable, { TableColumn } from '@/components/UniversalTable';
+import { JobConfig } from '@/types';
interface JobsTableProps {}
@@ -9,75 +9,60 @@ export default function JobsTable(props: JobsTableProps) {
const { jobs, status, refreshJobs } = useJobsList();
const isLoading = status === 'loading';
- return (
-
- {isLoading ? (
-
-
-
- ) : jobs.length === 0 ? (
-
-
No jobs available
-
-
- ) : (
-
-
-
-
- | Name |
- Steps |
- GPU |
- Status |
- Info |
-
-
-
- {jobs?.map((job, index) => {
- const jobConfig: JobConfig = JSON.parse(job.job_config);
- const totalSteps = jobConfig.config.process[0].train.steps;
+ const columns: TableColumn[] = [
+ {
+ title: 'Name',
+ key: 'name',
+ render: row => (
+
+ {row.name}
+
+ ),
+ },
+ {
+ title: 'Steps',
+ key: 'steps',
+ render: row => {
+ const jobConfig: JobConfig = JSON.parse(row.job_config);
+ const totalSteps = jobConfig.config.process[0].train.steps;
- // Style for alternating rows
- const rowClass = index % 2 === 0 ? 'bg-gray-900' : 'bg-gray-800';
+ return (
+
+
+ {row.step} / {totalSteps}
+
+
+
+ );
+ },
+ },
+ {
+ title: 'GPU',
+ key: 'gpu_ids',
+ },
+ {
+ title: 'Status',
+ key: 'status',
+ render: row => {
+ let statusClass = 'text-gray-400';
+ if (row.status === 'completed') statusClass = 'text-green-400';
+ if (row.status === 'failed') statusClass = 'text-red-400';
+ if (row.status === 'running') statusClass = 'text-blue-400';
- // Style based on job status
- let statusClass = 'text-gray-400';
- if (job.status === 'completed') statusClass = 'text-green-400';
- if (job.status === 'failed') statusClass = 'text-red-400';
- if (job.status === 'running') statusClass = 'text-blue-400';
+ return {row.status};
+ },
+ },
+ {
+ title: 'Info',
+ key: 'info',
+ className: 'truncate max-w-xs',
+ },
+ ];
- return (
-
- |
- {job.name} |
-
-
-
- {job.step} / {totalSteps}
-
-
-
- |
- {job.gpu_ids} |
- {job.status} |
- {job.info} |
-
- );
- })}
-
-
-
- )}
-
- );
+ return
;
}
diff --git a/ui/src/components/UniversalTable.tsx b/ui/src/components/UniversalTable.tsx
new file mode 100644
index 00000000..b86b9bc8
--- /dev/null
+++ b/ui/src/components/UniversalTable.tsx
@@ -0,0 +1,72 @@
+import Loading from './Loading';
+import classNames from 'classnames';
+
+export interface TableColumn {
+ title: string;
+ key: string;
+ render?: (row: any) => React.ReactNode;
+ className?: string;
+}
+
+interface TableRow {
+ [key: string]: any;
+}
+
+interface TableProps {
+ columns: TableColumn[];
+ rows: TableRow[];
+ isLoading: boolean;
+ onRefresh: () => void;
+}
+
+export default function UniversalTable({ columns, rows, isLoading, onRefresh = () => {} }: TableProps) {
+ return (
+
+ {isLoading ? (
+
+
+
+ ) : rows.length === 0 ? (
+
+
Empty
+
+
+ ) : (
+
+
+
+
+ {columns.map(column => (
+ |
+ {column.title}
+ |
+ ))}
+
+
+
+ {rows?.map((row, index) => {
+ // Style for alternating rows
+ const rowClass = index % 2 === 0 ? 'bg-gray-900' : 'bg-gray-800';
+
+ return (
+
+ {columns.map(column => (
+ |
+ {column.render ? column.render(row) : row[column.key]}
+ |
+ ))}
+
+ );
+ })}
+
+
+
+ )}
+
+ );
+}