diff --git a/ui/src/app/datasets/page.tsx b/ui/src/app/datasets/page.tsx index 2e2e43b3..9f5aeba2 100644 --- a/ui/src/app/datasets/page.tsx +++ b/ui/src/app/datasets/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Modal } from '@/components/Modal'; import Link from 'next/link'; import { TextInput } from '@/components/formInputs'; @@ -9,78 +9,116 @@ import { Button } from '@headlessui/react'; import { FaRegTrashAlt } from 'react-icons/fa'; import { openConfirm } from '@/components/ConfirmModal'; import { TopBar, MainContent } from '@/components/layout'; +import UniversalTable, { TableColumn } from '@/components/UniversalTable'; export default function Datasets() { const { datasets, status, refreshDatasets } = useDatasetList(); const [newDatasetName, setNewDatasetName] = useState(''); const [isNewDatasetModalOpen, setIsNewDatasetModalOpen] = useState(false); + + // Transform datasets array into rows with objects + const tableRows = datasets.map(dataset => ({ + name: dataset, + actions: dataset, // Pass full dataset name for actions + })); + + const columns: TableColumn[] = [ + { + title: 'Dataset Name', + key: 'name', + render: row => ( + + {row.name} + + ), + }, + { + title: 'Actions', + key: 'actions', + className: 'w-20 text-right', + render: row => ( + + ), + }, + ]; + + const handleDeleteDataset = (datasetName: string) => { + openConfirm({ + title: 'Delete Dataset', + message: `Are you sure you want to delete the dataset "${datasetName}"? This action cannot be undone.`, + type: 'warning', + confirmText: 'Delete', + onConfirm: () => { + fetch('/api/datasets/delete', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name: datasetName }), + }) + .then(res => res.json()) + .then(data => { + console.log('Dataset deleted:', data); + refreshDatasets(); + }) + .catch(error => { + console.error('Error deleting dataset:', error); + }); + }, + }); + }; + + const handleCreateDataset = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const response = await fetch('/api/datasets/create', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name: newDatasetName }), + }); + const data = await response.json(); + console.log('New dataset created:', data); + refreshDatasets(); + setNewDatasetName(''); + setIsNewDatasetModalOpen(false); + } catch (error) { + console.error('Error creating new dataset:', error); + } + }; + return ( <>
-

Datasets

+

Datasets

+ - {status === 'loading' &&

Loading...

} - {status === 'error' &&

Error fetching datasets

} - {status === 'success' && ( -
- {datasets.length === 0 &&

No datasets found

} - {datasets.map((dataset: string) => ( -
-
- - {dataset} - -
-
-
- -
-
- ))} -
- )} +
+ setIsNewDatasetModalOpen(false)} @@ -88,47 +126,25 @@ export default function Datasets() { size="md" >
-
{ - e.preventDefault(); - console.log('Creating new dataset'); - // make post with name to create new dataset - fetch('/api/datasets/create', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ name: newDatasetName }), - }) - .then(res => res.json()) - .then(data => { - console.log('New dataset created:', data); - refreshDatasets(); - setNewDatasetName(''); - setIsNewDatasetModalOpen(false); - }) - .catch(error => { - console.error('Error creating new dataset:', error); - }); - }} - > -
+ +
This will create a new folder with the name below in your dataset folder.
-
+
setNewDatasetName(value)} />
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

- -
- ) : ( -
- - - - - - - - - - - - {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 ( - - - - - - - - ); - })} - -
NameStepsGPUStatusInfo
- {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 => ( + + ))} + + + + {rows?.map((row, index) => { + // Style for alternating rows + const rowClass = index % 2 === 0 ? 'bg-gray-900' : 'bg-gray-800'; + + return ( + + {columns.map(column => ( + + ))} + + ); + })} + +
+ {column.title} +
+ {column.render ? column.render(row) : row[column.key]} +
+
+ )} +
+ ); +}