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

126 lines
4.4 KiB
TypeScript

import { useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { listPublicationPoints } from "../api/service";
import type { PublicationPointRecord } 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 { formatDurationMs, formatInt, truncateMiddle } from "../lib/format";
import { withRunParam } from "../lib/run";
import { useRun } from "../lib/useRun";
export default function PublicationPointsPage() {
const { runId } = useRun();
const navigate = useNavigate();
const pager = useCursorPager(runId);
const [needle, setNeedle] = useState("");
const ppsQuery = useQuery({
queryKey: ["pps", runId, pager.cursor],
queryFn: () => listPublicationPoints(runId, { limit: 50, cursor: pager.cursor }),
placeholderData: (prev) => prev,
});
const rows = useMemo(() => {
const items = ppsQuery.data?.items ?? [];
const q = needle.trim().toLowerCase();
if (!q) return items;
return items.filter((pp) =>
[pp.manifestRsyncUri, pp.publicationPointRsyncUri, pp.rrdpNotificationUri, pp.rsyncBaseUri, pp.ppId]
.filter(Boolean)
.some((value) => value!.toLowerCase().includes(q)),
);
}, [ppsQuery.data, needle]);
const columns: Column<PublicationPointRecord>[] = [
{
key: "manifest",
header: "Publication point",
render: (pp) => (
<div>
<div className="cell-main mono text-small ellipsis" style={{ maxWidth: 380 }} title={pp.manifestRsyncUri ?? pp.ppId}>
{pp.manifestRsyncUri ? truncateMiddle(pp.manifestRsyncUri, 68) : pp.ppId}
</div>
<div className="text-faint text-small mono">{pp.ppId}</div>
</div>
),
},
{
key: "source",
header: "Sync source",
render: (pp) => <span className="chip">{pp.repoSyncSource ?? pp.source ?? "—"}</span>,
},
{
key: "state",
header: "Terminal state",
render: (pp) => <StatusPill status={pp.repoTerminalState} />,
},
{ key: "objects", header: "Objects", numeric: true, render: (pp) => formatInt(pp.objects) },
{
key: "rejected",
header: "Rejected",
numeric: true,
render: (pp) =>
pp.rejectedObjects ? (
<span style={{ color: "var(--red-700)", fontWeight: 600 }}>{formatInt(pp.rejectedObjects)}</span>
) : (
"0"
),
},
{ key: "warnings", header: "Warnings", numeric: true, render: (pp) => formatInt(pp.warnings) },
{
key: "duration",
header: "Sync time",
numeric: true,
render: (pp) => formatDurationMs(pp.repoSyncDurationMs),
},
];
return (
<div className="page">
<PageHeader title="Publication Points" subtitle="All publication points indexed for the active run" />
<Panel
title="All publication points"
tools={
<input
type="search"
value={needle}
onChange={(e) => setNeedle(e.target.value)}
placeholder="Filter current page (URI/id)…"
aria-label="Filter current page"
style={{ minWidth: 220, padding: "5px 8px", border: "1px solid var(--border)", borderRadius: "var(--radius-m)" }}
/>
}
flush
>
<DataTable
columns={columns}
rows={rows}
rowKey={(pp) => pp.ppId}
loading={ppsQuery.isPending}
error={ppsQuery.isError ? ppsQuery.error : undefined}
onRetry={() => ppsQuery.refetch()}
emptyTitle="No publication points indexed"
caption="Publication points"
onRowClick={(pp) =>
navigate(withRunParam(`/publication-points/${encodeURIComponent(pp.ppId)}`, runId))
}
/>
<CursorPagerControls
page={pager.page}
canPrev={pager.canPrev}
hasNext={ppsQuery.data?.nextCursor != null}
loading={ppsQuery.isFetching}
onPrev={pager.goPrev}
onNext={() => pager.goNext(ppsQuery.data?.nextCursor ?? null)}
itemCount={rows.length}
/>
</Panel>
</div>
);
}