zebflow 0.10.10

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
import { useState, cx } from "zeb/react";
import Button from "@/components/ui/button";
import Input from "@/components/ui/input";
import ConfirmDialog from "@/components/ui/confirm-dialog";
import { StudioTable, StudioTd, StudioTh, StudioThead } from "@/components/ui/studio-data-table";
import { stringifyCell } from "@/components/db/cell-format";
import { edgePropertyColumns } from "@/components/db/edge-tables";
import { useEdgeRows } from "@/components/db/use-edge-rows";

/**
 * An edge table's Data tab: its edges as rows, each removable, and a line
 * to add one.
 *
 * An edge has no key of its own to select it by — its two ends are its
 * address — so this is not the row grid, and nothing here edits a cell.
 */
export default function EdgeTablePanel({ edgeTable, runDbQuery }) {
  const edges = useEdgeRows({ edgeTable, runDbQuery });
  const [pendingRemove, setPendingRemove] = useState(null);
  const edge = edgeTable.edge;
  const properties = edgePropertyColumns(edgeTable);
  const columns = [edge.source, edge.destination, ...properties.map((attr) => attr.name)];

  return (
    <div className="flex min-h-0 flex-1 flex-col">
      <div className="flex flex-wrap items-center gap-3 border-b border-border/70 px-3 py-2 text-xs text-muted-foreground">
        <span>
          <span className="text-foreground">{edge.source_table}</span> → <span className="text-foreground">{edge.destination_table}</span>
        </span>
        <span>type {edge.label}</span>
        <span>{edges.count} {edges.count === 1 ? "edge" : "edges"}</span>
        {edges.count > edges.shown ? <span>first {edges.shown} shown</span> : null}
        <Button type="button" variant="ghost" size="sm" className="ml-auto" onClick={edges.reload} disabled={edges.busy}>
          Refresh
        </Button>
      </div>

      <AddEdgeRow edgeTable={edgeTable} busy={edges.busy} onAdd={edges.add} />

      {edges.status ? (
        <p className={cx("px-3 py-1 text-xs", edges.status.startsWith("Error") ? "text-danger" : "text-muted-foreground")}>
          {edges.status}
        </p>
      ) : null}

      <div className="min-h-0 flex-1 overflow-auto">
        {edges.rows.length === 0 ? (
          <div className="db-suite-empty">{edges.busy ? "Loading edges…" : "No edges yet."}</div>
        ) : (
          <StudioTable>
            <StudioThead>
              <tr>
                {columns.map((name) => <StudioTh key={name}>{name}</StudioTh>)}
                <StudioTh />
              </tr>
            </StudioThead>
            <tbody>
              {edges.rows.map((row, index) => (
                <tr key={`${row[edge.source]}-${row[edge.destination]}-${index}`}>
                  {columns.map((name) => <StudioTd key={name}>{stringifyCell(row[name])}</StudioTd>)}
                  <StudioTd className="text-right">
                    <Button type="button" variant="ghost" size="sm" onClick={() => setPendingRemove(row)} disabled={edges.busy}>
                      Delete
                    </Button>
                  </StudioTd>
                </tr>
              ))}
            </tbody>
          </StudioTable>
        )}
      </div>

      <ConfirmDialog
        open={!!pendingRemove}
        onClose={() => setPendingRemove(null)}
        onConfirm={async () => {
          const row = pendingRemove;
          setPendingRemove(null);
          await edges.remove(row);
        }}
        title="Delete edge"
        message={`Delete the edge from ${String(pendingRemove?.[edge.source] ?? "")} to ${String(pendingRemove?.[edge.destination] ?? "")}? The two rows stay.`}
        confirmLabel="Delete"
        variant="destructive"
      />
    </div>
  );
}

/** One line of inputs: the two ends' keys and the edge's properties. */
function AddEdgeRow({ edgeTable, busy, onAdd }) {
  const edge = edgeTable.edge;
  const [source, setSource] = useState("");
  const [destination, setDestination] = useState("");
  const [values, setValues] = useState({});

  async function submit(event) {
    event?.preventDefault?.();
    if (!source.trim() || !destination.trim()) return;
    if (await onAdd(source.trim(), destination.trim(), values)) {
      setSource("");
      setDestination("");
      setValues({});
    }
  }

  return (
    <form onSubmit={submit} className="flex flex-wrap items-center gap-2 border-b border-border/70 px-3 py-2">
      <Input className="h-7 w-40 text-xs" value={source} placeholder={`${edge.source} · ${edge.source_table} key`} onInput={(event) => setSource(event?.target?.value || "")} disabled={busy} />
      <span className="text-xs text-muted-foreground">→</span>
      <Input className="h-7 w-40 text-xs" value={destination} placeholder={`${edge.destination} · ${edge.destination_table} key`} onInput={(event) => setDestination(event?.target?.value || "")} disabled={busy} />
      {edgePropertyColumns(edgeTable).map((attr) => (
        <Input
          key={attr.name}
          className="h-7 w-32 text-xs"
          value={values[attr.name] || ""}
          placeholder={attr.default ? `${attr.name} · default ${attr.default}` : attr.name}
          onInput={(event) => setValues({ ...values, [attr.name]: event?.target?.value || "" })}
          disabled={busy}
        />
      ))}
      <Button type="submit" size="sm" variant="outline" disabled={busy || !source.trim() || !destination.trim()}>
        Add edge
      </Button>
    </form>
  );
}