import { useMemo } from "react"; import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { PackageOpen, RefreshCw } from "lucide-react"; import { getRepo, getRepoStats, listRepoObjects, listRepoPublicationPoints, } from "../api/service"; import type { PublicationPointRecord } from "../api/schemas"; import { CopyableValue } from "../components/CopyableValue"; import { CursorPagerControls } from "../components/CursorPagerControls"; import { DataTable, type Column } from "../components/DataTable"; import { KpiCard } from "../components/KpiCard"; import { objectFiltersFromParams, objectFiltersToParams, ObjectsTable, type ObjectFilterValues, } from "../components/ObjectsTable"; import { PageHeader } from "../components/PageHeader"; import { Panel } from "../components/Panel"; import { StatusPill } from "../components/StatusPill"; import { TabPanel, Tabs } from "../components/Tabs"; import { WorkflowStatus } from "../components/WorkflowStatus"; import { ErrorBlock, LoadingBlock, Notice } from "../components/StateBlock"; import { useCursorPager } from "../lib/cursor"; import { formatDurationMs, formatInt, formatUtc, truncateMiddle } from "../lib/format"; import { withRunParam } from "../lib/run"; import { useExportJob } from "../lib/useExportJob"; import { useRun } from "../lib/useRun"; function RepoPpsTable({ runId, repoId }: { runId: string; repoId: string }) { const navigate = useNavigate(); const pager = useCursorPager(`${runId}:${repoId}`); const ppsQuery = useQuery({ queryKey: ["repo-pps", runId, repoId, pager.cursor], queryFn: () => listRepoPublicationPoints(runId, repoId, { limit: 50, cursor: pager.cursor }), placeholderData: (prev) => prev, }); const columns: Column[] = [ { key: "manifest", header: "Manifest URI", render: (pp) => ( {pp.manifestRsyncUri ? truncateMiddle(pp.manifestRsyncUri, 72) : pp.ppId} ), }, { key: "source", header: "Sync source", render: (pp) => {pp.repoSyncSource ?? pp.source ?? "—"}, }, { key: "state", header: "Terminal state", render: (pp) => , }, { key: "objects", header: "Objects", numeric: true, render: (pp) => formatInt(pp.objects) }, { key: "rejected", header: "Rejected", numeric: true, render: (pp) => pp.rejectedObjects ? ( {formatInt(pp.rejectedObjects)} ) : ( "0" ), }, { key: "duration", header: "Sync time", numeric: true, render: (pp) => formatDurationMs(pp.repoSyncDurationMs), }, ]; return ( <> pp.ppId} loading={ppsQuery.isPending} error={ppsQuery.isError ? ppsQuery.error : undefined} onRetry={() => ppsQuery.refetch()} emptyTitle="No publication points" caption="Publication points in this repository" onRowClick={(pp) => navigate(withRunParam(`/publication-points/${encodeURIComponent(pp.ppId)}`, runId)) } /> pager.goNext(ppsQuery.data?.nextCursor ?? null)} itemCount={ppsQuery.data?.items.length} /> ); } export default function RepositoryDetailPage() { const { repoId = "" } = useParams(); const { runId, runQuery } = useRun(); const [searchParams, setSearchParams] = useSearchParams(); const tab = searchParams.get("tab") ?? "pps"; // Object filters live in the URL so they survive reloads and stay shareable. const objectFilters = useMemo(() => objectFiltersFromParams(searchParams), [searchParams]); const setObjectFilters = (next: ObjectFilterValues) => { setSearchParams(objectFiltersToParams(next, searchParams), { preventScrollReset: true }); }; const repoQuery = useQuery({ queryKey: ["repo", runId, repoId], queryFn: () => getRepo(runId, repoId), }); const statsQuery = useQuery({ queryKey: ["repo-stats", runId, repoId], queryFn: () => getRepoStats(runId, repoId), }); const exportJob = useExportJob(runId, { scope: "repo", repoId }); if (repoQuery.isError) { return (
repoQuery.refetch()} title="Failed to load repository" />
); } if (repoQuery.isPending) { return (
); } const repo = repoQuery.data; const stats = statsQuery.data; const setTab = (next: string) => { const params = new URLSearchParams(searchParams); params.set("tab", next); setSearchParams(params, { preventScrollReset: true }); }; return (
} actions={ <> } /> {exportJob.startMutation.isError ? ( Export failed to start: {exportJob.startMutation.error.message} ) : null} {exportJob.jobQuery.data ? : null}
Repository ID
URI
Host
{repo.host}
Transport
{repo.transport ?? "unknown"}
Sync phases
{Object.entries(stats?.phases ?? repo.phases ?? {}).map(([phase, count]) => ( {phase} {formatInt(count)} ))} {Object.keys(stats?.phases ?? repo.phases ?? {}).length === 0 && }
Terminal states
{Object.entries(stats?.terminalStates ?? repo.terminalStates ?? {}).map(([state, count]) => ( ))} {Object.keys(stats?.terminalStates ?? repo.terminalStates ?? {}).length === 0 && }
Run validated
{formatUtc(runQuery.data?.validationTime)}
listRepoObjects(runId, repoId, filters)} filters={objectFilters} onFiltersChange={setObjectFilters} />
); }