"use client"; 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"; import { del, get, post } from "../../lib/api"; import ConfirmationDialog from "../components/ConfirmationDialog"; import FileCrud from "../components/FileCrud"; import LoadingCard from "../components/Loading"; import { useNotifications } from "../components/NotificationContext"; import Pagination from "../components/Pagination"; export default function FileList() { const queryClient = useQueryClient(); const { addNotification } = useNotifications(); // Get available datasets const { data: datasets } = useQuery({ queryKey: ["datasets"], queryFn: () => get(`/files/all-datasets`) }); // Get files from all datasets const { data: allFiles, isLoading, error } = useQuery({ queryKey: ["all-files"], queryFn: async () => { if (!datasets) return []; const allFilesPromises = datasets.map((datasetName: string) => get(`/files/${datasetName}`).catch(() => []) ); const results = await Promise.all(allFilesPromises); return results.flat().map((file: any) => ({ ...file })); }, enabled: !!datasets }); // State for filters and search - initialize from localStorage const [enabledDatasets, setEnabledDatasets] = useState>(() => { if (typeof window !== "undefined") { const saved = localStorage.getItem("fileList:enabledDatasets"); return saved ? new Set(JSON.parse(saved)) : new Set(); } return new Set(); }); const [searchTerm, setSearchTerm] = useState(() => { if (typeof window !== "undefined") { return localStorage.getItem("fileList:searchTerm") || ""; } return ""; }); const [sortField, setSortField] = useState<"input" | "date">(() => { if (typeof window !== "undefined") { const saved = localStorage.getItem("fileList:sortField"); return (saved as "input" | "date") || "date"; } return "date"; }); const [sortDirection, setSortDirection] = useState<"asc" | "desc">(() => { if (typeof window !== "undefined") { const saved = localStorage.getItem("fileList: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("fileList:pageSize"); return saved ? parseInt(saved) : 25; } return 25; }); // Save filters to localStorage useEffect(() => { if (typeof window !== "undefined") { localStorage.setItem( "fileList:enabledDatasets", JSON.stringify(Array.from(enabledDatasets)) ); } }, [enabledDatasets]); useEffect(() => { if (typeof window !== "undefined") { localStorage.setItem("fileList:searchTerm", searchTerm); } }, [searchTerm]); useEffect(() => { if (typeof window !== "undefined") { localStorage.setItem("fileList:sortField", sortField); } }, [sortField]); useEffect(() => { if (typeof window !== "undefined") { localStorage.setItem("fileList:sortDirection", sortDirection); } }, [sortDirection]); useEffect(() => { if (typeof window !== "undefined") { localStorage.setItem("fileList:pageSize", pageSize.toString()); } }, [pageSize]); // Initialize enabled datasets when datasets are loaded useEffect(() => { if (datasets && enabledDatasets.size === 0) { const datasetNames = datasets .map((path: string) => path.split("/").pop()) .filter(Boolean); setEnabledDatasets(new Set(datasetNames)); } }, [datasets, enabledDatasets.size]); // Listen for WebSocket events useEffect(() => { const handleFileUpdate = (event: CustomEvent) => { const fileData = event.detail; // Refetch files when file updates occur queryClient.refetchQueries({ queryKey: ["all-files"] }); }; const handleTaskUpdate = (event: CustomEvent) => { const taskData = event.detail; // Refetch files when task updates occur (e.g., requeue processing) if (taskData.task === "handbrake") { queryClient.refetchQueries({ queryKey: ["all-files"] }); } }; window.addEventListener("fileUpdate", handleFileUpdate as EventListener); window.addEventListener("taskUpdate", handleTaskUpdate as EventListener); return () => { window.removeEventListener( "fileUpdate", handleFileUpdate as EventListener ); window.removeEventListener( "taskUpdate", handleTaskUpdate as EventListener ); }; }, [queryClient]); const [editFile, setEditFile] = useState(null); // Confirmation dialog state const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; file?: any; isBatch?: boolean; selectedFiles?: any[]; }>({ isOpen: false }); // Batch selection state const [selectedFiles, setSelectedFiles] = useState>(new Set()); // Expanded rows state const [expandedRows, setExpandedRows] = useState>(new Set()); // Dropdown state const [openDropdown, setOpenDropdown] = useState(null); const deleteMutation = useMutation({ mutationFn: async (params: { file?: string; files?: any[] }) => { if (params.files && params.files.length > 0) { // Batch delete const deletePromises = params.files.map((file) => del(`/files/${file.dataset}/${encodeURIComponent(file.input)}`) ); await Promise.all(deletePromises); return params.files; } else if (params.file) { // Single delete const fileData = deleteConfirm.file; return del( `/files/${fileData?.dataset || "pr0n"}/${encodeURIComponent(params.file)}` ); } }, onSuccess: (result, params) => { queryClient.refetchQueries({ queryKey: ["all-files"] }); setSelectedFiles(new Set()); // Clear selections after delete if (params.files && params.files.length > 0) { toast.success(`${params.files.length} files deleted successfully`); addNotification({ type: "success", title: "Files Deleted", message: `${params.files.length} files have been deleted successfully.` }); } else { toast.success("File deleted successfully"); addNotification({ type: "success", title: "File Deleted", message: `File "${params.file}" has been deleted successfully.` }); } }, onError: (error, params) => { console.error("Failed to delete file(s):", error); toast.error("Failed to delete file(s)"); addNotification({ type: "error", title: "Delete Failed", message: `Failed to delete file(s). Please try again.` }); } }); const requeueMutation = useMutation({ mutationFn: ({ dataset, file }: { dataset: string; file: string }) => post(`/files/${dataset}/${encodeURIComponent(file)}/requeue`), onSuccess: (_, { file }) => { queryClient.refetchQueries({ queryKey: ["all-files"] }); queryClient.refetchQueries({ queryKey: ["tasks"] }); toast.success("File requeued for processing"); addNotification({ type: "success", title: "File Requeued", message: `File "${file}" has been requeued for processing.` }); }, onError: (error, { file }) => { console.error("Failed to requeue file:", error); toast.error("Failed to requeue file"); addNotification({ type: "error", title: "Requeue Failed", message: `Failed to requeue file "${file}". Please try again.` }); } }); const handleEdit = (file: any) => { setEditFile(file); }; const handleEditClose = () => { setEditFile(null); }; // Confirmation dialog handlers const handleDeleteClick = (file: any) => { setDeleteConfirm({ isOpen: true, file, isBatch: false }); }; const handleBatchDeleteClick = () => { const filesToDelete = displayFiles.filter((file) => selectedFiles.has(`${file.dataset}-${file.input}`) ); setDeleteConfirm({ isOpen: true, isBatch: true, selectedFiles: filesToDelete }); }; const handleBatchRequeueClick = () => { const filesToRequeue = displayFiles.filter((file) => selectedFiles.has(`${file.dataset}-${file.input}`) ); filesToRequeue.forEach((file) => { requeueMutation.mutate({ dataset: file.dataset, file: file.input }); }); setSelectedFiles(new Set()); // Clear selection after requeue }; const handleDeleteConfirm = () => { if (deleteConfirm.isBatch && deleteConfirm.selectedFiles) { deleteMutation.mutate({ files: deleteConfirm.selectedFiles }); } else if (deleteConfirm.file) { deleteMutation.mutate({ file: deleteConfirm.file.input }); } setDeleteConfirm({ isOpen: false }); }; const handleDeleteCancel = () => { setDeleteConfirm({ isOpen: false }); }; // Batch selection handlers const handleSelectAll = () => { if (selectedFiles.size === displayFiles.length) { setSelectedFiles(new Set()); } else { const allIds = new Set( displayFiles.map((file) => `${file.dataset}-${file.input}`) ); setSelectedFiles(allIds); } }; const handleSelectFile = (file: any) => { const fileId = `${file.dataset}-${file.input}`; const newSelected = new Set(selectedFiles); if (newSelected.has(fileId)) { newSelected.delete(fileId); } else { newSelected.add(fileId); } setSelectedFiles(newSelected); }; // Expanded row handlers const handleToggleExpanded = (fileId: string) => { const newExpanded = new Set(expandedRows); if (newExpanded.has(fileId)) { newExpanded.delete(fileId); } else { newExpanded.add(fileId); } setExpandedRows(newExpanded); }; // Dropdown handlers const handleToggleDropdown = (fileId: string) => { setOpenDropdown(openDropdown === fileId ? null : fileId); }; const handleDropdownAction = (action: string, file: any) => { setOpenDropdown(null); switch (action) { case "edit": handleEdit(file); break; case "delete": handleDeleteClick(file); break; } }; // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( openDropdown && !(event.target as Element).closest(".dropdown-container") ) { setOpenDropdown(null); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [openDropdown]); const formatDate = (dateString: string) => { if (!dateString) return "-"; try { const date = new Date(dateString); return date.toLocaleString(); } catch { return dateString; } }; const toggleDataset = (datasetName: string) => { const newEnabled = new Set(enabledDatasets); if (newEnabled.has(datasetName)) { newEnabled.delete(datasetName); } else { newEnabled.add(datasetName); } setEnabledDatasets(newEnabled); }; const handleSort = (field: "input" | "date") => { if (sortField === field) { setSortDirection(sortDirection === "asc" ? "desc" : "asc"); } else { setSortField(field); setSortDirection("asc"); } }; // Filter and sort data const filteredAndSortedData = useMemo(() => { if (!allFiles) return []; let filtered = allFiles.filter( (file: any) => enabledDatasets.has(file.dataset) && (file.input.toLowerCase().includes(searchTerm.toLowerCase()) || (file.output && file.output.toLowerCase().includes(searchTerm.toLowerCase()))) ); filtered.sort((a: any, b: any) => { let aValue = a[sortField]; let bValue = b[sortField]; if (sortField === "date") { aValue = new Date(aValue).getTime(); bValue = new Date(bValue).getTime(); } else { 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; }, [allFiles, enabledDatasets, searchTerm, sortField, sortDirection]); // Handle pagination const paginatedData = useMemo(() => { const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; return filteredAndSortedData.slice(startIndex, endIndex); }, [filteredAndSortedData, currentPage, pageSize]); // Reset to page 1 when filters change useEffect(() => { setCurrentPage(1); }, [enabledDatasets, searchTerm, sortField, sortDirection]); const displayFiles = paginatedData; if (isLoading) return ; if (error) { return (

