import React, { useState, useEffect } from 'react'; import { Box, Typography, Card, CardContent, Button, Chip, IconButton, TextField, InputAdornment, Stack, CircularProgress, Alert, Pagination, FormControl, InputLabel, Select, Dialog, DialogTitle, DialogContent, DialogActions, Checkbox, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Tooltip, MenuItem, Breadcrumbs, Link, } from '@mui/material'; import { Search as SearchIcon, FilterList as FilterIcon, Delete as DeleteIcon, Block as BlockIcon, Refresh as RefreshIcon, Folder as FolderIcon, Cloud as CloudIcon, Computer as ComputerIcon, Storage as StorageIcon, CalendarToday as DateIcon, ArrowBack as ArrowBackIcon, RestoreFromTrash as RestoreFromTrashIcon, } from '@mui/icons-material'; import { format, formatDistanceToNow } from 'date-fns'; import { useNotifications } from '../contexts/NotificationContext'; import { useNavigate, useSearchParams } from 'react-router-dom'; interface IgnoredFile { id: string; file_hash: string; filename: string; original_filename: string; file_path: string; file_size: number; mime_type: string; source_type?: string; source_path?: string; source_identifier?: string; ignored_at: string; ignored_by: string; ignored_by_username?: string; reason?: string; created_at: string; } interface IgnoredFilesStats { total_ignored_files: number; by_source_type: Array<{ source_type?: string; count: number; total_size_bytes: number; }>; total_size_bytes: number; most_recent_ignored_at?: string; } const IgnoredFilesPage: React.FC = () => { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const [ignoredFiles, setIgnoredFiles] = useState([]); const [stats, setStats] = useState(null); const [sources, setSources] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [searchTerm, setSearchTerm] = useState(''); const [sourceTypeFilter, setSourceTypeFilter] = useState(''); const [selectedFiles, setSelectedFiles] = useState>(new Set()); const [bulkDeleteDialog, setBulkDeleteDialog] = useState(false); const [deletingFiles, setDeletingFiles] = useState(false); const { addNotification } = useNotifications(); // URL parameters for filtering const sourceTypeParam = searchParams.get('sourceType'); const sourceNameParam = searchParams.get('sourceName'); const sourceIdParam = searchParams.get('sourceId'); const pageSize = 25; const fetchIgnoredFiles = async () => { setLoading(true); setError(null); try { const token = localStorage.getItem('token'); if (!token) { throw new Error('No authentication token found'); } const params = new URLSearchParams({ limit: pageSize.toString(), offset: ((page - 1) * pageSize).toString(), }); if (searchTerm) { params.append('filename', searchTerm); } if (sourceTypeFilter) { params.append('source_type', sourceTypeFilter); } if (sourceIdParam) { params.append('source_identifier', sourceIdParam); } const response = await fetch(`/api/ignored-files?${params}`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error(`Failed to fetch ignored files: ${response.statusText}`); } const data = await response.json(); setIgnoredFiles(data.ignored_files); setTotalPages(Math.ceil(data.total / pageSize)); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load ignored files'); console.error('Error fetching ignored files:', err); } finally { setLoading(false); } }; const fetchStats = async () => { try { const token = localStorage.getItem('token'); if (!token) return; const response = await fetch('/api/ignored-files/stats', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, }); if (response.ok) { const data = await response.json(); setStats(data); } } catch (err) { console.error('Error fetching stats:', err); } }; useEffect(() => { // Set initial filters from URL params if (sourceTypeParam) { setSourceTypeFilter(sourceTypeParam); } fetchSources(); }, []); useEffect(() => { fetchIgnoredFiles(); fetchStats(); }, [page, searchTerm, sourceTypeFilter]); const fetchSources = async () => { try { const token = localStorage.getItem('token'); if (!token) return; const response = await fetch('/api/sources', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, }); if (response.ok) { const data = await response.json(); setSources(data); } } catch (err) { console.error('Error fetching sources:', err); } }; const handleSearch = (event: React.ChangeEvent) => { setSearchTerm(event.target.value); setPage(1); }; const handleSourceTypeFilter = (event: any) => { setSourceTypeFilter(event.target.value); setPage(1); }; const handleSelectFile = (fileId: string) => { const newSelected = new Set(selectedFiles); if (newSelected.has(fileId)) { newSelected.delete(fileId); } else { newSelected.add(fileId); } setSelectedFiles(newSelected); }; const handleSelectAll = () => { if (selectedFiles.size === ignoredFiles.length) { setSelectedFiles(new Set()); } else { setSelectedFiles(new Set(ignoredFiles.map(file => file.id))); } }; const handleDeleteSelected = async () => { if (selectedFiles.size === 0) return; setDeletingFiles(true); try { const token = localStorage.getItem('token'); if (!token) { throw new Error('No authentication token found'); } const response = await fetch('/api/ignored-files/bulk-delete', { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ ignored_file_ids: Array.from(selectedFiles) }), }); if (!response.ok) { throw new Error(`Failed to delete ignored files: ${response.statusText}`); } const data = await response.json(); addNotification({ type: 'success', title: 'Files Deleted', message: data.message }); setSelectedFiles(new Set()); setBulkDeleteDialog(false); fetchIgnoredFiles(); fetchStats(); } catch (err) { addNotification({ type: 'error', title: 'Delete Failed', message: err instanceof Error ? err.message : 'Failed to delete ignored files' }); } finally { setDeletingFiles(false); } }; const handleDeleteSingle = async (fileId: string) => { try { const token = localStorage.getItem('token'); if (!token) { throw new Error('No authentication token found'); } const response = await fetch(`/api/ignored-files/${fileId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error(`Failed to delete ignored file: ${response.statusText}`); } const data = await response.json(); addNotification({ type: 'success', title: 'Files Deleted', message: data.message }); fetchIgnoredFiles(); fetchStats(); } catch (err) { addNotification({ type: 'error', title: 'Delete Failed', message: err instanceof Error ? err.message : 'Failed to delete ignored file' }); } }; const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const getSourceIcon = (sourceType?: string) => { switch (sourceType) { case 'webdav': return ; case 'local_folder': return ; case 's3': return ; default: return ; } }; const getSourceTypeDisplay = (sourceType?: string) => { switch (sourceType) { case 'webdav': return 'WebDAV'; case 'local_folder': return 'Local Folder'; case 's3': return 'S3'; default: return sourceType || 'Unknown'; } }; const getSourceNameFromIdentifier = (sourceIdentifier?: string, sourceType?: string) => { // Try to find the source name from the sources list const source = sources.find(s => s.id === sourceIdentifier || s.name.toLowerCase().includes(sourceIdentifier?.toLowerCase() || '')); return source?.name || sourceIdentifier || 'Unknown Source'; }; const clearFilters = () => { setSourceTypeFilter(''); setSearchTerm(''); setPage(1); navigate('/ignored-files', { replace: true }); }; const uniqueSourceTypes = Array.from( new Set(ignoredFiles.map(file => file.source_type).filter(Boolean)) ); return ( {/* Breadcrumbs and Navigation */} navigate('/sources')} sx={{ display: 'flex', alignItems: 'center', textDecoration: 'none' }} > Sources Ignored Files {(sourceTypeParam || sourceNameParam) && ( )} Ignored Files {(sourceTypeParam || sourceNameParam || sourceIdParam) && ( )} {sourceTypeParam || sourceNameParam || sourceIdParam ? `Files from ${sourceNameParam || getSourceTypeDisplay(sourceTypeParam)} sources that have been deleted and will be ignored during future syncs.` : 'Files that have been deleted and will be ignored during future syncs from their sources.' } {/* Statistics Cards */} {stats && ( {stats.total_ignored_files} Total Ignored Files {formatFileSize(stats.total_size_bytes)} Total Size {stats.most_recent_ignored_at && ( {formatDistanceToNow(new Date(stats.most_recent_ignored_at), { addSuffix: true })} Most Recent )} )} {/* Filters and Search */} ), }} sx={{ flexGrow: 1 }} /> Source Type {/* Bulk Actions */} {selectedFiles.size > 0 && ( {selectedFiles.size} file{selectedFiles.size !== 1 ? 's' : ''} selected )} {/* Files Table */} 0 && selectedFiles.size < ignoredFiles.length} checked={ignoredFiles.length > 0 && selectedFiles.size === ignoredFiles.length} onChange={handleSelectAll} /> Filename Source Size Ignored Date Reason Actions {loading ? ( ) : error ? ( {error} ) : ignoredFiles.length === 0 ? ( No ignored files found ) : ( ignoredFiles.map((file) => ( handleSelectFile(file.id)} /> {file.filename} {file.filename !== file.original_filename && ( Original: {file.original_filename} )} {file.mime_type} {getSourceIcon(file.source_type)} {getSourceTypeDisplay(file.source_type)} {file.source_path && ( {file.source_path} )} {formatFileSize(file.file_size)} {format(new Date(file.ignored_at), 'MMM dd, yyyy')} {formatDistanceToNow(new Date(file.ignored_at), { addSuffix: true })} {file.reason || 'No reason provided'} handleDeleteSingle(file.id)} color="success" > )) )}
{/* Pagination */} {totalPages > 1 && ( setPage(newPage)} color="primary" /> )}
{/* Bulk Delete Confirmation Dialog */} setBulkDeleteDialog(false)}> Confirm Bulk Delete Are you sure you want to remove {selectedFiles.size} file{selectedFiles.size !== 1 ? 's' : ''} from the ignored list? These files will be eligible for syncing again if encountered from their sources. This action allows them to be re-imported during future syncs.
); }; export default IgnoredFilesPage;