zebflow 0.11.14

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
import { useEffect, useState } from "zeb/react";
import Button from "@/components/ui/button";
import Input from "@/components/ui/input";
import GraphDiagram from "@/components/db/graph-diagram";
import GraphWalkRunner from "@/components/db/graph-walk-runner";
import { graphModel, walkTemplates } from "@/components/db/graph-walks";

/**
 * The Graph tab: every table of rows and every kind of edge between them,
 * drawn; choose an arrow to walk it. Offered only where the engine's
 * relations are a graph.
 *
 * The edge types come from the catalog's edge tables, with `SHOW EDGES`
 * adding the loose ones; `tablesHref` is where a box opens its table.
 */
export default function GraphTabPanel({ tables, runDbQuery, tablesHref }) {
  const [shownEdges, setShownEdges] = useState([]);
  const [error, setError] = useState("");
  const [selectedId, setSelectedId] = useState("");
  const [key, setKey] = useState("");

  async function load() {
    try {
      const shown = await runDbQuery("SHOW EDGES", { readOnly: true, limit: 500 });
      setShownEdges(shown.objects || []);
      setError("");
    } catch (failure) {
      setShownEdges([]);
      setError(String(failure?.message || failure));
    }
  }

  useEffect(() => {
    load();
  }, [tables?.length]);

  const { nodes, links } = graphModel(tables, shownEdges);
  const selected = links.find((link) => link.id === selectedId) || null;

  return (
    <section className="db-suite-panel db-suite-panel-fill">
      <div className="flex min-h-0 flex-1 flex-col gap-4 overflow-auto p-4 lg:flex-row">
        <div className="min-w-0 flex-1 overflow-auto rounded-lg border border-border/70 bg-accent/10">
          <div className="flex items-center justify-between gap-3 border-b border-border/70 px-3 py-2 text-xs text-muted-foreground">
            <span>
              {nodes.length} {nodes.length === 1 ? "table" : "tables"} · {links.length} edge {links.length === 1 ? "type" : "types"} · dashed is loose
            </span>
            <Button type="button" variant="ghost" size="sm" onClick={load}>Refresh</Button>
          </div>
          {error ? <p className="px-3 py-2 text-xs text-danger">{error}</p> : null}
          {nodes.length ? (
            <GraphDiagram
              nodes={nodes}
              links={links}
              selectedId={selectedId}
              onSelectLink={setSelectedId}
              tableHref={(table) => `${tablesHref}?table=${encodeURIComponent(table)}`}
            />
          ) : (
            <div className="db-suite-empty">No tables yet.</div>
          )}
        </div>

        <div className="flex w-full min-w-0 flex-col gap-3 lg:w-[28rem]">
          {selected ? (
            <>
              <div>
                <p className="text-sm font-medium text-foreground">{selected.type}</p>
                <p className="text-xs text-muted-foreground">
                  {selected.from} → {selected.to} · {selected.loose ? "loose: written by an API or an import, no edge table" : `edge table ${selected.edgeTable.key}`}
                </p>
              </div>
              <Input
                className="h-8 text-xs"
                value={key}
                placeholder={`start row: a ${selected.from} or ${selected.to} key`}
                onInput={(event) => setKey(event?.target?.value || "")}
              />
              <GraphWalkRunner walks={walkTemplates(selected, { tables, links, key })} runDbQuery={runDbQuery} />
            </>
          ) : (
            <p className="text-sm text-muted-foreground">
              Choose an arrow to walk that kind of edge. Choose a box to open its table.
            </p>
          )}
        </div>
      </div>
    </section>
  );
}