spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import Link from 'next/link';2import { Section } from '@/components/ui/section';3import { EmptyState } from '@/components/ui/empty-state';4import { Badge, MatchBadge } from '@/components/ui/badge';5import { JsonView } from '@/components/ui/json-view';6import { getContributingSources, getChangeEvents } from '@/lib/queries/cancers';7import { CODE_SYSTEM_LABEL, codeUrl } from '@/lib/site';8import { fmtDate, fmtDateTime, humanize } from '@/lib/format';9import type { CancerBundle } from '../load';1011export async function SourcesTab({ b }: { b: CancerBundle }) {12 const [contrib, events] = await Promise.all([getContributingSources(b.cancer.id, b.descendants), getChangeEvents('cancer', b.cancer.id)]);13 return (14 <div className="space-y-8">15 <Section id="contributing" kicker="Provenance" title="Sources contributing to this entity" description="Every source that supplied a code, alias, edge, observation or derived value, with the ingest runs involved.">16 {contrib.length ? (17 <div className="ci-table-wrap">18 <table className="ci-table">19 <thead>20 <tr>21 <th>Source</th>22 <th>License</th>23 <th>Contributions</th>24 <th>Ingest runs</th>25 <th>Last retrieved</th>26 </tr>27 </thead>28 <tbody>29 {contrib.map((s) => (30 <tr key={s.slug}>31 <td>32 <Link className="ci-link" href={`/source/${s.slug}`}>33 {s.name}34 </Link>{' '}35 <span className="ci-mono text-[11px] text-ink-3">{s.slug}</span>36 </td>37 <td>38 <Badge tone={s.license_status === 'approved' ? 'ok' : 'warn'}>{s.license_status}</Badge>39 </td>40 <td className="text-[12.5px]">{s.kinds.map(humanize).join(', ')}</td>41 <td className="text-[11.5px]">42 {s.run_ids.length ? (43 s.run_ids.slice(0, 5).map((r) => (44 <Link key={r} href={`/admin/runs/${r}`} className="ci-mono ci-link block">45 {r}46 </Link>47 ))48 ) : (49 <span className="text-ink-4">—</span>50 )}51 {s.run_ids.length > 5 ? <span className="text-ink-3">+{s.run_ids.length - 5} more</span> : null}52 </td>53 <td className="whitespace-nowrap text-[12.5px]">{s.last_retrieved ? fmtDateTime(s.last_retrieved) : '—'}</td>54 </tr>55 ))}56 </tbody>57 </table>58 </div>59 ) : (60 <EmptyState compact>No source contribution recorded.</EmptyState>61 )}62 </Section>6364 <Section id="codes" kicker="Identifiers" title={`Cross-reference codes (${b.codes.length})`} description="Every upstream identifier is kept with its match type; nothing is buried in JSON.">65 {b.codes.length ? (66 <div className="ci-table-wrap">67 <table className="ci-table">68 <thead>69 <tr>70 <th>System</th>71 <th>Code</th>72 <th>Match type</th>73 <th>Source</th>74 </tr>75 </thead>76 <tbody>77 {b.codes.map((k) => {78 const url = codeUrl(k.system, k.code);79 return (80 <tr key={`${k.system}-${k.code}`}>81 <td>{CODE_SYSTEM_LABEL[k.system] ?? k.system}</td>82 <td>83 {url ? (84 <a className="ci-mono ci-link" href={url} target="_blank" rel="noopener noreferrer">85 {k.code}86 </a>87 ) : (88 <span className="ci-mono">{k.code}</span>89 )}90 </td>91 <td>92 <MatchBadge matchType={k.match_type} />93 </td>94 <td>95 {k.source_slug ? (96 <Link className="ci-link" href={`/source/${k.source_slug}`}>97 {k.source_slug}98 </Link>99 ) : (100 '—'101 )}102 </td>103 </tr>104 );105 })}106 </tbody>107 </table>108 </div>109 ) : (110 <EmptyState compact>No cross-reference code.</EmptyState>111 )}112 </Section>113114 <Section id="aliases" kicker="Names" title={`Aliases (${b.aliases.length})`}>115 {b.aliases.length ? (116 <div className="ci-table-wrap">117 <table className="ci-table">118 <thead>119 <tr>120 <th>Alias</th>121 <th>Type</th>122 <th>Terminology</th>123 <th>Language</th>124 </tr>125 </thead>126 <tbody>127 {b.aliases.map((a, i) => (128 <tr key={`${a.alias}-${a.alias_type}-${i}`}>129 <td>{a.alias}</td>130 <td>131 <Badge tone="outline">{a.alias_type}</Badge>132 </td>133 <td className="text-[12.5px] text-ink-3">{a.source_terminology ?? a.source_slug ?? '—'}</td>134 <td className="ci-mono text-[12px]">en</td>135 </tr>136 ))}137 </tbody>138 </table>139 </div>140 ) : (141 <EmptyState compact>No alias recorded.</EmptyState>142 )}143 </Section>144145 <Section id="history" kicker="Change history" title="Change events" description="Creation, updates, merges and ranking changes recorded for this entity.">146 {events.length ? (147 <ol className="divide-y divide-rule text-[13.5px]">148 {events.map((e) => (149 <li key={e.id} className="py-2">150 <div className="flex flex-wrap items-baseline gap-2">151 <Badge>{humanize(e.kind)}</Badge>152 <span>{e.summary}</span>153 <span className="ml-auto text-[12px] text-ink-3">{fmtDate(e.created_at)}</span>154 </div>155 {e.ingest_run_id ? (156 <Link href={`/admin/runs/${e.ingest_run_id}`} className="ci-mono ci-link text-[11.5px]">157 {e.ingest_run_id}158 </Link>159 ) : null}160 {e.before || e.after ? (161 <details className="mt-1 text-[12px]">162 <summary className="ci-link">diff</summary>163 <div className="mt-1 grid gap-3 sm:grid-cols-2">164 <div>165 <p className="ci-kicker">Before</p>166 <JsonView data={e.before} />167 </div>168 <div>169 <p className="ci-kicker">After</p>170 <JsonView data={e.after} />171 </div>172 </div>173 </details>174 ) : null}175 </li>176 ))}177 </ol>178 ) : (179 <EmptyState compact>No change event recorded yet. Created {fmtDate(b.cancer.created_at)}; last updated {fmtDate(b.cancer.updated_at)}.</EmptyState>180 )}181 </Section>182 </div>183 );184}185