mirror of https://github.com/aredn/aredn.git
680 lines
30 KiB
Plaintext
Executable File
680 lines
30 KiB
Plaintext
Executable File
{%
|
|
/*
|
|
* Part of AREDN® -- Used for creating Amateur Radio Emergency Data Networks
|
|
* Copyright (C) 2024 Tim Wilkinson
|
|
* See Contributors file for additional contributors
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation version 3 of the License.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* Additional Terms:
|
|
*
|
|
* Additional use restrictions exist on the AREDN® trademark and logo.
|
|
* See AREDNLicense.txt for more info.
|
|
*
|
|
* Attributions to the AREDN® Project must be retained in the source code.
|
|
* If importing this code into a new or existing project attribution
|
|
* to the AREDN® project must be added to the source code.
|
|
*
|
|
* You must not misrepresent the origin of the material contained within.
|
|
*
|
|
* Modified versions must be modified to attribute to the original source
|
|
* and be marked in reasonable ways as differentiate it from the original
|
|
* version
|
|
*/
|
|
%}
|
|
{%
|
|
if (request.env.REQUEST_METHOD === "PUT") {
|
|
if ("options" in request.args) {
|
|
configuration.prepareChanges();
|
|
const options = json(request.args.options);
|
|
const dhcp = configuration.getDHCP();
|
|
let f = fs.open(dhcp.reservations, "w");
|
|
if (f) {
|
|
const base = iptoarr(dhcp.start)[3];
|
|
for (let i = 0; i < length(options); i++) {
|
|
const o = options[i];
|
|
if (o.reserved) {
|
|
const ip = iptoarr(o.ip)[3];
|
|
if (length(o.name) > 0 && ip >= base && match(o.mac, /^([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]$/)) {
|
|
f.write(`${o.mac} ${ip - base + 2} ${o.name}${o.noprop ? " #NOPROP" : ""}\n`);
|
|
}
|
|
}
|
|
}
|
|
f.close();
|
|
}
|
|
}
|
|
if ("advtags" in request.args) {
|
|
configuration.prepareChanges();
|
|
const advtags = json(request.args.advtags);
|
|
const dhcp = configuration.getDHCP();
|
|
let f = fs.open(dhcp.dhcptags, "w");
|
|
if (f) {
|
|
for (let i = 0; i < length(advtags); i++) {
|
|
const t = advtags[i];
|
|
f.write(`${t.name} ${t.type} ${t.match}\n`);
|
|
}
|
|
f.close();
|
|
}
|
|
}
|
|
if ("advoptions" in request.args) {
|
|
configuration.prepareChanges();
|
|
const advoptions = json(request.args.advoptions);
|
|
const dhcp = configuration.getDHCP();
|
|
let f = fs.open(dhcp.dhcpoptions, "w");
|
|
if (f) {
|
|
for (let i = 0; i < length(advoptions); i++) {
|
|
const o = advoptions[i];
|
|
f.write(`${o.name} ${o.always ? "force" : "onrequest"} ${o.type} ${o.value}\n`);
|
|
}
|
|
f.close();
|
|
}
|
|
}
|
|
print(_R("changes"));
|
|
return;
|
|
}
|
|
if (request.env.REQUEST_METHOD === "DELETE") {
|
|
configuration.revertModalChanges();
|
|
print(_R("changes"));
|
|
return;
|
|
}
|
|
const dhcp = configuration.getDHCP();
|
|
const start = iptoarr(dhcp.start);
|
|
const end = iptoarr(dhcp.end);
|
|
const leases = [];
|
|
const options = [];
|
|
const advoptions = [];
|
|
const advtags = [];
|
|
for (let i = start[3]; i <= end[3]; i++) {
|
|
push(options, { mac: "", ip: `${start[0]}.${start[1]}.${start[2]}.${i}`, name: "", noprop: false, reserved: false, leased: false });
|
|
}
|
|
let reservations = 0;
|
|
let active = 0;
|
|
let f = fs.open(dhcp.reservations);
|
|
if (f) {
|
|
for (let l = f.read("line"); length(l); l = f.read("line")) {
|
|
// mac, last-ip, name, flags
|
|
const v = match(trim(l), /^([^ ]+) ([^ ]+) ([^ ]+) ?(.*)/);
|
|
if (v) {
|
|
const o = options[int(v[2]) - 2];
|
|
if (o) {
|
|
o.mac = v[1];
|
|
o.name = v[3];
|
|
o.noprop = v[4] == "#NOPROP";
|
|
o.reserved = true;
|
|
reservations++;
|
|
}
|
|
}
|
|
}
|
|
f.close();
|
|
}
|
|
f = fs.open(dhcp.leases);
|
|
if (f) {
|
|
for (let l = f.read("line"); length(l); l = f.read("line")) {
|
|
// ?, mac, ip, name, ?
|
|
const v = match(l, /^(.+) (.+) (.+) (.+) (.+)$/);
|
|
if (v) {
|
|
const ip = iptoarr(v[3]);
|
|
const o = options[ip[3] - start[3]];
|
|
if (o) {
|
|
o.leased = true;
|
|
if (o.mac === "") {
|
|
o.mac = v[2];
|
|
}
|
|
if (o.name === "") {
|
|
o.name = v[4];
|
|
}
|
|
active++;
|
|
}
|
|
}
|
|
}
|
|
f.close();
|
|
}
|
|
f = fs.open(dhcp.dhcptags);
|
|
if (f) {
|
|
for (let l = f.read("line"); length(l); l = f.read("line")) {
|
|
const m = match(replace(l, /\n+$/, ""), /^([^\W_]+)\s(\w+)\s(.+)$/);
|
|
if (m) {
|
|
const p = replace(replace(replace(m[3], /"/g, """), /</g, "<"), />/g, ">");
|
|
push(advtags, { name: m[1], type: m[2], match: p });
|
|
}
|
|
}
|
|
f.close();
|
|
}
|
|
f = fs.open(dhcp.dhcpoptions);
|
|
if (f) {
|
|
for (let l = f.read("line"); length(l); l = f.read("line")) {
|
|
const m = match(replace(l, /\n+$/, ""), /^(\S*)\s(force|onrequest)\s(\d+)\s(.+)$/);
|
|
if (m) {
|
|
push(advoptions, { name: m[1], always: m[2] === "force", type: int(m[3]), value: m[4] });
|
|
}
|
|
}
|
|
f.close();
|
|
}
|
|
const dhcpOptionTypes = {
|
|
"1": "netmask",
|
|
"2": "time-offset",
|
|
"3": "router",
|
|
"6": "dns-server",
|
|
"7": "log-server",
|
|
"9": "lpr-server",
|
|
"13": "boot-file-size",
|
|
"15": "domain-name",
|
|
"16": "swap-server",
|
|
"17": "root-path",
|
|
"18": "extension-path",
|
|
"19": "ip-forward-enable",
|
|
"20": "non-local-source-routing",
|
|
"21": "policy-filter",
|
|
"22": "max-datagram-reassembly",
|
|
"23": "default-ttl",
|
|
"26": "mtu",
|
|
"27": "all-subnets-local",
|
|
"31": "router-discovery",
|
|
"32": "router-solicitation",
|
|
"33": "static-route",
|
|
"34": "trailer-encapsulation",
|
|
"35": "arp-timeout",
|
|
"36": "ethernet-encap",
|
|
"37": "tcp-ttl",
|
|
"38": "tcp-keepalive",
|
|
"40": "nis-domain",
|
|
"41": "nis-server",
|
|
"42": "ntp-server",
|
|
"44": "netbios-ns",
|
|
"45": "netbios-dd",
|
|
"46": "netbios-nodetype",
|
|
"47": "netbios-scope",
|
|
"48": "x-windows-fs",
|
|
"49": "x-windows-dm",
|
|
"58": "T1",
|
|
"59": "T2",
|
|
"60": "vendor-class",
|
|
"64": "nis+-domain",
|
|
"65": "nis+-server",
|
|
"66": "tftp-server",
|
|
"67": "bootfile-name",
|
|
"68": "mobile-ip-home",
|
|
"69": "smtp-server",
|
|
"70": "pop3-server",
|
|
"71": "nntp-server",
|
|
"74": "irc-server",
|
|
"77": "user-class",
|
|
"80": "rapid-commit",
|
|
"93": "client-arch",
|
|
"94": "client-interface-id",
|
|
"97": "client-machine-id",
|
|
"100": "posix-timezone",
|
|
"101": "tzdb-timezone",
|
|
"108": "ipv6-only",
|
|
"119": "domain-search",
|
|
"120": "sip-server",
|
|
"121": "classless-static-route",
|
|
"125": "vendor-id-encap",
|
|
"150": "tftp-server-address",
|
|
"255": "server-ip-address"
|
|
};
|
|
%}
|
|
<div class="dialog">
|
|
{{_R("dialog-header", "LAN DHCP")}}
|
|
<div>
|
|
<div class="dhcp">
|
|
<div>
|
|
<div class="cols">
|
|
<div>
|
|
<div class="o">Address Reservations</div>
|
|
<div class="m">Hostnames with fixed addresses</div>
|
|
</div>
|
|
<button class="plus" {{length(options) === reservations ? "disabled" : ""}}>+</button>
|
|
</div>
|
|
</div>
|
|
{{_H("Creates a permenant mapping between a device MAC address and an IP address on the LAN network.
|
|
The given hostname is available to everyone on the mesh unless the entry is marked as <b>do not propagate</b>")}}
|
|
<form>
|
|
<div id="dhcp-reservations">
|
|
<div class="reservation-label adr">
|
|
<div>
|
|
<div>hostname</div>
|
|
<div>ip address</div>
|
|
<div>mac a‌ddress</div>
|
|
<div>do not propagate</div>
|
|
</div>
|
|
<div></div>
|
|
</div>
|
|
{% if (reservations > 0) {
|
|
for (let i = 0; i < length(options); i++) {
|
|
const o = options[i];
|
|
if (o.reserved) {
|
|
%}
|
|
<div class="reservation adr" data-ip="{{o.ip}}">
|
|
<div>
|
|
<input name="hostname" type="text" required placeholder="hostname" value="{{o.name}}">
|
|
<select class="dhcp-addresses">
|
|
</select>
|
|
<input name="mac" type="text" required placeholder="mac a‌ddress" pattern="([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]" value="{{o.mac}}">
|
|
<label><input type="checkbox" {{o.noprop ? "checked" : ""}}></label>
|
|
</div>
|
|
<button>-</button>
|
|
</div>
|
|
{% }
|
|
}
|
|
} %}
|
|
</div>
|
|
</form>
|
|
<hr>
|
|
<div class="o">Active Leases</div>
|
|
<div class="m">Addresses currently in use</div>
|
|
{{_H("The list of active leases currently allocated to LAN devices. Any of these leases can be promoted
|
|
to a permanent mapping to allow IP Addresses to be fixed to specific devices.")}}
|
|
{% if (active > 0) {
|
|
%}
|
|
<div class="lease-label adr">
|
|
<div>
|
|
<div>hostname</div>
|
|
<div>ip address</div>
|
|
<div>mac a‌ddress</div>
|
|
</div>
|
|
<div></div>
|
|
</div>
|
|
{%
|
|
for (let i = 0; i < length(options); i++) {
|
|
const o = options[i];
|
|
if (o.leased) {
|
|
%}
|
|
<div class="lease adr" data-ip="{{o.ip}}">
|
|
<div>
|
|
<input readonly type="text" value="{{o.name}}">
|
|
<input readonly type="text" value="{{o.ip}}">
|
|
<input readonly type="text" value="{{o.mac}}">
|
|
</div>
|
|
<button {{o.reserved ? "disabled" : ""}}>+</button>
|
|
</div>
|
|
{% }
|
|
}
|
|
} %}
|
|
</div>
|
|
{{_R("dialog-advanced")}}
|
|
<div>
|
|
{% if (includeAdvanced) { %}
|
|
<div class="dhcp-tags">
|
|
<div class="cols">
|
|
<div>
|
|
<div class="o">Tags</div>
|
|
<div class="m">Tags for advanced options</div>
|
|
</div>
|
|
<button>+</button>
|
|
</div>
|
|
<div class="dhcptag-label adr">
|
|
<div class="row">
|
|
<div>tag</div>
|
|
<div>type</div>
|
|
<div>match</div>
|
|
<div></div>
|
|
</div>
|
|
<div></div>
|
|
</div>
|
|
<div class="list noborder">{%
|
|
for (let i = 0; i < length(advtags); i++) {
|
|
const t = advtags[i];
|
|
%}<div class="tag adr">
|
|
<div class="row">
|
|
<input name="tag_name" type="text" required value="{{t.name}}">
|
|
<select name="tag_type" required>
|
|
<option value="">-</option>
|
|
<option value="vendorclass" {{t.type == "vendorclass" ? "selected": ""}}>Vendor Class</option>
|
|
<option value="userclass" {{t.type == "userclass" ? "selected": ""}}>User Class</option>
|
|
<option value="mac" {{t.type == "mac" ? "selected": ""}}>MAC Address</option>
|
|
<option value="circuitid" {{t.type == "circuitid" ? "selected": ""}}>Agent Circuit ID</option>
|
|
<option value="remoteid" {{t.type == "remoteid" ? "selected": ""}}>Agent Remote ID</option>
|
|
<option value="subscriberid" {{t.type == "subscriberid" ? "selected": ""}}>Subscriber-ID</option>
|
|
</select>
|
|
<input name="tag_match" type="text" required value="{{t.match}}">
|
|
<div></div>
|
|
</div>
|
|
<button>-</button>
|
|
</div>{%
|
|
}
|
|
%}</div>
|
|
</div>
|
|
<div class="dhcp-options">
|
|
<div class="cols">
|
|
<div>
|
|
<div class="o">Options</div>
|
|
<div class="m">Advanced options</div>
|
|
</div>
|
|
<button>+</button>
|
|
</div>
|
|
<div class="dhcpoption-label adr">
|
|
<div class="row">
|
|
<div>tag</div>
|
|
<div>option</div>
|
|
<div>value</div>
|
|
<div>always</div>
|
|
</div>
|
|
<div></div>
|
|
</div>
|
|
<div class="list noborder">{%
|
|
for (let i = 0; i < length(advoptions); i++) {
|
|
const o = advoptions[i];
|
|
%}<div class="option adr">
|
|
<div class="row">
|
|
<select name="option_name">
|
|
<option value="{{o.name}}" selected>{{o.name}}</option>
|
|
</select>
|
|
<input name="option_type" type="text" required list="dhcp-option-type-list" value="{{dhcpOptionTypes[o.type] ? `${o.type}: ${dhcpOptionTypes[o.type]}` : o.type}}" pattern="([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(: .+)?">
|
|
<input name="option_value" type="text" required value="{{o.value}}">
|
|
<label><input name="option_always" type="checkbox" {{o.always ? "checked" : ""}}></label>
|
|
</div>
|
|
<button>-</button>
|
|
</div>{%
|
|
}
|
|
%}</div>
|
|
</div>
|
|
{% } %}
|
|
</div>
|
|
</div>
|
|
<datalist id="dhcp-option-type-list">
|
|
{%
|
|
for (let k in dhcpOptionTypes) {
|
|
print(`<option value="${k}: ${dhcpOptionTypes[k]}"></option>`);
|
|
}
|
|
%}
|
|
</datalist>
|
|
{{_R("dialog-footer")}}
|
|
<script>
|
|
(function(){
|
|
{{_R("open")}}
|
|
const options = {{sprintf("%J", options)}};
|
|
function update()
|
|
{
|
|
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
|
|
swap: "none",
|
|
values: { options: JSON.stringify(options) }
|
|
});
|
|
}
|
|
function refreshIPSelectors()
|
|
{
|
|
const addrs = htmx.findAll(".dhcp-addresses");
|
|
for (let i = 0; i < addrs.length; i++) {
|
|
let opt = "";
|
|
for (let j = 0; j < options.length; j++) {
|
|
const o = options[j];
|
|
const self = o.ip === addrs[i].parentNode.parentNode.dataset.ip;
|
|
if (self || (!o.leased && !o.reserved)) {
|
|
opt += `<option value="${o.ip}" ${self ? "selected" : ""}>${o.ip}</option>`;
|
|
}
|
|
}
|
|
addrs[i].innerHTML = opt;
|
|
}
|
|
}
|
|
refreshIPSelectors();
|
|
htmx.on("#ctrl-modal .dialog .dhcp", "change", event => {
|
|
const target = event.target;
|
|
switch (target.nodeName) {
|
|
case "SELECT":
|
|
{
|
|
const oldip = target.parentNode.parentNode.dataset.ip;
|
|
const newip = target.value;
|
|
const oo = options.find(o => o.ip == oldip);
|
|
const no = options.find(o => o.ip == newip);
|
|
Object.assign(no, { name: oo.name, mac: oo.mac, noprop: oo.noprop, reserved: true, leased: false });
|
|
Object.assign(oo, { name: "", mac: "", noprop: false, reserved: false, leased: false });
|
|
target.parentNode.parentNode.dataset.ip = newip;
|
|
refreshIPSelectors();
|
|
update();
|
|
break;
|
|
}
|
|
case "INPUT":
|
|
switch (target.type) {
|
|
case "text":
|
|
{
|
|
if (target.validity.valid) {
|
|
const ip = target.parentNode.parentNode.dataset.ip;
|
|
const o = options.find(o => o.ip == ip);
|
|
if (target.name === "hostname") {
|
|
o.name = target.value;
|
|
}
|
|
else {
|
|
o.mac = target.value;
|
|
}
|
|
update();
|
|
}
|
|
break;
|
|
}
|
|
case "checkbox":
|
|
{
|
|
const ip = target.parentNode.parentNode.parentNode.dataset.ip;
|
|
const o = options.find(o => o.ip == ip);
|
|
o.noprop = target.checked;
|
|
update();
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
htmx.on("#ctrl-modal .dialog .dhcp", "click", event => {
|
|
const target = event.target;
|
|
switch (target.nodeName) {
|
|
case "BUTTON":
|
|
switch (target.innerText) {
|
|
case "+":
|
|
{
|
|
const ip = target.parentNode.dataset.ip;
|
|
if (ip) {
|
|
target.disabled = true;
|
|
const o = options.find(o => o.ip == ip);
|
|
o.reserved = true;
|
|
o.noprop = true;
|
|
const item = document.createElement("div");
|
|
item.innerHTML = `<div class="reservation adr" data-ip="${o.ip}"><div style="white-space:nowrap"><input name="hostname" type="text" placeholder="hostname" value="${o.name}" required> <select class="dhcp-addresses"></select> <input name="mac" type="text" placeholder="mac a‌ddress" required pattern="([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]" value="${o.mac}"><label> <input type="checkbox" ${o.noprop ? "checked" : ""}></label></div><button>-</button></div>`;
|
|
htmx.find("#dhcp-reservations").appendChild(item.firstChild);
|
|
update();
|
|
refreshIPSelectors();
|
|
}
|
|
else {
|
|
function newDHCPEntry()
|
|
{
|
|
const o = options.find(o => !o.reserved && !o.leased);
|
|
if (o) {
|
|
Object.assign(o, { name: "", mac: "", noprop: true, reserved: true, leased: false });
|
|
const item = document.createElement("div");
|
|
item.innerHTML = `<div class="reservation adr" data-ip="${o.ip}"><div style="white-space:nowrap"><input name="hostname" type="text" placeholder="hostname" value="${o.name}" required> <select class="dhcp-addresses"></select> <input name="mac" type="text" placeholder="mac a‌ddress" required pattern="([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]" value="${o.mac}"><label> <input type="checkbox" ${o.noprop ? "checked" : ""}></label></div><button>-</button></div>`;
|
|
const fc = item.firstChild;
|
|
htmx.find("#dhcp-reservations").appendChild(fc);
|
|
htmx.find(fc, "input").focus();
|
|
htmx.on(fc, "keypress", event => {
|
|
if (event.keyCode === 13 && event.target.nodeName == "INPUT" && htmx.closest(event.target, ".reservation") === htmx.find("#dhcp-reservations .reservation:last-child")) {
|
|
event.preventDefault();
|
|
newDHCPEntry();
|
|
}
|
|
});
|
|
}
|
|
if (!options.find(o => !o.reserved && !o.leased)) {
|
|
htmx.find("#ctrl-modal .dialog .dhcp .plus").disabled = true;
|
|
}
|
|
refreshIPSelectors();
|
|
}
|
|
newDHCPEntry();
|
|
}
|
|
break;
|
|
}
|
|
case "-":
|
|
{
|
|
const ip = target.parentNode.dataset.ip;
|
|
const o = options.find(o => o.ip == ip);
|
|
o.reserved = false;
|
|
if (o.leased) {
|
|
const l = htmx.find(`#ctrl-modal .dialog .lease[data-ip="${ip}"] button`);
|
|
l.disabled = false;
|
|
}
|
|
const item = target.parentNode;
|
|
htmx.remove(item);
|
|
htmx.find("#ctrl-modal .dialog .dhcp button").disabled = false;
|
|
refreshIPSelectors();
|
|
update();
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
{% if (includeAdvanced) { %}
|
|
function refreshAdvOptions()
|
|
{
|
|
const tagnames = [];
|
|
const names = htmx.findAll("#ctrl-modal .dialog .dhcp-tags .list .row input[name=tag_name]");
|
|
for (let i = 0; i < names.length; i++) {
|
|
if (names[i].validity.valid && tagnames.indexOf(names[i].value) === -1) {
|
|
tagnames.push(names[i].value);
|
|
}
|
|
}
|
|
const options = htmx.findAll("#ctrl-modal .dialog .dhcp-options .list .row select[name=option_name]");
|
|
for (let i = 0; i < options.length; i++) {
|
|
const v = options[i].value;
|
|
options[i].innerHTML = "<option value=''>[all]</option>" + tagnames.map(t => `<option value="${t}" ${t === v ? "selected": ""}>${t}</option>`).join("");
|
|
}
|
|
}
|
|
function updateTags()
|
|
{
|
|
const advtags = [];
|
|
const tags = htmx.findAll("#ctrl-modal .dialog .dhcp-tags .list .row");
|
|
for (let i = 0; i < tags.length; i++) {
|
|
const name = htmx.find(tags[i], "input[name=tag_name]");
|
|
const type = htmx.find(tags[i], "select[name=tag_type]");
|
|
const match = htmx.find(tags[i], "input[name=tag_match]");
|
|
if (name.validity.valid && type.validity.valid && match.validity.valid) {
|
|
advtags.push({ name: name.value, type: type.value, match: match.value });
|
|
}
|
|
}
|
|
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
|
|
swap: "none",
|
|
values: { advtags: JSON.stringify(advtags) }
|
|
});
|
|
}
|
|
const dhcpOptionTypes = {%print(dhcpOptionTypes);%};
|
|
function updateOptions()
|
|
{
|
|
const advoptions = [];
|
|
const options = htmx.findAll("#ctrl-modal .dialog .dhcp-options .list .row");
|
|
for (let i = 0; i < options.length; i++) {
|
|
const name = htmx.find(options[i], "select[name=option_name]");
|
|
const type = htmx.find(options[i], "input[name=option_type]");
|
|
const value = htmx.find(options[i], "input[name=option_value]");
|
|
const always = htmx.find(options[i], "input[name=option_always]");
|
|
const tvalue = type.validity.valid ? `${parseInt(type.value)}` : "";
|
|
if (name.validity.valid && type.validity.valid && value.validity.valid) {
|
|
advoptions.push({ name: name.value, type: tvalue, value: value.value, always: !!always.checked });
|
|
}
|
|
if (dhcpOptionTypes[tvalue]) {
|
|
type.value = `${tvalue}: ${dhcpOptionTypes[tvalue]}`;
|
|
}
|
|
}
|
|
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
|
|
swap: "none",
|
|
values: { advoptions: JSON.stringify(advoptions) }
|
|
});
|
|
}
|
|
htmx.on("#ctrl-modal .dialog .dhcp-tags", "click", event => {
|
|
const target = event.target;
|
|
switch (target.nodeName) {
|
|
case "BUTTON":
|
|
switch (target.innerText) {
|
|
case "+":
|
|
function addNewTag()
|
|
{
|
|
const item = document.createElement("div");
|
|
item.innerHTML = `<div class="tag adr"><div class="row"><input name="tag_name" type="text" required value=""> <select name="tag_type" required><option value="">-</option><option value="vendorclass">Vendor Class</option><option value="userclass">User Class</option><option value="mac">MAC Address</option><option value="circuitid">Agent Circuit ID</option><option value="remoteid">Agent Remote ID</option><option value="subscriberid">Subscriber-ID</option></select> <input name="tag_match" type="text" required value=""><div></div></div><button>-</button></div>`;
|
|
const fc = item.firstChild;
|
|
htmx.find("#ctrl-modal .dialog .dhcp-tags .list").appendChild(fc);
|
|
htmx.find(fc, "input").focus();
|
|
htmx.on(fc, "keypress", event => {
|
|
if (event.keyCode === 13 && event.target.nodeName == "INPUT" && htmx.closest(event.target, ".tag") === htmx.find(".dhcp-tags .list .tag:last-child")) {
|
|
event.preventDefault();
|
|
addNewTag();
|
|
}
|
|
});
|
|
}
|
|
addNewTag();
|
|
break;
|
|
case "-":
|
|
const row = target.parentNode;
|
|
row.parentNode.removeChild(row);
|
|
refreshAdvOptions();
|
|
updateTags();
|
|
updateOptions();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
htmx.on("#ctrl-modal .dialog .dhcp-tags", "change", _ => {
|
|
refreshAdvOptions();
|
|
updateTags();
|
|
});
|
|
htmx.on("#ctrl-modal .dialog .dhcp-options", "click", event => {
|
|
const target = event.target;
|
|
switch (target.nodeName) {
|
|
case "BUTTON":
|
|
switch (target.innerText) {
|
|
case "+":
|
|
function addNewOption()
|
|
{
|
|
const item = document.createElement("div");
|
|
item.innerHTML = `<div class="option adr"><div class="row"><select name="option_name"><option value="">[all]</option></select> <input name="option_type" type="text" required list="dhcp-option-type-list" pattern="([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(: .+)?"> <input name="option_value" type="text" required value=""> <label><input name="option_always" type="checkbox"></label></div><button>-</button></div>`;
|
|
const fc = item.firstChild;
|
|
htmx.find("#ctrl-modal .dialog .dhcp-options .list").appendChild(fc);
|
|
refreshAdvOptions();
|
|
htmx.find(fc, "select").focus();
|
|
htmx.on(fc, "keypress", event => {
|
|
if (event.keyCode === 13 && event.target.nodeName == "INPUT" && htmx.closest(event.target, ".option") === htmx.find(".dhcp-options .list .option:last-child")) {
|
|
event.preventDefault();
|
|
addNewOption();
|
|
}
|
|
});
|
|
}
|
|
addNewOption();
|
|
break;
|
|
case "-":
|
|
const row = target.parentNode;
|
|
row.parentNode.removeChild(row);
|
|
updateOptions();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
htmx.on("#ctrl-modal .dialog .dhcp-options", "change", _ => {
|
|
updateOptions();
|
|
});
|
|
refreshAdvOptions();
|
|
{% } %}
|
|
})();
|
|
</script>
|
|
</div>
|