Disable records instead of deleting them
A disabled record stays in Jarl with its ID, TTL and comment but is left out of the zone; the zone file lists it as a comment. Web UI toggle, jarl record disable|enable, record.disable|enable operations and API endpoints. Database schema 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5f04ab523b
commit
295c549479
29 files changed
+815
-60
No files matched your search
@@ -436,6 +436,8 @@ async function recordsTab(el, zone, query) {
|
||||
// Filters and table
|
||||
const filter = input({ type: "search", placeholder: "Filter by name or data…", value: query.get("q") || "" });
|
||||
const typeFilter = select([["", "All types"], ...[...new Set(records.map((r) => r.type))].sort()], "");
|
||||
// Shown once the zone has disabled records.
|
||||
const stateFilter = select([["", "Enabled and disabled"], ["on", "Enabled only"], ["off", "Disabled only"]], "");
|
||||
const list = h("div");
|
||||
let editing = null;
|
||||
let current = records;
|
||||
@@ -445,18 +447,26 @@ async function recordsTab(el, zone, query) {
|
||||
};
|
||||
const draw = () => {
|
||||
const q = filter.value.trim().toLowerCase();
|
||||
const rows = current.filter((r) => (!q || r.name.includes(q) || r.data.toLowerCase().includes(q) || (r.comment || "").toLowerCase().includes(q)) && (!typeFilter.value || r.type === typeFilter.value));
|
||||
const off = current.filter((r) => r.disabled).length;
|
||||
stateFilter.style.display = off || stateFilter.value ? "" : "none";
|
||||
stateFilter.options[2].textContent = "Disabled only (" + off + ")";
|
||||
const rows = current.filter((r) => (!q || r.name.includes(q) || r.data.toLowerCase().includes(q) || (r.comment || "").toLowerCase().includes(q)) && (!typeFilter.value || r.type === typeFilter.value) &&
|
||||
(!stateFilter.value || !!r.disabled === (stateFilter.value === "off")));
|
||||
mount(list, table([
|
||||
{ label: "Name", render: (r) => reverse && addressOf(r.name) ? [r.relative_name, " ", h("span", { class: "muted small" }, addressOf(r.name))] : r.relative_name, tdClass: "name" },
|
||||
{ label: "TTL", render: (r) => r.ttl !== undefined && r.ttl !== null ? String(r.ttl) : h("span", { class: "muted", title: "zone default" }, String(r.effective_ttl)), tdClass: "mono" },
|
||||
{ label: "Type", render: (r) => h("strong", null, r.type) },
|
||||
{ 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.disabled && r.pending !== "disable" ? [badge("disabled"), " "] : null, r.pending ? [pendingBadge(r.pending), " "] : null, h("span", { class: "muted" }, r.comment || "")] },
|
||||
{ label: "", render: (r) => r.pending === "delete" ? "" : h("span", null,
|
||||
h("button", { class: "small", title: "Edit", onclick: () => edit(r) }, "Edit"), " ",
|
||||
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: "Leave the record out of the zone, but keep it here", onclick: () => toggle(r) }, "Disable"), " ",
|
||||
h("button", { class: "small danger", title: "Delete", onclick: () => del(r) }, "Delete")), tdClass: "actions-cell" },
|
||||
], rows, { empty: current.length ? "No records match." : "No records yet.", rowClass: (r, i) => [
|
||||
r.pending ? "pending-" + r.pending : "",
|
||||
r.disabled ? "disabled" : "",
|
||||
// A record followed by more of the same name: a faint line, so the
|
||||
// full line marks where the next name starts.
|
||||
rows[i + 1] && rows[i + 1].name === r.name ? "same-name-next" : "",
|
||||
@@ -478,6 +488,17 @@ async function recordsTab(el, zone, query) {
|
||||
await redraw();
|
||||
} catch (e) { toast(e.message, "err", 8000); }
|
||||
};
|
||||
// toggle disables or enables a record, and the PTR record that goes
|
||||
// with an address record.
|
||||
const toggle = async (r) => {
|
||||
const verb = r.disabled ? "enable" : "disable";
|
||||
const ptr = r.type === "A" || r.type === "AAAA";
|
||||
try {
|
||||
const res = await change("POST", "/records/" + r.id + "/" + verb, undefined, { query: ptr ? { ptr: 1 } : undefined });
|
||||
toast(describeResult(res));
|
||||
await redraw();
|
||||
} catch (e) { toast(e.message, "err", 8000); }
|
||||
};
|
||||
const edit = (r) => { editing = r; draw(); };
|
||||
const mountEdit = () => {
|
||||
const r = editing;
|
||||
@@ -525,12 +546,13 @@ async function recordsTab(el, zone, query) {
|
||||
};
|
||||
filter.addEventListener("input", draw);
|
||||
typeFilter.addEventListener("change", draw);
|
||||
stateFilter.addEventListener("change", draw);
|
||||
mount(el,
|
||||
h("div", { class: "panel" },
|
||||
reverse && rinfo ? h("div", { class: "muted small", style: { marginBottom: "6px" } }, "Reverse zone for " + rinfo.network + ": name records by address (e.g. " + rinfo.example + ") and give the host name as data.") : null,
|
||||
form, willAdd, suggest, hint, err),
|
||||
h("div", { class: "panel" },
|
||||
h("div", { class: "actions", style: { marginBottom: "8px" } }, filter, typeFilter,
|
||||
h("div", { class: "actions", style: { marginBottom: "8px" } }, filter, typeFilter, stateFilter,
|
||||
h("span", { class: "muted small" }, "Highlighted rows are pending changes (not live until applied). " +
|
||||
(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));
|
||||
|
||||
Reference in new issue
Block a user