Disable reservations; subnet enable/disable buttons; page-head CSS fix
release / release (push) Successful in 5m53s
release / release (push) Successful in 5m53s
- A disabled reservation is kept but ignored: the client gets a pool address like any other (steward reservation disable|enable, buttons and a checkbox in the web UI). Schema migration 3. - Subnets get Disable/Enable buttons on their page and in the list. - A rule left without its selector in style.css broke the page heading's layout, so the New ... buttons sat cramped under the description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7b3f0f764c
commit
6ad1bb9a0d
21 files changed
+165
-37
No files matched your search
@@ -1,5 +1,5 @@
|
||||
// Reservations: fixed addresses for clients.
|
||||
import { h, mount, table, input, modal, errorBox, field, confirmDialog, select, debounce, badge } from "../ui.js";
|
||||
import { h, mount, table, input, modal, errorBox, field, confirmDialog, select, debounce, badge, checkbox } from "../ui.js";
|
||||
import { get, change, can } from "../api.js";
|
||||
import { optionsEditor, subnetLabel } from "../widgets.js";
|
||||
|
||||
@@ -7,7 +7,7 @@ import { optionsEditor, subnetLabel } from "../widgets.js";
|
||||
export function reservationsTable(rs, done, opts = {}) {
|
||||
const write = can("config", true);
|
||||
return table([
|
||||
{ label: "Address", render: (r) => h("span", { class: "mono" }, r.ip) },
|
||||
{ label: "Address", render: (r) => [h("span", { class: "mono" }, r.ip), r.disabled ? [" ", badge("disabled", "off")] : null] },
|
||||
{ label: "Client", render: (r) => h("span", { class: "mono" }, r.client_id ? "id " + r.client_id : r.mac) },
|
||||
{ label: "Host name", render: (r) => r.hostname || h("span", { class: "muted" }, "–") },
|
||||
opts.subnet ? null : { label: "Subnet", render: (r) => r.subnet_id ? h("a", { href: "#/subnets/" + r.subnet_id }, r.subnet) : "–" },
|
||||
@@ -18,9 +18,18 @@ export function reservationsTable(rs, done, opts = {}) {
|
||||
{ label: "Comment", render: (r) => r.comment },
|
||||
write ? { label: "", tdClass: "actions-cell", render: (r) => [
|
||||
h("button", { class: "small reveal", onclick: () => reservationDialog(r, done) }, "Edit"), " ",
|
||||
h("button", { class: "small reveal", title: r.disabled ? "Use this reservation again" : "Keep it, but ignore it: the client gets a pool address like any other",
|
||||
onclick: () => toggle(r, done) }, r.disabled ? "Enable" : "Disable"), " ",
|
||||
h("button", { class: "small reveal", onclick: () => del(r, done) }, "Delete"),
|
||||
] } : null,
|
||||
].filter(Boolean), rs, { empty: opts.empty || "No reservations." });
|
||||
].filter(Boolean), rs, { empty: opts.empty || "No reservations.", rowClass: (r) => r.disabled ? "disabled" : "" });
|
||||
}
|
||||
|
||||
async function toggle(r, done) {
|
||||
try {
|
||||
await change("PATCH", "/reservations/" + r.id, { disabled: !r.disabled });
|
||||
done();
|
||||
} catch (e) { modal(r.disabled ? "Not enabled" : "Not disabled", errorBox(e)); }
|
||||
}
|
||||
|
||||
async function del(r, done) {
|
||||
@@ -44,6 +53,7 @@ export async function reservationDialog(r, done, preset = {}) {
|
||||
const host = input({ value: r ? r.hostname : preset.hostname || "", placeholder: "nas" });
|
||||
const opts = await optionsEditor(r ? r.options : [], inherited);
|
||||
const comment = input({ value: r ? r.comment : "" });
|
||||
const disabled = checkbox("Disabled: keep it, but ignore it (the client gets a pool address like any other)", r ? r.disabled : false);
|
||||
const err = h("div");
|
||||
const body = h("div", null, h("div", { class: "form-grid" },
|
||||
field("Address", ip, "Its subnet is the one whose network holds it. Inside a pool is fine: nobody else gets it."),
|
||||
@@ -51,12 +61,14 @@ export async function reservationDialog(r, done, preset = {}) {
|
||||
field("Client identifier", cid, "Optional: match the client's identifier (option 61, hex) instead of its MAC address."),
|
||||
field("Host name", host, "Sent to the client."),
|
||||
field("Options", opts, "Only for this client; they win over the subnet's."),
|
||||
field("Comment", comment)), err);
|
||||
field("Comment", comment),
|
||||
r ? [h("label"), disabled] : null), err);
|
||||
await modal(r ? "Edit reservation " + r.ip : "New reservation", body, [
|
||||
{ label: "Cancel", value: null },
|
||||
{ label: r ? "Save" : "Add reservation", kind: "primary", onclick: async () => {
|
||||
mount(err);
|
||||
const v = { ip: ip.value.trim(), mac: mac.value.trim(), client_id: cid.value.trim(), hostname: host.value.trim(), options: opts.value(), comment: comment.value };
|
||||
if (r) v.disabled = disabled.input.checked;
|
||||
try {
|
||||
await change(r ? "PATCH" : "POST", r ? "/reservations/" + r.id : "/reservations", v);
|
||||
done();
|
||||
|
||||
@@ -21,7 +21,11 @@ export async function render(el, params) {
|
||||
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: "subnet-usage" }, usageBar(s.usage)),
|
||||
can("config", true) ? h("div", { class: "actions" }, h("button", {
|
||||
title: s.disabled ? "Answer this subnet's clients again" : "Stop answering this subnet's clients; its configuration and leases stay",
|
||||
onclick: () => toggleSubnet(s) }, s.disabled ? "Enable subnet" : "Disable subnet")) : null),
|
||||
s.disabled ? h("div", { class: "msg warn" }, "This subnet is disabled: its clients get no answers. Its configuration and leases are kept.") : null,
|
||||
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);
|
||||
@@ -29,6 +33,14 @@ export async function render(el, params) {
|
||||
else await settingsTab(body, s);
|
||||
}
|
||||
|
||||
export async function toggleSubnet(s) {
|
||||
if (!s.disabled && !await confirmDialog("Disable subnet", "Stop answering the clients of " + subnetLabel(s) + "? They keep their addresses until their leases run out, then have none. The configuration and leases stay; Enable brings it back.", "Disable", "danger")) return;
|
||||
try {
|
||||
await change("PATCH", "/subnets/" + s.id, { disabled: !s.disabled });
|
||||
rerender();
|
||||
} catch (e) { modal(s.disabled ? "Not enabled" : "Not disabled", errorBox(e)); }
|
||||
}
|
||||
|
||||
async function leasesTab(el, s) {
|
||||
const search = input({ type: "search", placeholder: "address, MAC or host name" });
|
||||
const out = h("div");
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { h, mount, table, modal, errorBox, badge } from "../ui.js";
|
||||
import { get, change, can } from "../api.js";
|
||||
import { usageBar, subnetForm, fmtSeconds, subnetLabel } from "../widgets.js";
|
||||
import { toggleSubnet } from "./subnet.js";
|
||||
|
||||
export async function render(el, params, query) {
|
||||
const subnets = await get("/subnets");
|
||||
@@ -18,8 +19,9 @@ export async function render(el, params, query) {
|
||||
{ label: "In use", render: (s) => usageBar(s.usage) },
|
||||
{ label: "Reserved", class: "num", tdClass: "num", render: (s) => String(s.usage.reservations) },
|
||||
{ label: "Lease time", render: (s) => s.lease_time ? fmtSeconds(s.lease_time) : h("span", { class: "muted" }, "default") },
|
||||
{ label: "", render: (s) => s.disabled ? badge("disabled", "off") : null },
|
||||
], subnets, { empty: "No subnets yet. Add one for each network Steward should serve." })));
|
||||
{ label: "", tdClass: "actions-cell", render: (s) => [s.disabled ? [badge("disabled", "off"), " "] : null,
|
||||
can("config", true) ? h("button", { class: "small reveal", onclick: () => toggleSubnet(s) }, s.disabled ? "Enable" : "Disable") : null] },
|
||||
], subnets, { empty: "No subnets yet. Add one for each network Steward should serve.", rowClass: (s) => s.disabled ? "disabled" : "" })));
|
||||
if (query.get("new") === "1" && can("config", true)) newSubnet();
|
||||
}
|
||||
|
||||
|
||||
@@ -110,11 +110,9 @@ header.top .spacer { flex: 1; }
|
||||
header.top form.search input { width: 220px; }
|
||||
header.top .user { color: var(--muted); }
|
||||
main { padding: 16px; max-width: 1400px; margin: 0 auto; }
|
||||
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
||||
background: var(--warn-soft); color: var(--text); border-bottom: 1px solid var(--border);
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.page-head { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.page-head { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
|
||||
.page-head > .actions { padding: 4px 0; }
|
||||
.filter-row + .panel, .actions + .panel { margin-top: 4px; }
|
||||
.page-head .grow { flex: 1; min-width: 200px; }
|
||||
.actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
||||
|
||||
|
||||
Reference in new issue
Block a user