import { useMemo, useRef } from "react"; import Fuse from "fuse.js"; import { useVirtualizer } from "@tanstack/react-virtual"; import { GitFork, RefreshCw, RotateCcw } from "lucide-react"; import { SiGithub, SiGitea } from "react-icons/si"; import type { Repository } from "@/lib/db/schema"; import { Button } from "@/components/ui/button"; import { formatDate, getStatusColor } from "@/lib/utils"; import type { FilterParams } from "@/types/filter"; import { Skeleton } from "@/components/ui/skeleton"; import { useGiteaConfig } from "@/hooks/useGiteaConfig"; interface RepositoryTableProps { repositories: Repository[]; isLoading: boolean; filter: FilterParams; setFilter: (filter: FilterParams) => void; onMirror: ({ repoId }: { repoId: string }) => Promise; onSync: ({ repoId }: { repoId: string }) => Promise; onRetry: ({ repoId }: { repoId: string }) => Promise; loadingRepoIds: Set; } export default function RepositoryTable({ repositories, isLoading, filter, setFilter, onMirror, onSync, onRetry, loadingRepoIds, }: RepositoryTableProps) { const tableParentRef = useRef(null); const { giteaConfig } = useGiteaConfig(); // Helper function to construct Gitea repository URL const getGiteaRepoUrl = (repository: Repository): string | null => { if (!giteaConfig?.url) { return null; } // Only provide Gitea links for repositories that have been or are being mirrored const validStatuses = ['mirroring', 'mirrored', 'syncing', 'synced']; if (!validStatuses.includes(repository.status)) { return null; } // Use mirroredLocation if available, otherwise construct from repository data let repoPath: string; if (repository.mirroredLocation) { repoPath = repository.mirroredLocation; } else { // Fallback: construct the path based on repository data const owner = repository.organization || repository.owner; repoPath = `${owner}/${repository.name}`; } // Ensure the base URL doesn't have a trailing slash const baseUrl = giteaConfig.url.endsWith('/') ? giteaConfig.url.slice(0, -1) : giteaConfig.url; return `${baseUrl}/${repoPath}`; }; const hasAnyFilter = Object.values(filter).some( (val) => val?.toString().trim() !== "" ); const filteredRepositories = useMemo(() => { let result = repositories; if (filter.status) { result = result.filter((repo) => repo.status === filter.status); } if (filter.owner) { result = result.filter((repo) => repo.owner === filter.owner); } if (filter.organization) { result = result.filter( (repo) => repo.organization === filter.organization ); } if (filter.searchTerm) { const fuse = new Fuse(result, { keys: ["name", "fullName", "owner", "organization"], threshold: 0.3, }); result = fuse.search(filter.searchTerm).map((res) => res.item); } return result; }, [repositories, filter]); const rowVirtualizer = useVirtualizer({ count: filteredRepositories.length, getScrollElement: () => tableParentRef.current, estimateSize: () => 65, overscan: 5, }); return isLoading ? (
Repository
Owner
Organization
Last Mirrored
Status
Actions
Links
{Array.from({ length: 5 }).map((_, i) => (
))}
) : filteredRepositories.length === 0 ? (

No repositories found

{hasAnyFilter ? "Try adjusting your search or filter criteria." : "Configure your GitHub connection to start mirroring repositories."}

{hasAnyFilter ? ( ) : ( )}
) : (
{/* table header */}
Repository
Owner
Organization
Last Mirrored
Status
Actions
Links
{/* table body wrapper (for a parent in virtualization) */}
{rowVirtualizer.getVirtualItems().map((virtualRow, index) => { const repo = filteredRepositories[virtualRow.index]; const isLoading = loadingRepoIds.has(repo.id ?? ""); return (
{/* Repository */}
{repo.name}
{repo.fullName}
{repo.isPrivate && ( Private )}
{/* Owner */}

{repo.owner}

{/* Organization */}

{repo.organization || "-"}

{/* Last Mirrored */}

{repo.lastMirrored ? formatDate(new Date(repo.lastMirrored)) : "Never"}

{/* Status */}
{repo.status}
{/* Actions */}
onMirror({ repoId: repo.id ?? "" })} onSync={() => onSync({ repoId: repo.id ?? "" })} onRetry={() => onRetry({ repoId: repo.id ?? "" })} />
{/* Links */}
{(() => { const giteaUrl = getGiteaRepoUrl(repo); // Determine tooltip based on status and configuration let tooltip: string; if (!giteaConfig?.url) { tooltip = "Gitea not configured"; } else if (repo.status === 'imported') { tooltip = "Repository not yet mirrored to Gitea"; } else if (repo.status === 'failed') { tooltip = "Repository mirroring failed"; } else if (repo.status === 'mirroring') { tooltip = "Repository is being mirrored to Gitea"; } else if (giteaUrl) { tooltip = "View on Gitea"; } else { tooltip = "Gitea repository not available"; } return giteaUrl ? ( ) : ( ); })()}
); })}
{/* Status Bar */}
{hasAnyFilter ? `Showing ${filteredRepositories.length} of ${repositories.length} repositories` : `${repositories.length} ${repositories.length === 1 ? 'repository' : 'repositories'} total`}
{hasAnyFilter && ( Filters applied )}
); } function RepoActionButton({ repo, isLoading, onMirror, onSync, onRetry, }: { repo: { id: string; status: string }; isLoading: boolean; onMirror: () => void; onSync: () => void; onRetry: () => void; }) { let label = ""; let icon = <>; let onClick = () => {}; let disabled = isLoading; if (repo.status === "failed") { label = "Retry"; icon = ; onClick = onRetry; } else if (["mirrored", "synced", "syncing"].includes(repo.status)) { label = "Sync"; icon = ; onClick = onSync; disabled ||= repo.status === "syncing"; } else if (["imported", "mirroring"].includes(repo.status)) { label = "Mirror"; icon = ; onClick = onMirror; disabled ||= repo.status === "mirroring"; } else { return null; // unsupported status } return ( ); }