rpki/ui/rpki-explorer/src/pages/RepositoriesPage.tsx

149 lines
5.0 KiB
TypeScript

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<RepositoryRecord>[] = [
{
key: "host",
header: "Repository",
render: (repo) => (
<div>
<div className="cell-main">{repo.host}</div>
<div className="text-faint text-small mono ellipsis" style={{ maxWidth: 320 }} title={repo.uri}>
{truncateMiddle(repo.uri, 64)}
</div>
</div>
),
},
{
key: "transport",
header: "Transport",
render: (repo) => <span className="chip">{repo.transport ?? "unknown"}</span>,
},
{ 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 ? (
<span style={{ color: "var(--red-700)", fontWeight: 600 }}>{formatInt(repo.rejectedObjects)}</span>
) : (
"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) => (
<span className="chip-list">
{Object.entries(repo.terminalStates ?? {}).map(([state, count]) => (
<StatusPill key={state} status={state} label={`${state} ${formatInt(count)}`} />
))}
</span>
),
},
];
return (
<div className="page">
<PageHeader
title="Repositories"
subtitle={
runQuery.data
? `${formatInt(runQuery.data.counts?.objects)} objects across all repositories in ${runQuery.data.runId}`
: undefined
}
/>
<Panel
title="All repositories"
tools={
<div className="filter-field">
<label htmlFor="repo-filter" className="sr-only">
Filter current page
</label>
<input
id="repo-filter"
type="search"
value={needle}
onChange={(e) => setNeedle(e.target.value)}
placeholder="Filter current page (host/URI)…"
style={{ minWidth: 220 }}
/>
</div>
}
flush
>
<DataTable
columns={columns}
rows={rows}
rowKey={(repo) => 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))
}
/>
<CursorPagerControls
page={pager.page}
canPrev={pager.canPrev}
hasNext={reposQuery.data?.nextCursor != null}
loading={reposQuery.isFetching}
onPrev={pager.goPrev}
onNext={() => pager.goNext(reposQuery.data?.nextCursor ?? null)}
itemCount={rows.length}
/>
</Panel>
</div>
);
}