// Leases: every address handed out, with filters; reserve or delete one. import { h, mount, table, time, input, select, confirmDialog, toast, modal, errorBox, debounce, badge } from "../ui.js"; import { get, api, change, can } from "../api.js"; import { stateText, subnetLabel } from "../widgets.js"; // leaseHost is the host name to show: the reservation's, or what the // client said. function leaseHost(l) { if (l.reserved_hostname && l.hostname && l.reserved_hostname !== l.hostname) return [l.reserved_hostname, h("span", { class: "muted small" }, " (says " + l.hostname + ")")]; return l.reserved_hostname || l.hostname || h("span", { class: "muted" }, "–"); } // leasesTable shows leases with their actions; after a change, done() is // called. export function leasesTable(leases, done, opts = {}) { const write = can("leases", true), config = can("config", true); return table([ { label: "Address", render: (l) => h("span", { class: "mono" }, l.ip) }, { label: "MAC address", render: (l) => h("span", { class: "mono" }, l.mac || "–") }, { label: "Host name", render: leaseHost }, { label: "State", render: (l) => [stateBadge(l.state), l.reservation ? [" ", badge("reserved", "accent")] : null] }, { label: "Ends", render: (l) => time(l.end) }, opts.subnet ? null : { label: "Subnet", render: (l) => l.subnet || "–" }, { label: "Interface", render: (l) => h("span", { class: "mono" }, l.interface) }, { label: "", tdClass: "actions-cell", render: (l) => [ config && !l.reservation && l.mac && l.state !== "declined" ? h("button", { class: "small reveal", title: "Give this client this address from now on", onclick: () => reserve(l, done) }, "Reserve") : null, " ", write ? h("button", { class: "small reveal", title: "Forget this lease: the address is free at once", onclick: () => del(l, done) }, "Delete") : null, ] }, ].filter(Boolean), leases, { empty: opts.empty || "No leases." }); } function stateBadge(st) { return badge(stateText(st), { active: "ok", declined: "warn" }[st] || ""); } async function reserve(l, done) { const host = input({ value: l.hostname || "", placeholder: "host name (optional)" }); const err = h("div"); await modal("Reserve " + l.ip, h("div", null, h("p", null, l.mac + " keeps " + l.ip + " from now on."), h("div", { class: "form-grid" }, h("label", null, "Host name"), host), err), [ { label: "Cancel", value: null }, { label: "Reserve", kind: "primary", onclick: async () => { try { await change("POST", "/leases/" + l.ip + "/reserve", { hostname: host.value.trim() }); done(); return true; } catch (e) { mount(err, errorBox(e)); return false; } } }, ]); } async function del(l, done) { if (!await confirmDialog("Delete lease", "Forget the lease of " + l.ip + (l.mac ? " (" + l.mac + ")" : "") + "? The address is free at once; if the client still uses it, it gets a NAK when it next renews and asks for a new address.", "Delete", "danger")) return; try { await api("DELETE", "/leases/" + l.ip); toast("Deleted the lease of " + l.ip); done(); } catch (e) { toast(e.message, "err", 8000); } } export async function render(el, params, query) { const subnets = await get("/subnets"); const subnetSel = select([["", "all subnets"], ...subnets.map((s) => [String(s.id), subnetLabel(s)])], query.get("subnet") || ""); const stateSel = select([["current", "active and held back"], ["active", "active"], ["expired", "expired"], ["released", "released"], ["declined", "held back"], ["", "all"]], query.get("state") || "current"); const search = input({ type: "search", placeholder: "address, MAC or host name", value: query.get("q") || "" }); const out = h("div"); const draw = async () => { const st = stateSel.value === "current" ? "" : stateSel.value; let leases = await get("/leases", { subnet: subnetSel.value, state: st, q: search.value.trim() }); if (stateSel.value === "current") leases = leases.filter((l) => l.state === "active" || l.state === "declined"); const q = new URLSearchParams(); if (subnetSel.value) q.set("subnet", subnetSel.value); if (stateSel.value !== "current") q.set("state", stateSel.value); if (search.value.trim()) q.set("q", search.value.trim()); history.replaceState(null, "", "#/leases" + (q.toString() ? "?" + q : "")); mount(out, h("div", { class: "muted small" }, leases.length + (leases.length === 1 ? " lease" : " leases")), leasesTable(leases, draw)); }; subnetSel.addEventListener("change", draw); stateSel.addEventListener("change", draw); search.addEventListener("input", debounce(draw, 300)); mount(el, h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "Leases"), h("div", { class: "muted" }, "Addresses handed out. Expired and released leases are kept a while, so a client that comes back gets its old address."))), h("div", { class: "filter-row" }, subnetSel, stateSel, search, h("button", { onclick: draw }, "Refresh")), h("div", { class: "panel" }, out)); await draw(); }