SPB Git

spb/akos

Public
8.8 KB · 107 lines tsx
1import { useState, type FormEvent } from "react";2import { Link } from "react-router";3import { api, ApiError, type Debt, type Endpoint, type Evidence } from "../api";4import { fmtDate } from "../hooks";5import { Badge, ErrorBox, JsonTree, Td } from "./ui";67export function EvidenceList({ evidence }: { evidence: Evidence[] }) {8  if (!evidence.length) return <div className="empty">No machine evidence recorded yet.</div>;9  return (10    <div className="stack">11      {[...evidence].reverse().map((e) => (12        <details key={e.id} className="card tight">13          <summary><Badge v={e.result} /> <b>{e.kind}</b> <span className="hint">{fmtDate(e.created_at)} · {e.created_by ?? "system"}</span></summary>14          <div style={{ marginTop: 8 }}>15            {e.command && <div className="hint">command: <code>{e.command}</code></div>}16            {e.output_excerpt && <pre style={{ marginTop: 6 }}>{e.output_excerpt}</pre>}17            {e.output_hash && <div className="hint">output sha256 <code>{e.output_hash}</code></div>}18            {Object.keys(e.details ?? {}).length > 0 && <JsonTree data={e.details} />}19          </div>20        </details>21      ))}22    </div>23  );24}2526export function DebtTable({ debt, onChange }: { debt: Debt[]; onChange?: () => void }) {27  const [err, setErr] = useState<ApiError | null>(null);28  if (!debt.length) return <div className="empty">No knowledge debt.</div>;29  const patch = async (id: string, status: string) => { setErr(null); try { await api(`debt/${id}`, { method: "PATCH", body: { status } }); onChange?.(); } catch (e) { setErr(e as ApiError); } };30  return (31    <div>32      <ErrorBox error={err} />33      <table className="tbl"><thead><tr><th>Severity</th><th>Kind</th><th>Subject</th><th>Description</th><th>Remediation</th><th>Status</th><th>Created</th></tr></thead><tbody>34        {debt.map((d) => (<tr key={d.id}>35          <Td l="severity"><Badge v={d.severity} /></Td><Td l="kind"><code>{d.kind}</code></Td>36          <Td l="subject"><SubjectLink type={d.subject_type} id={d.subject_id} label={d.subject_label ?? undefined} /></Td>37          <Td l="description" wide>{d.description}</Td><Td l="remediation" wide><span className="hint">{d.remediation ?? "—"}</span></Td>38          <Td l="status">{onChange ? <select aria-label="status" value={d.status} onChange={(e) => patch(d.id, e.target.value)}>{["open", "in_progress", "resolved", "accepted"].map((s) => <option key={s}>{s}</option>)}</select> : <Badge v={d.status} />}</Td>39          <Td l="created">{fmtDate(d.created_at)}</Td>40        </tr>))}41      </tbody></table>42    </div>43  );44}4546export function SubjectLink({ type, id, label }: { type: string; id: string; label?: string }) {47  const to = type === "akp" ? `/akps/${id}` : type === "api" ? `/apis/${id}` : type === "execution" ? `/executions/${id}` : type === "version" ? `/versions/${id}` : type === "dataset" ? `/datasets/${id}` : null;48  const text = label ?? `${type} ${id.slice(0, 8)}`;49  return to ? <Link to={to}>{text}</Link> : <span>{text}</span>;50}5152export function LifecycleButtons({ allowed, onTransition }: { allowed: string[]; onTransition: (to: string, reason: string) => Promise<void> }) {53  const [err, setErr] = useState<ApiError | null>(null); const [busy, setBusy] = useState<string | null>(null); const [reason, setReason] = useState("");54  if (!allowed.length) return <span className="hint">no transitions available</span>;55  return (56    <div>57      <div className="row"><input aria-label="Transition reason" placeholder="reason (optional)" style={{ maxWidth: 260 }} value={reason} onChange={(e) => setReason(e.target.value)} />58        {allowed.map((t) => <button key={t} className="btn sm" disabled={busy !== null} onClick={async () => { setBusy(t); setErr(null); try { await onTransition(t, reason); } catch (e) { setErr(e as ApiError); } finally { setBusy(null); } }}>→ {t}</button>)}</div>59      <ErrorBox error={err} />60    </div>61  );62}6364/** Execute form for one endpoint: query + path params → POST /execute. */65export function ExecuteForm({ endpoint, apiId, packageId, projects }: { endpoint: Endpoint; apiId?: string; packageId?: string; projects: { slug: string; name: string }[] }) {66  const [vals, setVals] = useState<Record<string, string>>(() => Object.fromEntries(endpoint.parameters.filter((p) => p.example !== undefined && p.example !== null).map((p) => [p.name, String(p.example)])));67  const [project, setProject] = useState("default"); const [purpose, setPurpose] = useState("");68  const [res, setRes] = useState<Record<string, unknown> | null>(null); const [err, setErr] = useState<ApiError | null>(null); const [busy, setBusy] = useState(false);69  const submit = async (e: FormEvent) => {70    e.preventDefault(); setBusy(true); setErr(null); setRes(null);71    const path_params: Record<string, string> = {}, query: Record<string, string> = {};72    for (const p of endpoint.parameters) { const v = vals[p.name]; if (v === undefined || v === "") continue; (p.in === "path" ? path_params : query)[p.name] = v; }73    try { setRes(await api("execute", { body: { project_id: project, api_id: packageId ? undefined : apiId, package_id: packageId, endpoint_id: endpoint.id, operation_id: endpoint.operation_id, method: endpoint.method === "MCP" ? "GET" : endpoint.method, path_params, query, purpose, caller: "ui" } })); }74    catch (ex) { const e = ex as ApiError; setErr(e); const p = e.payload as { execution?: Record<string, unknown> } | undefined; if (p?.execution) setRes(p.execution); }75    finally { setBusy(false); }76  };77  const ex = res as { id?: string; state?: string; status_code?: number; duration_ms?: number; response_hash?: string; policy_decision?: { decision: string; reasons: string[] } } | null;78  return (79    <form onSubmit={submit} className="card tight">80      <div className="form-grid">81        {endpoint.parameters.map((p) => <div className="field" key={p.name}><label htmlFor={`p-${endpoint.id}-${p.name}`}>{p.name} <span className="hint">({p.in}{p.required ? ", required" : ""})</span></label><input id={`p-${endpoint.id}-${p.name}`} required={p.required} value={vals[p.name] ?? ""} onChange={(e) => setVals({ ...vals, [p.name]: e.target.value })} placeholder={p.description?.slice(0, 60)} /></div>)}82        <div className="field"><label htmlFor={`proj-${endpoint.id}`}>Project (policy mode applies)</label><select id={`proj-${endpoint.id}`} value={project} onChange={(e) => setProject(e.target.value)}>{projects.map((p) => <option key={p.slug} value={p.slug}>{p.name}</option>)}</select></div>83        <div className="field"><label htmlFor={`purp-${endpoint.id}`}>Purpose</label><input id={`purp-${endpoint.id}`} value={purpose} onChange={(e) => setPurpose(e.target.value)} /></div>84      </div>85      <div className="row"><button className="btn primary sm" disabled={busy}>{busy ? "Executing…" : `Execute ${endpoint.method} ${endpoint.path}`}</button></div>86      <ErrorBox error={err} />87      {ex && <div className="alert" style={{ marginTop: 8 }}>execution <Link to={`/executions/${ex.id}`}>{String(ex.id).slice(0, 8)}</Link> <Badge v={ex.state} /> status {ex.status_code ?? "—"} · {ex.duration_ms ?? "—"} ms · hash <code>{ex.response_hash?.slice(0, 16) ?? "—"}</code>{ex.policy_decision && <div className="hint">policy {ex.policy_decision.decision}: {ex.policy_decision.reasons.join("; ")}</div>}</div>}88    </form>89  );90}9192export function EndpointRow({ e, apiId, packageId, projects }: { e: Endpoint; apiId?: string; packageId?: string; projects: { slug: string; name: string }[] }) {93  return (94    <details className="card tight">95      <summary><code>{e.method}</code> <b>{e.path}</b> <span className="hint">{e.operation_id}</span> {e.inferred && <Badge v="warn" label="inferred" />} {e.summary && <span className="hint">— {e.summary}</span>}</summary>96      <div style={{ marginTop: 8 }} className="grid c2">97        <div><h3>Parameters</h3>{e.parameters.length ? <table className="tbl"><thead><tr><th>Name</th><th>In</th><th>Req</th><th>Schema</th><th>Description</th></tr></thead><tbody>{e.parameters.map((p) => <tr key={p.name}><Td l="name"><code>{p.name}</code></Td><Td l="in">{p.in}</Td><Td l="required">{p.required ? "yes" : ""}</Td><Td l="schema"><code>{p.schema?.type ? String(p.schema.type) : "—"}</code></Td><Td l="description" wide><span className="hint">{p.description}</span></Td></tr>)}</tbody></table> : <span className="hint">none</span>}98          {Object.keys(e.pagination ?? {}).length > 0 && <><h3>Pagination</h3><JsonTree data={e.pagination} open /></>}99          <h3>Execute</h3><ExecuteForm endpoint={e} apiId={apiId} packageId={packageId} projects={projects} /></div>100        <div><h3>Response schema</h3>{e.response_schema ? <JsonTree data={e.response_schema} open /> : <span className="hint">not documented</span>}101          {e.request_schema ? <><h3>Request schema</h3><JsonTree data={e.request_schema} /></> : null}102          <p className="hint"><Link to={`/endpoints/${e.id}`}>endpoint page →</Link></p></div>103      </div>104    </details>105  );106}107
Files · main
Menu

Theme