diff --git a/src/assets/styles/global.css b/src/assets/styles/global.css index 41c18d6..db8dc5e 100644 --- a/src/assets/styles/global.css +++ b/src/assets/styles/global.css @@ -30,8 +30,6 @@ html { @apply absolute invisible no-underline; margin-left: -1em; padding-right: 0.5em; - width: 80%; - max-width: 700px; cursor: pointer; } diff --git a/src/components/TRip/BreadcrumbNav.jsx b/src/components/TRip/BreadcrumbNav.jsx index c843603..1a83ccb 100644 --- a/src/components/TRip/BreadcrumbNav.jsx +++ b/src/components/TRip/BreadcrumbNav.jsx @@ -2,8 +2,8 @@ import React from "react"; export default function BreadcrumbNav({ currentPage }) { const pages = [ - { key: "request", label: "Request Media", href: "/qs2" }, - { key: "management", label: "Manage Requests", href: "/qs2/requests" }, + { key: "request", label: "Request Media", href: "/TRip" }, + { key: "management", label: "Manage Requests", href: "/TRip/requests" }, ]; return ( diff --git a/src/components/TRip/MediaRequestForm.jsx b/src/components/TRip/MediaRequestForm.jsx index bc27670..67cf062 100644 --- a/src/components/TRip/MediaRequestForm.jsx +++ b/src/components/TRip/MediaRequestForm.jsx @@ -400,7 +400,10 @@ export default function MediaRequestForm() { headers: { "Content-Type": "application/json; charset=utf-8", }, - body: JSON.stringify({ track_ids: allSelectedIds }), + body: JSON.stringify({ + track_ids: allSelectedIds, + target: selectedArtist.artist + }), }); if (!response.ok) { diff --git a/src/components/TRip/RequestManagement.jsx b/src/components/TRip/RequestManagement.jsx index 45632a4..bca355a 100644 --- a/src/components/TRip/RequestManagement.jsx +++ b/src/components/TRip/RequestManagement.jsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect, Suspense, lazy } from "react"; -import { toast } from 'react-toastify'; +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"; @@ -7,87 +7,120 @@ import { Button } from "@mui/joy"; import { Dialog } from "primereact/dialog"; import { confirmDialog, ConfirmDialog } from "primereact/confirmdialog"; import BreadcrumbNav from "./BreadcrumbNav"; +import { API_URL } from "@/config"; - -const STATUS_OPTIONS = ["Pending", "Completed", "Failed"]; -const TYPE_OPTIONS = ["Artist", "Album", "Track"]; - -const initialRequests = [ - { - id: 1, - type: "Artist", - artist: "Bring Me The Horizon", - album: "", - track: "", - status: "Pending", - details: { - requestedBy: "codey", - timestamp: "2025-07-01T12:00:00Z", - comments: "", - }, - }, - { - id: 2, - type: "Track", - artist: "We Butter The Bread With Butter", - album: "Das Album", - track: "20 km/h", - status: "Failed", - details: { - requestedBy: "codey", - timestamp: "2025-06-11T09:00:00Z", - comments: "Track not found in external database.", - }, - }, - { - id: 3, - type: "Track", - artist: "We Butter The Bread With Butter", - album: "Das Album", - track: "20 km/h", - status: "Completed", - details: { - requestedBy: "codey", - timestamp: "2025-06-11T09:00:00Z", - comments: "Track retrieved successfully.", - }, - }, -]; - +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(initialRequests); + const [requests, setRequests] = useState([]); const [filterType, setFilterType] = useState(null); const [filterStatus, setFilterStatus] = useState(null); - const [filteredRequests, setFilteredRequests] = useState(initialRequests); + const [filteredRequests, setFilteredRequests] = useState([]); const [selectedRequest, setSelectedRequest] = useState(null); const [isDialogVisible, setIsDialogVisible] = useState(false); + const pollingRef = useRef(null); + const pollingDetailRef = useRef(null); + const authFetch = async (url, options = {}) => fetch(url, { ...options, credentials: "include" }); - useEffect(() => { - let filtered = [...requests]; - if (filterType) { - filtered = filtered.filter((r) => r.type === filterType); - } - if (filterStatus) { - filtered = filtered.filter((r) => r.status === filterStatus); - } - setFilteredRequests(filtered); - }, [filterType, filterStatus, requests]); + const tarballUrl = (absPath) => { + if (!absPath) return null; + const filename = absPath.split("/").pop(); // get "SOMETHING.tar.gz" + return `${TAR_BASE_URL}${filename}`; + }; - const getStatusTextColor = (status) => { - switch (status) { - case "Pending": - return "text-yellow-500"; - case "Completed": - return "text-green-500"; - case "Failed": - return "text-red-500"; - default: - return "text-neutral-500"; + 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); + toast.error("Failed to fetch jobs list"); } }; + 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); + toast.error("Failed to fetch job details"); + 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(() => { + let filtered = [...requests]; + if (filterType) filtered = filtered.filter((r) => r.type === filterType); + if (filterStatus) filtered = filtered.filter((r) => r.status === filterStatus); + setFilteredRequests(filtered); + }, [filterType, filterStatus, requests]); + + const getStatusColorClass = (status) => { + switch (status) { + case "queued": return "bg-yellow-500 text-black"; + case "started": + case "compressing": return "bg-blue-500 text-white"; + case "finished": return "bg-green-500 text-white"; + case "failed": return "bg-red-500 text-white"; + default: return "bg-gray-500 text-white"; + } + }; + + const statusBodyTemplate = (rowData) => ( + + {rowData.status} + + ); + + const safeText = (val) => (val === 0 ? "0" : val || "—"); + const textWithEllipsis = (val, width = "12rem") => ( + {val || "—"} + ); + + 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) : Math.round(num * 100); + return `${pct}%`; + }; const confirmDelete = (requestId) => { confirmDialog({ @@ -103,391 +136,243 @@ export default function RequestManagement() { toast.success("Request deleted"); }; - const statusBodyTemplate = (rowData) => { - let colorClass = ""; + const actionBodyTemplate = (rowData) => ( + + ); - switch (rowData.status) { - case "Pending": - colorClass = "bg-yellow-300 text-yellow-900"; - break; - case "Completed": - colorClass = "bg-green-300 text-green-900"; - break; - case "Failed": - colorClass = "bg-red-300 text-red-900"; - break; - default: - colorClass = "bg-gray-300 text-gray-900"; - } - - return ( - - {rowData.status} - - ); - }; - - const actionBodyTemplate = (rowData) => { - return ( - - ); + const handleRowClick = async (e) => { + const detail = await fetchJobDetail(e.data.id); + if (detail) { setSelectedRequest(detail); setIsDialogVisible(true); } }; return ( -