SPB Git

spb/akos

Public
6.5 KB · 88 lines tsx
1import { type ReactNode, useState } from "react";2import { Link } from "react-router";3import type { ApiError, Job } from "../api";4import { useJob } from "../hooks";56export function Loading({ what = "Loading" }: { what?: string }) { return <div className="loading" role="status">{what}…</div>; }7export function ErrorBox({ error }: { error: ApiError | Error | string | null }) {8  if (!error) return null;9  const e = error as ApiError;10  return <div className="alert error" role="alert">{typeof error === "string" ? error : `${e.code ? e.code + ": " : ""}${e.message}`}{e.correlationId ? <span className="hint"> (ref {e.correlationId})</span> : null}</div>;11}12export function Empty({ children }: { children: ReactNode }) { return <div className="empty">{children}</div>; }1314const TONE: Record<string, string> = {15  succeeded: "good", done: "good", pass: "good", verified: "good", allow: "good", online: "good", ok: "good", resolved: "good", approved: "good", validated: "good", registered: "good", monitored: "good", tested: "good", independently_reproduced: "good", production: "good",16  failed: "critical", dead: "critical", fail: "critical", blocked: "critical", deny: "critical", critical: "critical", revoked: "critical", rejected: "critical", block: "critical",17  high: "serious", serious: "serious", quarantined: "serious", stale: "serious", deprecated: "serious", quarantine: "serious", unverified: "serious", research: "accent",18  medium: "warn", warn: "warn", warning: "warn", running: "warn", in_progress: "warn", intent: "warn", unobserved: "warn", unknown: "warn", discovery: "warn", open: "warn",19  low: "", info: "", queued: "", proposed: "", observed: "", documented: "", implemented: "accent", inspected: "", modeled: "", discovered: "", development: "", accepted: "",20};21export function Badge({ v, label }: { v: string | null | undefined; label?: string }) {22  const s = v ?? "—";23  return <span className={`badge ${TONE[s] ?? ""}`}>{label ?? s}</span>;24}25export function Bool({ v, yes = "yes", no = "no" }: { v: boolean | null | undefined; yes?: string; no?: string }) { return <span className={`badge ${v ? "good" : "warn"}`}>{v ? yes : no}</span>; }2627export function Td({ l, children, className, wide }: { l: string; children: ReactNode; className?: string; wide?: boolean }) {28  return <td data-l={l} className={`${className ?? ""} ${wide ? "wide" : ""}`.trim()}>{children}</td>;29}3031export function KV({ items }: { items: [string, ReactNode][] }) {32  return <dl className="kv">{items.map(([k, v]) => (<div key={k} style={{ display: "contents" }}><dt>{k}</dt><dd>{v ?? "—"}</dd></div>))}</dl>;33}3435export function Tabs({ tabs, active, onChange }: { tabs: string[]; active: string; onChange: (t: string) => void }) {36  return <div className="tabs" role="tablist">{tabs.map((t) => <button key={t} role="tab" aria-selected={t === active} className={t === active ? "active" : ""} onClick={() => onChange(t)}>{t}</button>)}</div>;37}3839export function JobStatus({ jobId, label = "job", onDone }: { jobId: string | null; label?: string; onDone?: (j: Job) => void }) {40  const job = useJob(jobId, onDone);41  if (!jobId) return null;42  if (!job) return <div className="alert">Submitted {label} {jobId.slice(0, 8)}… waiting for worker</div>;43  const terminal = ["done", "dead", "cancelled", "failed"].includes(job.state);44  return (45    <div className={`alert ${job.state === "done" ? "ok" : job.state === "dead" ? "error" : ""}`}>46      <div className="row between"><span>{label} <code>{job.id.slice(0, 8)}</code> <Badge v={job.state} /> {!terminal && <span className="hint">(attempt {job.attempts}, polling…)</span>}</span><Link to={`/operations?job=${job.id}`} className="hint">details</Link></div>47      {job.error && <pre style={{ marginTop: 6 }}>{job.error}</pre>}48      {job.result && <JsonTree data={job.result} open />}49    </div>50  );51}5253export function JsonTree({ data, open = false, depth = 0 }: { data: unknown; open?: boolean; depth?: number }) {54  if (data === null || data === undefined) return <span className="lit">null</span>;55  if (typeof data === "string") return <span className="str">"{data.length > 400 ? data.slice(0, 400) + "…" : data}"</span>;56  if (typeof data === "number" || typeof data === "boolean") return <span className="num">{String(data)}</span>;57  const isArr = Array.isArray(data);58  const entries = isArr ? (data as unknown[]).map((v, i) => [String(i), v] as [string, unknown]) : Object.entries(data as Record<string, unknown>);59  if (entries.length === 0) return <span className="lit">{isArr ? "[]" : "{}"}</span>;60  const inner = (61    <ul>{entries.slice(0, 300).map(([k, v]) => (<li key={k}><span className="key">{isArr ? `[${k}]` : k}</span>: <JsonTree data={v} open={open && depth < 2} depth={depth + 1} /></li>))}{entries.length > 300 && <li className="lit">… {entries.length - 300} more</li>}</ul>62  );63  const wrapper = <details open={open || depth === 0}><summary>{isArr ? `array[${entries.length}]` : `object{${entries.length}}`}</summary>{inner}</details>;64  return depth === 0 ? <div className="jsontree">{wrapper}</div> : wrapper;65}6667export function CopyBox({ text }: { text: string }) {68  const [ok, setOk] = useState(false);69  return (70    <div className="codebox">71      <button className="btn sm copy" onClick={() => { navigator.clipboard?.writeText(text).then(() => { setOk(true); setTimeout(() => setOk(false), 1500); }); }}>{ok ? "copied" : "copy"}</button>72      <pre>{text}</pre>73    </div>74  );75}7677export function Confirm({ label, onConfirm, className = "btn sm danger", disabled }: { label: string; onConfirm: () => void; className?: string; disabled?: boolean }) {78  const [arm, setArm] = useState(false);79  if (!arm) return <button className={className} disabled={disabled} onClick={() => setArm(true)}>{label}</button>;80  return <span className="row"><button className={className} onClick={() => { setArm(false); onConfirm(); }}>confirm {label}</button><button className="btn sm" onClick={() => setArm(false)}>cancel</button></span>;81}8283export function Pager({ offset, limit, total, onChange }: { offset: number; limit: number; total: number; onChange: (o: number) => void }) {84  return <div className="pager"><span>{total === 0 ? "0" : `${offset + 1}–${Math.min(offset + limit, total)}`} of {total}</span><button className="btn sm" disabled={offset === 0} onClick={() => onChange(Math.max(0, offset - limit))}>‹ prev</button><button className="btn sm" disabled={offset + limit >= total} onClick={() => onChange(offset + limit)}>next ›</button></div>;85}8687export function Spinner() { return <span className="hint">working…</span>; }88
Files · c744bcc51af6f1462b2f0d25ff87b7354eea4551
Menu

Theme