|
|
@@ -1,5 +1,9 @@
|
|
|
"use client";
|
|
|
-import { TrashIcon } from "@heroicons/react/24/outline";
|
|
|
+import {
|
|
|
+ ChevronDownIcon,
|
|
|
+ ChevronRightIcon,
|
|
|
+ TrashIcon
|
|
|
+} from "@heroicons/react/24/outline";
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
|
import toast from "react-hot-toast";
|
|
|
@@ -53,12 +57,23 @@ export default function TaskList({
|
|
|
}>({ isOpen: false, isBatch: false });
|
|
|
const [selectedTasks, setSelectedTasks] = useState<Set<string>>(new Set());
|
|
|
|
|
|
+ // Expanded rows state
|
|
|
+ const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
|
|
|
+
|
|
|
+ // Dataset and status filter state
|
|
|
+ const [enabledDatasets, setEnabledDatasets] = useState<Set<string>>(
|
|
|
+ new Set()
|
|
|
+ );
|
|
|
+ const [enabledStatuses, setEnabledStatuses] = useState<Set<string>>(
|
|
|
+ new Set(["pending", "processing", "failed", "skipped"])
|
|
|
+ );
|
|
|
+
|
|
|
// State for filters and search
|
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
|
const [sortField, setSortField] = useState<
|
|
|
- "id" | "type" | "status" | "progress"
|
|
|
- >("id");
|
|
|
- const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
|
|
|
+ "id" | "status" | "progress" | "dataset" | "updated_at"
|
|
|
+ >("updated_at");
|
|
|
+ const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
|
|
|
|
|
|
// Pagination state
|
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
|
@@ -115,6 +130,16 @@ export default function TaskList({
|
|
|
setEditTask(null);
|
|
|
};
|
|
|
|
|
|
+ // Initialize enabled datasets when data is loaded
|
|
|
+ useEffect(() => {
|
|
|
+ if (data && data.length > 0 && enabledDatasets.size === 0) {
|
|
|
+ const datasets = data
|
|
|
+ .map((task: any) => task.dataset)
|
|
|
+ .filter((dataset: any): dataset is string => Boolean(dataset));
|
|
|
+ setEnabledDatasets(new Set(datasets));
|
|
|
+ }
|
|
|
+ }, [data, enabledDatasets.size]);
|
|
|
+
|
|
|
// Confirmation dialog handlers
|
|
|
const handleDeleteClick = (task: any) => {
|
|
|
setDeleteConfirm({
|
|
|
@@ -169,11 +194,47 @@ export default function TaskList({
|
|
|
setSelectedTasks(newSelected);
|
|
|
};
|
|
|
|
|
|
- const truncateText = (text: string) => {
|
|
|
- return text && text.length > 30 ? text.substring(0, 30) + "..." : text;
|
|
|
+ // Expanded row handlers
|
|
|
+ const handleToggleExpanded = (taskId: string) => {
|
|
|
+ const newExpanded = new Set(expandedRows);
|
|
|
+ if (newExpanded.has(taskId)) {
|
|
|
+ newExpanded.delete(taskId);
|
|
|
+ } else {
|
|
|
+ newExpanded.add(taskId);
|
|
|
+ }
|
|
|
+ setExpandedRows(newExpanded);
|
|
|
};
|
|
|
|
|
|
- const handleSort = (field: "id" | "type" | "status" | "progress") => {
|
|
|
+ // Dataset and status filter handlers
|
|
|
+ const toggleDataset = (datasetName: string) => {
|
|
|
+ const newEnabled = new Set(enabledDatasets);
|
|
|
+ if (newEnabled.has(datasetName)) {
|
|
|
+ newEnabled.delete(datasetName);
|
|
|
+ } else {
|
|
|
+ newEnabled.add(datasetName);
|
|
|
+ }
|
|
|
+ setEnabledDatasets(newEnabled);
|
|
|
+ };
|
|
|
+
|
|
|
+ const toggleStatus = (statusName: string) => {
|
|
|
+ const newEnabled = new Set(enabledStatuses);
|
|
|
+ if (newEnabled.has(statusName)) {
|
|
|
+ newEnabled.delete(statusName);
|
|
|
+ } else {
|
|
|
+ newEnabled.add(statusName);
|
|
|
+ }
|
|
|
+ setEnabledStatuses(newEnabled);
|
|
|
+ };
|
|
|
+
|
|
|
+ const truncateText = (text: string, maxLength: number = 30) => {
|
|
|
+ return text && text.length > maxLength
|
|
|
+ ? text.substring(0, maxLength) + "..."
|
|
|
+ : text;
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSort = (
|
|
|
+ field: "id" | "status" | "progress" | "dataset" | "updated_at"
|
|
|
+ ) => {
|
|
|
if (sortField === field) {
|
|
|
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
|
|
|
} else {
|
|
|
@@ -188,16 +249,26 @@ export default function TaskList({
|
|
|
|
|
|
let filtered = data.filter(
|
|
|
(task: any) =>
|
|
|
- task.id.toString().toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
- (task.type &&
|
|
|
- task.type.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
|
|
- (task.status &&
|
|
|
- task.status.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
|
|
- (task.progress &&
|
|
|
- task.progress
|
|
|
- .toString()
|
|
|
- .toLowerCase()
|
|
|
- .includes(searchTerm.toLowerCase()))
|
|
|
+ enabledDatasets.has(task.dataset || "") &&
|
|
|
+ enabledStatuses.has(task.status || "") &&
|
|
|
+ (task.id.toString().toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
+ (task.status &&
|
|
|
+ task.status.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
|
|
+ (task.progress &&
|
|
|
+ task.progress
|
|
|
+ .toString()
|
|
|
+ .toLowerCase()
|
|
|
+ .includes(searchTerm.toLowerCase())) ||
|
|
|
+ (task.dataset &&
|
|
|
+ task.dataset.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
|
|
+ (task.input &&
|
|
|
+ task.input.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
|
|
+ (task.output &&
|
|
|
+ task.output.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
|
|
+ (task.error_message &&
|
|
|
+ task.error_message
|
|
|
+ .toLowerCase()
|
|
|
+ .includes(searchTerm.toLowerCase())))
|
|
|
);
|
|
|
|
|
|
filtered.sort((a: any, b: any) => {
|
|
|
@@ -215,7 +286,14 @@ export default function TaskList({
|
|
|
});
|
|
|
|
|
|
return filtered;
|
|
|
- }, [data, searchTerm, sortField, sortDirection]);
|
|
|
+ }, [
|
|
|
+ data,
|
|
|
+ enabledDatasets,
|
|
|
+ enabledStatuses,
|
|
|
+ searchTerm,
|
|
|
+ sortField,
|
|
|
+ sortDirection
|
|
|
+ ]);
|
|
|
|
|
|
// Handle pagination
|
|
|
const paginatedData = useMemo(() => {
|
|
|
@@ -235,7 +313,7 @@ export default function TaskList({
|
|
|
// Reset to page 1 when filters change
|
|
|
useEffect(() => {
|
|
|
setCurrentPage(1);
|
|
|
- }, [searchTerm, sortField, sortDirection]);
|
|
|
+ }, [enabledDatasets, enabledStatuses, searchTerm, sortField, sortDirection]);
|
|
|
|
|
|
const displayTasks = paginatedData;
|
|
|
|
|
|
@@ -322,6 +400,48 @@ export default function TaskList({
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ {/* Dataset Toggles */}
|
|
|
+ <div className="flex flex-wrap gap-2">
|
|
|
+ <span className="text-sm text-gray-600 dark:text-gray-400 mr-2">
|
|
|
+ Datasets:
|
|
|
+ </span>
|
|
|
+ {Array.from(enabledDatasets).map((datasetName) => (
|
|
|
+ <label key={datasetName} className="flex items-center gap-2">
|
|
|
+ <input
|
|
|
+ type="checkbox"
|
|
|
+ checked={enabledDatasets.has(datasetName)}
|
|
|
+ onChange={() => toggleDataset(datasetName)}
|
|
|
+ className="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
|
|
+ />
|
|
|
+ <span className="text-sm text-gray-700 dark:text-gray-300">
|
|
|
+ {datasetName}
|
|
|
+ </span>
|
|
|
+ </label>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Status Toggles */}
|
|
|
+ <div className="flex flex-wrap gap-2">
|
|
|
+ <span className="text-sm text-gray-600 dark:text-gray-400 mr-2">
|
|
|
+ Status:
|
|
|
+ </span>
|
|
|
+ {["pending", "processing", "completed", "failed"].map(
|
|
|
+ (statusName) => (
|
|
|
+ <label key={statusName} className="flex items-center gap-2">
|
|
|
+ <input
|
|
|
+ type="checkbox"
|
|
|
+ checked={enabledStatuses.has(statusName)}
|
|
|
+ onChange={() => toggleStatus(statusName)}
|
|
|
+ className="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
|
|
+ />
|
|
|
+ <span className="text-sm text-gray-700 dark:text-gray-300 capitalize">
|
|
|
+ {statusName}
|
|
|
+ </span>
|
|
|
+ </label>
|
|
|
+ )
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
@@ -352,6 +472,14 @@ export default function TaskList({
|
|
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
|
<thead className="bg-gray-50 dark:bg-gray-800">
|
|
|
<tr>
|
|
|
+ {context === "tasks" && (
|
|
|
+ <th
|
|
|
+ scope="col"
|
|
|
+ className="px-4 py-3 text-left text-xs font-semibold text-gray-700 dark:text-gray-200 uppercase tracking-wider"
|
|
|
+ >
|
|
|
+ {/* Expand/collapse column */}
|
|
|
+ </th>
|
|
|
+ )}
|
|
|
{context === "tasks" && (
|
|
|
<th
|
|
|
scope="col"
|
|
|
@@ -392,12 +520,12 @@ export default function TaskList({
|
|
|
: ""
|
|
|
}`}
|
|
|
onClick={
|
|
|
- context === "tasks" ? () => handleSort("type") : undefined
|
|
|
+ context === "tasks" ? () => handleSort("status") : undefined
|
|
|
}
|
|
|
>
|
|
|
- Type{" "}
|
|
|
+ Status{" "}
|
|
|
{context === "tasks" &&
|
|
|
- sortField === "type" &&
|
|
|
+ sortField === "status" &&
|
|
|
(sortDirection === "asc" ? "↑" : "↓")}
|
|
|
</th>
|
|
|
<th
|
|
|
@@ -408,12 +536,14 @@ export default function TaskList({
|
|
|
: ""
|
|
|
}`}
|
|
|
onClick={
|
|
|
- context === "tasks" ? () => handleSort("status") : undefined
|
|
|
+ context === "tasks"
|
|
|
+ ? () => handleSort("progress")
|
|
|
+ : undefined
|
|
|
}
|
|
|
>
|
|
|
- Status{" "}
|
|
|
+ Progress{" "}
|
|
|
{context === "tasks" &&
|
|
|
- sortField === "status" &&
|
|
|
+ sortField === "progress" &&
|
|
|
(sortDirection === "asc" ? "↑" : "↓")}
|
|
|
</th>
|
|
|
<th
|
|
|
@@ -425,15 +555,31 @@ export default function TaskList({
|
|
|
}`}
|
|
|
onClick={
|
|
|
context === "tasks"
|
|
|
- ? () => handleSort("progress")
|
|
|
+ ? () => handleSort("dataset")
|
|
|
: undefined
|
|
|
}
|
|
|
>
|
|
|
- Progress{" "}
|
|
|
+ Dataset{" "}
|
|
|
{context === "tasks" &&
|
|
|
- sortField === "progress" &&
|
|
|
+ sortField === "dataset" &&
|
|
|
(sortDirection === "asc" ? "↑" : "↓")}
|
|
|
</th>
|
|
|
+ <th
|
|
|
+ scope="col"
|
|
|
+ className="px-4 py-3 text-left text-xs font-semibold text-gray-700 dark:text-gray-200 uppercase tracking-wider"
|
|
|
+ >
|
|
|
+ Preset
|
|
|
+ </th>
|
|
|
+ {context === "tasks" && (
|
|
|
+ <>
|
|
|
+ <th
|
|
|
+ scope="col"
|
|
|
+ className="px-4 py-3 text-left text-xs font-semibold text-gray-700 dark:text-gray-200 uppercase tracking-wider"
|
|
|
+ >
|
|
|
+ Input
|
|
|
+ </th>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
{context === "tasks" && (
|
|
|
<th
|
|
|
scope="col"
|
|
|
@@ -446,52 +592,123 @@ export default function TaskList({
|
|
|
</thead>
|
|
|
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
|
|
{displayTasks.length > 0 ? (
|
|
|
- displayTasks.map((task: any) => (
|
|
|
- <tr key={task.id}>
|
|
|
- {context === "tasks" && (
|
|
|
- <td className="px-4 py-2 whitespace-nowrap">
|
|
|
- <input
|
|
|
- type="checkbox"
|
|
|
- checked={selectedTasks.has(task.id.toString())}
|
|
|
- onChange={() => handleSelectTask(task)}
|
|
|
- className="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
|
|
- />
|
|
|
- </td>
|
|
|
- )}
|
|
|
- <td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
|
- {task.id}
|
|
|
- </td>
|
|
|
- <td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
|
- {truncateText(task.type)}
|
|
|
- </td>
|
|
|
- <td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
|
- {truncateText(task.status)}
|
|
|
- </td>
|
|
|
- <td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
|
- {truncateText(task.progress ?? "-")}
|
|
|
- </td>
|
|
|
- {context === "tasks" && (
|
|
|
- <td className="px-4 py-2 whitespace-nowrap text-sm text-right">
|
|
|
- <button
|
|
|
- className="inline-flex items-center rounded-md bg-yellow-500 px-3 py-1 text-xs font-medium text-white shadow-sm hover:bg-yellow-600 focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:ring-offset-2 mr-2"
|
|
|
- onClick={() => handleEdit(task)}
|
|
|
- >
|
|
|
- Edit
|
|
|
- </button>
|
|
|
- <button
|
|
|
- className="inline-flex items-center rounded-md bg-red-600 px-3 py-1 text-xs font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
|
|
|
- onClick={() => handleDeleteClick(task)}
|
|
|
+ displayTasks.map((task: any) => {
|
|
|
+ const taskId = task.id.toString();
|
|
|
+ const isExpanded = expandedRows.has(taskId);
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <tr key={task.id}>
|
|
|
+ {context === "tasks" && (
|
|
|
+ <td className="px-4 py-2 whitespace-nowrap">
|
|
|
+ <button
|
|
|
+ onClick={() => handleToggleExpanded(taskId)}
|
|
|
+ className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
|
+ >
|
|
|
+ {isExpanded ? (
|
|
|
+ <ChevronDownIcon className="h-4 w-4" />
|
|
|
+ ) : (
|
|
|
+ <ChevronRightIcon className="h-4 w-4" />
|
|
|
+ )}
|
|
|
+ </button>
|
|
|
+ </td>
|
|
|
+ )}
|
|
|
+ {context === "tasks" && (
|
|
|
+ <td className="px-4 py-2 whitespace-nowrap">
|
|
|
+ <input
|
|
|
+ type="checkbox"
|
|
|
+ checked={selectedTasks.has(task.id.toString())}
|
|
|
+ onChange={() => handleSelectTask(task)}
|
|
|
+ className="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
|
|
+ />
|
|
|
+ </td>
|
|
|
+ )}
|
|
|
+ <td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
|
+ {task.id}
|
|
|
+ </td>
|
|
|
+ <td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
|
+ {truncateText(task.status)}
|
|
|
+ </td>
|
|
|
+ <td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
|
+ {truncateText(task.progress ?? "-")}
|
|
|
+ </td>
|
|
|
+ <td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
|
+ {truncateText(task.dataset ?? "-")}
|
|
|
+ </td>
|
|
|
+ <td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
|
+ {truncateText(task.preset ?? "-")}
|
|
|
+ </td>
|
|
|
+ {context === "tasks" && (
|
|
|
+ <>
|
|
|
+ <td className="px-4 py-2 text-sm text-gray-900 dark:text-gray-100 max-w-xs">
|
|
|
+ <div className="truncate" title={task.input}>
|
|
|
+ {task.input
|
|
|
+ ? truncateText(task.input, 30)
|
|
|
+ : "-"}
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ {context === "tasks" && (
|
|
|
+ <td className="px-4 py-2 whitespace-nowrap text-sm text-right">
|
|
|
+ <button
|
|
|
+ className="inline-flex items-center rounded-md bg-yellow-500 px-3 py-1 text-xs font-medium text-white shadow-sm hover:bg-yellow-600 focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:ring-offset-2 mr-2"
|
|
|
+ onClick={() => handleEdit(task)}
|
|
|
+ >
|
|
|
+ Edit
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ className="inline-flex items-center rounded-md bg-red-600 px-3 py-1 text-xs font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
|
|
|
+ onClick={() => handleDeleteClick(task)}
|
|
|
+ >
|
|
|
+ Delete
|
|
|
+ </button>
|
|
|
+ </td>
|
|
|
+ )}
|
|
|
+ </tr>
|
|
|
+ {context === "tasks" && isExpanded && (
|
|
|
+ <tr
|
|
|
+ key={`${task.id}-expanded`}
|
|
|
+ className="bg-gray-50 dark:bg-gray-800"
|
|
|
>
|
|
|
- Delete
|
|
|
- </button>
|
|
|
- </td>
|
|
|
- )}
|
|
|
- </tr>
|
|
|
- ))
|
|
|
+ <td colSpan={7} className="px-4 py-3">
|
|
|
+ <div className="space-y-3 text-sm">
|
|
|
+ <div>
|
|
|
+ <div className="font-medium text-gray-900 dark:text-gray-100 mb-1">
|
|
|
+ Output
|
|
|
+ </div>
|
|
|
+ <div className="text-gray-700 dark:text-gray-300 font-mono text-xs bg-white dark:bg-gray-900 p-2 rounded border max-h-32 overflow-y-auto">
|
|
|
+ {task.output || "No output available"}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <div className="font-medium text-gray-900 dark:text-gray-100 mb-1">
|
|
|
+ Created
|
|
|
+ </div>
|
|
|
+ <div className="text-gray-700 dark:text-gray-300 font-mono text-xs bg-white dark:bg-gray-900 p-2 rounded border">
|
|
|
+ {task.created_at
|
|
|
+ ? new Date(task.created_at).toLocaleString()
|
|
|
+ : "N/A"}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <div className="font-medium text-gray-900 dark:text-gray-100 mb-1">
|
|
|
+ Error
|
|
|
+ </div>
|
|
|
+ <div className="text-gray-700 dark:text-gray-300 font-mono text-xs bg-white dark:bg-gray-900 p-2 rounded border max-h-32 overflow-y-auto">
|
|
|
+ {task.error_message || "No error"}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ );
|
|
|
+ })
|
|
|
) : (
|
|
|
<tr>
|
|
|
<td
|
|
|
- colSpan={context === "tasks" ? 6 : 4}
|
|
|
+ colSpan={context === "tasks" ? 8 : 6}
|
|
|
className="px-4 py-4 text-center text-gray-500 dark:text-gray-400"
|
|
|
>
|
|
|
No tasks found matching your filters.
|