spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { ProvenanceDTO } from "@dci/core";2import { ExternalLink } from "lucide-react";3import type { ReactNode } from "react";4import { ConfidenceBadge, EstimateMark, ScopeBadge } from "@/components/ui/badge";5import { SourceBadge } from "@/components/ui/source-badge";6import { cn } from "@/components/ui/cn";7import { ProvenancePopover } from "@/components/ui/popover";8import { EmptyState } from "@/components/ui/states";9import { TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";10import { fieldLabel, valueHint } from "@/lib/field-labels";11import { fmtDate, fmtValue } from "@/lib/format";1213/** Provenance rows backing one or more fields (API field names are camelCase; snake_case tolerated). */14export function provFor(items: ProvenanceDTO[] | undefined, ...fields: string[]): ProvenanceDTO[] {15 if (!items?.length) return [];16 const want = new Set(fields.flatMap((f) => [f, f.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`), f.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())]));17 return items.filter((p) => want.has(p.field));18}1920export { FIELD_LABEL, fieldLabel } from "@/lib/field-labels";2122/** Value + inline provenance popover for the rows backing `fields`. Renders nothing extra when no provenance exists. */23export function WithProvenance({ items, fields, children, className }: { items: ProvenanceDTO[] | undefined; fields: string | string[]; children: ReactNode; className?: string }) {24 const rows = provFor(items, ...(Array.isArray(fields) ? fields : [fields]));25 return (26 <span className={cn("inline-flex items-center gap-1", className)}>27 {children}28 {rows.length > 0 && <ProvenancePopover items={rows} size={12} />}29 </span>30 );31}3233/** Full provenance table: field, value, source, first/last observed, confidence, method, link. Stacks to cards < 640 px. */34export function ProvenanceTable({ items, emptyDescription = "No field-level provenance has been published for this record yet." }: { items: ProvenanceDTO[]; emptyDescription?: string }) {35 if (!items.length) return <EmptyState compact title="No provenance rows" description={emptyDescription} />;36 const sorted = [...items].sort((a, b) => fieldLabel(a.field).localeCompare(fieldLabel(b.field)) || (a.lastObserved < b.lastObserved ? 1 : -1));37 return (38 <Table dense>39 <THead>40 <tr>41 <Th>Field</Th>42 <Th>Value</Th>43 <Th>Source</Th>44 <Th className="hidden lg:table-cell">First observed</Th>45 <Th className="hidden lg:table-cell">Last observed</Th>46 <Th>Confidence</Th>47 <Th className="hidden xl:table-cell">Method</Th>48 <Th className="hidden md:table-cell">Link</Th>49 </tr>50 </THead>51 <TBody>52 {sorted.map((p, i) => (53 <Tr key={`${p.field}-${p.sourceId}-${i}`}>54 <Td primary>55 <span className="inline-flex items-center gap-1.5">56 {fieldLabel(p.field)}57 {p.isWinner && (58 <span className="inline-flex h-[16px] items-center rounded-xs border border-accent/40 bg-accent-soft px-1 text-[9.5px] font-medium uppercase tracking-[0.06em] text-accent-ink" title="Winner — the observation backing the displayed value">59 shown60 </span>61 )}62 </span>63 </Td>64 <Td label="Value" className="figure max-w-[220px] text-[12.5px] lg:max-w-[300px]">65 <span className="min-w-0 [overflow-wrap:anywhere]">66 {p.isEstimate && <EstimateMark className="mr-0.5" />}67 {fmtValue(p.value, valueHint(p.field))}68 {p.scope && <ScopeBadge scope={p.scope} className="ml-1.5 align-middle" />}69 </span>70 </Td>71 <Td label="Source">72 <span className="inline-flex min-w-0 flex-wrap items-center gap-1.5 [overflow-wrap:anywhere]">73 <span className="text-ink">{p.sourceName}</span>74 <SourceBadge kind={p.sourceKind} name={p.sourceName} url={p.url} retrievedAt={p.retrievedAt} documentId={p.documentId} />75 </span>76 </Td>77 <Td label="First observed" className="figure hidden text-[12px] text-ink-3 lg:table-cell">78 {fmtDate(p.firstObserved)}79 </Td>80 <Td label="Last observed" className="figure hidden text-[12px] text-ink-3 lg:table-cell">81 {fmtDate(p.lastObserved)}82 </Td>83 <Td label="Confidence">84 <ConfidenceBadge level={p.confidence} size="xs" />85 </Td>86 <Td label="Method" className="figure hidden text-[11.5px] text-ink-3 xl:table-cell">87 {p.method ?? "—"}88 </Td>89 <Td label="Link" className="hidden md:table-cell">90 <a href={p.url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-[12px] text-ink-2 hover:text-accent" title={p.url}>91 source <ExternalLink className="size-3" />92 </a>93 </Td>94 </Tr>95 ))}96 </TBody>97 </Table>98 );99}100