import { useQuery } from "@tanstack/react-query"; import { ChevronRight, Database, FileCode2, GitBranch, Loader2, PanelLeftClose, PanelRightClose } from "lucide-react"; import type { Dispatch, ReactNode, SetStateAction } from "react"; import { useMemo } from "react"; import { CopyableValue } from "../../components/common/CopyableValue"; import { CursorPager } from "../../components/common/CursorPager"; import { advancePage, previousPage, type PageState } from "../../components/common/cursorPaging"; import { createEmptyPageState, type RepositoryBrowserState } from "./repositoryBrowserState"; import { getLatestRun, listObjectsForPublicationPoint, listPublicationPointsForRepo, listRepos, type ObjectInstanceRecord, type PublicationPointRecord, type RepositoryRecord } from "../../api/queryService"; import { normalizeApiError } from "../../api/client"; const REPO_PAGE_SIZE = 50; const PP_PAGE_SIZE = 50; const OBJECT_PAGE_SIZE = 50; interface RepositoriesPageProps { browserState: RepositoryBrowserState; onBrowserStateChange: Dispatch>; onOpenObject?: (objectInstanceId: string) => void; } export function RepositoriesPage({ browserState, onBrowserStateChange, onOpenObject }: RepositoriesPageProps) { const { selectedRepoId, selectedPpId, repoFilter, ppFilter, objectFilter, repoPage, ppPage, objectPage, isRepoPanelCollapsed, isPpPanelCollapsed } = browserState; const setRepoFilter = (value: string) => onBrowserStateChange((state) => ({ ...state, repoFilter: value })); const setPpFilter = (value: string) => onBrowserStateChange((state) => ({ ...state, ppFilter: value })); const setObjectFilter = (value: string) => onBrowserStateChange((state) => ({ ...state, objectFilter: value })); const setRepoPage = (updater: SetStateAction) => { onBrowserStateChange((state) => ({ ...state, repoPage: typeof updater === "function" ? updater(state.repoPage) : updater })); }; const setPpPage = (updater: SetStateAction) => { onBrowserStateChange((state) => ({ ...state, ppPage: typeof updater === "function" ? updater(state.ppPage) : updater })); }; const setObjectPage = (updater: SetStateAction) => { onBrowserStateChange((state) => ({ ...state, objectPage: typeof updater === "function" ? updater(state.objectPage) : updater })); }; const latestRunQuery = useQuery({ queryKey: ["repositories-latest-run"], queryFn: getLatestRun }); const runId = latestRunQuery.data?.data.runId ?? "latest"; const reposQuery = useQuery({ queryKey: ["repositories", runId, repoPage.cursor], queryFn: () => listRepos(runId, REPO_PAGE_SIZE, repoPage.cursor), enabled: Boolean(latestRunQuery.data?.data.runId) }); const repos = useMemo(() => reposQuery.data?.data ?? [], [reposQuery.data?.data]); const filteredRepos = useMemo(() => filterRepositories(repos, repoFilter), [repoFilter, repos]); const selectedRepo = selectedRepoId ? repos.find((repo) => repo.repoId === selectedRepoId) ?? null : null; const ppsQuery = useQuery({ queryKey: ["repository-publication-points", runId, selectedRepo?.repoId ?? "", ppPage.cursor], queryFn: () => listPublicationPointsForRepo(runId, selectedRepo?.repoId ?? "", PP_PAGE_SIZE, ppPage.cursor), enabled: Boolean(selectedRepo?.repoId) }); const publicationPoints = useMemo(() => ppsQuery.data?.data ?? [], [ppsQuery.data?.data]); const filteredPublicationPoints = useMemo(() => filterPublicationPoints(publicationPoints, ppFilter), [ppFilter, publicationPoints]); const selectedPp = selectedPpId ? publicationPoints.find((pp) => pp.ppId === selectedPpId) ?? null : null; const objectsQuery = useQuery({ queryKey: ["publication-point-objects", runId, selectedPp?.ppId ?? "", objectPage.cursor], queryFn: () => listObjectsForPublicationPoint(runId, selectedPp?.ppId ?? "", OBJECT_PAGE_SIZE, objectPage.cursor), enabled: Boolean(selectedPp?.ppId) }); const objects = useMemo(() => objectsQuery.data?.data ?? [], [objectsQuery.data?.data]); const filteredObjects = useMemo(() => filterObjects(objects, objectFilter), [objectFilter, objects]); const repoTotal = reposQuery.data?.page?.nextCursor ? undefined : repoPage.previous.length * REPO_PAGE_SIZE + repos.length; const ppTotal = selectedRepo?.publicationPoints; const objectTotal = selectedPp?.objects; const error = latestRunQuery.error ?? reposQuery.error ?? ppsQuery.error ?? objectsQuery.error; return (

Repository browser · live query service

Repository / publication point / object browser

Browse repo trees first, then expand publication points and object rows only on demand.

{latestRunQuery.data?.data.runId ?? "Loading run"} {repos.length ? `${repos.length} repositories loaded` : "waiting for query service"}
{error ? (
Repository API unavailable {normalizeApiError(error).message}
) : null}
} isCollapsed={isRepoPanelCollapsed} label="Repositories" meta={reposQuery.isFetching ? "loading" : rangeLabel(repoPage.previous.length + 1, REPO_PAGE_SIZE, repos.length, repoTotal, repoFilter, filteredRepos.length)} onToggleCollapse={() => onBrowserStateChange((state) => ({ ...state, isRepoPanelCollapsed: !state.isRepoPanelCollapsed }))} toggleLabel={isRepoPanelCollapsed ? "Expand repositories column" : "Collapse repositories column"} /> {isRepoPanelCollapsed ? ( } label="Repositories" value={repoTotal ?? repos.length} /> ) : ( <>
{filteredRepos.map((repo) => (
))}
setRepoPage((page) => advancePage(page, reposQuery.data?.page?.nextCursor ?? null))} onPrevious={() => setRepoPage(previousPage)} previousCount={repoPage.previous.length} rangeLabel={rangeLabel(repoPage.previous.length + 1, REPO_PAGE_SIZE, repos.length, repoTotal, repoFilter, filteredRepos.length)} /> )}
} isCollapsed={isPpPanelCollapsed} label="Publication Points" meta={ppsQuery.isFetching ? "loading" : selectedRepo ? rangeLabel(ppPage.previous.length + 1, PP_PAGE_SIZE, publicationPoints.length, ppTotal, ppFilter, filteredPublicationPoints.length) : "select repo"} onToggleCollapse={() => onBrowserStateChange((state) => ({ ...state, isPpPanelCollapsed: !state.isPpPanelCollapsed }))} toggleLabel={isPpPanelCollapsed ? "Expand publication points column" : "Collapse publication points column"} /> {isPpPanelCollapsed ? ( } label="Publication Points" value={ppTotal ?? publicationPoints.length} /> ) : ( <> {!selectedRepo ? (
Select a repository to load its publication points.
) : null} {ppsQuery.isFetching ? : null}
{filteredPublicationPoints.map((pp) => (
{pp.manifestRsyncUri ? : null}
))}
setPpPage((page) => advancePage(page, ppsQuery.data?.page?.nextCursor ?? null))} onPrevious={() => setPpPage(previousPage)} pageNumber={ppPage.previous.length + 1} previousCount={ppPage.previous.length} rangeLabel={selectedRepo ? rangeLabel(ppPage.previous.length + 1, PP_PAGE_SIZE, publicationPoints.length, ppTotal, ppFilter, filteredPublicationPoints.length) : undefined} /> )}
} label="Objects" meta={objectsQuery.isFetching ? "loading" : selectedPp ? rangeLabel(objectPage.previous.length + 1, OBJECT_PAGE_SIZE, objects.length, objectTotal, objectFilter, filteredObjects.length) : "select PP"} /> {!selectedPp ? (
Select a publication point to load its objects.
) : null} {objectsQuery.isFetching ? : null} {selectedPp && !objectsQuery.isFetching ? ( ) : null} setObjectPage((page) => advancePage(page, objectsQuery.data?.page?.nextCursor ?? null))} onPrevious={() => setObjectPage(previousPage)} pageNumber={objectPage.previous.length + 1} previousCount={objectPage.previous.length} rangeLabel={selectedPp ? rangeLabel(objectPage.previous.length + 1, OBJECT_PAGE_SIZE, objects.length, objectTotal, objectFilter, filteredObjects.length) : undefined} />
); } function PanelTitle({ icon, isCollapsed, label, meta, onToggleCollapse, toggleLabel }: { icon: ReactNode; isCollapsed?: boolean; label: string; meta: string; onToggleCollapse?: () => void; toggleLabel?: string; }) { return (

{label}

{label}

{icon} {meta} {onToggleCollapse ? ( ) : null}
); } function CollapsedPanelSummary({ icon, label, value }: { icon: ReactNode; label: string; value: number }) { return (
{icon} {label} {value.toLocaleString()}
); } function CurrentPageFilter({ disabled, label, onChange, placeholder, value }: { disabled?: boolean; label: string; onChange: (value: string) => void; placeholder: string; value: string; }) { return ( ); } function ObjectTable({ objects, onOpenObject }: { objects: ObjectInstanceRecord[]; onOpenObject?: (objectInstanceId: string) => void }) { if (objects.length === 0) { return
No objects returned for this publication point.
; } return ( {objects.map((object) => ( ))}
Type URI Hash Source Result Action
{object.objectType} {object.sourceSection}
); } function StatusPill({ state }: { state: string }) { const normalized = state.toLowerCase(); const className = normalized.includes("fail") || normalized.includes("reject") || normalized.includes("error") ? "warning" : normalized.includes("cache") || normalized.includes("fallback") ? "fallback" : "healthy"; return {state}; } function LoadingLine() { return (
Loading selected branch...
); } function shortName(uri: string) { return uri.split("/").filter(Boolean).at(-1) ?? uri; } function shortHash(value: string, prefixLength: number) { return `${value.slice(0, prefixLength)}...`; } function rangeLabel(pageNumber: number, pageSize: number, pageRowCount: number, total: number | undefined, filter: string, visibleRowCount = pageRowCount) { const start = pageRowCount === 0 ? 0 : (pageNumber - 1) * pageSize + 1; const end = pageRowCount === 0 ? 0 : start + pageRowCount - 1; const totalText = total === undefined ? "unknown" : total.toLocaleString(); const filterText = filter.trim() ? ` · ${visibleRowCount.toLocaleString()}/${pageRowCount.toLocaleString()} matched on page` : ""; return `${start.toLocaleString()}-${end.toLocaleString()}/${totalText} shown${filterText}`; } function matchesNeedle(values: Array, needle: string) { const normalized = needle.trim().toLowerCase(); if (!normalized) { return true; } return values.some((value) => String(value ?? "").toLowerCase().includes(normalized)); } function filterRepositories(repos: RepositoryRecord[], filter: string) { return repos.filter((repo) => matchesNeedle([repo.host, repo.uri, repo.transport, repo.objects, repo.rejectedObjects], filter)); } function filterPublicationPoints(publicationPoints: PublicationPointRecord[], filter: string) { return publicationPoints.filter((pp) => matchesNeedle([ pp.ppId, pp.manifestRsyncUri, pp.publicationPointRsyncUri, pp.rsyncBaseUri, pp.rrdpNotificationUri, pp.source, pp.repoSyncSource, pp.repoTerminalState, pp.objects, pp.rejectedObjects ], filter)); } function filterObjects(objects: ObjectInstanceRecord[], filter: string) { return objects.filter((object) => matchesNeedle([ object.objectType, object.uri, object.sha256, object.sourceSection, object.result, object.rejectReason, object.rejected ? "rejected" : "accepted" ], filter)); }