import { useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { listRepos } from "../api/service"; import type { RepositoryRecord } from "../api/schemas"; import { CursorPagerControls } from "../components/CursorPagerControls"; import { DataTable, type Column } from "../components/DataTable"; import { PageHeader } from "../components/PageHeader"; import { Panel } from "../components/Panel"; import { StatusPill } from "../components/StatusPill"; import { useCursorPager } from "../lib/cursor"; import { formatBytes, formatDurationMs, formatInt, truncateMiddle } from "../lib/format"; import { withRunParam } from "../lib/run"; import { useRun } from "../lib/useRun"; export default function RepositoriesPage() { const { runId, runQuery } = useRun(); const navigate = useNavigate(); const pager = useCursorPager(runId); const [needle, setNeedle] = useState(""); const reposQuery = useQuery({ queryKey: ["repos", runId, pager.cursor], queryFn: () => listRepos(runId, { limit: 50, cursor: pager.cursor }), placeholderData: (prev) => prev, }); const rows = useMemo(() => { const items = reposQuery.data?.items ?? []; const q = needle.trim().toLowerCase(); if (!q) return items; return items.filter( (repo) => repo.host.toLowerCase().includes(q) || repo.uri.toLowerCase().includes(q), ); }, [reposQuery.data, needle]); const columns: Column[] = [ { key: "host", header: "Repository", render: (repo) => (
{repo.host}
{truncateMiddle(repo.uri, 64)}
), }, { key: "transport", header: "Transport", render: (repo) => {repo.transport ?? "unknown"}, }, { key: "pps", header: "PPs", numeric: true, render: (repo) => formatInt(repo.publicationPoints) }, { key: "objects", header: "Objects", numeric: true, render: (repo) => formatInt(repo.objects) }, { key: "rejected", header: "Rejected", numeric: true, render: (repo) => repo.rejectedObjects ? ( {formatInt(repo.rejectedObjects)} ) : ( "0" ), }, { key: "bytes", header: "Downloaded", numeric: true, render: (repo) => formatBytes(repo.downloadBytes) }, { key: "duration", header: "Sync time", numeric: true, render: (repo) => formatDurationMs(repo.syncDurationMsTotal), }, { key: "terminal", header: "Terminal states", render: (repo) => ( {Object.entries(repo.terminalStates ?? {}).map(([state, count]) => ( ))} ), }, ]; return (
setNeedle(e.target.value)} placeholder="Filter current page (host/URI)…" style={{ minWidth: 220 }} />
} flush > repo.repoId} loading={reposQuery.isPending} error={reposQuery.isError ? reposQuery.error : undefined} onRetry={() => reposQuery.refetch()} emptyTitle={needle.trim() ? "No matches on this page — clear the filter" : "No repositories indexed"} emptyHint={ needle.trim() ? "The filter only narrows the rows of the current page." : "The query service has not indexed any run yet." } caption="Repositories" onRowClick={(repo) => navigate(withRunParam(`/repositories/${encodeURIComponent(repo.repoId)}`, runId)) } /> pager.goNext(reposQuery.data?.nextCursor ?? null)} itemCount={rows.length} /> ); }