Files
steward/internal/web/static/assets/pages/subnet.js
T
abrown38andClaude Opus 5.5 d0ee2c8844 UI: activity client links, online badges, ended-leases toggle, All activity
- Activity: a client's MAC address opens its leases; "Only this" filters
  the list to that client.
- Reservations: Online is a green online / grey offline badge.
- A subnet's leases: "Include ended leases" is a checkbox.
- Dashboard: "All activity" is a button beside the heading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:52:36 +01:00

84 lines
4.3 KiB
JavaScript

// A subnet's page: its leases, its reservations and its settings.
import { h, mount, badge, errorBox, confirmDialog, modal, input, debounce, readOnly, checkbox } from "../ui.js";
import { get, change, can } from "../api.js";
import { usageBar, subnetForm, fmtSeconds, subnetLabel } from "../widgets.js";
import { leasesTable } from "./leases.js";
import { reservationsTable, reservationDialog } from "./reservations.js";
import { rerender } from "../app.js";
const tabs = [["leases", "Leases"], ["reservations", "Reservations"], ["settings", "Settings"]];
export async function render(el, params) {
const id = params.id;
const tab = params.tab || "leases";
const s = await get("/subnets/" + id);
const body = h("div");
mount(el,
h("nav", { class: "back-link" }, h("a", { href: "#/subnets" }, "← Subnets")),
h("div", { class: "page-head" },
h("div", { class: "grow" },
h("h1", null, subnetLabel(s), " ", s.disabled ? badge("disabled", "off") : null),
h("div", { class: "muted" }, "On ", h("span", { class: "mono" }, s.interface), " · pools ",
s.pools.length ? h("span", { class: "mono" }, s.pools.map((p) => p.start + "-" + p.end).join(", ")) : "none",
" · lease time ", s.lease_time ? fmtSeconds(s.lease_time) : "the default")),
h("div", { class: "subnet-usage" }, usageBar(s.usage))),
h("div", { class: "tabs" }, tabs.map(([k, label]) => h("a", { href: "#/subnets/" + id + "/" + k, class: tab === k ? "active" : "" }, label))),
body);
if (tab === "leases") await leasesTab(body, s);
else if (tab === "reservations") await reservationsTab(body, s);
else await settingsTab(body, s);
}
async function leasesTab(el, s) {
const search = input({ type: "search", placeholder: "address, MAC or host name" });
const out = h("div");
const toggle = checkbox("Include ended leases (expired and released)", false);
toggle.input.addEventListener("change", () => draw());
const draw = async () => {
let ls = await get("/leases", { subnet: s.id, q: search.value.trim() });
if (!toggle.input.checked) ls = ls.filter((l) => l.state === "active" || l.state === "declined");
mount(out, leasesTable(ls, rerender, { subnet: true, empty: toggle.input.checked ? "No leases." : "No current leases." }));
};
search.addEventListener("input", debounce(draw, 300));
mount(el, h("div", { class: "filter-row" }, search, toggle), h("div", { class: "panel" }, out));
await draw();
}
async function reservationsTab(el, s) {
const rs = await get("/reservations", { subnet: s.id });
mount(el,
can("config", true) ? h("div", { class: "filter-row" }, h("button", { class: "primary", onclick: () => reservationDialog(null, rerender) }, "New reservation")) : null,
h("div", { class: "panel" }, reservationsTable(rs, rerender, { subnet: true, empty: "No reservations in this subnet." })));
}
async function settingsTab(el, s) {
const settings = await get("/settings");
const form = await subnetForm(s, settings);
if (!can("config", true)) {
mount(el, h("div", { class: "msg info" }, "You can see the subnet's settings but not change them (that needs config:write)."), readOnly(h("div", { class: "panel" }, form)));
return;
}
const err = h("div");
const save = async (dry) => {
mount(err);
try {
const r = await change("PATCH", "/subnets/" + s.id, form.values(), { query: dry ? { dry_run: 1 } : undefined });
if (!dry && r.applied) rerender();
} catch (e) { mount(err, errorBox(e)); }
};
const del = async () => {
const n = s.usage.reservations;
if (!await confirmDialog("Delete subnet", "Delete " + subnetLabel(s) + (n ? " and its " + n + (n === 1 ? " reservation" : " reservations") : "") + "? Its clients get no answers from then on; their leases are kept until they run out. History can undo it.", "Delete", "danger")) return;
try {
await change("DELETE", "/subnets/" + s.id);
location.hash = "#/subnets";
} catch (e) { modal("Not deleted", errorBox(e)); }
};
mount(el, h("div", { class: "panel" }, form, err,
h("div", { class: "actions", style: { marginTop: "14px" } },
h("button", { class: "primary", onclick: () => save(false) }, "Save"),
h("button", { onclick: () => save(true), title: "Check the change without saving it" }, "Check"),
h("span", { class: "grow" }),
h("button", { class: "danger", onclick: del }, "Delete subnet"))));
}