Files
NeoTachyon/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/local_devices.js
T

261 lines
6.4 KiB
JavaScript

"use strict";
"require baseclass";
"require rpc";
"require ui";
"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) {
return `${name || ""}`.trim().replace(/\.lan$/i, "");
}
function addLocalDeviceChoice(choices, ip, name) {
const normalizedIp = `${ip || ""}`.trim();
const normalizedName = normalizeLocalDeviceName(name);
if (!normalizedIp || !normalizedName) {
return;
}
if (!main.validateIP(normalizedIp).valid) {
return;
}
choices[normalizedIp] = normalizedName;
}
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) {
const choices = {};
const routerIps = buildRouterIpMap(networkInterfaces);
if (hostHints && typeof hostHints === "object") {
Object.values(hostHints).forEach((hint) => {
if (!hint || typeof hint !== "object") {
return;
}
[
...normalizeOptionValues(hint.ipaddrs),
...normalizeOptionValues(hint.ipv4),
...normalizeOptionValues(hint.ipv6),
].forEach((ip) => {
addLocalDeviceChoice(choices, ip, hint.name);
});
});
}
if (dhcpLeases && Array.isArray(dhcpLeases.dhcp_leases)) {
dhcpLeases.dhcp_leases.forEach((lease) => {
if (!lease || typeof lease !== "object") {
return;
}
addLocalDeviceChoice(choices, lease.ipaddr, lease.hostname);
});
}
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(() => []),
])
.then(([hostHints, dhcpLeases, networkInterfaces]) => {
localDeviceChoicesCache = buildLocalDeviceChoices(
hostHints,
dhcpLeases,
networkInterfaces,
);
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 hasSingleIpValue(values) {
return normalizeOptionValues(values).some(
(value) => main.validateIP(value).valid,
);
}
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);