Spaces:
Sleeping
Sleeping
| "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> | |
| ); | |
| } | |