SPB Git

spb/akos

Public
3.0 KB · 31 lines tsx
1import { Link, useSearchParams } from "react-router";2import { qs, type ExecutionRow, type Project } from "../api";3import { useFetch, fmtDate } from "../hooks";4import { Badge, Empty, ErrorBox, Loading, Pager, Td } from "../components/ui";56const STATES = ["intent", "running", "succeeded", "failed", "blocked", "quarantined", "unobserved"];7export default function Executions() {8  const [sp, setSp] = useSearchParams();9  const project_id = sp.get("project_id") ?? "", state = sp.get("state") ?? "", offset = Number(sp.get("offset") ?? 0), limit = 50;10  const { data, error, loading } = useFetch<{ executions: ExecutionRow[]; total: number }>(`executions${qs({ project_id, state, limit, offset })}`);11  const projects = useFetch<{ projects: Project[] }>("projects");12  const set = (k: string, v: string) => setSp((p) => { const n = new URLSearchParams(p); if (v) n.set(k, v); else n.delete(k); if (k !== "offset") n.delete("offset"); return n; });13  return (14    <div>15      <h1>Execution Observatory</h1><p className="hint">Every mediated call: intent persisted before the request, outcome after. Blocked and quarantined rows are policy decisions, not errors.</p>16      <div className="filters">17        <select aria-label="Project" value={project_id} onChange={(e) => set("project_id", e.target.value)}><option value="">all projects</option>{projects.data?.projects.map((p) => <option key={p.id} value={p.slug}>{p.name}</option>)}</select>18        <select aria-label="State" value={state} onChange={(e) => set("state", e.target.value)}><option value="">all states</option>{STATES.map((s) => <option key={s}>{s}</option>)}</select>19      </div>20      {loading && <Loading />}<ErrorBox error={error} />21      {data && data.executions.length === 0 && <Empty>No executions match.</Empty>}22      {data && data.executions.length > 0 && (<>23        <table className="tbl"><thead><tr><th>When</th><th>State</th><th>Decision</th><th>Project</th><th>API / AKP</th><th>Request</th><th>Status</th><th>ms</th><th>Caller</th></tr></thead><tbody>24          {data.executions.map((e) => <tr key={e.id}><Td l="when"><Link to={`/executions/${e.id}`}>{fmtDate(e.created_at)}</Link></Td><Td l="state"><Badge v={e.state} />{e.provisional && <Badge v="warn" label="provisional" />}{e.observed === false && <Badge v="unobserved" />}</Td><Td l="decision"><Badge v={e.decision ?? "—"} /></Td><Td l="project">{e.project_slug} <span className="hint">{e.policy_mode}</span></Td><Td l="api">{e.akp_id ? `${e.akp_id} v${e.akp_version}` : e.api_name ?? <span className="hint">unregistered</span>}</Td><Td l="request" wide><span className="truncate mono">{e.method} {e.url}</span>{e.error && <div className="hint" style={{ color: "var(--critical)" }}>{e.error.slice(0, 120)}</div>}</Td><Td l="status">{e.status_code ?? "—"}</Td><Td l="ms" className="num">{e.duration_ms ?? "—"}</Td><Td l="caller"><span className="hint">{e.agent_name ?? e.caller}</span></Td></tr>)}25        </tbody></table>26        <Pager offset={offset} limit={limit} total={data.total} onChange={(o) => set("offset", String(o))} />27      </>)}28    </div>29  );30}31
Files · da58f809c01c422e65c786b05d6ffa3079c9feba
Menu

Theme