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:
1 parent
27d1f9e538
commit
ad8eb3fc3d
7 files changed
+93
-5
No files matched your search
@@ -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" } },
|
||||
|
||||
Reference in new issue
Block a user