"use client"; import { useState } from "react"; import { type ColumnDef, type Table as TableType, getCoreRowModel, useReactTable, } from "@tanstack/react-table"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { AlertCircle, Archive, CheckCircle2, CircleDot, Eye, GitBranch, GitCommitHorizontal, GitFork, KeyRound, Loader2, RefreshCw, ShieldCheck, } from "lucide-react"; import { DataTable } from "@/components/tables/DataTable"; import { cn } from "@/lib/utils"; import type { ForgejoRepository, ForgejoRepositoryValidationResponse, } from "@/lib/api-forgejo"; const repositoryLabel = (repo: ForgejoRepository) => repo.display_name || `${repo.owner}/${repo.repo}`; const repositoryTone = (repo: ForgejoRepository) => { if (repo.last_sync_error) return "danger"; if (!repo.active || repo.is_archived) return "muted"; if (!repo.has_webhook_secret || !repo.last_sync_at) return "warning"; return "success"; }; const toneClasses = { success: { rail: "border-l-[color:var(--success)]", row: "bg-[color:rgba(52,211,153,0.025)] hover:bg-[color:rgba(52,211,153,0.07)]", icon: "border-[color:rgba(52,211,153,0.28)] bg-[color:rgba(52,211,153,0.13)] text-[color:var(--success)]", dot: "bg-[color:var(--success)]", }, warning: { rail: "border-l-[color:var(--warning)]", row: "bg-[color:rgba(251,191,36,0.025)] hover:bg-[color:rgba(251,191,36,0.075)]", icon: "border-[color:rgba(251,191,36,0.3)] bg-[color:rgba(251,191,36,0.13)] text-[color:var(--warning)]", dot: "bg-[color:var(--warning)]", }, danger: { rail: "border-l-[color:var(--danger)]", row: "bg-[color:rgba(248,113,113,0.028)] hover:bg-[color:rgba(248,113,113,0.08)]", icon: "border-[color:rgba(248,113,113,0.3)] bg-[color:rgba(248,113,113,0.13)] text-[color:var(--danger)]", dot: "bg-[color:var(--danger)]", }, muted: { rail: "border-l-[color:var(--border-strong)]", row: "hover:bg-[color:var(--surface-muted)]", icon: "border-[color:var(--border)] bg-[color:var(--surface-muted)] text-muted", dot: "bg-[color:var(--text-quiet)]", }, } as const; const formatSyncTime = (value: string | null) => { if (!value) return null; const date = new Date(value); if (Number.isNaN(date.getTime())) return null; return { date: date.toLocaleDateString(), time: date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }), }; }; type RepositorySyncResult = { created: number; updated: number; open: number; closed: number; total: number; }; interface RepositoriesTableProps { repositories: ForgejoRepository[]; isLoading: boolean; onEdit?: (repository: ForgejoRepository) => void; onDelete?: (repository: ForgejoRepository) => void; onViewDetails?: (repository: ForgejoRepository) => void; onSync?: (repository: ForgejoRepository) => Promise; onValidate?: ( repository: ForgejoRepository, ) => Promise; } export function ForgejoRepositoriesTable({ repositories, isLoading, onEdit, onDelete, onViewDetails, onSync, onValidate, }: RepositoriesTableProps) { // onEdit available for future use const _ = onEdit; const table = useReactTable({ data: repositories, columns: columns(onSync, onValidate, onViewDetails), getCoreRowModel: getCoreRowModel(), }); return ( , title: "No Git Project repositories yet", description: "Add repositories so Pipeline can sync issues into Git Projects.", actionHref: "/git-projects/repositories/new", actionLabel: "Add repository", }} rowActions={{ getEditHref: (row) => `/git-projects/repositories/${row.id}/edit`, onDelete: onDelete ?? undefined, cellClassName: "px-3 py-3 align-middle md:px-5", }} tableClassName="min-w-[900px] w-full text-left text-sm" headerClassName="bg-[linear-gradient(90deg,rgba(96,165,250,0.16),rgba(52,211,153,0.1),rgba(251,191,36,0.08))] text-xs font-semibold uppercase tracking-wider text-[color:var(--text-muted)]" headerCellClassName="px-3 py-3 md:px-5" cellClassName="px-3 py-4 align-middle md:px-5" rowClassName={(row) => { const tone = repositoryTone(row.original); return cn( "border-l-4 transition-colors", toneClasses[tone].rail, toneClasses[tone].row, ); }} /> ); } const columns = ( onSync?: (repository: ForgejoRepository) => Promise, onValidate?: ( repository: ForgejoRepository, ) => Promise, onViewDetails?: (repository: ForgejoRepository) => void, ): ColumnDef[] => [ { accessorKey: "displayName", header: ({ column }) => { return ( ); }, cell: ({ row }) => { const repo = row.original; const tone = repositoryTone(repo); return (
{repositoryLabel(repo)} {repo.default_branch ? ( {repo.default_branch} ) : null}
{repo.owner}/{repo.repo} {repo.connection?.name ? ( / {repo.connection.name} ) : null} {repo.description ? ( {repo.description} ) : null} {repo.topics.length ? (
{repo.topics.slice(0, 3).map((topic) => ( {topic} ))} {repo.topics.length > 3 ? ( +{repo.topics.length - 3} ) : null}
) : null}
); }, }, { accessorKey: "connection", header: "Connection", cell: ({ row }) => { const connection = row.original.connection; return (
{connection?.name} {connection?.base_url}
); }, }, { accessorKey: "status", header: "Status", cell: ({ row }) => { const repo = row.original; const isActive = repo.active; return (
{isActive ? "Active" : "Inactive"} {repo.is_archived ? ( Archived ) : null} {repo.has_webhook_secret ? ( Webhook ) : ( No secret )} {repo.last_sync_error ? ( Sync error ) : repo.last_sync_at ? ( Synced ) : ( New )}
); }, }, { accessorKey: "openIssues", header: "Issues", cell: ({ row }) => (
0 ? "border-[color:rgba(251,191,36,0.28)] bg-[color:rgba(251,191,36,0.1)]" : "border-[color:rgba(52,211,153,0.24)] bg-[color:rgba(52,211,153,0.08)]", )} > {row.original.open_issues_count} open
), }, { accessorKey: "lastSync", header: "Last Sync", cell: ({ row }) => { const lastSyncAt = row.original.last_sync_at; const lastSyncError = row.original.last_sync_error; const tone = repositoryTone(row.original); const syncTime = formatSyncTime(lastSyncAt); if (!syncTime) { return (
Never
); } return (
{syncTime.date} {syncTime.time} {lastSyncError && ( Error: {lastSyncError.substring(0, 50)}... )}
); }, }, { id: "actions", cell: ({ row }) => ( ), }, ]; function ActionsCell({ repository, onSync, onValidate, onViewDetails, }: { repository: ForgejoRepository; onSync?: (repository: ForgejoRepository) => Promise; onValidate?: ( repository: ForgejoRepository, ) => Promise; onViewDetails?: (repository: ForgejoRepository) => void; }) { const [isSyncLoading, setIsSyncLoading] = useState(false); const [isValidateLoading, setIsValidateLoading] = useState(false); const [syncResult, setSyncResult] = useState<{ created: number; updated: number; open: number; closed: number; total: number; } | null>(null); const [validateResult, setValidateResult] = useState<{ ok: boolean; repo_exists?: boolean; error_message?: string; } | null>(null); const handleSync = async () => { if (!onSync) return; setIsSyncLoading(true); try { const result = await onSync(repository); setSyncResult(result); } finally { setIsSyncLoading(false); } }; const handleValidate = async () => { if (!onValidate) return; setIsValidateLoading(true); try { const result = await onValidate(repository); setValidateResult({ ok: result.status.ok, repo_exists: result.repo_exists ?? undefined, error_message: result.status.error_message ?? undefined, }); } finally { setIsValidateLoading(false); } }; return (
{onSync && ( )} {onValidate && ( )} {onViewDetails && ( )}
); } // Filter component export function RepositoriesTableFilter({ value, onChange, }: { value: string; onChange: (value: string) => void; }) { return ( onChange(e.target.value)} className="h-9 w-[150px] rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] px-3 py-1 text-sm text-strong focus:border-[color:var(--accent)] focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)] lg:w-[250px]" /> ); } // Column toggle component export function RepositoriesTableToggle({ table, }: { table: TableType; }) { return (
Show:
{table .getAllColumns() .filter((column) => column.getCanHide()) .map((column) => { return ( ); })}
); }