import React, { useState, useEffect, useRef } from "react"; import { toast } from "react-toastify"; import { DataTable } from "primereact/datatable"; import { Column } from "primereact/column"; import { Dropdown } from "primereact/dropdown"; import { Button } from "@mui/joy"; import { Dialog } from "primereact/dialog"; import { authFetch } from "@/utils/authFetch"; import { confirmDialog, ConfirmDialog } from "primereact/confirmdialog"; import BreadcrumbNav from "./BreadcrumbNav"; import { API_URL } from "@/config"; const STATUS_OPTIONS = ["Queued", "Started", "Compressing", "Finished", "Failed"]; const TAR_BASE_URL = "https://codey.lol/m/m2"; // configurable prefix export default function RequestManagement() { const [requests, setRequests] = useState([]); const [filterType, setFilterType] = useState(null); const [filterStatus, setFilterStatus] = useState(null); const [filteredRequests, setFilteredRequests] = useState([]); const [selectedRequest, setSelectedRequest] = useState(null); const [isDialogVisible, setIsDialogVisible] = useState(false); const pollingRef = useRef(null); const pollingDetailRef = useRef(null); const tarballUrl = (absPath, quality) => { if (!absPath) return null; const filename = absPath.split("/").pop(); // get "SOMETHING.tar.gz" return `${TAR_BASE_URL}/${quality}/${filename}`; }; const fetchJobs = async () => { try { const res = await authFetch(`${API_URL}/trip/jobs/list`); if (!res.ok) throw new Error("Failed to fetch jobs"); const data = await res.json(); setRequests(Array.isArray(data.jobs) ? data.jobs : []); } catch (err) { console.error(err); if (!toast.isActive('fetch-fail-toast')) { toast.error("Failed to fetch jobs list", { toastId: 'fetch-fail-toast', }); } } }; const fetchJobDetail = async (jobId) => { try { const res = await authFetch(`${API_URL}/trip/job/${jobId}`); if (!res.ok) throw new Error("Failed to fetch job details"); return await res.json(); } catch (err) { console.error(err); if (!toast.isActive('fetch-job-fail-toast')) { toast.error("Failed to fetch job details", { toastId: "fetch-job-fail-toast", }); } return null; } }; useEffect(() => { fetchJobs(); }, []); useEffect(() => { if (isDialogVisible && selectedRequest) { // Start polling const poll = async () => { const updated = await fetchJobDetail(selectedRequest.id); if (updated) setSelectedRequest(updated); }; pollingDetailRef.current = setInterval(poll, 1500); return () => { if (pollingDetailRef.current) { clearInterval(pollingDetailRef.current); pollingDetailRef.current = null; } }; } }, [isDialogVisible, selectedRequest?.id]); useEffect(() => { const hasActive = requests.some((j) => ["Queued", "Started", "Compressing"].includes(j.status)); if (hasActive && !pollingRef.current) pollingRef.current = setInterval(fetchJobs, 1500); else if (!hasActive && pollingRef.current) { clearInterval(pollingRef.current); pollingRef.current = null; } return () => { if (pollingRef.current) clearInterval(pollingRef.current); pollingRef.current = null; }; }, [requests]); useEffect(() => { const filtered = requests.filter((r) => { const typeMatch = !filterType || r.type === filterType; const statusMatch = filterStatus === "all" || filterStatus === null || r.status === filterStatus; return typeMatch && statusMatch; }); setFilteredRequests(filtered); }, [filterType, filterStatus, requests]); const getStatusColorClass = (status) => { switch (status) { case "Queued": return "bg-yellow-700 text-white"; case "Started": return "bg-blue-700 text-white"; case "Compressing": return "bg-orange-700 text-white"; case "Finished": return "bg-green-700 text-white"; case "Failed": return "bg-red-700 text-white"; default: return "bg-gray-700 text-white"; } }; const getQualityColorClass = (quality) => { switch (quality) { case "FLAC": return "bg-green-700 text-white"; case "Lossy": return "bg-yellow-700 text-white"; default: return "bg-gray-700 text-white"; } }; const statusBodyTemplate = (rowData) => ( {rowData.status} ); const qualityBodyTemplate = (rowData) => ( {rowData.quality} ); const safeText = (val) => (val === 0 ? "0" : val || "—"); const textWithEllipsis = (val, width = "12rem") => ( {val || "—"} ); const truncate = (text, maxLen) => maxLen <= 3 ? text.slice(0, maxLen) : text.length <= maxLen ? text : text.slice(0, maxLen - 3) + '...'; const basename = (p) => (typeof p === "string" ? p.split("/").pop() : ""); const formatProgress = (p) => { if (p === null || p === undefined || p === "") return "—"; const num = Number(p); if (Number.isNaN(num)) return "—"; const pct = num > 1 ? Math.round(num) : num; return `${pct}%`; }; const confirmDelete = (requestId) => { confirmDialog({ message: "Are you sure you want to delete this request?", header: "Confirm Delete", icon: "pi pi-exclamation-triangle", accept: () => deleteRequest(requestId), }); }; const deleteRequest = (requestId) => { setRequests((prev) => prev.filter((r) => r.id !== requestId)); toast.success("Request deleted"); }; const actionBodyTemplate = (rowData) => ( ); const handleRowClick = async (e) => { const detail = await fetchJobDetail(e.data.id); if (detail) { setSelectedRequest(detail); setIsDialogVisible(true); } }; return (