HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import type { ReactNode } from 'react';2import { CompareButton } from '@/components/compare/compare-button';3import { CompareTrayBar } from '@/components/compare/compare-tray-bar';4import { FilterBar, type FilterField } from '@/components/listing/filters';5import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';6import { Pagination, withParams } from '@/components/ui/pagination';7import { Container, Note, PageHeader } from '@/components/ui/section';8import { EmptyState, Unavailable } from '@/components/ui/unavailable';9import { safe } from '@/lib/api';10import { fmtInt } from '@/lib/format';11import type { EntitySummary, Page } from '@/lib/types';1213/*14 Intelligence listing: the typed table used by /agents /tools /datasets /frameworks. Same URL contract as `TypedListing`15 (q · org · sort · offset, GET FilterBar) but with an optional per-page `enrich(items)` step that fetches extra facts for the16 rows shown (relations, metric history…) and passes them to the column renderers, plus honest empty states:17 no filters → "No X recorded yet — connectors: …"; filters → "No X match these filters".18*/19export type IntelColumn<X> = {20 key: string;21 label: string;22 num?: boolean;23 primary?: boolean;24 wide?: boolean;25 hideStack?: boolean;26 className?: string;27 render: (e: EntitySummary, extra: X | undefined) => ReactNode;28};2930const LIMIT = 40;31const PARAM_KEYS = ['q', 'org', 'sort', 'order', 'category', 'kind', 'since', 'until', 'manufacturer', 'offset'];3233export async function IntelListing<X = never>({34 title,35 eyebrow,36 lede,37 basePath,38 searchParams,39 fetch,40 columns,41 sorts,42 filters = [],43 enrich,44 connectors,45 emptyHint,46 note,47 compare = false,48 headerAside,49 children,50 caption,51}: {52 title: string;53 eyebrow: string;54 lede?: ReactNode;55 basePath: string;56 searchParams: Record<string, string | undefined>;57 fetch: (q: Record<string, string | number | undefined>) => Promise<Page<EntitySummary>>;58 columns: IntelColumn<X>[];59 sorts: { value: string; label: string }[];60 filters?: FilterField[];61 /** Fetch extra facts for the rows on this page (keyed by slug). Failures per row are tolerated (undefined). */62 enrich?: (items: EntitySummary[]) => Promise<Map<string, X>>;63 /** Connector names that feed this type (for the honest empty state). */64 connectors?: string[];65 emptyHint?: ReactNode;66 note?: ReactNode;67 compare?: boolean;68 headerAside?: ReactNode;69 children?: ReactNode;70 caption?: string;71}) {72 const current: Record<string, string | undefined> = {};73 for (const k of PARAM_KEYS) if (searchParams[k]) current[k] = searchParams[k];74 const offset = Math.max(0, Number(current.offset) || 0);75 const sort = current.sort ?? sorts[0]?.value;76 const page = await safe(fetch({ ...current, sort, limit: LIMIT, offset }));77 const extras = page && enrich && page.items.length ? await enrich(page.items).catch(() => new Map<string, X>()) : new Map<string, X>();78 const href = (patch: Record<string, string | number | undefined | null>) => withParams(basePath, current, patch);79 const filtered = Object.keys(current).some((k) => !['sort', 'order', 'offset'].includes(k));80 const cols = columns.length + (compare ? 1 : 0);81 const lower = title.toLowerCase();82 return (83 <Container wide>84 <PageHeader eyebrow={eyebrow} title={title} lede={lede} aside={page || headerAside ? <div className="flex flex-col items-start gap-2 md:items-end">{page && <p className="tnum text-sm text-ink-3">{fmtInt(page.total)} total</p>}{headerAside}</div> : undefined}>85 <FilterBar action={basePath} className="mt-6" resetHref={basePath} fields={[{ kind: 'text', name: 'q', label: 'Name', value: current.q, placeholder: 'Search by name' }, ...filters]} sort={{ value: sort, options: sorts }} />86 </PageHeader>87 {children}88 <div className="pb-16">89 {!page ? (90 <Unavailable what={title} />91 ) : page.items.length === 0 && !offset ? (92 filtered ? (93 <EmptyState title={`No ${lower} match these filters`}>{emptyHint ?? 'Remove a filter or search for another name.'}</EmptyState>94 ) : (95 <EmptyState title={`No ${lower} recorded yet`} className="[&_p]:max-w-2xl [&_p]:mx-auto">96 {emptyHint}97 {connectors && connectors.length > 0 && (98 <span className="mt-1 block">99 Connectors that will populate this type: <span className="mono text-ink-2">{connectors.join(' · ')}</span>. Nothing is listed until a source has been crawled.100 </span>101 )}102 </EmptyState>103 )104 ) : (105 <>106 <div className="md:overflow-x-auto">107 <DataTable caption={caption ?? title}>108 <thead>109 <tr>110 {columns.map((c) => (111 <Th key={c.key} num={c.num}>112 {c.label}113 </Th>114 ))}115 {compare && <Th className="w-24" aria-label="Compare" />}116 </tr>117 </thead>118 <tbody>119 {page.items.length === 0 && <EmptyRow cols={cols}>No rows on this page.</EmptyRow>}120 {page.items.map((e) => (121 <tr key={e.id}>122 {columns.map((c) => (123 <Td key={c.key} label={c.primary ? undefined : c.label} num={c.num} primary={c.primary} wide={c.wide} hideStack={c.hideStack} className={c.className}>124 {c.render(e, extras.get(e.slug))}125 </Td>126 ))}127 {compare && (128 <Td className="text-right">129 <CompareButton e={e} size="sm" />130 </Td>131 )}132 </tr>133 ))}134 </tbody>135 </DataTable>136 </div>137 <Pagination total={page.total} limit={LIMIT} offset={offset} makeHref={(o) => href({ offset: o || undefined })} className="mt-4" />138 {note && <Note className="mt-3">{note}</Note>}139 </>140 )}141 </div>142 {compare && <CompareTrayBar />}143 </Container>144 );145}146147export function Dash() {148 return <span className="text-ink-3">—</span>;149}150export function str(v: unknown): string | null {151 return typeof v === 'string' && v.trim() ? v : null;152}153export function list(v: unknown): string[] {154 return Array.isArray(v) ? v.filter((x) => x !== null && x !== undefined).map(String) : [];155}156