// Address sets, and dynamic sets' entries. import { h, mount, table, modal, errorBox, field, input, checkbox, confirmDialog, badge, toast } from "../ui.js"; import { get, api, change, can } from "../api.js"; import { rerender } from "../app.js"; import { listEditor, fmtSeconds, parseSeconds, rowClass } from "../widgets.js"; export async function render(el) { const sets = await get("/sets"); const write = can("config", true); mount(el, h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "Address sets"), h("div", { class: "muted" }, "Named lists of IPv4 and IPv6 addresses, prefixes and ranges, mixed. A dynamic set takes entries at run time, each with a timeout (a captive portal's signed-in clients).")), write ? h("div", { class: "actions" }, h("button", { class: "primary", onclick: () => setDialog(null) }, "Add set")) : null), h("div", { class: "panel" }, table([ { label: "Name", render: (s) => h("strong", null, s.name) }, { label: "Entries", render: (s) => s.dynamic ? [badge("dynamic", "accent"), s.timeout ? " timeout " + fmtSeconds(s.timeout) : "", " ", s.pending ? null : h("button", { class: "small", onclick: () => entries(s) }, "Entries…")] : h("span", { class: "mono small" }, s.entries.join(", ")) }, { label: "Comment", render: (s) => s.comment }, write ? { label: "", tdClass: "actions-cell", render: (s) => s.pending === "deleted" ? badge("deleted", "err") : [ h("button", { class: "small reveal", onclick: () => setDialog(s) }, "Edit"), " ", h("button", { class: "small reveal", onclick: () => del(s) }, "Delete")] } : null, ].filter(Boolean), sets, { empty: "No sets yet.", rowClass }))); } async function del(s) { if (!await confirmDialog("Delete set", "Delete set " + s.name + "?", "Delete", "danger")) return; try { await change("DELETE", "/sets/" + s.id); rerender(); } catch (e) { modal("Not deleted", errorBox(e)); } } async function setDialog(s) { s = s || { entries: [] }; const name = input({ value: s.name || "", placeholder: "trusted" }); const list = listEditor(s.entries, { placeholder: "192.168.1.0/24, 2001:db8:1::/64, 192.168.1.100-192.168.1.199", addLabel: "Add entry" }); const dynamic = checkbox("dynamic: entries are added at run time (the Entries button, hc set entries, a program with a token), each for a time", s.dynamic); // A dynamic set has no configured entries; only it has a timeout. const entryFields = field("Entries", list).filter(Boolean); const timeoutFields = field("Entry timeout", timeout, "How long an entry stays unless it is added with its own. Empty: until removed (or a reboot).").filter(Boolean); const showKind = () => { for (const n of entryFields) n.classList.toggle("hidden", dynamic.input.checked); for (const n of timeoutFields) n.classList.toggle("hidden", !dynamic.input.checked); }; dynamic.input.addEventListener("change", showKind); showKind(); const timeout = input({ value: s.timeout ? fmtSeconds(s.timeout) : "", placeholder: "1h, 1d (none: until removed)", class: "short" }); const comment = input({ value: s.comment || "" }); const err = h("div"); await modal(s.id ? "Edit set " + s.name : "Add set", h("div", null, h("div", { class: "form-grid" }, field("Name", name), ...entryFields, h("label"), dynamic, ...timeoutFields, field("Comment", comment)), err), [ { label: "Cancel", value: null }, { label: s.id ? "Save" : "Add set", kind: "primary", onclick: async () => { mount(err); const t = parseSeconds(timeout.value); if (t === null) { mount(err, h("div", { class: "msg err" }, "Write the timeout as 1h, 30m or 1d.")); return false; } try { await change(s.id ? "PATCH" : "POST", s.id ? "/sets/" + s.id : "/sets", { name: name.value.trim(), entries: dynamic.input.checked ? [] : list.value(), dynamic: dynamic.input.checked, timeout: dynamic.input.checked ? t : 0, comment: comment.value.trim() }); rerender(); return true; } catch (e) { mount(err, errorBox(e)); return false; } } }, ], { wide: true, sticky: true }); } // entries shows and changes a dynamic set's entries. async function entries(s) { const body = h("div"); const write = can("sets", true); const draw = async () => { const es = await get("/sets/" + encodeURIComponent(s.name) + "/elements"); const addr = input({ placeholder: "192.168.1.50 or 2001:db8:1::50", class: "mono" }); const t = input({ placeholder: s.timeout ? fmtSeconds(s.timeout) : "none", class: "short" }); mount(body, table([ { label: "Address", render: (e) => h("span", { class: "mono" }, e.address) }, { label: "Expires in", render: (e) => e.timeout ? fmtSeconds(e.timeout) : "never" }, write ? { label: "", render: (e) => h("button", { class: "small", onclick: async () => { try { await api("DELETE", "/sets/" + encodeURIComponent(s.name) + "/elements/" + encodeURIComponent(e.address)); draw(); } catch (x) { toast(x.message, "err"); } } }, "Remove") } : null, ].filter(Boolean), es, { empty: "No entries." }), write ? h("div", { class: "filter-row" }, addr, t, h("button", { class: "primary small", onclick: async () => { const secs = parseSeconds(t.value); if (secs === null) { toast("Write the timeout as 1h, 30m or 1d", "err"); return; } try { await api("POST", "/sets/" + encodeURIComponent(s.name) + "/elements", { body: { address: addr.value.trim(), timeout: secs } }); draw(); } catch (x) { toast(x.message, "err"); } } }, "Add")) : null); }; await draw(); await modal("Entries of " + s.name, body, [{ label: "Close", value: null }], { wide: true }); }