import {useEffect, useMemo, useState} from 'react'; import {useNavigate} from 'react-router-dom'; import {Badge} from '@/components/ui/badge'; import {Button} from '@/components/ui/button'; import {Checkbox} from '@/components/ui/checkbox'; import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@/components/ui/table'; import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from '@/components/ui/tooltip'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import {ChevronLeft, ChevronRight, Download, Eye, FileIcon, FolderIcon, Loader2, MoreVertical, Trash2} from 'lucide-react'; import {Select, SelectOption} from '@/components/ui/select'; import {formatBytes, formatRelativeTime} from '@/lib/file-utils'; import type {S3Object} from '@/types'; interface ObjectsTableProps { bucketName: string; objects: S3Object[]; currentPath: string; searchQuery: string; selectedFileKeys: Set; isDragActive: boolean; isLoading?: boolean; isTruncated?: boolean; nextContinuationToken?: string; itemsPerPage: number; onNavigateToFolder: (key: string) => void; onDeleteObject: (object: S3Object) => void; onToggleFileSelection: (key: string) => void; onSelectAllFiles: () => void; onPageChange: (token?: string) => void; onItemsPerPageChange: (count: number) => void; initialPageToken?: string; initialItemsPerPage?: number; } type SortColumn = 'name' | 'size' | 'modified'; type SortDirection = 'asc' | 'desc'; export function ObjectsTable({ bucketName, objects, currentPath, searchQuery, selectedFileKeys, isDragActive, isLoading = false, isTruncated = false, nextContinuationToken, itemsPerPage, onNavigateToFolder, onDeleteObject, onToggleFileSelection, onSelectAllFiles, onPageChange, onItemsPerPageChange, initialPageToken, initialItemsPerPage, }: ObjectsTableProps) { const navigate = useNavigate(); const [sortColumn, setSortColumn] = useState('name'); const [sortDirection, setSortDirection] = useState('asc'); // Store tokens for each page: [undefined (page 1), token1 (page 2), token2 (page 3), ...] const [pageTokens, setPageTokens] = useState<(string | undefined)[]>([undefined]); const [currentPageIndex, setCurrentPageIndex] = useState(0); const [initialized, setInitialized] = useState(false); // Initialize from URL params on first load useEffect(() => { if (!initialized && initialItemsPerPage && initialItemsPerPage !== itemsPerPage) { onItemsPerPageChange(initialItemsPerPage); setInitialized(true); } if (!initialized && initialPageToken && initialPageToken !== nextContinuationToken) { // If we have an initial page token, trigger page change onPageChange(initialPageToken); setInitialized(true); } if (!initialized && !initialPageToken && !initialItemsPerPage) { setInitialized(true); } }, [initialized, initialPageToken, initialItemsPerPage, itemsPerPage, nextContinuationToken, onPageChange, onItemsPerPageChange]); const filteredObjects = useMemo(() => { const query = searchQuery.toLowerCase(); const filtered = objects.filter((obj) => obj.key.toLowerCase().includes(query)); return [...filtered].sort((a, b) => { const aIsFolder = a.isFolder ? 1 : 0; const bIsFolder = b.isFolder ? 1 : 0; if (aIsFolder !== bIsFolder) return bIsFolder - aIsFolder; let compareValue = 0; switch (sortColumn) { case 'name': { const aName = a.key.replace(currentPath, '').replace('/', '').toLowerCase(); const bName = b.key.replace(currentPath, '').replace('/', '').toLowerCase(); compareValue = aName.localeCompare(bName); break; } case 'size': compareValue = a.size - b.size; break; case 'modified': { const aDate = new Date(a.lastModified).getTime(); const bDate = new Date(b.lastModified).getTime(); compareValue = aDate - bDate; break; } } return sortDirection === 'asc' ? compareValue : -compareValue; }); }, [objects, searchQuery, sortColumn, sortDirection, currentPath]); // Effect 2: Reset pagination ONLY on path navigation useEffect(() => { setPageTokens([undefined]); setCurrentPageIndex(0); }, [currentPath]); // Update page tokens when we get a new next token useEffect(() => { if (nextContinuationToken && isTruncated) { setPageTokens(prev => { const newTokens = [...prev]; // Only add the token if we don't have it yet const nextIndex = currentPageIndex + 1; if (nextIndex >= newTokens.length) { newTokens[nextIndex] = nextContinuationToken; } return newTokens; }); } }, [nextContinuationToken, isTruncated, currentPageIndex]); const hasPrevious = currentPageIndex > 0; const hasNext = isTruncated; const handleNextPage = () => { if (hasNext && nextContinuationToken) { const nextIndex = currentPageIndex + 1; setCurrentPageIndex(nextIndex); onPageChange(nextContinuationToken); window.scrollTo({ top: 0, behavior: 'smooth' }); } }; const handlePreviousPage = () => { if (hasPrevious) { const prevIndex = currentPageIndex - 1; setCurrentPageIndex(prevIndex); const previousToken = pageTokens[prevIndex]; onPageChange(previousToken); window.scrollTo({ top: 0, behavior: 'smooth' }); } }; const handleItemsPerPageChange = (value: string) => { onItemsPerPageChange(Number(value)); setPageTokens([undefined]); // Reset to first page setCurrentPageIndex(0); }; const handleSort = (column: SortColumn) => { if (sortColumn === column) { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); } else { setSortColumn(column); setSortDirection('asc'); } }; return ( <>
!obj.isFolder).length > 0 && selectedFileKeys.size === filteredObjects.filter(obj => !obj.isFolder).length } onCheckedChange={onSelectAllFiles} aria-label="Select all files" /> handleSort('name')} > Objects {sortColumn === 'name' && (sortDirection === 'asc' ? '↑' : '↓')} Type Storage Class handleSort('size')} > Size {sortColumn === 'size' && (sortDirection === 'asc' ? '↑' : '↓')} handleSort('modified')} > Modified {sortColumn === 'modified' && (sortDirection === 'asc' ? '↑' : '↓')} {isLoading ? (
Loading objects...
) : filteredObjects.length === 0 ? ( {searchQuery ? 'No objects found matching your search' : isDragActive ? 'Drop files or folders here' : 'No objects in this location'} ) : ( filteredObjects.map((obj) => ( {obj.isFolder ? ( ) : ( onToggleFileSelection(obj.key)} aria-label={`Select file ${obj.key}`} /> )}
{obj.isFolder ? ( ) : ( )} {obj.isFolder ? ( ) : ( )}
{obj.isFolder ? 'Directory' : (obj.contentType || 'application/octet-stream')} {obj.storageClass && ( {obj.storageClass} )} {obj.isFolder ? null : formatBytes(obj.size)} {obj.lastModified ? (() => { const d = new Date(obj.lastModified); return (
{d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric', })} {d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, })} CET
UTC {d.toLocaleString('en-GB', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZone: 'UTC', })} UTC
Relative {formatRelativeTime(d)}
Timestamp {d.toISOString()}
); })() : null}
{!obj.isFolder && ( navigate(`/buckets/${bucketName}/objects/${encodeURIComponent(obj.key)}`)}> View Details Download onDeleteObject(obj)} > Delete )}
)) )}
{/* Pagination Controls */} {(filteredObjects.length > 0 || hasPrevious) && (
{/* Items per page selector */}
Items per page:
{/* Pagination info and controls */}
Page {currentPageIndex + 1} • Showing {filteredObjects.length} item{filteredObjects.length !== 1 ? 's' : ''}
)} ); }