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"; import "./RequestManagement.css"; 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 [isLoading, setIsLoading] = useState(true); 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 (showLoading = true) => { try { if (showLoading) setIsLoading(true); 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', }); } } finally { setIsLoading(false); } }; 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 computePct = (p) => { if (p === null || p === undefined || p === "") return 0; const num = Number(p); if (Number.isNaN(num)) return 0; return Math.min(100, Math.max(0, num > 1 ? Math.round(num) : Math.round(num * 100))); }; const progressBarTemplate = (rowData) => { const p = rowData.progress; if (p === null || p === undefined || p === "") return "—"; const num = Number(p); if (Number.isNaN(num)) return "—"; const pct = computePct(p); const getProgressColor = () => { if (rowData.status === "Failed") return "bg-red-500"; if (rowData.status === "Finished") return "bg-green-500"; if (pct < 30) return "bg-blue-400"; if (pct < 70) return "bg-blue-500"; return "bg-blue-600"; }; 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 (

Manage Requests

({ label: s, value: s }))]} onChange={(e) => setFilterStatus(e.value)} placeholder="Filter by Status" className="min-w-[180px]" />
{isLoading ? (
{[...Array(5)].map((_, i) => (
))}
) : (

No requests found

Requests you submit will appear here

} onRowClick={handleRowClick} resizableColumns={false} className="w-full" style={{ width: '100%' }} > ( {row.id.split("-").slice(-1)[0]} )} /> textWithEllipsis(row.target, "100%")} /> row.tracks} /> Tarball } body={(row) => { const url = tarballUrl(row.tarball, row.quality || "FLAC"); const encodedURL = encodeURI(url); if (!url) return "—"; const fileName = url.split("/").pop(); return ( {truncate(fileName, 28)} ); }} />
)} setIsDialogVisible(false)} breakpoints={{ "960px": "95vw" }} modal dismissableMask className="dark:bg-neutral-900 dark:text-neutral-100" > {selectedRequest ? (
{/* --- Metadata Card --- */}
{selectedRequest.id &&

ID: {selectedRequest.id}

} {selectedRequest.target &&

Target: {selectedRequest.target}

} {selectedRequest.tracks &&

# Tracks: {selectedRequest.tracks}

} {selectedRequest.quality && (

Quality:{" "} {selectedRequest.quality}

)}
{/* --- Status / Progress Card --- */}
{selectedRequest.status && (

Status:{" "} {selectedRequest.status}

)} {selectedRequest.progress !== undefined && selectedRequest.progress !== null && (
Progress:
= 100 ? '999px' : 0, borderBottomRightRadius: computePct(selectedRequest.progress) >= 100 ? '999px' : 0 }} data-pct={computePct(selectedRequest.progress)} aria-valuenow={Math.min(100, Math.max(0, Number(selectedRequest.progress) > 1 ? Math.round(selectedRequest.progress) : selectedRequest.progress * 100))} aria-valuemin={0} aria-valuemax={100} />
{formatProgress(selectedRequest.progress)}
)}
{/* --- Timestamps Card --- */}
{selectedRequest.enqueued_at &&

Enqueued: {new Date(selectedRequest.enqueued_at).toLocaleString()}

} {selectedRequest.started_at &&

Started: {new Date(selectedRequest.started_at).toLocaleString()}

} {selectedRequest.ended_at &&

Ended: {new Date(selectedRequest.ended_at).toLocaleString()}

}
{/* --- Tarball Card --- */} { selectedRequest.tarball && ( ) }
) : (

Loading...

) }
); }