| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892 |
- "use client";
- import {
- ChevronDownIcon,
- ChevronRightIcon,
- TrashIcon
- } from "@heroicons/react/24/outline";
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
- import React, { useEffect, useMemo, useState } from "react";
- import toast from "react-hot-toast";
- import { del, get } from "../../lib/api";
- import ConfirmationDialog from "./ConfirmationDialog";
- import LoadingCard from "./Loading";
- import { useNotifications } from "./NotificationContext";
- import Pagination from "./Pagination";
- import TaskCrud from "./TaskCrud";
- export default function TaskList({
- limit = 10,
- context = "homepage"
- }: {
- limit?: number;
- context?: "homepage" | "tasks";
- }) {
- const queryClient = useQueryClient();
- const { addNotification } = useNotifications();
- const { data, isLoading, error } = useQuery({
- queryKey: ["tasks"],
- queryFn: () => get("/tasks")
- });
- // Listen for WebSocket events
- useEffect(() => {
- const handleTaskUpdate = (event: CustomEvent) => {
- const taskData = event.detail;
- console.log("[TaskList] Received task update:", taskData);
- // For progress updates, update the cache directly instead of refetching
- if (taskData.type === "progress" && taskData.taskId !== undefined) {
- console.log(
- `[TaskList] Updating progress for task ${taskData.taskId} to ${taskData.progress}%`
- );
- queryClient.setQueryData(["tasks"], (oldData: any) => {
- if (!oldData) {
- console.log("[TaskList] No cached task data available");
- return oldData;
- }
- const updated = oldData.map((task: any) =>
- task.id === taskData.taskId
- ? { ...task, progress: taskData.progress }
- : task
- );
- console.log(
- "[TaskList] Updated task cache:",
- updated.find((t: any) => t.id === taskData.taskId)
- );
- return updated;
- });
- } else {
- console.log("[TaskList] Non-progress update, refetching tasks");
- // For other task updates (created, completed, failed), do a full refetch
- queryClient.refetchQueries({ queryKey: ["tasks"] });
- }
- };
- const handleFileUpdate = (event: CustomEvent) => {
- const _fileData = event.detail;
- // Refetch tasks when file updates occur (e.g., new files detected)
- queryClient.refetchQueries({ queryKey: ["tasks"] });
- };
- window.addEventListener("taskUpdate", handleTaskUpdate as EventListener);
- window.addEventListener("fileUpdate", handleFileUpdate as EventListener);
- return () => {
- window.removeEventListener(
- "taskUpdate",
- handleTaskUpdate as EventListener
- );
- window.removeEventListener(
- "fileUpdate",
- handleFileUpdate as EventListener
- );
- };
- }, [queryClient]);
- const [editTask, setEditTask] = useState<any>(null);
- // Confirmation dialog and batch selection state
- const [deleteConfirm, setDeleteConfirm] = useState<{
- isOpen: boolean;
- task?: any;
- isBatch: boolean;
- selectedTasks?: any[];
- }>({ 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 - initialize from localStorage
- const [enabledDatasets, setEnabledDatasets] = useState<Set<string>>(() => {
- if (typeof window !== "undefined") {
- const saved = localStorage.getItem("taskList:enabledDatasets");
- return saved ? new Set(JSON.parse(saved)) : new Set();
- }
- return new Set();
- });
- const [enabledStatuses, setEnabledStatuses] = useState<Set<string>>(() => {
- if (typeof window !== "undefined") {
- const saved = localStorage.getItem("taskList:enabledStatuses");
- return saved
- ? new Set(JSON.parse(saved))
- : new Set(["pending", "processing", "failed", "skipped"]);
- }
- return new Set(["pending", "processing", "failed", "skipped"]);
- });
- // State for filters and search - initialize from localStorage
- const [searchTerm, setSearchTerm] = useState(() => {
- if (typeof window !== "undefined") {
- return localStorage.getItem("taskList:searchTerm") || "";
- }
- return "";
- });
- const [sortField, setSortField] = useState<
- "id" | "status" | "progress" | "dataset" | "updated_at"
- >(() => {
- if (typeof window !== "undefined") {
- const saved = localStorage.getItem("taskList:sortField");
- return (
- (saved as "id" | "status" | "progress" | "dataset" | "updated_at") ||
- "updated_at"
- );
- }
- return "updated_at";
- });
- const [sortDirection, setSortDirection] = useState<"asc" | "desc">(() => {
- if (typeof window !== "undefined") {
- const saved = localStorage.getItem("taskList:sortDirection");
- return (saved as "asc" | "desc") || "desc";
- }
- return "desc";
- });
- // Pagination state
- const [currentPage, setCurrentPage] = useState(1);
- const [pageSize, setPageSize] = useState(() => {
- if (typeof window !== "undefined") {
- const saved = localStorage.getItem("taskList:pageSize");
- return saved ? parseInt(saved) : 25;
- }
- return 25;
- });
- // Save filters to localStorage
- useEffect(() => {
- if (typeof window !== "undefined") {
- localStorage.setItem(
- "taskList:enabledDatasets",
- JSON.stringify(Array.from(enabledDatasets))
- );
- }
- }, [enabledDatasets]);
- useEffect(() => {
- if (typeof window !== "undefined") {
- localStorage.setItem(
- "taskList:enabledStatuses",
- JSON.stringify(Array.from(enabledStatuses))
- );
- }
- }, [enabledStatuses]);
- useEffect(() => {
- if (typeof window !== "undefined") {
- localStorage.setItem("taskList:searchTerm", searchTerm);
- }
- }, [searchTerm]);
- useEffect(() => {
- if (typeof window !== "undefined") {
- localStorage.setItem("taskList:sortField", sortField);
- }
- }, [sortField]);
- useEffect(() => {
- if (typeof window !== "undefined") {
- localStorage.setItem("taskList:sortDirection", sortDirection);
- }
- }, [sortDirection]);
- useEffect(() => {
- if (typeof window !== "undefined") {
- localStorage.setItem("taskList:pageSize", pageSize.toString());
- }
- }, [pageSize]);
- const deleteMutation = useMutation({
- mutationFn: (params: { task?: string; tasks?: any[] }) => {
- if (params.tasks) {
- // Batch delete
- return Promise.all(
- params.tasks.map((task) => del(`/tasks/${task.id}`))
- );
- } else if (params.task) {
- // Single delete
- return del(`/tasks/${params.task}`);
- }
- throw new Error("Invalid delete parameters");
- },
- onSuccess: (_, params) => {
- queryClient.refetchQueries({ queryKey: ["tasks"] });
- if (params.tasks) {
- toast.success(`${params.tasks.length} tasks deleted successfully`);
- addNotification({
- type: "success",
- title: "Tasks Deleted",
- message: `${params.tasks.length} tasks have been deleted successfully.`
- });
- setSelectedTasks(new Set()); // Clear selection after batch delete
- } else {
- toast.success("Task deleted successfully");
- addNotification({
- type: "success",
- title: "Task Deleted",
- message: `Task with ID "${params.task}" has been deleted successfully.`
- });
- }
- },
- onError: (error, _params) => {
- console.error("Failed to delete task(s):", error);
- toast.error("Failed to delete task(s)");
- addNotification({
- type: "error",
- title: "Delete Failed",
- message: `Failed to delete task(s). Please try again.`
- });
- }
- });
- const handleEdit = (task: any) => {
- setEditTask(task);
- };
- const handleEditClose = () => {
- 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({
- isOpen: true,
- task,
- isBatch: false
- });
- };
- const handleBatchDeleteClick = () => {
- const tasksToDelete = displayTasks.filter((task) =>
- selectedTasks.has(task.id.toString())
- );
- setDeleteConfirm({
- isOpen: true,
- isBatch: true,
- selectedTasks: tasksToDelete
- });
- };
- const handleDeleteConfirm = () => {
- if (deleteConfirm.isBatch && deleteConfirm.selectedTasks) {
- deleteMutation.mutate({ tasks: deleteConfirm.selectedTasks });
- } else if (deleteConfirm.task) {
- deleteMutation.mutate({ task: deleteConfirm.task.id.toString() });
- }
- setDeleteConfirm({ isOpen: false, isBatch: false });
- };
- const handleDeleteCancel = () => {
- setDeleteConfirm({ isOpen: false, isBatch: false });
- };
- // Batch selection handlers
- const handleSelectAll = () => {
- if (selectedTasks.size === displayTasks.length) {
- setSelectedTasks(new Set());
- } else {
- const allIds = new Set(displayTasks.map((task) => task.id.toString()));
- setSelectedTasks(allIds);
- }
- };
- const handleSelectTask = (task: any) => {
- const taskId = task.id.toString();
- const newSelected = new Set(selectedTasks);
- if (newSelected.has(taskId)) {
- newSelected.delete(taskId);
- } else {
- newSelected.add(taskId);
- }
- setSelectedTasks(newSelected);
- };
- // 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);
- };
- // 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 {
- setSortField(field);
- setSortDirection("asc");
- }
- };
- // Filter and sort data
- const filteredAndSortedData = useMemo(() => {
- if (!data || !Array.isArray(data)) return [];
- const filtered = data.filter(
- (task: any) =>
- 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) => {
- let aValue = a[sortField];
- let bValue = b[sortField];
- if (typeof aValue === "string") {
- aValue = aValue.toLowerCase();
- bValue = bValue.toLowerCase();
- }
- if (aValue < bValue) return sortDirection === "asc" ? -1 : 1;
- if (aValue > bValue) return sortDirection === "asc" ? 1 : -1;
- return 0;
- });
- return filtered;
- }, [
- data,
- enabledDatasets,
- enabledStatuses,
- searchTerm,
- sortField,
- sortDirection
- ]);
- // Handle pagination
- const paginatedData = useMemo(() => {
- if (context === "homepage") {
- // For homepage, just apply limit
- return limit
- ? filteredAndSortedData.slice(0, limit)
- : filteredAndSortedData;
- } else {
- // For tasks page, apply pagination
- const startIndex = (currentPage - 1) * pageSize;
- const endIndex = startIndex + pageSize;
- return filteredAndSortedData.slice(startIndex, endIndex);
- }
- }, [filteredAndSortedData, context, limit, currentPage, pageSize]);
- // Reset to page 1 when filters change
- useEffect(() => {
- setCurrentPage(1);
- }, [enabledDatasets, enabledStatuses, searchTerm, sortField, sortDirection]);
- const displayTasks = paginatedData;
- // Show loading state if data is still being fetched or if data is undefined
- if (isLoading || !data) return <LoadingCard message="Loading tasks..." />;
- if (error) {
- return (
- <div className="text-center p-8 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
- <div className="mb-6">
- <svg
- className="mx-auto h-16 w-16 text-red-400"
- fill="none"
- viewBox="0 0 24 24"
- stroke="currentColor"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
- />
- </svg>
- </div>
- <h2 className="text-xl font-semibold text-red-800 dark:text-red-200 mb-4">
- Failed to load tasks
- </h2>
- <p className="text-gray-600 dark:text-gray-400 mb-6">
- There was an error loading the tasks data.
- </p>
- <button
- onClick={() => queryClient.refetchQueries({ queryKey: ["tasks"] })}
- className="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
- >
- Try Again
- </button>
- </div>
- );
- }
- return (
- <>
- <div className="mb-6 p-4 border rounded bg-gray-50 dark:bg-gray-800">
- <div className="flex items-center justify-between mb-4">
- <h3 className="font-semibold">Tasks</h3>
- <div className="flex items-center gap-2">
- <span className="text-sm text-gray-600 dark:text-gray-400">
- Total: {filteredAndSortedData.length} tasks
- </span>
- {context === "homepage" ? (
- <a
- href="/tasks"
- className="inline-flex items-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
- >
- <svg
- className="h-4 w-4 mr-2"
- fill="none"
- viewBox="0 0 24 24"
- stroke="currentColor"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M9 5l7 7-7 7"
- />
- </svg>
- View All Tasks
- </a>
- ) : null}
- </div>
- </div>
- {/* Filter Controls - Only show on tasks page */}
- {context === "tasks" && (
- <div className="mb-4 space-y-4">
- {/* Search */}
- <div className="flex items-center gap-4">
- <div className="flex-1">
- <input
- type="text"
- placeholder="Search tasks..."
- value={searchTerm}
- onChange={(e) => setSearchTerm(e.target.value)}
- className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
- />
- </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", "skipped"].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>
- )}
- {/* Batch Actions */}
- {selectedTasks.size > 0 && (
- <div className="mb-4 flex items-center gap-2">
- <span className="text-sm text-gray-700 dark:text-gray-300">
- {selectedTasks.size} task{selectedTasks.size !== 1 ? "s" : ""}{" "}
- selected
- </span>
- <button
- onClick={handleBatchDeleteClick}
- className="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
- >
- <TrashIcon className="h-4 w-4 mr-2" />
- Delete Selected
- </button>
- <button
- onClick={() => setSelectedTasks(new Set())}
- className="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
- >
- Clear Selection
- </button>
- </div>
- )}
- <div className="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 shadow-sm">
- <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"
- className="px-4 py-3 text-left text-xs font-semibold text-gray-700 dark:text-gray-200 uppercase tracking-wider"
- >
- <input
- type="checkbox"
- checked={
- selectedTasks.size === displayTasks.length &&
- displayTasks.length > 0
- }
- onChange={handleSelectAll}
- className="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
- />
- </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 ${
- context === "tasks"
- ? "cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
- : ""
- }`}
- onClick={
- context === "tasks" ? () => handleSort("id") : undefined
- }
- >
- ID{" "}
- {context === "tasks" &&
- sortField === "id" &&
- (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 ${
- context === "tasks"
- ? "cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
- : ""
- }`}
- onClick={
- context === "tasks" ? () => handleSort("status") : undefined
- }
- >
- Status{" "}
- {context === "tasks" &&
- sortField === "status" &&
- (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 ${
- context === "tasks"
- ? "cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
- : ""
- }`}
- onClick={
- context === "tasks"
- ? () => handleSort("progress")
- : undefined
- }
- >
- Progress{" "}
- {context === "tasks" &&
- sortField === "progress" &&
- (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 ${
- context === "tasks"
- ? "cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
- : ""
- }`}
- onClick={
- context === "tasks"
- ? () => handleSort("dataset")
- : undefined
- }
- >
- Dataset{" "}
- {context === "tasks" &&
- 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"
- className="px-4 py-3 text-right text-xs font-semibold text-gray-700 dark:text-gray-200 uppercase tracking-wider"
- >
- Actions
- </th>
- )}
- </tr>
- </thead>
- <tbody className="divide-y divide-gray-200 dark:divide-gray-800">
- {displayTasks.length > 0 ? (
- displayTasks.map((task: any) => {
- const taskId = task.id.toString();
- const isExpanded = expandedRows.has(taskId);
- return (
- <React.Fragment key={task.id}>
- <tr>
- {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">
- {task.progress !== null &&
- task.progress !== undefined ? (
- <div className="flex items-center gap-2">
- <div className="flex-1 min-w-[60px]">
- <div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
- <div
- className="h-full bg-indigo-600 transition-all duration-300"
- style={{ width: `${task.progress}%` }}
- />
- </div>
- </div>
- <span className="text-xs font-medium tabular-nums">
- {task.progress}%
- </span>
- </div>
- ) : (
- "-"
- )}
- </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"
- >
- <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>
- )}
- </React.Fragment>
- );
- })
- ) : (
- <tr>
- <td
- 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.
- </td>
- </tr>
- )}
- </tbody>
- </table>
- </div>
- </div>
- {context === "tasks" && (
- <Pagination
- currentPage={currentPage}
- totalItems={filteredAndSortedData.length}
- pageSize={pageSize}
- onPageChange={setCurrentPage}
- onPageSizeChange={(newPageSize) => {
- setPageSize(newPageSize);
- setCurrentPage(1);
- }}
- />
- )}
- {context === "tasks" && editTask && (
- <TaskCrud editTask={editTask} onEditClose={handleEditClose} />
- )}
- <ConfirmationDialog
- isOpen={deleteConfirm.isOpen}
- title={deleteConfirm.isBatch ? "Delete Selected Tasks" : "Delete Task"}
- message={
- deleteConfirm.isBatch
- ? `Are you sure you want to delete ${deleteConfirm.selectedTasks?.length} selected task(s)? This action cannot be undone.`
- : `Are you sure you want to delete task with ID "${deleteConfirm.task?.id}"? This action cannot be undone.`
- }
- confirmText="Delete"
- cancelText="Cancel"
- type="danger"
- onConfirm={handleDeleteConfirm}
- onClose={handleDeleteCancel}
- isLoading={deleteMutation.isPending}
- />
- </>
- );
- }
|