1import { useState, type FormEvent } from "react";2import { Link, useSearchParams } from "react-router";3import { api, ApiError, qs, type ApiRow, type Project } from "../api";4import { useFetch, ago } from "../hooks";5import { Badge, Bool, Empty, ErrorBox, JobStatus, Loading, Td } from "../components/ui";67export default function Apis() {8 const [sp, setSp] = useSearchParams();9 const q = sp.get("q") ?? "", kind = sp.get("kind") ?? "", lifecycle = sp.get("lifecycle") ?? "", provider = sp.get("provider") ?? "";10 const { data, error, loading, reload } = useFetch<{ apis: ApiRow[] }>(`apis${qs({ q, kind, lifecycle })}`);11 const projects = useFetch<{ projects: Project[] }>("projects");12 const [showForm, setShowForm] = useState(false);13 const [form, setForm] = useState({ mode: "openapi", url: "", name: "", license: "", records_path: "", project_id: "default", examples: "" });14 const [jobId, setJobId] = useState<string | null>(null); const [err, setErr] = useState<ApiError | null>(null); const [busy, setBusy] = useState(false);15 const set = (k: string, v: string) => setSp((p) => { const n = new URLSearchParams(p); if (v) n.set(k, v); else n.delete(k); return n; });16 const submit = async (e: FormEvent) => {17 e.preventDefault(); setBusy(true); setErr(null);18 const examples: Record<string, Record<string, string>> = {};19 for (const line of form.examples.split("\n").map((s) => s.trim()).filter(Boolean)) { const [op, kv] = line.split("=", 2); if (!op || !kv) continue; const i = kv.indexOf("="); const k = i >= 0 ? kv.slice(0, i) : kv, v = i >= 0 ? kv.slice(i + 1) : ""; (examples[op] ??= {})[k] = v; }20 try {21 const r = await api<{ job_id: string }>("discover", { body: { project_id: form.project_id, [form.mode === "openapi" ? "openapi_url" : "probe_url"]: form.url, name: form.name || null, license: form.license || null, records_path: form.records_path || null, examples } });22 setJobId(r.job_id);23 } catch (ex) { setErr(ex as ApiError); } finally { setBusy(false); }24 };25 const rows = (data?.apis ?? []).filter((a) => !provider || a.provider_slug === provider);26 return (27 <div>28 <div className="row between"><h1>API Atlas</h1><button className="btn primary" onClick={() => setShowForm((s) => !s)}>{showForm ? "Close" : "Discover API"}</button></div>29 {showForm && (30 <form className="card" onSubmit={submit}>31 <h3>Discover a new API</h3><p className="hint">Fetches the documentation (OpenAPI) or probes one documented public endpoint through the gateway, records evidence, compiles a knowledge package (Python/TypeScript/MCP clients + tests) and schedules verification.</p>32 <div className="form-grid">33 <div className="field"><label htmlFor="d-mode">Source</label><select id="d-mode" value={form.mode} onChange={(e) => setForm({ ...form, mode: e.target.value })}><option value="openapi">OpenAPI / Swagger document URL</option><option value="probe">Probe a public endpoint URL (schema inferred)</option></select></div>34 <div className="field"><label htmlFor="d-url">URL</label><input id="d-url" required type="url" placeholder="https://…" value={form.url} onChange={(e) => setForm({ ...form, url: e.target.value })} /></div>35 <div className="field"><label htmlFor="d-name">Name (optional)</label><input id="d-name" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} /></div>36 <div className="field"><label htmlFor="d-lic">License (enables raw snapshots)</label><input id="d-lic" placeholder="e.g. CC-BY-4.0" value={form.license} onChange={(e) => setForm({ ...form, license: e.target.value })} /></div>37 <div className="field"><label htmlFor="d-rp">Records path (JSON path to rows, e.g. $[1] or $.data)</label><input id="d-rp" value={form.records_path} onChange={(e) => setForm({ ...form, records_path: e.target.value })} /></div>38 <div className="field"><label htmlFor="d-proj">Project</label><select id="d-proj" value={form.project_id} onChange={(e) => setForm({ ...form, project_id: e.target.value })}>{(projects.data?.projects ?? [{ slug: "default", name: "Default" } as Project]).map((p) => <option key={p.slug} value={p.slug}>{p.name} ({p.policy_mode ?? ""})</option>)}</select></div>39 <div className="field span2"><label htmlFor="d-ex">Example query params per operation (one per line: operation_id=key=value) — used for live verification and fixtures</label><textarea id="d-ex" value={form.examples} onChange={(e) => setForm({ ...form, examples: e.target.value })} /></div>40 </div>41 <ErrorBox error={err} />42 <div className="row"><button className="btn primary" disabled={busy}>{busy ? "Submitting…" : "Start discovery"}</button></div>43 <div style={{ marginTop: 10 }}><JobStatus jobId={jobId} label="discover" onDone={() => reload()} /></div>44 </form>45 )}46 <div className="filters">47 <input aria-label="Filter by name" placeholder="filter by name / provider" value={q} onChange={(e) => set("q", e.target.value)} />48 <select aria-label="Kind" value={kind} onChange={(e) => set("kind", e.target.value)}><option value="">all kinds</option>{["rest", "graphql", "sdk", "mcp"].map((k) => <option key={k}>{k}</option>)}</select>49 <select aria-label="Lifecycle" value={lifecycle} onChange={(e) => set("lifecycle", e.target.value)}><option value="">all lifecycles</option>{["discovered", "inspected", "modeled", "implemented", "tested", "registered", "monitored", "quarantined", "stale", "deprecated", "revoked"].map((k) => <option key={k}>{k}</option>)}</select>50 {provider && <button className="btn" onClick={() => set("provider", "")}>provider: {provider} ✕</button>}51 </div>52 {loading && <Loading />}<ErrorBox error={error} />53 {data && rows.length === 0 && <Empty>No APIs match. Use “Discover API” to register one.</Empty>}54 {rows.length > 0 && (55 <table className="tbl"><thead><tr><th>API</th><th>Provider</th><th>Kind</th><th>Lifecycle</th><th>Knowledge</th><th>Endpoints</th><th>Packages</th><th>Executions</th><th>Debt</th><th>Updated</th></tr></thead><tbody>56 {rows.map((a) => (<tr key={a.id}>57 <Td l="api"><Link to={`/apis/${a.id}`}><b>{a.name}</b></Link> {a.opaque_internal_implementation && <Badge v="critical" label="opaque" />}<div className="hint mono truncate">{a.base_url ?? ""}</div></Td>58 <Td l="provider">{a.provider_name}</Td><Td l="kind">{a.kind}</Td><Td l="lifecycle"><Badge v={a.lifecycle} /></Td><Td l="knowledge"><Badge v={a.knowledge_state} /></Td>59 <Td l="endpoints" className="num">{a.endpoints}</Td><Td l="packages" className="num">{a.packages}</Td><Td l="executions" className="num">{a.executions}</Td><Td l="debt" className="num">{a.open_debt ? <Badge v="warn" label={String(a.open_debt)} /> : <Bool v={true} yes="none" />}</Td><Td l="updated">{ago(a.updated_at)}</Td>60 </tr>))}61 </tbody></table>)}62 </div>63 );64}65