import { useMemo } from "react"; import { Link, useSearchParams } from "react-router-dom"; import { useQueries, useQuery } from "@tanstack/react-query"; import { getStatsReasons, getStatsValidation, listIssues, listRepos, } from "../api/service"; import { KpiCard } from "../components/KpiCard"; import { PageHeader } from "../components/PageHeader"; import { Panel } from "../components/Panel"; import { StatusPill } from "../components/StatusPill"; import { ErrorBlock, LoadingBlock } from "../components/StateBlock"; import { CursorPagerControls } from "../components/CursorPagerControls"; import { DataTable, type Column } from "../components/DataTable"; import { CopyableValue } from "../components/CopyableValue"; import type { ObjectInstanceRecord } from "../api/schemas"; import { useCursorPager } from "../lib/cursor"; import { formatInt, formatPercent, objectTypeLabel, truncateMiddle } from "../lib/format"; import { withRunParam } from "../lib/run"; import { useRun } from "../lib/useRun"; import { useNavigate } from "react-router-dom"; export default function ValidationPage() { const { runId } = useRun(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const reason = searchParams.get("reason") ?? ""; const type = searchParams.get("type") ?? ""; const repoId = searchParams.get("repoId") ?? ""; const filterKey = JSON.stringify([runId, reason, type, repoId]); const pager = useCursorPager(filterKey); const [validationQuery, reasonsQuery, reposQuery] = useQueries({ queries: [ { queryKey: ["stats", runId, "validation"], queryFn: () => getStatsValidation(runId), staleTime: 60_000, }, { queryKey: ["stats", runId, "reasons"], queryFn: () => getStatsReasons(runId), staleTime: 60_000, }, { queryKey: ["repos", runId, "filter-options"], queryFn: () => listRepos(runId, { limit: 200 }), staleTime: 5 * 60_000, }, ], }); const issuesQuery = useQuery({ queryKey: ["issues", runId, reason, type, repoId, pager.cursor], queryFn: () => listIssues(runId, { limit: 50, cursor: pager.cursor, reason: reason || undefined, type: type || undefined, repoId: repoId || undefined, }), placeholderData: (prev) => prev, }); const validationEntries = useMemo(() => { const map = validationQuery.data ?? {}; return Object.entries(map).sort((a, b) => b[1] - a[1]); }, [validationQuery.data]); const validationTotal = validationEntries.reduce((sum, [, n]) => sum + n, 0); const reasonRows = useMemo(() => { const map = reasonsQuery.data ?? {}; return Object.entries(map) .map(([text, count]) => ({ text, count })) .sort((a, b) => b.count - a.count) .slice(0, 10); }, [reasonsQuery.data]); const maxReason = reasonRows[0]?.count ?? 0; const setParam = (key: string, value: string) => { const params = new URLSearchParams(searchParams); if (value) params.set(key, value); else params.delete(key); setSearchParams(params, { preventScrollReset: true }); }; const columns: Column[] = [ { key: "type", header: "Type", render: (obj) => {objectTypeLabel(obj.objectType)}, }, { key: "uri", header: "URI", render: (obj) => , }, { key: "result", header: "Result", render: (obj) => , }, { key: "reason", header: "Reject reason", render: (obj) => obj.rejectReason ? ( {truncateMiddle(obj.rejectReason, 72)} ) : ( ), }, { key: "source", header: "Source", render: (obj) => , }, ]; return (
{validationQuery.isError ? ( validationQuery.refetch()} /> ) : validationQuery.isPending ? ( ) : validationEntries.length === 0 ? (

No validation stats recorded.

) : (
{validationEntries.map(([name, count]) => (
{formatInt(count)} · {formatPercent(count, validationTotal)}
))}
)}
{reasonsQuery.isError ? ( reasonsQuery.refetch()} /> ) : reasonsQuery.isPending ? ( ) : reasonRows.length === 0 ? (

No rejected objects in this run.

) : (
{reasonRows.map((row) => ( ))}
)}
setParam("reason", e.target.value)} placeholder="substring…" className="mono" />
{(reason || type || repoId) && ( Clear filters )}
} flush > obj.objectInstanceId} loading={issuesQuery.isPending} error={issuesQuery.isError ? issuesQuery.error : undefined} onRetry={() => issuesQuery.refetch()} emptyTitle="No validation issues" emptyHint="Nothing was rejected or errored in this run (or filters are too strict)." caption="Validation issues" onRowClick={(obj) => navigate(withRunParam(`/objects/${encodeURIComponent(obj.objectInstanceId)}`, runId)) } /> pager.goNext(issuesQuery.data?.nextCursor ?? null)} itemCount={issuesQuery.data?.items.length} />
); }