Merge branch 'view-ui'

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-09 15:17:49 +00:00
commit ca0f148b78
29 files changed
+623 -55

No files matched your search

+26 -10
View File
@@ -1,4 +1,4 @@
import { h, mount, table, field, input, select, textarea, toast, errorBox, display, badge, confirmDialog, modal, checkbox, time, diffView } from "../ui.js";
import { h, mount, table, field, input, select, textarea, toast, errorBox, display, badge, confirmDialog, modal, checkbox, time, diffView, backLink, viewLink, lastList } from "../ui.js";
import { api, get, change, describeResult, rrtypes, state, draftQuery } from "../api.js";
import { rerender } from "../app.js";
import { optionsEditor } from "./options.js";
@@ -7,9 +7,12 @@ import { isReverseZone, reverseInfo, ptrOwner, addressOf } from "../names.js";
import { dnssecTab } from "./dnssec.js";
import { registrarTab } from "./registrar.js";
let renderSeq = 0;
export async function render(el, params, query) {
const id = params.id;
const zone = await get("/zones/" + id, draftQuery());
const [zone, views] = await Promise.all([get("/zones/" + id, draftQuery()), get("/views")]);
const view = views.find((v) => v.id === zone.view_id);
const tab = params.tab || (zone.type === "primary" ? "records" : "settings");
const tabs = [
zone.type === "primary" ? ["records", "Records (" + zone.record_count + ")"] : null,
@@ -21,21 +24,30 @@ export async function render(el, params, query) {
zone.registrar && state.me && state.me.admin ? ["registrar", "Registrar"] : null,
].filter(Boolean);
const body = h("div");
mount(el,
h("div", { class: "page-head" },
const head = h("div", { class: "page-head" },
h("div", { class: "grow" },
backLink("Zones", lastList.zones || "#/zones"),
h("h1", { class: "mono" }, zone.display_name),
h("div", { class: "muted" },
badge(zone.type, "accent"), " ",
zone.pending ? [pendingBadge(zone.pending), " "] : null,
zone.view ? ["view ", h("a", { href: "#/views" }, zone.view), " · "] : null,
zone.view ? ["view ", viewLink(view, zone.view, "#/zones/" + id + (params.tab ? "/" + params.tab : "")), " · "] : null,
zone.type === "primary" ? "serial " + zone.soa.serial + " · " : null,
zone.dnssec_policy ? [h("a", { href: "#/zones/" + id + "/dnssec" }, badge(zone.dnssec_policy === "insecure" ? "DNSSEC: turning off" : "DNSSEC", zone.dnssec_policy === "insecure" ? "warn" : "ok")), " "] : null,
zone.zone_file ? h("span", { class: "mono small" }, zone.zone_file) : null)),
h("div", { class: "actions" },
h("button", { class: "danger", onclick: () => deleteZone(zone) }, "Delete zone"))),
h("div", { class: "tabs" }, tabs.map(([k, label]) => h("a", { href: "#/zones/" + id + "/" + k, class: k === tab ? "active" : "" }, label))),
body);
h("button", { class: "danger", onclick: () => deleteZone(zone) }, "Delete zone")));
const tabBar = h("div", { class: "tabs" }, tabs.map(([k, label]) => h("a", { href: "#/zones/" + id + "/" + k, class: k === tab ? "active" : "" }, label)));
// The header and tabs show at once. Below them, switching tabs keeps the
// previous tab's content (faded) until the new one is loaded, rather
// than blanking the page; opening the zone shows "Loading…" there.
const seq = ++renderSeq;
const prev = el.querySelector(`:scope > .zone-body[data-zone="${id}"]`);
const holder = prev || h("div", { class: "zone-body" }, h("div", { class: "muted" }, "Loading…"));
holder.classList.add("stale");
body.className = "zone-body";
body.dataset.zone = id;
mount(el, head, tabBar, holder);
if (tab === "records") await recordsTab(body, zone, query);
else if (tab === "settings") await settingsTab(body, zone);
else if (tab === "options") await optionsEditor(body, { scope: "zone", scope_id: zone.id, catalogScope: "zone:" + zone.type, title: "Zone options" });
@@ -43,6 +55,11 @@ export async function render(el, params, query) {
else if (tab === "servers") await serversTab(body, zone);
else if (tab === "dnssec") await dnssecTab(body, zone);
else if (tab === "registrar") await registrarTab(body, zone);
// A later click (another tab) has taken over the page.
if (seq !== renderSeq || !holder.isConnected) return;
holder.replaceWith(body);
const first = body.querySelector("[data-autofocus]");
if (first) first.focus();
}
// ---- name servers: serials on the other servers, and the delegation
@@ -166,7 +183,7 @@ async function recordsTab(el, zone, query) {
const reverse = isReverseZone(zone.name);
const rinfo = reverse ? reverseInfo(zone.name) : null;
// Add form. In reverse zones records are named by address.
const fName = input({ placeholder: reverse && rinfo ? rinfo.example : "@ or www", style: { width: reverse ? "220px" : "180px" }, class: "mono", autocomplete: "off", spellcheck: "false" });
const fName = input({ placeholder: reverse && rinfo ? rinfo.example : "@ or www", style: { width: reverse ? "220px" : "180px" }, class: "mono", autocomplete: "off", spellcheck: "false", "data-autofocus": true });
const fType = select(types.map((t) => [t.type, t.type + (t.common ? "" : "")]), reverse ? "PTR" : "A", { style: { width: "110px" } });
const fTTL = input({ type: "number", placeholder: "default", min: 0, style: { width: "100px" } });
const fData = input({ class: "mono", placeholder: typeInfo.A.example });
@@ -388,7 +405,6 @@ async function recordsTab(el, zone, query) {
(reverse ? "Names are the last part of the address within " + zone.display_name + "." : "Names are relative to " + zone.display_name + "; end a name with '.' to make it absolute."))),
list));
draw();
fName.focus();
}
// ---- settings