Link views from the zone list, add breadcrumbs and view colours

The view shown in the zone list and on the zone page now links to that
view. The zone page and changeset page get breadcrumbs, and the zone list
keeps its filters in the address so going back restores them. Views can
be given a colour (Views > Edit, `jarl view color`, PUT /views/{id}/color)
so zones of the same view are easy to spot; the colour is UI-only, kept
outside changesets, and needs no deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-09 14:43:57 +00:00
1 parent b22ffee1bb
commit b05711b13e
20 files changed
+393 -30

No files matched your search

+12 -2
View File
@@ -1,4 +1,4 @@
import { h, mount, table, modal, field, input, select, textarea, lines, toast, errorBox, display, badge } from "../ui.js";
import { h, mount, table, modal, field, input, select, textarea, lines, toast, errorBox, display, badge, viewLink, lastList } from "../ui.js";
import { get, change, changeMany, describeResult, can, draftQuery } from "../api.js";
import { rerender } from "../app.js";
import { checkDomain, reverseZoneFor } from "../names.js";
@@ -8,13 +8,23 @@ export async function render(el, params, query) {
const filter = input({ type: "search", placeholder: "Filter zones…", value: query.get("q") || "" });
const viewSel = select([["", "All views"], ...views.map((v) => [v.name, v.name])], query.get("view") || "");
const typeSel = select([["", "All types"], "primary", "secondary", "forward", "stub", "static-stub", "mirror", "hint", "redirect", "in-view"], query.get("type") || "");
const viewsById = new Map(views.map((v) => [v.id, v]));
const list = h("div");
const draw = () => {
const q = filter.value.trim().toLowerCase();
// Keep the filters in the address, so going back to the list (or a
// breadcrumb) restores them.
const qs = new URLSearchParams();
if (filter.value.trim()) qs.set("q", filter.value.trim());
if (viewSel.value) qs.set("view", viewSel.value);
if (typeSel.value) qs.set("type", typeSel.value);
const hash = "#/zones" + (qs.toString() ? "?" + qs : "");
if (location.hash !== hash && !query.get("new")) history.replaceState(null, "", hash);
lastList.zones = hash;
const rows = zones.filter((z) => (!q || z.name.includes(q)) && (!viewSel.value || z.view === viewSel.value) && (!typeSel.value || z.type === typeSel.value));
mount(list, table([
{ label: "Zone", render: (z) => [z.pending === "delete" ? h("span", null, z.display_name) : h("a", { href: "#/zones/" + z.id }, z.display_name), z.pending ? [" ", pendingBadge(z.pending)] : null], tdClass: "name" },
views.length ? { label: "View", render: (z) => z.view || h("span", { class: "muted" }, "–") } : null,
views.length ? { label: "View", render: (z) => viewLink(viewsById.get(z.view_id), z.view) } : null,
{ 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" },