Spaces:
Running
Running
"use client"; | |
import { useEffect, useState } from "react"; | |
import { | |
Table, | |
TableBody, | |
TableCell, | |
TableHead, | |
TableHeader, | |
TableRow, | |
} from "@/components/ui/table"; | |
import { Button } from "@/components/ui/button"; | |
import { DownloadIcon, EyeOpenIcon, TrashIcon } from "@radix-ui/react-icons"; | |
import { ChevronLeft, ChevronRight, Loader2 } from "lucide-react"; | |
import { formatDatetime } from "@/utils/formatDatetime"; | |
import { createClient } from "@/utils/supabase/client"; | |
import { toast } from "@/hooks/use-toast"; | |
interface FileTableProps { | |
fetchData: () => Promise<void>; | |
ragData: any[]; | |
} | |
export default function FileTable({ fetchData, ragData }: FileTableProps) { | |
const supabase = createClient(); | |
const [loadingMap, setLoadingMap] = useState<Record<string, boolean>>({}); | |
const [currentPage, setCurrentPage] = useState(1); | |
const [sortedData, setSortedData] = useState<any[]>([]); | |
const itemsPerPage = 10; // Adjust as needed | |
const pagesToShow = 2; // Number of page buttons to display at once | |
useEffect(() => { | |
// Sort data by created_at in descending order (newest first) | |
if (ragData && ragData.length > 0) { | |
const sorted = [...ragData].sort((a, b) => { | |
return ( | |
new Date(b.created_at).getTime() - new Date(a.created_at).getTime() | |
); | |
}); | |
setSortedData(sorted); | |
} else { | |
setSortedData([]); | |
} | |
}, [ragData]); | |
const downloadAllFiles = async () => { | |
try { | |
const res = await fetch("/api/download-all"); | |
if (!res.ok) { | |
throw new Error("Gagal mengunduh file ZIP"); | |
} | |
const blob = await res.blob(); | |
const url = URL.createObjectURL(blob); | |
const link = document.createElement("a"); | |
link.href = url; | |
link.download = "all-files.zip"; | |
document.body.appendChild(link); | |
link.click(); | |
document.body.removeChild(link); | |
URL.revokeObjectURL(url); | |
} catch (error) { | |
toast({ | |
title: "Gagal", | |
description: "Tidak dapat mengunduh semua file.", | |
variant: "destructive", | |
}); | |
} | |
}; | |
const deleteAllFiles = async () => { | |
const confirmed = window.confirm("Yakin ingin menghapus SEMUA file?"); | |
if (!confirmed) return; | |
try { | |
const fileNames = sortedData.map((item) => item.name); | |
setLoadingMap( | |
fileNames.reduce((acc, name) => ({ ...acc, [name]: true }), {}), | |
); | |
const { error } = await supabase.storage | |
.from("pnp-bot-storage") | |
.remove(fileNames); | |
if (error) { | |
toast({ | |
title: "Gagal menghapus semua file", | |
description: error.message, | |
variant: "destructive", | |
}); | |
} else { | |
toast({ | |
title: "Semua file berhasil dihapus", | |
description: `${fileNames.length} file telah dihapus.`, | |
}); | |
fetchData(); // refresh data | |
} | |
} catch (err) { | |
console.error("Gagal menghapus semua file:", err); | |
toast({ | |
title: "Terjadi kesalahan", | |
description: "Tidak dapat menghapus semua file.", | |
variant: "destructive", | |
}); | |
} finally { | |
setLoadingMap({}); | |
} | |
}; | |
const deleteItem = async (fileName: string) => { | |
const confirmed = window.confirm( | |
`Yakin ingin menghapus file "${fileName}"?`, | |
); | |
if (!confirmed) return; | |
try { | |
setLoadingMap((prev) => ({ ...prev, [fileName]: true })); | |
const { error } = await supabase.storage | |
.from("pnp-bot-storage") | |
.remove([fileName]); | |
if (error) { | |
toast({ | |
title: "Gagal menghapus file", | |
description: error.message, | |
variant: "destructive", | |
}); | |
} else { | |
toast({ | |
title: "File berhasil dihapus", | |
description: `File "${fileName}" telah dihapus.`, | |
}); | |
fetchData(); // refresh daftar file | |
} | |
} catch (err) { | |
console.error("Gagal menghapus:", err); | |
toast({ | |
title: "Terjadi kesalahan", | |
description: "Tidak dapat menghapus file.", | |
variant: "destructive", | |
}); | |
} finally { | |
setLoadingMap((prev) => ({ ...prev, [fileName]: false })); | |
} | |
}; | |
// Lihat File (Open in New Tab) | |
const inspectItem = (fileName: string) => { | |
const { data } = supabase.storage | |
.from("pnp-bot-storage") | |
.getPublicUrl(fileName); | |
if (!data?.publicUrl) { | |
toast({ | |
title: "Gagal membuka file", | |
description: `File "${fileName}" tidak memiliki URL publik.`, | |
variant: "destructive", | |
}); | |
return; | |
} | |
window.open(data.publicUrl, "_blank"); | |
}; | |
// Unduh File | |
const downloadItem = async (fileName: string) => { | |
try { | |
// Retrieve the file as a blob using the download method | |
const { data, error } = await supabase.storage | |
.from("pnp-bot-storage") // Use your bucket name | |
.download(fileName); | |
if (error) { | |
toast({ | |
title: "Gagal mengunduh file", | |
description: error.message || "Terjadi kesalahan saat mengunduh.", | |
variant: "destructive", | |
}); | |
return; | |
} | |
// Create a link element to download the file | |
const url = URL.createObjectURL(data); | |
const link = document.createElement("a"); | |
link.href = url; | |
link.download = fileName; | |
// Programmatically trigger the download | |
document.body.appendChild(link); | |
link.click(); | |
document.body.removeChild(link); | |
// Clean up the object URL | |
URL.revokeObjectURL(url); | |
toast({ | |
title: "Unduh berhasil", | |
description: `File "${fileName}" berhasil diunduh.`, | |
duration: 2000, | |
}); | |
} catch (err) { | |
console.error("Gagal mengunduh:", err); | |
toast({ | |
title: "Terjadi kesalahan", | |
description: "Tidak dapat mengunduh file.", | |
variant: "destructive", | |
}); | |
} | |
}; | |
// Calculate pagination | |
const totalPages = Math.ceil(sortedData.length / itemsPerPage); | |
const paginatedData = sortedData.slice( | |
(currentPage - 1) * itemsPerPage, | |
currentPage * itemsPerPage, | |
); | |
const goToNextPage = () => { | |
if (currentPage < totalPages) { | |
setCurrentPage(currentPage + 1); | |
} | |
}; | |
const goToPrevPage = () => { | |
if (currentPage > 1) { | |
setCurrentPage(currentPage - 1); | |
} | |
}; | |
const goToPage = (page: number) => { | |
setCurrentPage(page); | |
}; | |
// Calculate the range of page numbers to display | |
const startPage = Math.max(1, currentPage - Math.floor(pagesToShow / 2)); | |
const endPage = Math.min(totalPages, startPage + pagesToShow - 1); | |
const pageNumbers = Array.from( | |
{ length: endPage - startPage + 1 }, | |
(_, index) => startPage + index, | |
); | |
return ( | |
<div className="space-y-4"> | |
<Table> | |
<TableHeader className="bg-slate-100"> | |
<TableRow> | |
<TableHead className="text-center">#</TableHead> | |
<TableHead className="min-w-[240px]">Name</TableHead> | |
<TableHead>Uploaded At</TableHead> | |
<TableHead>File Size</TableHead> | |
<TableHead className="text-center"> | |
<div className="flex justify-center gap-2"> | |
<Button | |
size="sm" | |
variant="outline" | |
className="hover:bg-blue-600 hover:text-white" | |
onClick={downloadAllFiles} | |
> | |
<DownloadIcon className="mr-1 h-4 w-4" /> | |
All | |
</Button> | |
<Button | |
size="sm" | |
variant="destructive" | |
className="hover:bg-red-800" | |
onClick={deleteAllFiles} | |
> | |
<TrashIcon className="mr-1 h-4 w-4" /> | |
All | |
</Button> | |
</div> | |
</TableHead> | |
</TableRow> | |
</TableHeader> | |
<TableBody> | |
{paginatedData && paginatedData.length > 0 ? ( | |
paginatedData.map((item: any, index: number) => ( | |
<TableRow key={index}> | |
<TableCell className="text-center"> | |
{(currentPage - 1) * itemsPerPage + index + 1} | |
</TableCell> | |
<TableCell className="min-w-[240px] font-medium"> | |
{item.name} | |
</TableCell> | |
<TableCell>{formatDatetime(item.created_at)}</TableCell> | |
<TableCell>{item.metadata.size}</TableCell> | |
<TableCell className="flex justify-center gap-2"> | |
<Button | |
variant={"secondary"} | |
className="hover:bg-neutral-500 hover:text-white" | |
onClick={() => inspectItem(item.name)} | |
> | |
<EyeOpenIcon className="h-4 w-4" /> | |
</Button> | |
<Button | |
variant="outline" | |
className="hover:bg-blue-600 hover:text-white" | |
onClick={() => downloadItem(item.name)} | |
> | |
<DownloadIcon className="h-4 w-4" /> | |
</Button> | |
<Button | |
variant={"destructive"} | |
className="hover:bg-red-800" | |
onClick={() => deleteItem(item.name)} | |
> | |
{loadingMap[item.name] ? ( | |
<Loader2 className="h-4 w-4 animate-spin" /> | |
) : ( | |
<TrashIcon className="h-4 w-4" /> | |
)} | |
</Button> | |
</TableCell> | |
</TableRow> | |
)) | |
) : ( | |
<TableRow> | |
<TableCell colSpan={5} className="py-4 text-center"> | |
No Data Available | |
</TableCell> | |
</TableRow> | |
)} | |
</TableBody> | |
</Table> | |
{/* Pagination Controls */} | |
{sortedData.length > 0 && ( | |
<div className="flex items-center justify-between px-4 py-3"> | |
<div className="text-sm text-muted-foreground"> | |
Showing{" "} | |
<span className="font-medium"> | |
{(currentPage - 1) * itemsPerPage + 1} | |
</span>{" "} | |
to{" "} | |
<span className="font-medium"> | |
{Math.min(currentPage * itemsPerPage, sortedData.length)} | |
</span>{" "} | |
of <span className="font-medium">{sortedData.length}</span> files | |
</div> | |
<div className="flex items-center space-x-2"> | |
<Button | |
variant="outline" | |
size="sm" | |
onClick={goToPrevPage} | |
disabled={currentPage === 1} | |
className="px-3" | |
> | |
<ChevronLeft className="h-4 w-4" /> | |
</Button> | |
{/* Always show first page */} | |
<Button | |
variant="outline" | |
size="sm" | |
onClick={() => goToPage(1)} | |
className={currentPage === 1 ? "bg-blue-500 text-white" : ""} | |
disabled={currentPage === 1} | |
> | |
1 | |
</Button> | |
{/* Show "..." if current page is far from start */} | |
{currentPage > 3 && <span className="px-2">...</span>} | |
{/* Dynamic page numbers (middle range) */} | |
<div className="flex space-x-1"> | |
{Array.from({ length: Math.min(3, totalPages - 2) }, (_, i) => { | |
let page; | |
if (currentPage <= 2) | |
page = i + 2; // Near start: 2, 3, 4 | |
else if (currentPage >= totalPages - 1) | |
page = totalPages - 2 + i; // Near end | |
else page = currentPage - 1 + i; // Middle range | |
if (page > 1 && page < totalPages) { | |
return ( | |
<Button | |
key={page} | |
variant="outline" | |
size="sm" | |
className={ | |
currentPage === page ? "bg-blue-500 text-white" : "" | |
} | |
onClick={() => goToPage(page)} | |
> | |
{page} | |
</Button> | |
); | |
} | |
return null; | |
})} | |
</div> | |
{/* Show "..." if current page is far from end */} | |
{currentPage < totalPages - 2 && <span className="px-2">...</span>} | |
{/* Always show last page (if different from first) */} | |
{totalPages > 1 && ( | |
<Button | |
variant="outline" | |
size="sm" | |
onClick={() => goToPage(totalPages)} | |
disabled={currentPage === totalPages} | |
className={ | |
currentPage === totalPages ? "bg-blue-500 text-white" : "" | |
} | |
> | |
{totalPages} | |
</Button> | |
)} | |
<Button | |
variant="outline" | |
size="sm" | |
onClick={goToNextPage} | |
disabled={currentPage === totalPages || totalPages === 0} | |
className="px-3" | |
> | |
<ChevronRight className="h-4 w-4" /> | |
</Button> | |
</div> | |
</div> | |
)} | |
</div> | |
); | |
} | |