291 lines
7.6 KiB
JavaScript
291 lines
7.6 KiB
JavaScript
"use strict";
|
|
"require baseclass";
|
|
"require rpc";
|
|
"require ui";
|
|
"require uci";
|
|
"require view.tachyon.main as main";
|
|
|
|
const callHostHints = rpc.declare({
|
|
object: "luci-rpc",
|
|
method: "getHostHints",
|
|
expect: { "": {} },
|
|
});
|
|
const callDHCPLeases = rpc.declare({
|
|
object: "luci-rpc",
|
|
method: "getDHCPLeases",
|
|
expect: { "": {} },
|
|
});
|
|
const callNetworkInterfaceDump = rpc.declare({
|
|
object: "network.interface",
|
|
method: "dump",
|
|
expect: { interface: [] },
|
|
});
|
|
|
|
let localDeviceChoicesCache = null;
|
|
let localDeviceChoicesPromise = null;
|
|
|
|
function normalizeOptionValues(value) {
|
|
if (!value) {
|
|
return [];
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
return value
|
|
.filter(Boolean)
|
|
.map((item) => `${item}`.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
return `${value}`
|
|
.split(/\s+/)
|
|
.map((item) => item.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function normalizeLocalDeviceName(name) {
|
|
const clean = `${name || ""}`.trim().replace(/\.lan$/i, "");
|
|
return clean === "*" ? "" : clean;
|
|
}
|
|
|
|
function addLocalDeviceChoice(choices, ip, name, mac) {
|
|
const normalizedIp = `${ip || ""}`.trim();
|
|
const normalizedMac = `${mac || ""}`.trim().toLowerCase();
|
|
const normalizedName = normalizeLocalDeviceName(name);
|
|
|
|
if (normalizedIp && main.validateIP(normalizedIp).valid) {
|
|
if (normalizedName) {
|
|
choices[normalizedIp] = `${normalizedName} (${normalizedIp})`;
|
|
} else if (normalizedMac) {
|
|
choices[normalizedIp] = `${normalizedIp} [${normalizedMac}]`;
|
|
} else if (!choices[normalizedIp]) {
|
|
choices[normalizedIp] = `${normalizedIp}`;
|
|
}
|
|
}
|
|
|
|
if (normalizedMac && /^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/.test(normalizedMac)) {
|
|
const formattedMac = normalizedMac.replace(/-/g, ":");
|
|
if (normalizedName) {
|
|
choices[formattedMac] = `${normalizedName} [MAC: ${formattedMac}]`;
|
|
} else if (normalizedIp) {
|
|
choices[formattedMac] = `${normalizedIp} [MAC: ${formattedMac}]`;
|
|
} else if (!choices[formattedMac]) {
|
|
choices[formattedMac] = `${formattedMac}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
function addRouterIp(routerIps, ip) {
|
|
const normalizedIp = `${ip || ""}`.trim();
|
|
|
|
if (!normalizedIp || !main.validateIP(normalizedIp).valid) {
|
|
return;
|
|
}
|
|
|
|
routerIps[normalizedIp] = true;
|
|
}
|
|
|
|
function buildRouterIpMap(networkInterfaces) {
|
|
const routerIps = {};
|
|
|
|
if (!Array.isArray(networkInterfaces)) {
|
|
return routerIps;
|
|
}
|
|
|
|
networkInterfaces.forEach((networkInterface) => {
|
|
const ipAddresses = [];
|
|
const ipv4Addresses =
|
|
networkInterface &&
|
|
typeof networkInterface === "object" &&
|
|
Array.isArray(networkInterface["ipv4-address"])
|
|
? networkInterface["ipv4-address"]
|
|
: [];
|
|
const ipv6Addresses =
|
|
networkInterface &&
|
|
typeof networkInterface === "object" &&
|
|
Array.isArray(networkInterface["ipv6-address"])
|
|
? networkInterface["ipv6-address"]
|
|
: [];
|
|
|
|
ipAddresses.push(...ipv4Addresses, ...ipv6Addresses);
|
|
ipAddresses.forEach((address) => {
|
|
addRouterIp(
|
|
routerIps,
|
|
address && typeof address === "object" ? address.address : address,
|
|
);
|
|
});
|
|
});
|
|
|
|
return routerIps;
|
|
}
|
|
|
|
function buildLocalDeviceChoices(hostHints, dhcpLeases, networkInterfaces, dhcpHosts) {
|
|
const choices = {};
|
|
const routerIps = buildRouterIpMap(networkInterfaces);
|
|
|
|
if (hostHints && typeof hostHints === "object") {
|
|
Object.entries(hostHints).forEach(([mac, hint]) => {
|
|
if (!hint || typeof hint !== "object") {
|
|
return;
|
|
}
|
|
|
|
[
|
|
...normalizeOptionValues(hint.ipaddrs),
|
|
...normalizeOptionValues(hint.ipv4),
|
|
...normalizeOptionValues(hint.ipv6),
|
|
].forEach((ip) => {
|
|
addLocalDeviceChoice(choices, ip, hint.name, mac);
|
|
});
|
|
});
|
|
}
|
|
|
|
if (dhcpLeases && Array.isArray(dhcpLeases.dhcp_leases)) {
|
|
dhcpLeases.dhcp_leases.forEach((lease) => {
|
|
if (!lease || typeof lease !== "object") {
|
|
return;
|
|
}
|
|
|
|
addLocalDeviceChoice(choices, lease.ipaddr, lease.hostname, lease.mac);
|
|
});
|
|
}
|
|
|
|
if (Array.isArray(dhcpHosts)) {
|
|
dhcpHosts.forEach((host) => {
|
|
if (host && host.ip && main.validateIP(host.ip).valid) {
|
|
addLocalDeviceChoice(choices, host.ip, host.name, host.mac);
|
|
}
|
|
});
|
|
}
|
|
|
|
Object.keys(routerIps).forEach((ip) => {
|
|
delete choices[ip];
|
|
});
|
|
|
|
return choices;
|
|
}
|
|
|
|
function loadLocalDeviceChoices() {
|
|
if (localDeviceChoicesCache) {
|
|
return Promise.resolve(localDeviceChoicesCache);
|
|
}
|
|
|
|
if (localDeviceChoicesPromise) {
|
|
return localDeviceChoicesPromise;
|
|
}
|
|
|
|
localDeviceChoicesPromise = Promise.all([
|
|
callHostHints().catch(() => ({})),
|
|
callDHCPLeases().catch(() => ({})),
|
|
callNetworkInterfaceDump().catch(() => []),
|
|
uci.load("dhcp").then(() => uci.sections("dhcp", "host")).catch(() => []),
|
|
])
|
|
.then(([hostHints, dhcpLeases, networkInterfaces, dhcpHosts]) => {
|
|
localDeviceChoicesCache = buildLocalDeviceChoices(
|
|
hostHints,
|
|
dhcpLeases,
|
|
networkInterfaces,
|
|
dhcpHosts
|
|
);
|
|
return localDeviceChoicesCache;
|
|
})
|
|
.finally(() => {
|
|
localDeviceChoicesPromise = null;
|
|
});
|
|
|
|
return localDeviceChoicesPromise;
|
|
}
|
|
|
|
function sortLocalDeviceChoiceValues(choices) {
|
|
return Object.keys(choices).sort((a, b) => {
|
|
const byName = `${choices[a]}`.localeCompare(`${choices[b]}`);
|
|
return byName || a.localeCompare(b);
|
|
});
|
|
}
|
|
|
|
function isIpOrMac(value) {
|
|
const val = `${value || ""}`.trim();
|
|
return (
|
|
main.validateIP(val).valid ||
|
|
/^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/.test(val)
|
|
);
|
|
}
|
|
|
|
function hasSingleIpValue(values) {
|
|
return normalizeOptionValues(values).some(isIpOrMac);
|
|
}
|
|
|
|
function preloadLocalDeviceChoicesForValues(values) {
|
|
return hasSingleIpValue(values)
|
|
? loadLocalDeviceChoices()
|
|
: Promise.resolve(null);
|
|
}
|
|
|
|
function createLocalDeviceDynamicListWidget(option, section_id, cfgvalue) {
|
|
const values = normalizeOptionValues(
|
|
cfgvalue != null ? cfgvalue : option.default,
|
|
);
|
|
const shouldResolveExistingLabels = hasSingleIpValue(values);
|
|
|
|
return (
|
|
shouldResolveExistingLabels ? loadLocalDeviceChoices() : Promise.resolve({})
|
|
).then((initialChoices) => {
|
|
const choices = localDeviceChoicesCache || initialChoices || {};
|
|
const widget = new ui.DynamicList(values, choices, {
|
|
id: option.cbid(section_id),
|
|
sort: sortLocalDeviceChoiceValues(choices),
|
|
optional: option.optional || option.rmempty,
|
|
datatype: option.datatype,
|
|
placeholder: option.placeholder,
|
|
validate: option.validate.bind(option, section_id),
|
|
disabled: option.readonly != null ? option.readonly : option.map.readonly,
|
|
});
|
|
const node = widget.render();
|
|
let choicesLoaded = Boolean(localDeviceChoicesCache);
|
|
let choicesLoading = false;
|
|
|
|
const loadChoices = () => {
|
|
if (choicesLoaded || choicesLoading) {
|
|
return;
|
|
}
|
|
|
|
choicesLoading = true;
|
|
loadLocalDeviceChoices()
|
|
.then((loadedChoices) => {
|
|
widget.clearChoices();
|
|
widget.addChoices(
|
|
sortLocalDeviceChoiceValues(loadedChoices),
|
|
loadedChoices,
|
|
);
|
|
choicesLoaded = true;
|
|
})
|
|
.finally(() => {
|
|
choicesLoading = false;
|
|
});
|
|
};
|
|
|
|
const maybeLoadChoices = (ev) => {
|
|
if (
|
|
ev.target &&
|
|
typeof ev.target.closest === "function" &&
|
|
ev.target.closest(".cbi-dropdown")
|
|
) {
|
|
loadChoices();
|
|
}
|
|
};
|
|
|
|
node.addEventListener("mousedown", maybeLoadChoices, true);
|
|
node.addEventListener("focusin", maybeLoadChoices, true);
|
|
|
|
return node;
|
|
});
|
|
}
|
|
|
|
const EntryPoint = {
|
|
createLocalDeviceDynamicListWidget,
|
|
hasSingleIpValue,
|
|
loadLocalDeviceChoices,
|
|
normalizeOptionValues,
|
|
preloadLocalDeviceChoicesForValues,
|
|
};
|
|
|
|
return baseclass.extend(EntryPoint);
|