SPB Git

spb/akos

Public
5.3 KB · 34 lines tsx
1import { useState, type FormEvent } from "react";2import { Link } from "react-router";3import { api, ApiError, type Agent, type Project } from "../api";4import { useFetch, ago, fmtDate } from "../hooks";5import { Badge, Empty, ErrorBox, Loading, Td } from "../components/ui";6import { useMe } from "../App";78export default function Projects() {9  const { me } = useMe();10  const { data, error, loading, reload } = useFetch<{ projects: Project[] }>("projects");11  const agents = useFetch<{ agents: Agent[] }>("agents");12  const [form, setForm] = useState({ slug: "", name: "", policy_mode: "development" });13  const [ag, setAg] = useState({ name: "", kind: "claude-code", project_id: "default", execute: true, discover: true, export: false });14  const [err, setErr] = useState<ApiError | null>(null);15  const canWrite = me?.role === "admin" || me?.role === "operator";16  const create = async (e: FormEvent) => { e.preventDefault(); setErr(null); try { await api("projects", { body: form }); setForm({ slug: "", name: "", policy_mode: "development" }); reload(); } catch (ex) { setErr(ex as ApiError); } };17  const createAgent = async (e: FormEvent) => { e.preventDefault(); setErr(null); try { await api("agents", { body: { name: ag.name, kind: ag.kind, project_id: ag.project_id, permissions: { execute: ag.execute, discover: ag.discover, export: ag.export } } }); setAg({ ...ag, name: "" }); agents.reload(); } catch (ex) { setErr(ex as ApiError); } };18  return (19    <div>20      <h1>Agents & Projects</h1><ErrorBox error={err} />21      <div className="grid c2">22        <div className="card"><h3>Projects</h3>{loading && <Loading />}<ErrorBox error={error} />23          {data && data.projects.length === 0 && <Empty>No projects.</Empty>}24          {data && data.projects.length > 0 && <table className="tbl"><thead><tr><th>Project</th><th>Mode</th><th>Executions</th><th>24 h</th><th>Datasets</th><th>Agents</th></tr></thead><tbody>{data.projects.map((p) => <tr key={p.id}><Td l="project"><Link to={`/projects/${p.id}`}><b>{p.name}</b></Link> <span className="hint">{p.slug}</span></Td><Td l="mode"><Badge v={p.policy_mode} /></Td><Td l="executions" className="num">{p.executions ?? 0}</Td><Td l="24h" className="num">{p.executions_24h ?? 0}</Td><Td l="datasets" className="num">{p.datasets ?? 0}</Td><Td l="agents" className="num">{p.agents ?? 0}</Td></tr>)}</tbody></table>}25          {canWrite && <form onSubmit={create} style={{ marginTop: 12 }}><h3>New project</h3><div className="form-grid"><div className="field"><label htmlFor="p-slug">Slug</label><input id="p-slug" required pattern="[a-z0-9\-]+" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} /></div><div className="field"><label htmlFor="p-name">Name</label><input id="p-name" required value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} /></div><div className="field"><label htmlFor="p-mode">Policy mode</label><select id="p-mode" value={form.policy_mode} onChange={(e) => setForm({ ...form, policy_mode: e.target.value })}>{["discovery", "development", "research", "production"].map((m) => <option key={m}>{m}</option>)}</select></div></div><button className="btn primary">Create project</button></form>}</div>26        <div className="card"><h3>Agents</h3>{agents.loading && <Loading />}<ErrorBox error={agents.error} />27          {agents.data && agents.data.agents.length === 0 && <Empty>No agents yet. Agents are created on first execution (agent_name) or below.</Empty>}28          {agents.data && agents.data.agents.length > 0 && <table className="tbl"><thead><tr><th>Agent</th><th>Project</th><th>Permissions</th><th>Budget/day</th><th>Executions</th><th>Last seen</th></tr></thead><tbody>{agents.data.agents.map((a) => <tr key={a.id}><Td l="agent"><b>{a.name}</b> <span className="hint">{a.kind}</span></Td><Td l="project">{a.project_slug ?? "—"}</Td><Td l="permissions">{Object.entries(a.permissions).filter(([, v]) => v).map(([k]) => k).join(", ") || "none"}</Td><Td l="budget" className="num">{a.budget?.max_executions_per_day ?? "—"}</Td><Td l="executions" className="num">{a.executions ?? 0} <span className="hint">({a.executions_24h ?? 0} / 24 h)</span></Td><Td l="last seen">{a.last_seen_at ? ago(a.last_seen_at) : fmtDate(a.created_at)}</Td></tr>)}</tbody></table>}29          {canWrite && <form onSubmit={createAgent} style={{ marginTop: 12 }}><h3>New agent</h3><div className="form-grid"><div className="field"><label htmlFor="a-name">Name</label><input id="a-name" required value={ag.name} onChange={(e) => setAg({ ...ag, name: e.target.value })} /></div><div className="field"><label htmlFor="a-kind">Kind</label><input id="a-kind" value={ag.kind} onChange={(e) => setAg({ ...ag, kind: e.target.value })} /></div><div className="field"><label htmlFor="a-proj">Project</label><select id="a-proj" value={ag.project_id} onChange={(e) => setAg({ ...ag, project_id: e.target.value })}>{data?.projects.map((p) => <option key={p.id} value={p.slug}>{p.name}</option>)}</select></div><div className="field"><label>Permissions</label><span className="row">{(["execute", "discover", "export"] as const).map((k) => <label key={k} style={{ margin: 0 }}><input type="checkbox" style={{ width: "auto", minHeight: 0 }} checked={ag[k]} onChange={(e) => setAg({ ...ag, [k]: e.target.checked })} /> {k}</label>)}</span></div></div><button className="btn primary">Create agent</button></form>}</div>30      </div>31    </div>32  );33}34
Files · main
Menu

Theme