Failed to load files

There was an error loading the files data.

); } return ( <>

Files

Total: {filteredAndSortedData.length} files
{/* Filter Controls */}
{/* Search */}
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" />
{/* Dataset Toggles */}
Datasets: {datasets?.map((datasetPath: string) => { const datasetName = datasetPath.split("/").pop(); if (!datasetName) return null; return ( ); })}
{/* Batch Actions */} {selectedFiles.size > 0 && (
{selectedFiles.size} file{selectedFiles.size !== 1 ? "s" : ""}{" "} selected
)}
{displayFiles.length > 0 ? ( displayFiles.map((file: any) => { const fileId = `${file.dataset}-${file.input}`; const isExpanded = expandedRows.has(fileId); return ( <> {isExpanded && ( )} ); }) ) : ( )}
{/* Expand toggle column */} 0 } onChange={handleSelectAll} className="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" /> Dataset handleSort("input")} > Input{" "} {sortField === "input" && (sortDirection === "asc" ? "↑" : "↓")} handleSort("date")} > Date{" "} {sortField === "date" && (sortDirection === "asc" ? "↑" : "↓")} Actions
handleSelectFile(file)} className="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" /> {file.dataset}
{file.input}
{formatDate(file.date)}
{openDropdown === fileId && (
)}
Output
{file.output || "No output available"}
No files found matching your filters.
{ setPageSize(newPageSize); setCurrentPage(1); }} /> {editFile && ( )} ); }