SPB Git

spb/akos

Public
6.9 KB · 65 lines tsx
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
Files · da58f809c01c422e65c786b05d6ffa3079c9feba
Menu

Theme