/** * CrawledPagesSection Component * * Displays a filterable table of discovered pages with: * - Search by URL * - Filter by status, analysis status, discovery source, reliability status * - Pagination support * - View page details * - Sort by columns * * @component * @layer Presentation */ import { useState, useMemo } from 'react'; import { Search, ExternalLink, ChevronDown, ChevronUp } from 'lucide-react'; import type { DiscoveredPage } from '@domain/entities'; import { PageStatus, AnalysisStatus, DiscoverySource } from '@domain/entities'; import { ReliabilityStatus } from '@archer/domain'; import { PAGE_STATUS_CONFIG, ANALYSIS_STATUS_CONFIG, DISCOVERY_SOURCE_CONFIG } from '@domain/constants'; import { Pagination } from '@/components/ui/Pagination'; import { ReliabilityStatusBadge } from '@/components/ui/ReliabilityStatusBadge'; interface CrawledPagesSectionProps { pages: DiscoveredPage[]; /** * Optional page analyses for displaying reliability status * Maps page ID to its analysis */ pageAnalyses?: Map; } /** * Status Badge Component */ function StatusBadge({ status }: { status: PageStatus }) { const config = PAGE_STATUS_CONFIG[status]; const colorClasses: Record = { green: 'bg-green-100 text-green-800 border-green-200', yellow: 'bg-yellow-100 text-yellow-800 border-yellow-200', red: 'bg-red-100 text-red-800 border-red-200', gray: 'bg-gray-100 text-gray-700 border-gray-200', }; // Extract color name from config.color (e.g., "text-green-600" -> "green") const colorName = config.color.split('-')[1] || 'gray'; return ( {config.label} ); } /** * Analysis Status Badge Component */ function AnalysisStatusBadge({ status }: { status: AnalysisStatus }) { const config = ANALYSIS_STATUS_CONFIG[status]; const colorClasses: Record = { green: 'bg-green-100 text-green-800 border-green-200', blue: 'bg-blue-100 text-blue-800 border-blue-200', yellow: 'bg-yellow-100 text-yellow-800 border-yellow-200', orange: 'bg-orange-100 text-orange-800 border-orange-200', red: 'bg-red-100 text-red-800 border-red-200', gray: 'bg-gray-100 text-gray-700 border-gray-200', }; // Extract color name from config.color (e.g., "text-green-600" -> "green") const colorName = config.color.split('-')[1] || 'gray'; return ( {config.label} ); } /** * Discovery Source Badge Component */ function DiscoverySourceBadge({ source }: { source: DiscoverySource }) { const config = DISCOVERY_SOURCE_CONFIG[source]; const colorClasses: Record = { blue: 'bg-blue-100 text-blue-800 border-blue-200', green: 'bg-green-100 text-green-800 border-green-200', purple: 'bg-purple-100 text-purple-800 border-purple-200', }; // Extract color name from config.color (e.g., "text-green-600" -> "green") const colorName = config.color.split('-')[1] || 'blue'; return ( {config.label} ); } /** * Format date to relative time */ function formatRelativeTime(dateInput?: string | Date): string { if (!dateInput) return 'Never'; const date = typeof dateInput === 'string' ? new Date(dateInput) : dateInput; const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffDays = Math.floor(diffMs / 86400000); if (diffDays === 0) return 'Today'; if (diffDays === 1) return 'Yesterday'; if (diffDays < 7) return `${diffDays}d ago`; if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`; return date.toLocaleDateString(); } /** * CrawledPagesSection Component */ export function CrawledPagesSection({ pages, pageAnalyses }: CrawledPagesSectionProps) { const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useState('ALL'); const [analysisStatusFilter, setAnalysisStatusFilter] = useState('ALL'); const [discoverySourceFilter, setDiscoverySourceFilter] = useState('ALL'); const [reliabilityFilter, setReliabilityFilter] = useState('all'); const [sortField, setSortField] = useState<'url' | 'depth' | 'discoveredAt' | 'lastAnalyzedAt'>('discoveredAt'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(20); // Filtered and sorted pages const filteredPages = useMemo(() => { let filtered = pages.filter((page) => { // Search filter const matchesSearch = searchQuery === '' || page.url.toLowerCase().includes(searchQuery.toLowerCase()) || page.title?.toLowerCase().includes(searchQuery.toLowerCase()); // Status filter const matchesStatus = statusFilter === 'ALL' || page.status === statusFilter; // Analysis status filter const matchesAnalysisStatus = analysisStatusFilter === 'ALL' || page.analysisStatus === analysisStatusFilter; // Discovery source filter const matchesDiscoverySource = discoverySourceFilter === 'ALL' || page.discoverySource === discoverySourceFilter; // Reliability status filter const pageReliabilityStatus = pageAnalyses?.get(page.id)?.reliabilityStatus; const matchesReliability = reliabilityFilter === 'all' || (pageReliabilityStatus && pageReliabilityStatus === reliabilityFilter); return matchesSearch && matchesStatus && matchesAnalysisStatus && matchesDiscoverySource && matchesReliability; }); // Sort filtered.sort((a, b) => { let aVal, bVal; switch (sortField) { case 'url': aVal = a.url; bVal = b.url; break; case 'depth': aVal = a.depth; bVal = b.depth; break; case 'discoveredAt': aVal = a.discoveredAt; bVal = b.discoveredAt; break; case 'lastAnalyzedAt': aVal = a.lastAnalyzedAt || ''; bVal = b.lastAnalyzedAt || ''; break; default: return 0; } if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1; if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1; return 0; }); return filtered; }, [pages, searchQuery, statusFilter, analysisStatusFilter, discoverySourceFilter, reliabilityFilter, sortField, sortDirection, pageAnalyses]); // Paginated pages for current view const paginatedPages = useMemo(() => { const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; return filteredPages.slice(startIndex, endIndex); }, [filteredPages, currentPage, pageSize]); // Reset to page 1 when filters change const resetPagination = () => { setCurrentPage(1); }; // Handle sort const handleSort = (field: typeof sortField) => { if (sortField === field) { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); } else { setSortField(field); setSortDirection('asc'); } }; // Sort icon const SortIcon = ({ field }: { field: typeof sortField }) => { if (sortField !== field) return null; return sortDirection === 'asc' ? : ; }; return (
{/* Filters */}
{/* Search */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent" />
{/* Status Filter */} {/* Analysis Status Filter */} {/* Discovery Source Filter */} {/* Reliability Status Filter */} {/* Results Count */}
{filteredPages.length} {filteredPages.length === 1 ? 'page' : 'pages'}
{/* Table */}
{paginatedPages.length === 0 ? ( ) : ( paginatedPages.map((page) => { const reliabilityStatus = pageAnalyses?.get(page.id)?.reliabilityStatus; return ( ); }) )}
handleSort('url')} >
URL
Status Analysis Source Reliability handleSort('depth')} >
Depth
Elements handleSort('lastAnalyzedAt')} >
Last Analyzed
{filteredPages.length === 0 ? 'No pages found matching your filters' : 'No pages on this page'}
{page.title && (
{page.title}
)}
{reliabilityStatus ? ( ) : ( - )} {page.depth} {page.elementCount || '-'} {formatRelativeTime(page.lastAnalyzedAt)}
{/* Pagination */} {filteredPages.length > 0 && ( { setPageSize(size); resetPagination(); }} /> )}
); }