Переглянути джерело

feat: hide completed tasks by default in task list filter

- Remove 'completed' from default enabled statuses in TaskList component
- Users will now see pending, processing, failed, and skipped tasks by default
- Completed tasks can still be viewed by manually checking the completed filter
Timothy Pomeroy 1 місяць тому
батько
коміт
308081fbce
1 змінених файлів з 287 додано та 70 видалено
  1. 287 70
      apps/web/src/app/components/TaskList.tsx

+ 287 - 70
apps/web/src/app/components/TaskList.tsx

@@ -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.