zebflow 0.11.14

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
import { useEffect, useState, cx } from "zeb/react";
import Button from "@/components/ui/button";
import Textarea from "@/components/ui/textarea";
import { StudioTable, StudioTd, StudioTh, StudioThead } from "@/components/ui/studio-data-table";
import { stringifyCell } from "@/components/db/cell-format";

/**
 * The walks offered for one edge type: pick one, change it if you like, run
 * it, read the rows. Owns the statement being edited and its answer; a new
 * set of walks — another arrow, another start row — puts the first one back.
 */
export default function GraphWalkRunner({ walks, runDbQuery }) {
  const [chosen, setChosen] = useState("");
  const [sql, setSql] = useState("");
  const [result, setResult] = useState(null);
  const [status, setStatus] = useState("");
  const [busy, setBusy] = useState(false);
  const signature = walks.map((walk) => walk.sql).join("\n");

  useEffect(() => {
    const first = walks.find((walk) => walk.id === chosen) || walks[0];
    setChosen(first?.id || "");
    setSql(first?.sql || "");
    setResult(null);
    setStatus("");
  }, [signature]);

  async function run() {
    if (!sql.trim()) return;
    setBusy(true);
    setStatus("Running…");
    try {
      const answer = await runDbQuery(sql, { readOnly: true, limit: 200 });
      setResult(answer);
      setStatus(`${answer.rows.length} ${answer.rows.length === 1 ? "row" : "rows"}`);
    } catch (error) {
      setResult(null);
      setStatus(`Error · ${String(error?.message || error)}`);
    } finally {
      setBusy(false);
    }
  }

  return (
    <div className="flex min-h-0 flex-col gap-3">
      <div className="flex flex-wrap gap-1.5">
        {walks.map((walk) => (
          <button
            key={walk.id}
            type="button"
            onClick={() => {
              setChosen(walk.id);
              setSql(walk.sql);
              setResult(null);
              setStatus("");
            }}
            className={cx(
              "rounded-md border px-2 py-1 text-xs transition-colors",
              walk.id === chosen ? "border-primary text-foreground" : "border-border text-muted-foreground hover:text-foreground",
            )}
          >
            {walk.title}
          </button>
        ))}
      </div>

      <Textarea className="font-mono text-xs" value={sql} rows={5} onInput={(event) => setSql(event?.target?.value || "")} />

      <div className="flex items-center gap-3">
        <Button type="button" size="sm" onClick={run} disabled={busy || !sql.trim()}>
          {busy ? "Running…" : "Run"}
        </Button>
        {status ? (
          <span className={cx("text-xs", status.startsWith("Error") ? "text-danger" : "text-muted-foreground")}>{status}</span>
        ) : null}
      </div>

      {result && result.rows.length ? (
        <div className="max-h-72 overflow-auto rounded-md border border-border/70">
          <StudioTable>
            <StudioThead>
              <tr>
                {result.columns.map((column, index) => <StudioTh key={`${column}-${index}`}>{column}</StudioTh>)}
              </tr>
            </StudioThead>
            <tbody>
              {result.rows.map((row, index) => (
                <tr key={index}>
                  {row.map((cell, at) => <StudioTd key={at}>{stringifyCell(cell)}</StudioTd>)}
                </tr>
              ))}
            </tbody>
          </StudioTable>
        </div>
      ) : null}
    </div>
  );
}