Give each view its own page, and add a CNAME from an A record
Views open a page like a zone's, with Settings, Options and Zones tabs (and New zone in that view); the Views page lists and orders them. A and AAAA records get a + CNAME button that fills in the add form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7be0762f48
commit
c696d02513
6 files changed
+126
-36
No files matched your search
@@ -61,6 +61,13 @@ All notable changes to Jarl are documented here. The format follows
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Web UI: each view has its own page, like a zone: Settings (match lists,
|
||||||
|
comment, colour), Options and Zones tabs (with **New zone in VIEW**),
|
||||||
|
and Delete. The Views page lists them in order, with the arrows that
|
||||||
|
reorder them; view links everywhere open the view's page.
|
||||||
|
- Web UI: A and AAAA records have a **+ CNAME** button that fills in the
|
||||||
|
add form with a CNAME pointing to the record's name, leaving only the
|
||||||
|
alias's name to type.
|
||||||
- Options are kept in one fixed order, in named.conf, the web UI and the
|
- Options are kept in one fixed order, in named.conf, the web UI and the
|
||||||
CLI: grouped by topic (files, listening, access control, recursion and
|
CLI: grouped by topic (files, listening, access control, recursion and
|
||||||
forwarding, DNSSEC, transfers and notify, then the rest), alphabetical
|
forwarding, DNSSEC, transfers and notify, then the rest), alphabetical
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const routes = [
|
|||||||
{ pattern: /^zones$/, page: zones, nav: "zones" },
|
{ pattern: /^zones$/, page: zones, nav: "zones" },
|
||||||
{ pattern: /^zones\/(\d+)(?:\/(\w+))?$/, page: zone, nav: "zones", params: ["id", "tab"] },
|
{ pattern: /^zones\/(\d+)(?:\/(\w+))?$/, page: zone, nav: "zones", params: ["id", "tab"] },
|
||||||
{ pattern: /^views$/, page: views, nav: "views" },
|
{ pattern: /^views$/, page: views, nav: "views" },
|
||||||
|
{ pattern: /^views\/(\d+)(?:\/(\w+))?$/, page: views, nav: "views", params: ["id", "tab"] },
|
||||||
{ pattern: /^acls$/, page: acls, nav: "acls" },
|
{ pattern: /^acls$/, page: acls, nav: "acls" },
|
||||||
{ pattern: /^keys$/, page: keys, nav: "keys" },
|
{ pattern: /^keys$/, page: keys, nav: "keys" },
|
||||||
{ pattern: /^options$/, page: options, nav: "options" },
|
{ pattern: /^options$/, page: options, nav: "options" },
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { h, mount, table, field, input, select, textarea, toast, errorBox, confirmDialog, modal, lines, checkbox, colorPicker, backLink, safeBack, viewHref } from "../ui.js";
|
import { h, mount, table, field, input, select, textarea, toast, errorBox, confirmDialog, modal, lines, checkbox, colorPicker, backLink, safeBack, viewHref, badge } from "../ui.js";
|
||||||
import { amlContext, suggestions } from "../aml.js";
|
import { amlContext, suggestions } from "../aml.js";
|
||||||
import { api, get, change, describeResult } from "../api.js";
|
import { api, get, change, describeResult } from "../api.js";
|
||||||
|
import { newZoneDialog, dnssecLock, pendingBadge } from "./zones.js";
|
||||||
import { rerender } from "../app.js";
|
import { rerender } from "../app.js";
|
||||||
import { optionsEditor } from "./options.js";
|
import { optionsEditor } from "./options.js";
|
||||||
|
|
||||||
export async function render(el, params, query) {
|
export async function render(el, params, query) {
|
||||||
|
// Old links (#/views?view=ID) open the view's page.
|
||||||
|
if (query.get("view")) {
|
||||||
|
history.replaceState(null, "", viewHref(query.get("view"), query.get("back") || ""));
|
||||||
|
return rerender();
|
||||||
|
}
|
||||||
|
if (params.id) return viewPage(el, params, query);
|
||||||
const [views, zones] = await Promise.all([get("/views"), get("/zones")]);
|
const [views, zones] = await Promise.all([get("/views"), get("/zones")]);
|
||||||
const viewless = zones.filter((z) => !z.view_id);
|
const viewless = zones.filter((z) => !z.view_id);
|
||||||
const optsEl = h("div");
|
|
||||||
const selected = query.get("view") ? views.find((v) => String(v.id) === query.get("view")) : null;
|
|
||||||
// Opened from a view link on a zone or the zone list: offer the way back
|
|
||||||
// (kept while picking other views here).
|
|
||||||
const back = safeBack(query.get("back"));
|
|
||||||
const move = async (i, d) => {
|
const move = async (i, d) => {
|
||||||
const ids = views.map((v) => v.id);
|
const ids = views.map((v) => v.id);
|
||||||
[ids[i], ids[i + d]] = [ids[i + d], ids[i]];
|
[ids[i], ids[i + d]] = [ids[i + d], ids[i]];
|
||||||
@@ -23,31 +25,83 @@ export async function render(el, params, query) {
|
|||||||
};
|
};
|
||||||
mount(el,
|
mount(el,
|
||||||
h("div", { class: "page-head" },
|
h("div", { class: "page-head" },
|
||||||
h("div", { class: "grow" }, back ? backLink(backLabel(back, zones), back) : null, h("h1", null, "Views"),
|
h("div", { class: "grow" }, h("h1", null, "Views"),
|
||||||
h("div", { class: "muted" }, "A client sees the first view it matches (by source address, destination address or TSIG key). Order matters.")),
|
h("div", { class: "muted" }, "A client sees the first view it matches (by source address, destination address or TSIG key). Order matters.")),
|
||||||
h("div", { class: "actions" }, h("button", { class: "primary", onclick: () => viewDialog(null, viewless) }, "New view"))),
|
h("div", { class: "actions" }, h("button", { class: "primary", onclick: () => viewDialog(viewless) }, "New view"))),
|
||||||
views.length === 0 ? h("div", { class: "msg info" },
|
views.length === 0 ? h("div", { class: "msg info" },
|
||||||
"No views: every client sees the same zones. Views let you serve different answers to different clients, for example an internal view with private records and recursion for your LAN, and an external view with only public records.") : null,
|
"No views: every client sees the same zones. Views let you serve different answers to different clients, for example an internal view with private records and recursion for your LAN, and an external view with only public records.") : null,
|
||||||
h("div", { class: "panel" }, table([
|
h("div", { class: "panel" }, table([
|
||||||
{ label: "#", render: (v, i) => String(i + 1) },
|
{ label: "#", render: (v, i) => String(i + 1) },
|
||||||
{ label: "View", render: (v) => h("a", { href: viewHref(v.id, back), class: v.color ? "vc-" + v.color : null }, h("strong", null, v.name)) },
|
{ label: "View", render: (v) => h("a", { href: viewHref(v.id), class: v.color ? "vc-" + v.color : null }, h("strong", null, v.name)) },
|
||||||
{ label: "Match clients", render: (v) => (v.match_clients || []).join("; ") || h("span", { class: "muted" }, "any"), tdClass: "data" },
|
{ label: "Match clients", render: (v) => (v.match_clients || []).join("; ") || h("span", { class: "muted" }, "any"), tdClass: "data" },
|
||||||
{ label: "Match destinations", render: (v) => (v.match_destinations || []).join("; ") || h("span", { class: "muted" }, "any"), tdClass: "data" },
|
{ label: "Match destinations", render: (v) => (v.match_destinations || []).join("; ") || h("span", { class: "muted" }, "any"), tdClass: "data" },
|
||||||
{ label: "Zones", render: (v) => h("a", { href: "#/zones?view=" + encodeURIComponent(v.name) }, String(v.zone_count)) },
|
{ label: "Zones", render: (v) => h("a", { href: viewHref(v.id) + "/zones" }, String(v.zone_count)) },
|
||||||
{ label: "Comment", render: (v) => h("span", { class: "muted" }, v.comment || "") },
|
{ label: "Comment", render: (v) => h("span", { class: "muted" }, v.comment || "") },
|
||||||
{ label: "", render: (v, i) => h("span", null,
|
{ label: "Order", render: (v, i) => h("span", null,
|
||||||
h("button", { class: "small", disabled: i === 0, title: "Move up", onclick: () => move(i, -1) }, "↑"), " ",
|
h("button", { class: "small", disabled: i === 0, title: "Move up: clients are matched against it earlier", onclick: () => move(i, -1) }, "↑"), " ",
|
||||||
h("button", { class: "small", disabled: i === views.length - 1, title: "Move down", onclick: () => move(i, 1) }, "↓"), " ",
|
h("button", { class: "small", disabled: i === views.length - 1, title: "Move down", onclick: () => move(i, 1) }, "↓")), tdClass: "actions-cell" },
|
||||||
h("button", { class: "small", onclick: () => viewDialog(v, viewless) }, "Edit"), " ",
|
], views, { empty: "No views." })));
|
||||||
h("button", { class: "small danger", onclick: () => deleteView(v) }, "Delete")), tdClass: "actions-cell" },
|
|
||||||
], views, { empty: "No views.", rowClass: (v) => selected && v.id === selected.id ? "selected" : "" })),
|
|
||||||
selected ? optsEl : (views.length ? h("p", { class: "muted" }, "Select a view to see its options.") : null));
|
|
||||||
if (selected) {
|
|
||||||
await optionsEditor(optsEl, { scope: "view", scope_id: selected.id, catalogScope: "view", title: "Options of view " + selected.name,
|
|
||||||
intro: "View options override the server options for clients in this view (for example recursion and allow-recursion)." });
|
|
||||||
// With a long list, scroll to the selected view's options.
|
|
||||||
if (views.length > 3) optsEl.scrollIntoView({ block: "start" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// viewPage is one view, laid out like a zone: its settings, its options
|
||||||
|
// and its zones, each on a tab.
|
||||||
|
async function viewPage(el, params, query) {
|
||||||
|
const id = Number(params.id);
|
||||||
|
const [views, zones] = await Promise.all([get("/views"), get("/zones")]);
|
||||||
|
const i = views.findIndex((v) => v.id === id);
|
||||||
|
const v = views[i];
|
||||||
|
const back = safeBack(query.get("back"));
|
||||||
|
if (!v) {
|
||||||
|
mount(el, backLink("Views", "#/views"), h("div", { class: "msg err" }, "View " + id + " doesn't exist (any more)."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const inView = zones.filter((z) => z.view_id === id);
|
||||||
|
const tab = params.tab || "settings";
|
||||||
|
const base = viewHref(id);
|
||||||
|
const suffix = back ? "?back=" + encodeURIComponent(back) : "";
|
||||||
|
const tabs = [["settings", "Settings"], ["options", "Options"], ["zones", "Zones (" + inView.length + ")"]];
|
||||||
|
const head = h("div", { class: "page-head" },
|
||||||
|
h("div", { class: "grow" },
|
||||||
|
back ? backLink(backLabel(back, zones), back) : backLink("Views", "#/views"),
|
||||||
|
h("h1", { class: v.color ? "vc-" + v.color : null }, v.name),
|
||||||
|
h("div", { class: "muted" },
|
||||||
|
"view " + (i + 1) + " of " + views.length + " · clients: " + ((v.match_clients || []).join("; ") || "any") +
|
||||||
|
((v.match_destinations || []).length ? " · destinations: " + v.match_destinations.join("; ") : "") +
|
||||||
|
(v.comment ? " · " + v.comment : ""))),
|
||||||
|
h("div", { class: "actions" }, h("button", { class: "danger", onclick: () => deleteView(v) }, "Delete view")));
|
||||||
|
const tabBar = h("div", { class: "tabs" }, tabs.map(([k, label]) => h("a", { href: base + "/" + k + suffix, class: k === tab ? "active" : "" }, label)));
|
||||||
|
const body = h("div");
|
||||||
|
mount(el, head, tabBar, body);
|
||||||
|
if (tab === "options") {
|
||||||
|
await optionsEditor(body, { scope: "view", scope_id: id, catalogScope: "view", title: "View options",
|
||||||
|
intro: "View options override the server options for clients in this view (for example recursion and allow-recursion)." });
|
||||||
|
} else if (tab === "zones") {
|
||||||
|
await zonesTab(body, v, views, zones, inView);
|
||||||
|
} else {
|
||||||
|
await settingsTab(body, v);
|
||||||
|
}
|
||||||
|
const first = body.querySelector("[data-autofocus]");
|
||||||
|
if (first) first.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function settingsTab(el, v) {
|
||||||
|
const f = await viewForm(v, []);
|
||||||
|
mount(el, h("div", { class: "panel" }, f.body,
|
||||||
|
h("div", { class: "actions", style: { marginTop: "12px" } }, h("button", { class: "primary", onclick: async () => { if (await f.save()) rerender(); } }, "Save settings"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function zonesTab(el, v, views, zones, inView) {
|
||||||
|
mount(el, h("div", { class: "panel" },
|
||||||
|
h("div", { class: "actions", style: { marginBottom: "10px" } },
|
||||||
|
h("button", { class: "primary", onclick: async () => newZoneDialog(views, zones, await get("/settings"), { view: v.name }) }, "New zone in " + v.name),
|
||||||
|
h("a", { href: "#/zones?view=" + encodeURIComponent(v.name) }, "Show on the Zones page")),
|
||||||
|
table([
|
||||||
|
{ label: "Zone", render: (z) => [h("a", { href: "#/zones/" + z.id }, z.display_name), dnssecLock(z), z.disabled ? [" ", badge("disabled", "off")] : null, z.pending ? [" ", pendingBadge(z.pending)] : null], tdClass: "name" },
|
||||||
|
{ label: "Type", render: (z) => badge(z.type, z.type === "primary" ? "accent" : "") },
|
||||||
|
{ label: "Records", render: (z) => (z.type === "primary" ? String(z.record_count) : ""), class: "right", tdClass: "right" },
|
||||||
|
{ label: "Serial", render: (z) => (z.type === "primary" ? String(z.soa.serial) : ""), tdClass: "mono" },
|
||||||
|
{ label: "Comment", render: (z) => h("span", { class: "muted" }, z.comment || "") },
|
||||||
|
], inView, { empty: "No zones in this view yet.", rowClass: (z) => (z.disabled ? "disabled" : "") })));
|
||||||
}
|
}
|
||||||
|
|
||||||
// backLabel names the page a back address leads to.
|
// backLabel names the page a back address leads to.
|
||||||
@@ -60,10 +114,12 @@ function backLabel(back, zones) {
|
|||||||
return /^#\/zones(\?|$)/.test(back) ? "Zones" : "Back";
|
return /^#\/zones(\?|$)/.test(back) ? "Zones" : "Back";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function viewDialog(v, viewless) {
|
// viewForm is the form for a view's settings (v null: a new view); save
|
||||||
|
// sends it and reports whether it worked.
|
||||||
|
async function viewForm(v, viewless) {
|
||||||
const err = h("div");
|
const err = h("div");
|
||||||
const ctx = await amlContext();
|
const ctx = await amlContext();
|
||||||
const name = input({ value: v ? v.name : "", placeholder: "internal" });
|
const name = input({ value: v ? v.name : "", placeholder: "internal", "data-autofocus": v ? null : true });
|
||||||
const mc = textarea(v ? (v.match_clients || []).join("\n") : "", { class: "mono", rows: 4, placeholder: "192.168.1.0/24\nlocalhost\n(empty = any)" });
|
const mc = textarea(v ? (v.match_clients || []).join("\n") : "", { class: "mono", rows: 4, placeholder: "192.168.1.0/24\nlocalhost\n(empty = any)" });
|
||||||
const md = textarea(v ? (v.match_destinations || []).join("\n") : "", { class: "mono", rows: 2, placeholder: "(empty = any)" });
|
const md = textarea(v ? (v.match_destinations || []).join("\n") : "", { class: "mono", rows: 2, placeholder: "(empty = any)" });
|
||||||
const mro = select([["", "(default: no)"], ["true", "yes"], ["false", "no"]], v && v.match_recursive_only !== undefined && v.match_recursive_only !== null ? String(v.match_recursive_only) : "");
|
const mro = select([["", "(default: no)"], ["true", "yes"], ["false", "no"]], v && v.match_recursive_only !== undefined && v.match_recursive_only !== null ? String(v.match_recursive_only) : "");
|
||||||
@@ -79,9 +135,8 @@ async function viewDialog(v, viewless) {
|
|||||||
field("Colour", color, "How the web UI shows this view, so zones of the same view are easy to spot. Only changes the web UI: it applies at once, with no deploy.")),
|
field("Colour", color, "How the web UI shows this view, so zones of the same view are easy to spot. Only changes the web UI: it applies at once, with no deploy.")),
|
||||||
!v && viewless.length ? h("div", { class: "msg warn" }, "Once views exist, named requires every zone to be inside a view. ", adopt) : null,
|
!v && viewless.length ? h("div", { class: "msg warn" }, "Once views exist, named requires every zone to be inside a view. ", adopt) : null,
|
||||||
err);
|
err);
|
||||||
await modal(v ? "Edit view " + v.name : "New view", body, [
|
const save = async () => {
|
||||||
{ label: "Cancel", value: null },
|
mount(err);
|
||||||
{ label: v ? "Save" : "Create view", kind: "primary", onclick: async () => {
|
|
||||||
const data = { name: name.value.trim(), match_clients: lines(mc.value), match_destinations: lines(md.value), comment: comment.value };
|
const data = { name: name.value.trim(), match_clients: lines(mc.value), match_destinations: lines(md.value), comment: comment.value };
|
||||||
data.match_recursive_only = mro.value === "" ? null : mro.value === "true";
|
data.match_recursive_only = mro.value === "" ? null : mro.value === "true";
|
||||||
if (!v && viewless.length && adopt.input.checked) data.adopt_zones = true;
|
if (!v && viewless.length && adopt.input.checked) data.adopt_zones = true;
|
||||||
@@ -94,11 +149,18 @@ async function viewDialog(v, viewless) {
|
|||||||
// A view created in a draft only exists in that draft so far.
|
// A view created in a draft only exists in that draft so far.
|
||||||
await api("PUT", "/views/" + id + "/color", { body: { color: color.value }, stage: r && r.staged ? r.changeset_id : undefined });
|
await api("PUT", "/views/" + id + "/color", { body: { color: color.value }, stage: r && r.staged ? r.changeset_id : undefined });
|
||||||
}
|
}
|
||||||
toast(r ? describeResult(r) : "Colour saved");
|
toast(r ? describeResult(r) : colorChanged ? "Colour saved" : "Nothing changed");
|
||||||
rerender();
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) { mount(err, errorBox(e)); return false; }
|
} catch (e) { mount(err, errorBox(e)); return false; }
|
||||||
} },
|
};
|
||||||
|
return { body, save };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function viewDialog(viewless) {
|
||||||
|
const f = await viewForm(null, viewless);
|
||||||
|
await modal("New view", f.body, [
|
||||||
|
{ label: "Cancel", value: null },
|
||||||
|
{ label: "Create view", kind: "primary", onclick: async () => { const ok = await f.save(); if (ok) rerender(); return ok; } },
|
||||||
], { wide: true, sticky: true });
|
], { wide: true, sticky: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +184,8 @@ async function deleteView(v) {
|
|||||||
try {
|
try {
|
||||||
const r = await change("DELETE", "/views/" + v.id, undefined, { query: cascade ? { cascade: 1 } : undefined });
|
const r = await change("DELETE", "/views/" + v.id, undefined, { query: cascade ? { cascade: 1 } : undefined });
|
||||||
toast(describeResult(r));
|
toast(describeResult(r));
|
||||||
rerender();
|
// Applied at once, the view is gone; staged, it is still there.
|
||||||
|
if (r.applied) location.hash = "#/views";
|
||||||
|
else rerender();
|
||||||
} catch (e) { toast(e.message, "err", 8000); }
|
} catch (e) { toast(e.message, "err", 8000); }
|
||||||
}
|
}
|
||||||
@@ -402,6 +402,20 @@ async function recordsTab(el, zone, query) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
for (const f of [fName, fData, fTTL]) f.addEventListener("input", preview);
|
for (const f of [fName, fData, fTTL]) f.addEventListener("input", preview);
|
||||||
|
// aliasTo fills the add form with a CNAME to a record's name: only the
|
||||||
|
// alias's name is left to type.
|
||||||
|
const aliasTo = (r) => {
|
||||||
|
fType.value = "CNAME";
|
||||||
|
showHint();
|
||||||
|
fData.value = r.relative_name === "@" ? "@" : r.relative_name;
|
||||||
|
fName.value = "";
|
||||||
|
fTTL.value = "";
|
||||||
|
fComment.value = "";
|
||||||
|
preview();
|
||||||
|
form.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||||
|
fName.placeholder = "alias for " + r.name.replace(/\.$/, "");
|
||||||
|
fName.focus();
|
||||||
|
};
|
||||||
const add = async (e) => {
|
const add = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
mount(err);
|
mount(err);
|
||||||
@@ -419,6 +433,7 @@ async function recordsTab(el, zone, query) {
|
|||||||
// Start the next record from scratch (the type stays, as records
|
// Start the next record from scratch (the type stays, as records
|
||||||
// are often added several of a kind at a time).
|
// are often added several of a kind at a time).
|
||||||
fName.value = "";
|
fName.value = "";
|
||||||
|
fName.placeholder = reverse && rinfo ? rinfo.example : "@ or www";
|
||||||
fData.value = "";
|
fData.value = "";
|
||||||
fTTL.value = "";
|
fTTL.value = "";
|
||||||
fComment.value = "";
|
fComment.value = "";
|
||||||
@@ -466,6 +481,8 @@ async function recordsTab(el, zone, query) {
|
|||||||
{ label: "Data", render: (r) => r.data, tdClass: "data" },
|
{ label: "Data", render: (r) => r.data, tdClass: "data" },
|
||||||
{ label: "Comment", render: (r) => [r.pending ? [pendingBadge(r.pending), " "] : null, h("span", { class: "muted" }, r.comment || "")] },
|
{ label: "Comment", render: (r) => [r.pending ? [pendingBadge(r.pending), " "] : null, h("span", { class: "muted" }, r.comment || "")] },
|
||||||
{ label: "", render: (r) => r.pending === "delete" ? "" : h("span", null,
|
{ label: "", render: (r) => r.pending === "delete" ? "" : h("span", null,
|
||||||
|
!reverse && (r.type === "A" || r.type === "AAAA") && !r.disabled
|
||||||
|
? [h("button", { class: "small", title: "Add a CNAME record that points to " + r.name.replace(/\.$/, ""), onclick: () => aliasTo(r) }, "+ CNAME"), " "] : null,
|
||||||
h("button", { class: "small", title: "Edit", onclick: () => edit(r) }, "Edit"), " ",
|
h("button", { class: "small", title: "Edit", onclick: () => edit(r) }, "Edit"), " ",
|
||||||
r.disabled
|
r.disabled
|
||||||
? h("button", { class: "small toggle", title: "Put the record back in the zone", onclick: () => toggle(r) }, "Enable")
|
? h("button", { class: "small toggle", title: "Put the record back in the zone", onclick: () => toggle(r) }, "Enable")
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ export async function render(el, params, query) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function newZoneDialog(views, zones, settings) {
|
// preset.view picks the view (opened from a view's page).
|
||||||
|
export async function newZoneDialog(views, zones, settings, preset = {}) {
|
||||||
const err = h("div");
|
const err = h("div");
|
||||||
// The zone's name matters most and is easy to get wrong, so the dialog
|
// The zone's name matters most and is easy to get wrong, so the dialog
|
||||||
// asks for a domain or a network and shows the resulting zone name.
|
// asks for a domain or a network and shows the resulting zone name.
|
||||||
@@ -60,7 +61,7 @@ export async function newZoneDialog(views, zones, settings) {
|
|||||||
const preview = h("div", { class: "name-preview", "aria-live": "polite" });
|
const preview = h("div", { class: "name-preview", "aria-live": "polite" });
|
||||||
const kindButtons = h("div", { class: "segmented", role: "group", "aria-label": "Kind of zone" });
|
const kindButtons = h("div", { class: "segmented", role: "group", "aria-label": "Kind of zone" });
|
||||||
const type = select([["primary", "primary (Jarl manages the records)"], ["secondary", "secondary (copy from another server)"], ["forward", "forward (send queries to other resolvers)"], ["stub", "stub"], ["static-stub", "static-stub"], ["mirror", "mirror"], ["hint", "hint (root hints)"], ["in-view", "in-view (share a zone from an earlier view)"]], "primary");
|
const type = select([["primary", "primary (Jarl manages the records)"], ["secondary", "secondary (copy from another server)"], ["forward", "forward (send queries to other resolvers)"], ["stub", "stub"], ["static-stub", "static-stub"], ["mirror", "mirror"], ["hint", "hint (root hints)"], ["in-view", "in-view (share a zone from an earlier view)"]], "primary");
|
||||||
const view = select([["", views.length ? "– choose a view –" : "(no views)"], ...views.map((v) => [v.name, v.name])], views.length === 1 ? views[0].name : "");
|
const view = select([["", views.length ? "– choose a view –" : "(no views)"], ...views.map((v) => [v.name, v.name])], preset.view || (views.length === 1 ? views[0].name : ""));
|
||||||
const ns = textarea((settings.nameservers || []).join("\n"), { class: "mono", rows: 3, placeholder: "ns1.example.com." });
|
const ns = textarea((settings.nameservers || []).join("\n"), { class: "mono", rows: 3, placeholder: "ns1.example.com." });
|
||||||
const nsAddr = textarea("", { class: "mono", rows: 2, placeholder: "ns1 192.0.2.1" });
|
const nsAddr = textarea("", { class: "mono", rows: 2, placeholder: "ns1 192.0.2.1" });
|
||||||
const contact = input({ value: settings.rname ? emailOf(settings.rname) : "", placeholder: "hostmaster@example.com" });
|
const contact = input({ value: settings.rname ? emailOf(settings.rname) : "", placeholder: "hostmaster@example.com" });
|
||||||
@@ -239,7 +240,7 @@ export function emailOf(rname) {
|
|||||||
// dnssecLock marks a signed zone with a padlock linking to its DNSSEC tab:
|
// dnssecLock marks a signed zone with a padlock linking to its DNSSEC tab:
|
||||||
// closed while signed, open while signing is being turned off (BIND's
|
// closed while signed, open while signing is being turned off (BIND's
|
||||||
// insecure policy).
|
// insecure policy).
|
||||||
function dnssecLock(z) {
|
export function dnssecLock(z) {
|
||||||
if (!z.dnssec_policy) return null;
|
if (!z.dnssec_policy) return null;
|
||||||
const off = z.dnssec_policy === "insecure";
|
const off = z.dnssec_policy === "insecure";
|
||||||
const a = h("a", {
|
const a = h("a", {
|
||||||
|
|||||||
@@ -276,9 +276,9 @@ export function viewLink(v, name, back) {
|
|||||||
return h("a", { href: viewHref(v.id, back), class: v.color ? "vc-" + v.color : null, title: "View " + v.name }, v.name);
|
return h("a", { href: viewHref(v.id, back), class: v.color ? "vc-" + v.color : null, title: "View " + v.name }, v.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// viewHref is the address of a view on the views page.
|
// viewHref is the address of a view's page.
|
||||||
export function viewHref(id, back) {
|
export function viewHref(id, back) {
|
||||||
return "#/views?view=" + id + (back ? "&back=" + encodeURIComponent(back) : "");
|
return "#/views/" + id + (back ? "?back=" + encodeURIComponent(back) : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// colorPicker picks a view colour; its value is "" for the default.
|
// colorPicker picks a view colour; its value is "" for the default.
|
||||||
|
|||||||
Reference in new issue
Block a user