Show and edit option values laid out as in named.conf

Values with blocks (logging, controls, ...) are shown over several lines
with indentation, and edited that way: the editor grows with its content,
Tab indents, Enter keeps the indentation. With a draft open, option tables
show the draft's values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-10 11:55:41 +00:00
1 parent 27d1f9e538
commit ad8eb3fc3d
7 files changed
+93 -5

No files matched your search

+38 -5
View File
@@ -1,5 +1,5 @@
import { h, mount, table, field, input, select, textarea, toast, errorBox, badge, confirmDialog, lines } from "../ui.js";
import { get, change, describeResult } from "../api.js";
import { get, change, describeResult, draftQuery } from "../api.js";
import { rerender } from "../app.js";
import { emailOf } from "./zones.js";
import { amlContext, suggestions } from "../aml.js";
@@ -28,7 +28,7 @@ function valueWidget(stmt, value, ctx) {
return { el: h("div", null, t, ctx ? suggestions(t, ctx) : null), get: () => "{ " + lines(t.value).map((l) => l.replace(/;$/, "") + ";").join(" ") + " }", hint: "One element per line: address or prefix, !negated, key NAME, ACL name, any, none, localhost, localnets. First match wins." };
}
if (kind === "raw") {
const t = textarea(value || "", { class: "mono", rows: 3 });
const t = codeArea(value || "");
// Statements such as allow-transfer and listen-on take an address
// match list after optional parameters: suggest names at the cursor.
const takesList = stmt && /address_match_element/.test(stmt.grammar || "");
@@ -39,9 +39,42 @@ function valueWidget(stmt, value, ctx) {
return { el: i, get: () => i.value.trim() };
}
// codeArea is a textarea for named.conf text: as tall as its content
// (up to 30 lines), Tab indents, Enter keeps the line's indentation. After
// Esc, Tab moves on to the next field again.
function codeArea(value) {
const t = textarea(value, { class: "mono code", spellcheck: "false" });
const fit = () => { t.rows = Math.min(30, Math.max(3, t.value.split("\n").length + 1)); };
fit();
let tabMoves = false;
const insert = (text) => {
const { selectionStart: a, selectionEnd: b } = t;
t.setRangeText(text, a, b, "end");
fit();
};
t.addEventListener("input", fit);
t.addEventListener("focus", () => { tabMoves = false; });
t.addEventListener("keydown", (ev) => {
if (ev.key === "Escape") { tabMoves = true; return; }
if (ev.key === "Tab" && !tabMoves && !ev.shiftKey) {
ev.preventDefault();
insert("\t");
} else if (ev.key === "Enter" && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
const line = t.value.slice(0, t.selectionStart).split("\n").pop();
let indent = line.match(/^\t*/)[0];
if (line.trimEnd().endsWith("{")) indent += "\t";
ev.preventDefault();
insert("\n" + indent);
}
});
return t;
}
// optionsEditor renders the options of one scope.
export async function optionsEditor(el, { scope, scope_id, catalogScope, title, intro }) {
const [opts, cat, ctx] = await Promise.all([get("/options", { scope, scope_id }), catalog(catalogScope), amlContext()]);
// With a draft open, show the options as they will be: a staged edit
// stays visible after Save.
const [opts, cat, ctx] = await Promise.all([get("/options", draftQuery({ scope, scope_id })), catalog(catalogScope), amlContext()]);
const byName = Object.fromEntries(cat.map((s) => [s.name, s]));
const err = h("div");
const listEl = h("div");
@@ -49,7 +82,7 @@ export async function optionsEditor(el, { scope, scope_id, catalogScope, title,
const draw = () => {
mount(listEl, table([
{ label: "Option", render: (o) => h("span", null, h("strong", { class: "mono" }, o.name), o.status ? [" ", badge(o.status, "warn")] : null), tdClass: "nowrap" },
{ label: "Value", render: (o) => editing === o.id ? editRow(o) : h("pre", { style: { padding: "2px 6px", whiteSpace: "pre-wrap" } }, o.value || "(no value)") },
{ label: "Value", render: (o) => editing === o.id ? editRow(o) : h("pre", { class: "code", style: { padding: "2px 6px", whiteSpace: "pre-wrap" } }, o.formatted || o.value || "(no value)") },
{ label: "Description", render: (o) => h("span", { class: "muted small" }, o.description || ""), tdClass: "small" },
{ label: "", render: (o) => editing === o.id ? "" : h("span", null,
h("button", { class: "small", onclick: () => { editing = o.id; draw(); } }, "Edit"), " ",
@@ -57,7 +90,7 @@ export async function optionsEditor(el, { scope, scope_id, catalogScope, title,
], opts, { empty: "No options set; BIND's defaults apply." }));
};
const editRow = (o) => {
const w = valueWidget(byName[o.name], o.value, ctx);
const w = valueWidget(byName[o.name], o.formatted || o.value, ctx);
const e2 = h("div");
return h("div", null, w.el, w.hint ? h("div", { class: "hint" }, w.hint) : null,
h("div", { class: "actions", style: { marginTop: "6px" } },