Files
abrown38andClaude Opus 5.5 4b79abf296 Replace breadcrumbs with back links; keep the zone header on tab switches
The zone page's breadcrumb (Zones › view) linked the view to a filtered
zone list while the view link below it opened the view. It is now a
single "← Zones" link back to the list with its filters, and a changeset
has "← History". View links carry where they came from, so the views page
offers "← example.org (external)" or "← Zones" (only the UI's own #/
addresses are accepted).

Switching a zone's tabs no longer blanks the page: the header and tabs
show at once and the previous tab stays, faded, until the next has
loaded. The router shows "Loading…" only for pages that take a while.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 15:07:55 +00:00

60 lines
3.5 KiB
JavaScript

import { h, mount, table, toast, confirmDialog, diffView, time, fmtTime, badge, plural, backLink, lastList } from "../ui.js";
import { api, get, useDraft } from "../api.js";
export async function render(el, params) {
if (params.id) return detail(el, params.id);
const list = h("div");
let rows = [];
const load = async (before) => {
const page = await get("/changesets", { status: "applied", limit: 50, before });
rows = rows.concat(page);
mount(list, table([
{ label: "#", render: (c) => h("a", { href: "#/history/" + c.id }, String(c.id)) },
{ label: "Applied", render: (c) => time(c.applied_at) },
{ label: "By", render: (c) => c.applied_by || c.username },
{ label: "Via", render: (c) => badge(c.via) },
{ label: "Description", render: (c) => h("a", { href: "#/history/" + c.id }, c.description || "(no description)") },
{ label: "Operations", render: (c) => String((c.ops || []).length), tdClass: "right" },
], rows, { empty: "Nothing has been applied yet." }),
page.length === 50 ? h("div", { style: { marginTop: "8px" } }, h("button", { onclick: () => load(rows[rows.length - 1].id) }, "Load older")) : null);
};
mount(el, h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "History"),
h("div", { class: "muted" }, "Every applied changeset, with the exact changes made to BIND's files. Any of them can be reverted."))),
h("div", { class: "panel" }, list));
await load();
}
async function detail(el, id) {
const c = await get("/changesets/" + id);
const res = c.result || {};
mount(el,
h("div", { class: "page-head" },
h("div", { class: "grow" },
backLink("History", lastList.history || "#/history"),
h("h1", null, "Changeset #" + c.id),
h("div", { class: "muted" }, c.description || "(no description)")),
h("div", { class: "actions" },
c.status === "applied" && c.undoable ? h("button", { onclick: () => revert(c) }, "Revert…") : null)),
h("div", { class: "panel" }, h("dl", { class: "kv" },
h("dt", null, "Status"), h("dd", null, badge(c.status, c.status === "applied" ? "ok" : "")),
h("dt", null, "Created"), h("dd", null, fmtTime(c.created_at) + " by " + c.username + " via " + c.via),
c.applied_at ? [h("dt", null, "Applied"), h("dd", null, fmtTime(c.applied_at) + " by " + c.applied_by)] : null,
(res.serials || []).length ? [h("dt", null, "Serials"), h("dd", { class: "mono" }, res.serials.map((s) => s.zone.replace(/\.$/, "") + (s.view ? " [" + s.view + "]" : "") + ": " + s.old + " → " + s.new).join(", "))] : null,
(res.warnings || []).length ? [h("dt", null, "Warnings"), h("dd", null, res.warnings.join("; "))] : null)),
h("div", { class: "panel" }, h("h2", null, plural((c.ops || []).length, "operation")), table([
{ label: "#", render: (o) => String(o.seq) },
{ label: "Change", render: (o) => o.summary || o.op, tdClass: "mono small" },
], c.ops || [])),
h("div", { class: "panel" }, h("h2", null, "File changes"), diffView(c.diff)));
}
async function revert(c) {
if (!(await confirmDialog("Revert changeset #" + c.id, "This creates a new changeset that undoes these changes. You can review it before applying.", "Create revert", "primary"))) return;
try {
const rev = await api("POST", "/changesets/" + c.id + "/revert");
useDraft(rev);
toast("Created changeset #" + rev.id + "; review and apply it");
location.hash = "#/changes";
} catch (e) { toast(e.message, "err", 8000); }
}