import { useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import type { ListResult, ObjectInstanceRecord } from "../api/schemas"; import type { ObjectFilters } from "../api/service"; import { useCursorPager } from "../lib/cursor"; import { objectTypeLabel, truncateMiddle } from "../lib/format"; import { withRunParam } from "../lib/run"; import { CopyableValue } from "./CopyableValue"; import { CursorPagerControls } from "./CursorPagerControls"; import { DataTable, type Column } from "./DataTable"; import { StatusPill } from "./StatusPill"; export interface ObjectFilterValues { type: string; result: string; rejectedOnly: boolean; q: string; } export const EMPTY_OBJECT_FILTERS: ObjectFilterValues = { type: "", result: "", rejectedOnly: false, q: "", }; /** Report-stream object type vocabulary (backend also accepts mft/cer/asa aliases). */ const TYPE_OPTIONS = ["roa", "manifest", "crl", "certificate", "aspa", "gbr"]; const RESULT_OPTIONS = ["ok", "error", "skipped"]; /** * Filter bar + paginated object table. The `fetcher` determines the scope * (global / repo / publication point / issues). Filter state is controlled * by the caller so standalone pages can sync it to the URL. */ export function ObjectsTable({ runId, queryKeyScope, fetcher, filters, onFiltersChange, showReason = true, }: { runId: string; queryKeyScope: string; fetcher: (filters: ObjectFilters) => Promise>; filters: ObjectFilterValues; onFiltersChange: (next: ObjectFilterValues) => void; showReason?: boolean; }) { const navigate = useNavigate(); const [qDraft, setQDraft] = useState(filters.q); // Keep the draft input in sync when filters change externally (URL nav). useEffect(() => { setQDraft(filters.q); }, [filters.q]); const filterKey = JSON.stringify([runId, filters.type, filters.result, filters.rejectedOnly, filters.q]); const pager = useCursorPager(filterKey); const query = useQuery({ queryKey: ["objects", queryKeyScope, runId, filters, pager.cursor], queryFn: () => fetcher({ limit: 50, cursor: pager.cursor, type: filters.type || undefined, result: filters.result || undefined, rejected: filters.rejectedOnly || undefined, q: filters.q || undefined, }), placeholderData: (prev) => prev, }); const columns: Column[] = useMemo( () => [ { key: "type", header: "Type", render: (obj) => {objectTypeLabel(obj.objectType)}, }, { key: "uri", header: "URI", render: (obj) => , }, { key: "sha256", header: "SHA-256", render: (obj) => ( {obj.sha256 ? truncateMiddle(obj.sha256, 18) : "—"} ), }, { key: "source", header: "Source", render: (obj) => , }, { key: "result", header: "Result", render: (obj) => , }, ...(showReason ? [ { key: "reason", header: "Reject reason", render: (obj: ObjectInstanceRecord) => obj.rejectReason ? ( {truncateMiddle(obj.rejectReason, 60)} ) : ( ), } satisfies Column, ] : []), ], [showReason], ); const submitQ = () => { if (qDraft.trim() !== filters.q) { onFiltersChange({ ...filters, q: qDraft.trim() }); } }; return (
setQDraft(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") submitQ(); }} onBlur={submitQ} placeholder="substring…" />
server-side filters
obj.objectInstanceId} loading={query.isPending} error={query.isError ? query.error : undefined} onRetry={() => query.refetch()} emptyTitle="No objects match" emptyHint="Try relaxing the filters." caption="RPKI objects" onRowClick={(obj) => navigate(withRunParam(`/objects/${encodeURIComponent(obj.objectInstanceId)}`, runId)) } /> pager.goNext(query.data?.nextCursor ?? null)} itemCount={query.data?.items.length} />
); }