misc
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) => (
|
||||
<span className={`inline-block px-3 py-1 rounded-full font-semibold text-sm ${getStatusColorClass(rowData.status)}`}>
|
||||
{rowData.status}
|
||||
</span>
|
||||
);
|
||||
|
||||
const safeText = (val) => (val === 0 ? "0" : val || "—");
|
||||
const textWithEllipsis = (val, width = "12rem") => (
|
||||
<span className="truncate block" style={{ maxWidth: width }} title={val || ""}>{val || "—"}</span>
|
||||
);
|
||||
|
||||
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) => (
|
||||
<Button
|
||||
color="neutral"
|
||||
variant="outlined"
|
||||
size="sm"
|
||||
sx={{
|
||||
color: "#e5e7eb",
|
||||
borderColor: "#6b7280",
|
||||
'&:hover': { backgroundColor: '#374151', borderColor: '#9ca3af' },
|
||||
}}
|
||||
onClick={(e) => { e.stopPropagation(); confirmDelete(rowData.id); }}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
|
||||
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 (
|
||||
<span
|
||||
className={`inline-block px-3 py-1 rounded-full font-semibold text-sm ${colorClass}`}
|
||||
aria-label={`Status: ${rowData.status}`}
|
||||
>
|
||||
{rowData.status}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const actionBodyTemplate = (rowData) => {
|
||||
return (
|
||||
<Button
|
||||
color="danger"
|
||||
size="sm"
|
||||
onClick={() => confirmDelete(rowData.id)}
|
||||
variant="outlined"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
const handleRowClick = async (e) => {
|
||||
const detail = await fetchJobDetail(e.data.id);
|
||||
if (detail) { setSelectedRequest(detail); setIsDialogVisible(true); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-white dark:bg-neutral-900 dark:text-neutral-100 rounded-xl shadow-md border border-neutral-200 dark:border-neutral-700 p-6 space-y-6"
|
||||
style={{ display: "inline-block" }}
|
||||
>
|
||||
<style>{`
|
||||
.p-datatable {
|
||||
background-color: white;
|
||||
color: #1a1a1a;
|
||||
border-color: #ccc;
|
||||
}
|
||||
.p-datatable-header {
|
||||
background-color: #f9f9f9;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.p-datatable-thead > tr > th {
|
||||
background-color: #f0f0f0;
|
||||
color: #222;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.p-datatable-tbody > tr > td {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* Dark mode improvements with brighter text */
|
||||
[data-theme="dark"] .p-datatable {
|
||||
background-color: #121212;
|
||||
color: #f0f0f0; /* brighter text */
|
||||
border-color: #333;
|
||||
<div className="w-max my-10 p-6 rounded-xl shadow-md
|
||||
bg-white dark:bg-neutral-900
|
||||
text-neutral-900 dark:text-neutral-100
|
||||
border border-neutral-200 dark:border-neutral-700
|
||||
sm:p-4 md:p-6">
|
||||
<style>{`
|
||||
/* Table and Dark Overrides */
|
||||
.p-datatable {
|
||||
table-layout: fixed !important;
|
||||
}
|
||||
[data-theme="dark"] .p-datatable-header {
|
||||
background-color: #1f1f1f;
|
||||
color: #f5f5f5; /* brighter */
|
||||
border-bottom: 1px solid #444;
|
||||
.p-datatable td span.truncate {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
[data-theme="dark"] .p-datatable {
|
||||
background-color: #121212 !important;
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
[data-theme="dark"] .p-datatable-thead > tr > th {
|
||||
background-color: #222222;
|
||||
color: #f5f5f5; /* brighter */
|
||||
border-bottom: 1px solid #555;
|
||||
background-color: #1f1f1f !important;
|
||||
color: #e5e7eb !important;
|
||||
border-bottom: 1px solid #374151;
|
||||
}
|
||||
[data-theme="dark"] .p-datatable-tbody > tr {
|
||||
background-color: #1a1a1a;
|
||||
border-bottom: 1px solid #333;
|
||||
transition: background-color 0.2s ease;
|
||||
color: #f0f0f0; /* brighter text */
|
||||
background-color: #1a1a1a !important;
|
||||
border-bottom: 1px solid #374151;
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
/* Zebra stripes */
|
||||
[data-theme="dark"] .p-datatable-tbody > tr:nth-child(odd) {
|
||||
background-color: #181818;
|
||||
color: #f0f0f0;
|
||||
background-color: #222 !important;
|
||||
}
|
||||
/* Hover effect */
|
||||
[data-theme="dark"] .p-datatable-tbody > tr:hover {
|
||||
background-color: #333333;
|
||||
cursor: pointer;
|
||||
color: #fff; /* brightest on hover */
|
||||
}
|
||||
|
||||
/* Dropdown inside dark mode */
|
||||
[data-theme="dark"] .p-dropdown,
|
||||
[data-theme="dark"] .p-dropdown-label,
|
||||
[data-theme="dark"] .p-dropdown-panel,
|
||||
[data-theme="dark"] .p-dropdown-item {
|
||||
background-color: #2a2a2a !important;
|
||||
color: #f0f0f0 !important; /* brighter */
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
[data-theme="dark"] .p-button {
|
||||
background-color: transparent !important;
|
||||
color: #f0f0f0 !important; /* brighter */
|
||||
border-color: #555 !important;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
[data-theme="dark"] .p-button:hover {
|
||||
background-color: #555 !important;
|
||||
background-color: #333 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
[data-theme="dark"] .p-button.p-button-danger {
|
||||
border-color: #ff4d4f !important;
|
||||
color: #ff4d4f !important;
|
||||
}
|
||||
[data-theme="dark"] .p-button.p-button-danger:hover {
|
||||
background-color: #ff4d4f !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
/* ===== PrimeReact Paginator Light Mode ===== */
|
||||
.p-paginator {
|
||||
background-color: #fff;
|
||||
color: #1a1a1a;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
.p-paginator .p-paginator-page,
|
||||
.p-paginator .p-paginator-next,
|
||||
.p-paginator .p-paginator-prev {
|
||||
color: #1a1a1a;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
.p-paginator .p-highlight {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* ===== PrimeReact Paginator Dark Mode ===== */
|
||||
/* Paginator Dark Mode */
|
||||
[data-theme="dark"] .p-paginator {
|
||||
background-color: #121212;
|
||||
color: #f0f0f0;
|
||||
border-top: 1px solid #333;
|
||||
background-color: #121212 !important;
|
||||
color: #e5e7eb !important;
|
||||
border-top: 1px solid #374151 !important;
|
||||
}
|
||||
[data-theme="dark"] .p-paginator .p-paginator-page,
|
||||
[data-theme="dark"] .p-paginator .p-paginator-next,
|
||||
[data-theme="dark"] .p-paginator .p-paginator-prev {
|
||||
color: #ccc;
|
||||
border: none;
|
||||
background: transparent;
|
||||
[data-theme="dark"] .p-paginator .p-paginator-prev,
|
||||
[data-theme="dark"] .p-paginator .p-paginator-first,
|
||||
[data-theme="dark"] .p-paginator .p-paginator-last {
|
||||
color: #e5e7eb !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
[data-theme="dark"] .p-paginator .p-paginator-page:hover,
|
||||
[data-theme="dark"] .p-paginator .p-paginator-next:hover,
|
||||
[data-theme="dark"] .p-paginator .p-paginator-prev:hover {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
background-color: #374151 !important;
|
||||
color: #fff !important;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
[data-theme="dark"] .p-paginator .p-highlight {
|
||||
background-color: #3b82f6; /* Tailwind blue-500 */
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
background-color: #6b7280 !important;
|
||||
color: #fff !important;
|
||||
border-radius: 0.25rem !important;
|
||||
}
|
||||
.p-paginator .p-paginator-first,
|
||||
.p-paginator .p-paginator-last {
|
||||
color: #1a1a1a;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
.p-paginator .p-paginator-first:hover,
|
||||
.p-paginator .p-paginator-last:hover {
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Dark Mode: First/Last page buttons */
|
||||
[data-theme="dark"] .p-paginator .p-paginator-first,
|
||||
[data-theme="dark"] .p-paginator .p-paginator-last {
|
||||
color: #ccc;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
[data-theme="dark"] .p-paginator .p-paginator-first:hover,
|
||||
[data-theme="dark"] .p-paginator .p-paginator-last:hover {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
/* ConfirmDialog - Light Theme */
|
||||
.p-confirm-dialog {
|
||||
background-color: #ffffff;
|
||||
color: #1a1a1a;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
.p-confirm-dialog .p-dialog-header {
|
||||
background-color: #f5f5f5;
|
||||
color: #222;
|
||||
}
|
||||
.p-confirm-dialog .p-dialog-content {
|
||||
background-color: #ffffff;
|
||||
color: #333;
|
||||
}
|
||||
.p-confirm-dialog .p-dialog-footer {
|
||||
background-color: #fafafa;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
.p-confirm-dialog .p-button {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* ConfirmDialog - Dark Theme */
|
||||
[data-theme='dark'] .p-confirm-dialog {
|
||||
background-color: #2a2a2a;
|
||||
color: #e5e5e5;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
[data-theme='dark'] .p-confirm-dialog .p-dialog-header {
|
||||
background-color: #1f1f1f;
|
||||
color: #ddd;
|
||||
}
|
||||
[data-theme='dark'] .p-confirm-dialog .p-dialog-content {
|
||||
background-color: #2a2a2a;
|
||||
color: #ccc;
|
||||
}
|
||||
[data-theme='dark'] .p-confirm-dialog .p-dialog-footer {
|
||||
background-color: #242424;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
[data-theme='dark'] .p-confirm-dialog .p-button {
|
||||
background-color: transparent !important;
|
||||
color: #ddd !important;
|
||||
border-color: #555 !important;
|
||||
}
|
||||
[data-theme='dark'] .p-confirm-dialog .p-button.p-button-danger {
|
||||
color: #ff4d4f !important;
|
||||
border-color: #ff4d4f !important;
|
||||
}
|
||||
[data-theme='dark'] .p-dialog-title {
|
||||
color: #eee;
|
||||
}
|
||||
[data-theme='dark'] .p-dialog-header-icon {
|
||||
color: #ccc;
|
||||
}
|
||||
/* PrimeReact Dialog Theming */
|
||||
.p-dialog {
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
|
||||
/* Dark mode for PrimeReact Dialog */
|
||||
[data-theme="dark"] .p-dialog {
|
||||
background-color: #1e1e1e;
|
||||
color: #ffffff;
|
||||
background-color: #1a1a1a !important;
|
||||
color: #e5e7eb !important;
|
||||
border-color: #374151 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .p-dialog .p-dialog-header {
|
||||
background-color: #1e1e1e;
|
||||
border-bottom: 1px solid #444;
|
||||
background-color: #121212 !important;
|
||||
color: #e5e7eb !important;
|
||||
border-bottom: 1px solid #374151 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .p-dialog .p-dialog-content {
|
||||
background-color: #2a2a2a;
|
||||
color: #ffffff;
|
||||
background-color: #1a1a1a !important;
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .p-dialog .p-dialog-footer {
|
||||
background-color: #1e1e1e;
|
||||
border-top: 1px solid #444;
|
||||
background-color: #121212 !important;
|
||||
border-top: 1px solid #374151 !important;
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
|
||||
`}</style>
|
||||
<BreadcrumbNav currentPage="management" />
|
||||
<h2 className="text-3xl font-semibold" style={{ marginTop: 0 }}>
|
||||
Media Request Management
|
||||
</h2>
|
||||
<h2 className="text-3xl font-semibold mt-0">Media Request Management</h2>
|
||||
|
||||
<div className="flex flex-wrap gap-6 mb-6">
|
||||
<Dropdown
|
||||
value={filterType}
|
||||
options={[
|
||||
{ label: "All Types", value: null },
|
||||
...TYPE_OPTIONS.map((t) => ({ label: t, value: t })),
|
||||
]}
|
||||
onChange={(e) => setFilterType(e.value)}
|
||||
placeholder="Filter by Type"
|
||||
className="min-w-[180px]"
|
||||
/>
|
||||
<Dropdown
|
||||
value={filterStatus}
|
||||
options={[
|
||||
{ label: "All Statuses", value: null },
|
||||
...STATUS_OPTIONS.map((s) => ({ label: s, value: s })),
|
||||
]}
|
||||
options={[{ label: "All Statuses", value: null }, ...STATUS_OPTIONS.map((s) => ({ label: s, value: s }))]}
|
||||
onChange={(e) => setFilterStatus(e.value)}
|
||||
placeholder="Filter by Status"
|
||||
className="min-w-[180px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
value={filteredRequests}
|
||||
paginator
|
||||
rows={10}
|
||||
removableSort
|
||||
sortMode="multiple"
|
||||
emptyMessage="No requests found."
|
||||
rowClassName="!py-4"
|
||||
className="min-w-full bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100"
|
||||
onRowClick={(e) => {
|
||||
setSelectedRequest(e.data);
|
||||
setIsDialogVisible(true);
|
||||
}}
|
||||
>
|
||||
<Column field="id" header="ID" sortable style={{ width: "5rem" }} />
|
||||
<Column field="type" header="Type" sortable />
|
||||
<Column field="artist" header="Artist" sortable />
|
||||
<Column field="album" header="Album" sortable />
|
||||
<Column field="track" header="Track" sortable />
|
||||
<Column
|
||||
field="status"
|
||||
header="Status"
|
||||
body={statusBodyTemplate}
|
||||
style={{ width: "10rem", textAlign: "center" }}
|
||||
sortable
|
||||
/>
|
||||
<Column
|
||||
body={actionBodyTemplate}
|
||||
header="Actions"
|
||||
style={{ width: "9rem", textAlign: "center" }}
|
||||
/>
|
||||
</DataTable>
|
||||
<div className="w-max overflow-x-auto rounded-lg">
|
||||
<DataTable
|
||||
value={filteredRequests}
|
||||
paginator
|
||||
rows={10}
|
||||
removableSort
|
||||
sortMode="multiple"
|
||||
emptyMessage="No requests found."
|
||||
responsiveLayout="scroll"
|
||||
onRowClick={handleRowClick}
|
||||
>
|
||||
<Column field="target" header="Target" sortable style={{ width: "12rem" }} body={(row) => textWithEllipsis(row.target, "10rem")} />
|
||||
<Column field="status" header="Status" body={statusBodyTemplate} style={{ width: "10rem", textAlign: "center" }} sortable />
|
||||
<Column field="progress" header="Progress" body={(row) => formatProgress(row.progress)} style={{ width: "8rem", textAlign: "center" }} sortable />
|
||||
<Column
|
||||
field="tarball"
|
||||
header="Tarball"
|
||||
body={(row) => {
|
||||
const url = tarballUrl(row.tarball);
|
||||
return url ? (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:underline truncate block"
|
||||
title={url.split("/").pop()}
|
||||
>
|
||||
{url.split("/").pop()}
|
||||
</a>
|
||||
) : (
|
||||
"—"
|
||||
);
|
||||
}}
|
||||
style={{ width: "18rem" }}
|
||||
/>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog />
|
||||
|
||||
<Dialog
|
||||
header="Request Details"
|
||||
visible={isDialogVisible}
|
||||
style={{ width: "500px" }}
|
||||
onHide={() => setIsDialogVisible(false)}
|
||||
breakpoints={{ '960px': '95vw' }}
|
||||
breakpoints={{ "960px": "95vw" }}
|
||||
modal
|
||||
dismissableMask
|
||||
className="dark:bg-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
{selectedRequest && (
|
||||
{selectedRequest ? (
|
||||
<div className="space-y-4 text-sm">
|
||||
<p><strong>ID:</strong> {selectedRequest.id}</p>
|
||||
<p><strong>Type:</strong> {selectedRequest.type}</p>
|
||||
<p><strong>Artist:</strong> {selectedRequest.artist}</p>
|
||||
{selectedRequest.album && <p><strong>Album:</strong> {selectedRequest.album}</p>}
|
||||
{selectedRequest.track && <p><strong>Track:</strong> {selectedRequest.track}</p>}
|
||||
<p className="text-sm flex items-center gap-2">
|
||||
<span className="font-semibold">Status:</span>
|
||||
<p><strong>Target:</strong> {selectedRequest.target}</p>
|
||||
|
||||
<p>
|
||||
<strong>Status:</strong>{" "}
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded-full text-xs font-bold ${selectedRequest.status === "Pending"
|
||||
? "bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100"
|
||||
: selectedRequest.status === "Completed"
|
||||
? "bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100"
|
||||
: "bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100"
|
||||
}`}
|
||||
className={`px-2 py-0.5 rounded-full text-xs font-bold ${getStatusColorClass(selectedRequest.status)}`}
|
||||
>
|
||||
{selectedRequest.status}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{selectedRequest.progress !== undefined && selectedRequest.progress !== null && (
|
||||
<p><strong>Progress:</strong> {formatProgress(selectedRequest.progress)}</p>
|
||||
)}
|
||||
|
||||
{selectedRequest.details && (
|
||||
<>
|
||||
<p><strong>Requested By:</strong> {selectedRequest.details.requestedBy}</p>
|
||||
<p><strong>Timestamp:</strong> {new Date(selectedRequest.details.timestamp).toLocaleString()}</p>
|
||||
{selectedRequest.details.comments && (
|
||||
<p><strong>Comments:</strong> {selectedRequest.details.comments}</p>
|
||||
)}
|
||||
</>
|
||||
{selectedRequest.enqueued_at && (
|
||||
<p><strong>Enqueued:</strong> {new Date(selectedRequest.enqueued_at).toLocaleString()}</p>
|
||||
)}
|
||||
{selectedRequest.started_at && (
|
||||
<p><strong>Started:</strong> {new Date(selectedRequest.started_at).toLocaleString()}</p>
|
||||
)}
|
||||
{selectedRequest.ended_at && (
|
||||
<p><strong>Ended:</strong> {new Date(selectedRequest.ended_at).toLocaleString()}</p>
|
||||
)}
|
||||
|
||||
{selectedRequest.tarball && (
|
||||
<p>
|
||||
<strong>Tarball:</strong>{" "}
|
||||
<a
|
||||
href={tarballUrl(selectedRequest.tarball)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
{tarballUrl(selectedRequest.tarball).split("/").pop()}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{Array.isArray(selectedRequest.tracks) && selectedRequest.tracks.length > 0 && (
|
||||
<div>
|
||||
<strong>Tracks:</strong>
|
||||
<ul className="list-disc pl-5">
|
||||
{selectedRequest.tracks.map((t, idx) => {
|
||||
if (t && typeof t === "object") {
|
||||
const tid = "track_id" in t ? t.track_id : t.id ?? idx;
|
||||
const st = t.status ?? "—";
|
||||
return <li key={idx}>{`${tid} — ${st}`}</li>;
|
||||
}
|
||||
return <li key={idx}>{String(t)}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p>Loading...</p>
|
||||
)}
|
||||
</Dialog>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user