"use client"; import Link from "next/link"; import { useMemo, useState } from "react"; import { Badge, EmptyState, RelativeTime, SearchInput, SkeletonRows, Table, TBody, Td, Th, THead, Tr } from "@/components/ui"; import { cn } from "@/components/ui/cn"; import { DASH, fmtInt } from "@/lib/format"; import { SOURCE_KIND_LABEL } from "@/lib/labels"; import { routes } from "@/lib/routes"; import type { SourceListItem } from "@/lib/admin-api"; import { ErrorNote, ExtLink, Mono, RefreshButton, Select } from "./primitives"; import { AdminPageHeader } from "./shell"; import { useAdminQuery } from "./use-admin-query"; type SortKey = "name" | "kind" | "documents" | "provenanceRows" | "facilities" | "lastDocumentAt"; export function SourcesTable() { const q = useAdminQuery("v1/sources", { refreshMs: 120_000 }); const [kind, setKind] = useState(""); const [search, setSearch] = useState(""); const [sort, setSort] = useState<{ key: SortKey; dir: "asc" | "desc" }>({ key: "provenanceRows", dir: "desc" }); const list = useMemo(() => q.data ?? [], [q.data]); const kinds = useMemo(() => [...new Set(list.map((s) => s.kind))].sort(), [list]); const rows = useMemo(() => { const s = search.trim().toLowerCase(); const out = list.filter((x) => (!kind || x.kind === kind) && (!s || x.name.toLowerCase().includes(s) || x.domain.toLowerCase().includes(s) || (x.connectorId ?? "").includes(s) || (x.license ?? "").toLowerCase().includes(s))); out.sort((a, b) => { const va = a[sort.key]; const vb = b[sort.key]; const cmp = typeof va === "number" && typeof vb === "number" ? va - vb : String(va ?? "").localeCompare(String(vb ?? "")); return (sort.dir === "asc" ? cmp : -cmp) || a.name.localeCompare(b.name); }); return out; }, [list, kind, search, sort]); const th = (key: SortKey, label: string, numeric?: boolean) => ( setSort((s) => ({ key, dir: s.key === key ? (s.dir === "asc" ? "desc" : "asc") : numeric ? "desc" : "asc" }))}> {label} ); const totals = useMemo(() => rows.reduce((a, s) => ({ documents: a.documents + s.documents, provenance: a.provenance + s.provenanceRows, facilities: a.facilities + s.facilities, licensed: a.licensed + (s.license ? 1 : 0) }), { documents: 0, provenance: 0, facilities: 0, licensed: 0 }), [rows]); return (
void q.refresh()} busy={q.refreshing} updatedAt={q.updatedAt} />} /> void q.refresh()} className="mb-3" />
{q.loading && !q.data ? ( ) : rows.length === 0 ? ( ) : (
{th("name", "Source")} {th("kind", "Kind")} {th("documents", "Documents", true)} {th("provenanceRows", "Provenance", true)} {th("facilities", "Facilities", true)} {th("lastDocumentAt", "Last document")} {rows.map((s) => ( ))}
License Attribution Docs
{s.name}
{s.domain} {s.connectorId && ( {s.connectorId} )} {s.robotsAllowed === false && ( robots blocked )}
{SOURCE_KIND_LABEL[s.kind] ?? s.kind} {s.license ?? "not recorded"} {s.attribution ?? DASH} {s.url ? link : {DASH}} {s.notes && ( ⓘ )} {fmtInt(s.documents)} {fmtInt(s.provenanceRows)} {fmtInt(s.facilities)} {s.lastDocumentAt ? : {DASH}}
{fmtInt(rows.length)} sources {fmtInt(totals.documents)} documents {fmtInt(totals.provenance)} provenance rows {fmtInt(totals.facilities)} facilities
)}
); }