mirror of
https://github.com/ntop/ntopng.git
synced 2026-07-22 14:18:00 +00:00
1476 lines
50 KiB
JavaScript
1476 lines
50 KiB
JavaScript
// 2014-20 - ntop.org
|
|
Date.prototype.format = function (format) { //author: meizz
|
|
var o = {
|
|
"M+": this.getMonth() + 1, //month
|
|
"d+": this.getDate(), //day
|
|
"h+": this.getHours(), //hour
|
|
"m+": this.getMinutes(), //minute
|
|
"s+": this.getSeconds(), //second
|
|
"q+": Math.floor((this.getMonth() + 3) / 3), //quarter
|
|
"S": this.getMilliseconds() //millisecond
|
|
}
|
|
|
|
if (/(y+)/.test(format)) format = format.replace(RegExp.$1,
|
|
(this.getFullYear() + "").substr(4 - RegExp.$1.length));
|
|
for (var k in o) if (new RegExp("(" + k + ")").test(format))
|
|
format = format.replace(RegExp.$1,
|
|
RegExp.$1.length == 1 ? o[k] :
|
|
("00" + o[k]).substr(("" + o[k]).length));
|
|
return format;
|
|
}
|
|
|
|
// Extended disable function
|
|
jQuery.fn.extend({
|
|
disable: function (state) {
|
|
return this.each(function () {
|
|
var $this = $(this);
|
|
if ($this.is('input, button, textarea, select'))
|
|
this.disabled = state;
|
|
else
|
|
$this.toggleClass('disabled', state);
|
|
});
|
|
}
|
|
});
|
|
|
|
const NTOPNG_MIN_VISUAL_VALUE = 0.005;
|
|
|
|
const REGEXES = {
|
|
ipv4: String.raw`^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`,
|
|
ipv6: String.raw`^((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]):){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*)$`,
|
|
domainName: String.raw`^([a-zA-Z]([a-zA-Z]|[0-9])?\.[a-zA-Z]{2,13}|[a-zA-Z0-9]([\-_.a-zA-Z0-9]{1,61}[a-zA-Z0-9])?\.[a-zA-Z]{2,13}|[a-zA-Z0-9]([\-_.a-zA-Z0-9]{1,61}[a-zA-Z0-9])?\.[a-zA-Z]{2,30}\.[a-zA-Z]{2,3})$`,
|
|
port: String.raw`^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$`,
|
|
latency: String.raw`^([0-9]*[.])?[0-9]+$`,
|
|
url: String.raw`^(https?\:\/\/[^\/\s]+(\/.*)?)$`,
|
|
emailUrl: String.raw`^smtps?:\/\/[\-a-zA-Z0-9:.]{1,256}$`,
|
|
macAddress: String.raw`^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$`,
|
|
hostname: String.raw`^(?!\s*$)[a-zA-Z0-9._: \-\/]{1,250}|^[a-zA-Z0-9._: \-\/]{1,250}@[0-9]{0,5}`,
|
|
username: String.raw`^[a-zA-Z0-9._@!-?]{3,30}$`,
|
|
singleword: String.raw`^(?=[a-zA-Z0-9._:\-]{3,253}$)(?!.*[_.:\-]{2})[^_.:\-].*[^_.:\-]$`,
|
|
multiword: String.raw`^([a-zA-Z0-9._:\-\s]{3,253})$`,
|
|
email: String.raw`^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[a-zA-Z]{2,})$`,
|
|
emailCommaList: String.raw`^((?:[a-zA-Z0-9.!#$%&'*+\-\/=?^_\`\|~]+@[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*)|([a-zA-Z\d.!#$%&'*+\-\/=?^_\`\|~]{1,128}))(?:,((?:[a-zA-Z0-9.!#$%&'*+\-\/=?^_\`\|~]+@[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*)|([a-zA-Z\d.!#$%&'*+\-\/=?^_\`\|~]{1,128})))*$`,
|
|
https: String.raw`^https?:\/\/.+$`,
|
|
token: String.raw`^[0-9a-f]{32}`,
|
|
score: String.raw`^[0-9]{1,5}`,
|
|
telegram_channel: String.raw`^[0-9\-]{1,15}`,
|
|
password: String.raw`^[\w\/\$\!\(\)\=\?\^\*\@_\-]{5,31}$`,
|
|
tls_certificate: String.raw`^[^=,]+=[^=,]+(,\s[^=,]+=[^=,]+)*$`,
|
|
domain_name_not_strict: String.raw`^[a-zA-Z0-9\-_~]+((\.[a-zA-Z0-9\-_~]+)+)$`,
|
|
non_quoted_text: String.raw`^[a-zA-Z0-9.-_]+$`,
|
|
};
|
|
|
|
export default class NtopUtils {
|
|
|
|
/* Show an overlay to hide loading */
|
|
static toggleOverlays(time = 500) {
|
|
$(`.overlay`).toggle(time);
|
|
}
|
|
|
|
static showOverlays(time = 500) {
|
|
$(`.overlay`).fadeIn(time);
|
|
}
|
|
|
|
static hideOverlays(time = 500) {
|
|
$(`.overlay`).fadeOut(time);
|
|
}
|
|
|
|
static get REGEXES() {
|
|
return REGEXES;
|
|
}
|
|
|
|
static getIPv4RegexWithCIDR() {
|
|
const length = REGEXES.ipv4.length;
|
|
return `${REGEXES.ipv4.substring(0, length - 1)}(\\/?)(\\b([0-9]|[12][0-9]|3[0-2])?\\b)$`;
|
|
}
|
|
|
|
static getIPv6RegexWithCIDR() {
|
|
const length = REGEXES.ipv6.length;
|
|
return `${REGEXES.ipv6.substring(0, length - 1)}(\\/?)\\b([0-9]|[1-9][0-9]|1[01][0-9]|12[0-8])?\\b$`;
|
|
}
|
|
|
|
/**
|
|
* Resolve a hostname by doing a DNS Resolve.
|
|
* @param {string} hostname The hostname to resolve
|
|
*/
|
|
static async resolveDNS(hostname = "ntop.org") {
|
|
|
|
// resolve the hostname by doing a fetch request to the backend
|
|
try {
|
|
const request = await fetch(`${http_prefix}/lua/rest/v2/get/dns/resolve.lua?hostname=${hostname}`);
|
|
const response = await request.json();
|
|
return response;
|
|
}
|
|
catch (err) {
|
|
// prints out the error if the request fails
|
|
console.error(`Something went wrong when resolving hostname: ${err}`)
|
|
}
|
|
|
|
// if the request has failed return a placeholder response
|
|
// indicating the failure
|
|
return { rc: -1, rc_str: "FAILED_HTTP_REQUEST" };
|
|
}
|
|
|
|
/**
|
|
* Replace the inputs which contain the [data-pattern] attribute
|
|
* with the [pattern] attribute.
|
|
*/
|
|
static initDataPatterns() {
|
|
// for each input with the data-pattern attribute
|
|
// substitute the data-pattern with the right regexes
|
|
$(`input[data-pattern]`).each(function () {
|
|
|
|
// if the pattern is empty then print a warn inside the console
|
|
const dataPattern = $(this).data('pattern');
|
|
if (!dataPattern) {
|
|
console.warn(`An empty data-pattern on an input was found!`, this);
|
|
return;
|
|
}
|
|
|
|
// build the regexp pattern for the input
|
|
const pattern = dataPattern.split('|').map(p => REGEXES[p].toString()).join('|');
|
|
// load the pattern
|
|
$(this).attr('pattern', pattern);
|
|
// remove the data-pattern from the input
|
|
$(this).removeAttr('data-pattern');
|
|
});
|
|
}
|
|
|
|
static is_good_ipv4(ipv4) {
|
|
return new RegExp(REGEXES.ipv4).test(ipv4);
|
|
}
|
|
|
|
static is_good_ipv6(ipv6) {
|
|
return new RegExp(REGEXES.ipv6).test(ipv6);
|
|
}
|
|
|
|
static is_mac_address(mac) {
|
|
return new RegExp(REGEXES.macAddress).test(mac);
|
|
}
|
|
|
|
static isNumeric(value) {
|
|
return /^\d+$/.test(value);
|
|
}
|
|
|
|
static is_network_mask(what, optional_mask) {
|
|
var elems = what.split("/");
|
|
var mask = null;
|
|
var ip_addr;
|
|
|
|
if (elems.length != 2) {
|
|
if (!optional_mask)
|
|
return null;
|
|
else
|
|
ip_addr = what;
|
|
} else {
|
|
ip_addr = elems[0];
|
|
|
|
if (!NtopUtils.isNumeric(elems[1]))
|
|
return null;
|
|
|
|
mask = parseInt(elems[1]);
|
|
|
|
if (mask < 0)
|
|
return null;
|
|
}
|
|
|
|
if (NtopUtils.is_good_ipv4(ip_addr)) {
|
|
if (mask === null)
|
|
mask = 32;
|
|
else if (mask > 32)
|
|
return null;
|
|
|
|
return {
|
|
type: "ipv4",
|
|
address: ip_addr,
|
|
mask: mask
|
|
};
|
|
} else if (NtopUtils.is_good_ipv6(elems[0])) {
|
|
if (mask === null)
|
|
mask = 128;
|
|
else if (mask > 128)
|
|
return (false);
|
|
|
|
return {
|
|
type: "ipv6",
|
|
address: ip_addr,
|
|
mask: mask
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static fbits(bits) {
|
|
const sizes = ['bps', 'Kbps', 'Mbps', 'Gbps', 'Tbps'];
|
|
|
|
if (typeof (bits) === "undefined")
|
|
return "-";
|
|
|
|
if (bits == 0) return '0';
|
|
if ((bits > 0) && (bits < NTOPNG_MIN_VISUAL_VALUE)) return ('< ' + NTOPNG_MIN_VISUAL_VALUE + ' bps');
|
|
var bits_log1000 = Math.log(bits) / Math.log(1000)
|
|
var i = parseInt(Math.floor(bits_log1000));
|
|
if (i < 0 || isNaN(i)) {
|
|
i = 0;
|
|
} else if (i >= sizes.length) { // prevents overflows
|
|
return "> " + sizes[sizes.length - 1]
|
|
}
|
|
|
|
if (i <= 1) {
|
|
return Math.round(bits / Math.pow(1000, i) * 100) / 100 + ' ' + sizes[i]
|
|
}
|
|
else {
|
|
var ret = parseFloat(bits / Math.pow(1000, i)).toFixed(2)
|
|
if (ret % 1 == 0)
|
|
ret = Math.round(ret)
|
|
return ret + ' ' + sizes[i]
|
|
}
|
|
}
|
|
|
|
static export_rate(eps) {
|
|
if (typeof (eps) === "undefined")
|
|
return "-";
|
|
|
|
var sizes = ['exp/s', 'Kexp/s'];
|
|
if (eps == 0) return '0';
|
|
if ((eps > 0) && (eps < NTOPNG_MIN_VISUAL_VALUE)) return ('< ' + NTOPNG_MIN_VISUAL_VALUE + ' exps/s');
|
|
var res = NtopUtils.scaleValue(eps, sizes, 1000);
|
|
|
|
// Round to two decimal digits
|
|
return Math.round(res[0] * 100) / 100 + ' ' + res[1];
|
|
}
|
|
|
|
static exports_format(exports) {
|
|
if (typeof (exports) === "undefined")
|
|
return "-";
|
|
|
|
var exports_label = i18n_ext.exports.toLowerCase();
|
|
|
|
var sizes = [exports_label, 'K ' + exports_label];
|
|
if (exports == 0) return '0';
|
|
if ((exports > 0) && (exports < NTOPNG_MIN_VISUAL_VALUE)) return ('< ' + NTOPNG_MIN_VISUAL_VALUE + ' exps/s');
|
|
var res = NtopUtils.scaleValue(exports, sizes, 1000);
|
|
|
|
// Round to two decimal digits
|
|
return Math.round(res[0] * 100) / 100 + ' ' + res[1];
|
|
}
|
|
|
|
static fbits_from_bytes(bytes) {
|
|
if (typeof (bytes) === "undefined")
|
|
return "-";
|
|
return (NtopUtils.fbits(bytes * 8));
|
|
}
|
|
|
|
static fpackets(pps) {
|
|
if (typeof (pps) === "undefined")
|
|
return "-";
|
|
|
|
var sizes = ['pps', 'Kpps', 'Mpps', 'Gpps', 'Tpps'];
|
|
if (pps == 0) return '0';
|
|
if ((pps > 0) && (pps < NTOPNG_MIN_VISUAL_VALUE)) return ('< ' + NTOPNG_MIN_VISUAL_VALUE + ' pps');
|
|
var res = NtopUtils.scaleValue(pps, sizes, 1000);
|
|
|
|
// Round to two decimal digits
|
|
return Math.round(res[0] * 100) / 100 + ' ' + res[1];
|
|
}
|
|
|
|
static fpoints(pps) {
|
|
if (typeof (pps) === "undefined")
|
|
return "-";
|
|
|
|
var sizes = ['pt/s', 'Kpt/s', 'Mpt/s', 'Gpt/s', 'Tpt/s'];
|
|
if (pps == 0) return '0';
|
|
if ((pps > 0) && (pps < NTOPNG_MIN_VISUAL_VALUE)) return ('< ' + NTOPNG_MIN_VISUAL_VALUE + ' pt/s');
|
|
var res = NtopUtils.scaleValue(pps, sizes, 1000);
|
|
|
|
// Round to two decimal digits
|
|
return Math.round(res[0] * 100) / 100 + ' ' + res[1];
|
|
}
|
|
|
|
static fflows(fps) {
|
|
if (typeof (fps) === "undefined")
|
|
return "-";
|
|
|
|
var sizes = ['fps', 'Kfps', 'Mfps', 'Gfps', 'Tfps'];
|
|
if (fps == 0) return '0 fps';
|
|
if ((fps > 0) && (fps < NTOPNG_MIN_VISUAL_VALUE)) return ('< ' + NTOPNG_MIN_VISUAL_VALUE + ' fps');
|
|
var res = NtopUtils.scaleValue(fps, sizes, 1000);
|
|
|
|
// Round to two decimal digits
|
|
return Math.round(res[0] * 100) / 100 + ' ' + res[1];
|
|
}
|
|
|
|
static fmsgs(mps) {
|
|
if (typeof (mps) === "undefined")
|
|
return "-";
|
|
|
|
var sizes = ['msg/s', 'Kmsg/s', 'Msg/s', 'Gmsg/s', 'Tmsg/s'];
|
|
if (mps == 0) return '0';
|
|
if ((mps > 0) && (mps < NTOPNG_MIN_VISUAL_VALUE)) return ('< ' + NTOPNG_MIN_VISUAL_VALUE + ' mps');
|
|
var res = NtopUtils.scaleValue(mps, sizes, 1000);
|
|
|
|
// Round to two decimal digits
|
|
return Math.round(res[0] * 100) / 100 + ' ' + res[1];
|
|
}
|
|
|
|
static fmillis(ms) {
|
|
|
|
if (ms === undefined) return '-';
|
|
const sizes = ['ms'];
|
|
const res = NtopUtils.scaleValue(ms, sizes, 1000);
|
|
return Math.round(res[0] * 100) / 100 + ' ' + res[1];
|
|
}
|
|
|
|
static fnone(val) {
|
|
|
|
if (val === undefined) return '-';
|
|
return Math.round(val * 100) / 100;
|
|
}
|
|
|
|
static falerts(aps) {
|
|
if (typeof (aps) === "undefined")
|
|
return "-";
|
|
|
|
// Round to two decimal digits
|
|
return Math.round(aps * 100) / 100 + ' alerts/s';
|
|
}
|
|
|
|
static fint(value) {
|
|
if (typeof (value) === "undefined")
|
|
return "-";
|
|
|
|
var x = Math.round(value);
|
|
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
}
|
|
|
|
static ffloat(value) {
|
|
if (typeof (value) === "undefined")
|
|
return "-";
|
|
|
|
var x = Math.round(value * 100) / 100.;
|
|
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
}
|
|
|
|
static fpercent(value) {
|
|
if (typeof (value) === "undefined")
|
|
return "-";
|
|
|
|
return Math.round(value * 100) / 100 + " %";
|
|
}
|
|
|
|
static percentage(value, total) {
|
|
if (total > 0) {
|
|
var pctg = Math.round((value * 10000) / total)
|
|
|
|
if (pctg > 0) {
|
|
/* Two decimals */
|
|
return (" [ " + (pctg / 100) + " % ] ")
|
|
}
|
|
}
|
|
|
|
return ("")
|
|
}
|
|
|
|
static fdate(when) {
|
|
var epoch = when * 1000;
|
|
var d = new Date(epoch);
|
|
|
|
return (d);
|
|
}
|
|
|
|
static capitaliseFirstLetter(string) {
|
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
}
|
|
|
|
static get_trend(actual, before) {
|
|
if ((actual === undefined) || (before === undefined) || (actual == before)) {
|
|
return ("<i class=\"fas fa-minus\"></i>");
|
|
} else if (actual > before) {
|
|
return ("<i class=\"fas fa-arrow-up\"></i>");
|
|
} else {
|
|
return ("<i class=\"fas fa-arrow-down\"></i>");
|
|
}
|
|
}
|
|
|
|
static abbreviateString(str, len) {
|
|
if (!str)
|
|
return "";
|
|
if (str.length < len)
|
|
return str;
|
|
return str.substring(0, len) + "...";
|
|
}
|
|
|
|
static toFixed2(num) {
|
|
if (!num) return "";
|
|
return num.toFixed(2);
|
|
}
|
|
|
|
// Convert bytes to human readable format
|
|
static bytesToSize(bytes) {
|
|
if (typeof (bytes) === "undefined")
|
|
return "-";
|
|
|
|
var precision = 2;
|
|
var kilobyte = 1024;
|
|
var megabyte = kilobyte * 1024;
|
|
var gigabyte = megabyte * 1024;
|
|
var terabyte = gigabyte * 1024;
|
|
|
|
if ((bytes >= 0) && (bytes < kilobyte))
|
|
if (bytes != 0)
|
|
return parseFloat(bytes.toFixed(precision)) + " Bytes";
|
|
else
|
|
return parseFloat(bytes) + " Bytes";
|
|
|
|
else if ((bytes >= kilobyte) && (bytes < megabyte))
|
|
return parseFloat((bytes / kilobyte).toFixed(precision)) + ' KB';
|
|
else if ((bytes >= megabyte) && (bytes < gigabyte))
|
|
return parseFloat((bytes / megabyte).toFixed(precision)) + ' MB';
|
|
else if ((bytes >= gigabyte) && (bytes < terabyte))
|
|
return parseFloat((bytes / gigabyte).toFixed(precision)) + ' GB';
|
|
else if (bytes >= terabyte)
|
|
return parseFloat((bytes / terabyte).toFixed(precision)) + ' TB';
|
|
else
|
|
return parseFloat(bytes.toFixed(precision)) + ' Bytes';
|
|
}
|
|
|
|
static drawTrend(current, last, withColor) {
|
|
if (current == last) {
|
|
return ("<i class=\"fas fa-minus\"></i>");
|
|
} else if (current > last) {
|
|
return ("<i class=\"fas fa-arrow-up\"" + withColor + "></i>");
|
|
} else {
|
|
return ("<i class=\"fas fa-arrow-down\"></i>");
|
|
}
|
|
}
|
|
|
|
static toggleAllTabs(enabled) {
|
|
if (enabled === true)
|
|
$("#historical-tabs-container").find("li").removeClass("disabled").find("a").attr("data-toggle", "tab");
|
|
else
|
|
$("#historical-tabs-container").find("li").addClass("disabled").find("a").removeAttr("data-toggle");
|
|
}
|
|
|
|
static disableAllDropdownsAndTabs() {
|
|
$("select").each(function () {
|
|
$(this).prop("disabled", true);
|
|
});
|
|
NtopUtils.toggleAllTabs(false)
|
|
}
|
|
|
|
static enableAllDropdownsAndTabs() {
|
|
$("select").each(function () {
|
|
$(this).prop("disabled", false);
|
|
});
|
|
NtopUtils.toggleAllTabs(true)
|
|
}
|
|
|
|
static capitalize(s) {
|
|
return s && s[0].toUpperCase() + s.slice(1);
|
|
}
|
|
|
|
static addCommas(nStr) {
|
|
nStr += '';
|
|
var x = nStr.split('.');
|
|
var x1 = x[0];
|
|
var x2 = x.length > 1 ? '.' + x[1] : '';
|
|
var rgx = /(\d+)(\d{3})/;
|
|
while (rgx.test(x1)) {
|
|
x1 = x1.replace(rgx, '$1' + ',' + '$2');
|
|
}
|
|
return x1 + x2;
|
|
}
|
|
|
|
static scaleValue(val, sizes, scale, decimals) {
|
|
if (val == 0) return [0, sizes[0]];
|
|
let factor = decimals ? (10 * decimals) : 10;
|
|
|
|
var i = parseInt(Math.floor(Math.log(val) / Math.log(scale)));
|
|
if (i < 0 || isNaN(i)) {
|
|
i = 0;
|
|
} else if (i >= sizes.length) {
|
|
i = sizes.length - 1;
|
|
}
|
|
|
|
return [Math.round((val / Math.pow(scale, i)) * factor) / factor, sizes[i]];
|
|
}
|
|
|
|
static formatValue(val, decimals) {
|
|
var sizes = ['', 'K', 'M', 'G', 'T'];
|
|
if (val == 0) return '0';
|
|
if ((val > 0) && (val < NTOPNG_MIN_VISUAL_VALUE)) return ('< ' + NTOPNG_MIN_VISUAL_VALUE);
|
|
if (decimals == undefined) decimals = 0;
|
|
var res = NtopUtils.scaleValue(val, sizes, 1000, decimals);
|
|
|
|
return res[0] + res[1];
|
|
}
|
|
|
|
static formatPackets(n) {
|
|
return (NtopUtils.addCommas(n.toFixed(0)) + " Pkts");
|
|
}
|
|
|
|
static bytesToVolume(bytes) {
|
|
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
if (bytes == 0) return '0 Bytes';
|
|
if ((bytes > 0) && (bytes < NTOPNG_MIN_VISUAL_VALUE)) return ('< ' + NTOPNG_MIN_VISUAL_VALUE + " Bytes");
|
|
var res = NtopUtils.scaleValue(bytes, sizes, 1024);
|
|
|
|
return parseFloat(res[0]) + " " + res[1];
|
|
};
|
|
|
|
static bytesToVolumeAndLabel(bytes) {
|
|
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
if (bytes == 0) return '0 Bytes';
|
|
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
|
return [(bytes / Math.pow(1024, i)).toFixed(2), sizes[i]];
|
|
};
|
|
|
|
static bitsToSize(bits, factor) {
|
|
factor = factor || 1000;
|
|
var sizes = ['bps', 'Kbps', 'Mbps', 'Gbps', 'Tbps'];
|
|
if (bits == 0) return '0 bps';
|
|
if ((bits > 0) && (bits < NTOPNG_MIN_VISUAL_VALUE)) return ('< ' + NTOPNG_MIN_VISUAL_VALUE + " bps");
|
|
var res = NtopUtils.scaleValue(bits, sizes, factor);
|
|
|
|
return res[0].toFixed(2) + " " + res[1];
|
|
};
|
|
|
|
static bitsToSize_no_comma(bits, factor) {
|
|
factor = factor || 1000;
|
|
var sizes = ['bps', 'Kbps', 'Mbps', 'Gbps', 'Tbps'];
|
|
if (bits == 0) return '0 bps';
|
|
if ((bits > 0) && (bits < NTOPNG_MIN_VISUAL_VALUE)) return ('< ' + NTOPNG_MIN_VISUAL_VALUE + " bps");
|
|
var res = NtopUtils.scaleValue(bits, sizes, factor);
|
|
|
|
return res[0] + " " + res[1];
|
|
};
|
|
|
|
static secondsToTime(seconds) {
|
|
|
|
if (seconds < 1) {
|
|
return ("< 1 sec")
|
|
}
|
|
|
|
let days = Math.floor(seconds / 86400)
|
|
let hours = Math.floor((seconds / 3600) - (days * 24))
|
|
let minutes = Math.floor((seconds / 60) - (days * 1440) - (hours * 60))
|
|
let sec = seconds % 60
|
|
let msg = "", msg_array = []
|
|
|
|
if (days > 0) {
|
|
let years = Math.floor(days / 365)
|
|
|
|
if (years > 0) {
|
|
days = days % 365
|
|
|
|
msg = years + " year"
|
|
if (years > 1) {
|
|
msg += "s"
|
|
}
|
|
|
|
msg_array.push(msg)
|
|
msg = ""
|
|
}
|
|
msg = days + " day"
|
|
if (days > 1) { msg += "s" }
|
|
msg_array.push(msg)
|
|
msg = ""
|
|
}
|
|
|
|
if (hours > 0) {
|
|
if (hours < 10) { msg = "0" }
|
|
msg += hours + ":";
|
|
}
|
|
|
|
if (minutes < 10) { msg += "0" }
|
|
msg += minutes + ":";
|
|
if (sec < 10) { msg += "0" }
|
|
msg += sec;
|
|
msg_array.push(msg)
|
|
|
|
return msg_array.join(", ")
|
|
}
|
|
|
|
static msecToTime(msec) {
|
|
if (msec >= 1000) {
|
|
return NtopUtils.secondsToTime(msec / 1000);
|
|
} else {
|
|
var x = Math.round(msec * 1000) / 1000.;
|
|
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + " ms";
|
|
}
|
|
}
|
|
|
|
static epoch2Seen(epoch) {
|
|
/* 08/01/13 15:12:37 [18 min, 13 sec ago] */
|
|
var d = new Date(epoch * 1000);
|
|
var tdiff = Math.floor(((new Date()).getTime() / 1000) - epoch);
|
|
|
|
return (d.format("dd/MM/yyyy hh:mm:ss") + " [" + NtopUtils.secondsToTime(tdiff) + " ago]");
|
|
}
|
|
|
|
/* ticks for graph x axis */
|
|
static graphGetXAxisTicksFormat(diff_epoch) {
|
|
var tickFormat;
|
|
|
|
if (diff_epoch <= 86400) {
|
|
tickFormat = "%H:%M:%S";
|
|
} else if (diff_epoch <= 2 * 86400) {
|
|
tickFormat = "%b %e, %H:%M:%S";
|
|
} else {
|
|
tickFormat = "%b %e";
|
|
}
|
|
|
|
return (tickFormat);
|
|
}
|
|
|
|
static paramsExtend(defaults, override) {
|
|
return $.extend({}, defaults, override);
|
|
}
|
|
|
|
static paramsToForm(form, params) {
|
|
form = $(form);
|
|
|
|
for (var k in params) {
|
|
if (params.hasOwnProperty(k)) {
|
|
var input = $('<input type="hidden" name="' + k + '" value="' + params[k] + '">');
|
|
input.appendTo(form);
|
|
}
|
|
}
|
|
|
|
return form;
|
|
}
|
|
|
|
/*
|
|
* This function creates a javascript object where each k->v pair of the input object
|
|
* translates into two pairs in the output object: a key_[i]->k and a val_[i]->v, where
|
|
* i is an incremental index.
|
|
*
|
|
* The output object can then be serialized to an URL. This conversion is required for
|
|
* handling special characters: since ntopng strips special characters in _GET keys,
|
|
* _GET values must be used.
|
|
*
|
|
* This function performs the inverse conversion of lua paramsPairsDecode.
|
|
*
|
|
*/
|
|
static paramsPairsEncode(params) {
|
|
var i = 0;
|
|
var res = {};
|
|
|
|
for (var k in params) {
|
|
res["key_" + i] = k;
|
|
res["val_" + i] = params[k];
|
|
i = i + 1;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
static hostkey2hostInfo(host_key) {
|
|
var info;
|
|
|
|
host_key = host_key.replace(/____/g, ":");
|
|
host_key = host_key.replace(/___/g, "/");
|
|
host_key = host_key.replace(/__/g, ".");
|
|
|
|
info = host_key.split("@");
|
|
return (info);
|
|
}
|
|
|
|
static handle_tab_state(nav_object, default_tab) {
|
|
$('a', nav_object).click(function (e) {
|
|
e.preventDefault();
|
|
});
|
|
|
|
// store the currently selected tab in the hash value
|
|
$(" > li > a", nav_object).on("shown.bs.tab", function (e) {
|
|
var id = $(e.target).attr("href").substr(1);
|
|
if (history.replaceState) {
|
|
// this will prevent the 'jump' to the hash
|
|
history.replaceState(null, null, "#" + id);
|
|
} else {
|
|
// fallback
|
|
window.location.hash = id;
|
|
}
|
|
});
|
|
|
|
// on load of the page: switch to the currently selected tab
|
|
var hash = window.location.hash;
|
|
if (!hash) hash = "#" + default_tab;
|
|
$('a[href="' + hash + '"]', nav_object).tab('show');
|
|
}
|
|
|
|
static tstampToDateString(html_tag, format, tdiff) {
|
|
tdiff = tdiff || 0;
|
|
var timestamp = parseInt(html_tag.html()) + tdiff;
|
|
var localized = d3.time.format(format)(new Date(timestamp * 1000));
|
|
html_tag.html(localized).removeClass("hidden");
|
|
return localized;
|
|
}
|
|
|
|
static noHtml(s) {
|
|
if (typeof s !== 'string') return '';
|
|
const div = document.createElement('div');
|
|
div.innerHTML = s; // The browser parse only the HTML
|
|
return div.textContent || ''; // return the text, no tags
|
|
}
|
|
|
|
static cleanCustomHostUrl(host) {
|
|
/* Remove starting http(s). */
|
|
return host
|
|
.replace(/^http:\/\//gi, '')
|
|
.replace(/^https:\/\//gi, '')
|
|
/* Remove starting www. */
|
|
.replace(/^www\./gi, '')
|
|
/* Remove non-allowed characters */
|
|
.replace(/[^0-9a-zA-Z\.:\/_-]/gi, '');
|
|
}
|
|
|
|
static parseQuery(queryString) {
|
|
const params = new URLSearchParams(
|
|
queryString[0] === '?' ? queryString.slice(1) : queryString
|
|
);
|
|
const query = Object.create(null);
|
|
for (const [key, val] of params.entries()) {
|
|
query[key] = val;
|
|
}
|
|
return query;
|
|
}
|
|
|
|
static getHistoryParameters(params) {
|
|
var cur_params = NtopUtils.parseQuery(window.location.search);
|
|
var new_params = $.extend(cur_params, params);
|
|
var new_query = "?" + $.param(new_params, true);
|
|
var baseUrl = [location.protocol, '//', location.host, location.pathname].join('');
|
|
|
|
return baseUrl + new_query;
|
|
}
|
|
|
|
// return true if the status code is different from 200
|
|
static check_status_code(status_code, status_text, $error_label) {
|
|
|
|
const is_different = status_code != 200;
|
|
|
|
if (is_different && $error_label != null) {
|
|
|
|
let message = i18n_ext.request_failed_message;
|
|
if (status_code && status_text) {
|
|
message += `: ${status_code} - ${status_text}`;
|
|
}
|
|
|
|
$error_label.text(message).show();
|
|
}
|
|
else if (is_different && $error_label == null) {
|
|
alert(`${i18n_ext.request_failed_message}: ${status_code} - ${status_text}`);
|
|
}
|
|
|
|
return is_different;
|
|
}
|
|
|
|
// To be used in conjunction with httpdocs/templates/config_list_components/import_modal.html
|
|
static importModalHelper(params) {
|
|
|
|
if (!params.loadConfigXHR) { throw ("importModalHelper:: Missing 'loadConfigXHR' param"); }
|
|
|
|
$(`input#import-input`).on('change', function () {
|
|
$(`#btn-confirm-import`).removeAttr("disabled");
|
|
});
|
|
|
|
$(`#import-modal`).on('hidden.bs.modal', function () {
|
|
$(`#import-input`).val('');
|
|
$("#import-error").hide().removeClass('text-warning').addClass('invalid-feedback');
|
|
$(`#btn-confirm-import`).attr("disabled", "disabled");
|
|
});
|
|
|
|
$("#import-modal").on("submit", "form", function (e) {
|
|
|
|
e.preventDefault();
|
|
|
|
const $button = $('#btn-confirm-import');
|
|
$button.attr("disabled", "");
|
|
|
|
// read configuration file
|
|
const file = $('#import-input')[0].files[0];
|
|
|
|
if (!file) {
|
|
$("#import-error").text(`${i18n_ext.no_file}`).show();
|
|
$button.removeAttr("disabled");
|
|
return;
|
|
}
|
|
|
|
const reader = new FileReader();
|
|
reader.readAsText(file, "UTF-8");
|
|
|
|
reader.onload = function (responseJSON) {
|
|
const fileName = file.name || '';
|
|
const isCsv = fileName.toLowerCase().endsWith('.csv');
|
|
|
|
if (!isCsv) {
|
|
// Client-side configuration file format check
|
|
let jsonConfiguration = null;
|
|
try {
|
|
jsonConfiguration = JSON.parse(reader.result);
|
|
} catch (e) {
|
|
|
|
ToastUtils.showToast({
|
|
id: 'import-error-toast',
|
|
level: 'error',
|
|
title: i18n("error"),
|
|
body: e,
|
|
delay: 3000
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!jsonConfiguration) {
|
|
$("#import-error").text(i18n_ext.rest_consts[responseJSON.rc_str] || 'Not Implemented Yet').show();
|
|
$button.removeAttr("disabled");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Submit configuration file
|
|
params.loadConfigXHR(reader.result, isCsv)
|
|
.done((response, status, xhr) => {
|
|
if (response.rc < 0) {
|
|
$("#import-error").text(response.rc_str).show();
|
|
return;
|
|
}
|
|
|
|
// if the operation was successful call the successCallback
|
|
if (params.successCallback) {
|
|
params.successCallback(response);
|
|
}
|
|
|
|
const key = $(`input[name='configuration']:checked`).val();
|
|
|
|
const body = (key == 'all')
|
|
? i18n("manage_configurations.messages.import_all_success")
|
|
: i18n("manage_configurations.messages.import_success");
|
|
|
|
// show a success alert message
|
|
ToastUtils.showToast({
|
|
id: 'import-configuration-alert',
|
|
level: 'success',
|
|
title: i18n("success"),
|
|
body: body,
|
|
delay: 2000
|
|
});
|
|
|
|
$("#import-modal").modal('hide');
|
|
|
|
})
|
|
.fail(({ responseJSON }) => {
|
|
const PARTIAL_IMPORT_RC = -28;
|
|
|
|
if (params.failureCallback) {
|
|
params.failureCallback(responseJSON);
|
|
}
|
|
|
|
if (responseJSON && responseJSON.rc > 0) return;
|
|
if (responseJSON.rc == PARTIAL_IMPORT_RC) {
|
|
$(`#import-error`).removeClass('invalid-feedback').addClass('text-warning');
|
|
}
|
|
|
|
$("#import-error").text(responseJSON.rc_str_hr || i18n_ext.rest_consts[responseJSON.rc_str] || i18n_ext.FAILED_HTTP_REQUEST).show();
|
|
|
|
})
|
|
.always(() => {
|
|
$button.removeAttr("disabled");
|
|
});
|
|
};
|
|
});
|
|
}
|
|
|
|
static serializeFormArray(serializedArray) {
|
|
const serialized = {};
|
|
serializedArray.forEach((obj) => {
|
|
/* if the object is an array */
|
|
if (obj.name.includes('[]')) {
|
|
return;
|
|
}
|
|
else {
|
|
|
|
// clean the string
|
|
if (typeof obj.value === "string") {
|
|
obj.value = obj.value.trim();
|
|
}
|
|
serialized[obj.name] = obj.value;
|
|
}
|
|
});
|
|
return serialized;
|
|
}
|
|
|
|
static cleanForm(formSelector) {
|
|
/* remove validation fields and tracks */
|
|
$(formSelector).find('input,select,textarea').each(function (i, input) {
|
|
$(this).removeClass(`is-valid`).removeClass(`is-invalid`);
|
|
});
|
|
/* reset all the values */
|
|
$(formSelector)[0].reset();
|
|
}
|
|
|
|
/**
|
|
* Make a fetch call with a timeout option
|
|
*/
|
|
static fetchWithTimeout(uri, options = {}, time = 5000) {
|
|
|
|
const controller = new AbortController()
|
|
const config = { ...options, signal: controller.signal }
|
|
|
|
return fetch(uri, config)
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
throw new Error(`${response.status}: ${response.statusText}`)
|
|
}
|
|
return response
|
|
})
|
|
.catch((error) => {
|
|
if (error.name === 'AbortError') {
|
|
throw new Error('Response timed out')
|
|
}
|
|
})
|
|
}
|
|
|
|
static setPref(action, csrf, success, failure) {
|
|
|
|
if (action == undefined) {
|
|
console.warn("An action key must be defined to set a preference!");
|
|
return;
|
|
}
|
|
|
|
const empty = () => { };
|
|
const request = $.post(`${http_prefix}/lua/update_prefs.lua`, { action: action, csrf: csrf });
|
|
request.done(success || empty);
|
|
request.fail(failure || empty);
|
|
}
|
|
|
|
/**
|
|
* Glue strings contained in array separated by a comma.
|
|
* @param {array} array The array of strings. I.e. ["Hello", "World"]
|
|
* @param {number} limit How many words the string contains
|
|
*
|
|
* @return {string} A string built by array's elements. i.e: "Hello, World"
|
|
*/
|
|
static arrayToListString(array, limit) {
|
|
|
|
if (array == undefined) return "";
|
|
|
|
if (array.length > limit) {
|
|
return array.slice(0, limit).join(", ") + ` ${i18n_ext.and_x_more.replace('$num', array.length - limit)}`;
|
|
}
|
|
|
|
return array.slice(0, limit).join(", ");
|
|
}
|
|
|
|
static buildURL(location, params = {}, hasReferer = false, refererParams = {}) {
|
|
|
|
const url = new URL(location, window.location);
|
|
|
|
for (const [name, value] of Object.entries(params)) {
|
|
if (value || value === 0)
|
|
url.searchParams.set(name, value);
|
|
continue;
|
|
}
|
|
|
|
if (hasReferer) {
|
|
|
|
const refUrl = new URL(window.location.href);
|
|
for (const [name, value] of Object.entries(refererParams)) {
|
|
if (!value) continue;
|
|
refUrl.searchParams.set(name, value);
|
|
}
|
|
|
|
url.searchParams.set('referer', refUrl.toString());
|
|
}
|
|
|
|
return url.toString();
|
|
}
|
|
|
|
static getEditPoolLink(href, poolId) {
|
|
const url = new URL(href, window.location);
|
|
url.searchParams.set('pool_id', poolId);
|
|
return url.toString();
|
|
}
|
|
|
|
static getPoolLink(poolType, poolId = 0) {
|
|
return `${http_prefix}/lua/rest/v2/get/${poolType}/pools.lua?pool=${poolId}`;
|
|
}
|
|
|
|
static async getPool(poolType, id = 0) {
|
|
|
|
if (poolType === null) throw 'A pool type must be defined!';
|
|
|
|
try {
|
|
|
|
const request = await fetch(NtopUtils.getPoolLink(poolType, id));
|
|
const pool = await request.json();
|
|
|
|
if (pool.rc < 0) {
|
|
return [false, {}];
|
|
}
|
|
|
|
return [true, pool.rsp[0]];
|
|
}
|
|
catch (err) {
|
|
return [false, {}];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save the scale of element inside the local storage
|
|
* @param {object} $element
|
|
* @param {object} scale
|
|
*/
|
|
static saveElementScale($element, scale = { width: 0, height: 0 }) {
|
|
|
|
const key = NtopUtils.generateScaleElementKey($element);
|
|
localStorage.setItem(key, JSON.stringify(scale));
|
|
}
|
|
|
|
static generateScaleElementKey($element) {
|
|
let identificator;
|
|
const page = location.pathname;
|
|
const elementId = $element.attr('id');
|
|
|
|
if (elementId !== "") {
|
|
identificator = elementId;
|
|
}
|
|
else {
|
|
const className = $element.attr('class');
|
|
identificator = className;
|
|
}
|
|
|
|
const key = `${identificator}-${page}-scale`;
|
|
return key;
|
|
}
|
|
|
|
/**
|
|
* Load the old scale value ofx element from the local storage
|
|
* @param {object} $element
|
|
*/
|
|
static loadElementScale($element) {
|
|
|
|
const key = NtopUtils.generateScaleElementKey($element);
|
|
const currentValue = localStorage.getItem(key);
|
|
if (currentValue == null) return undefined;
|
|
|
|
return JSON.parse(currentValue);
|
|
}
|
|
|
|
static fillFieldIfValid($field, value) {
|
|
|
|
if (value === undefined) {
|
|
$field.val('');
|
|
}
|
|
else {
|
|
$field.val(value);
|
|
}
|
|
|
|
}
|
|
|
|
static copyToClipboard(text, item) {
|
|
const el = document.createElement('textarea');
|
|
el.value = text;
|
|
el.setAttribute('readonly', '');
|
|
el.style.position = 'absolute';
|
|
el.style.left = '-9999px';
|
|
document.body.appendChild(el);
|
|
el.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(el);
|
|
$(item).attr("title", "Copied!").tooltip("dispose").tooltip().tooltip("show");
|
|
$(item).removeAttr("data-bs-original-title")
|
|
$(item).attr("title", text)
|
|
}
|
|
|
|
static stripTags(html) {
|
|
let t = document.createElement("div");
|
|
t.innerHTML = html;
|
|
return t.textContent || t.innerText || "";
|
|
}
|
|
|
|
static shortenLabel(label, len, last_char) {
|
|
let shortened_label = label
|
|
if (label.length > len + 5) {
|
|
if (last_char) {
|
|
let last_index = label.lastIndexOf(last_char)
|
|
const requested_label = label.slice(last_index)
|
|
if (len > last_index)
|
|
len = last_index
|
|
shortened_label = label.slice(0, len) + "... " + requested_label
|
|
} else {
|
|
shortened_label = label.slice(0, len) + "...";
|
|
}
|
|
}
|
|
|
|
return shortened_label
|
|
}
|
|
|
|
static sortAlphabetically(a, b) {
|
|
const nameA = a.label?.toUpperCase(); // ignore upper and lowercase
|
|
const nameB = b.label?.toUpperCase(); // ignore upper and lowercase
|
|
if (nameA < nameB) { return -1; }
|
|
if (nameA > nameB) { return 1; }
|
|
return 0;
|
|
}
|
|
|
|
/* This function, given a name and a value, return a string
|
|
* formatted in the following way:
|
|
* name [value]
|
|
* If max_name_len is different from 0, then it's going to cut the name string
|
|
* to max_name_len
|
|
*/
|
|
static formatNameValue(name, value, max_name_len) {
|
|
let label = name;
|
|
if (name != value) {
|
|
if (max_name_len && typeof (max_name_len) == 'number')
|
|
label = this.shortenLabel(label, max_name_len, '.');
|
|
|
|
label = `${label} [${value}]`
|
|
}
|
|
return label
|
|
}
|
|
|
|
/* This function, remove from a string the VLAN 0
|
|
* name@0 -> name
|
|
*/
|
|
static removeVlan(name) {
|
|
let label = name
|
|
const vlan_index = label.lastIndexOf('@');
|
|
if (vlan_index != -1) {
|
|
const vlan = label.slice(vlan_index + 1);
|
|
if (vlan == 0) {
|
|
label = label.slice(0, vlan_index);
|
|
}
|
|
}
|
|
|
|
return label
|
|
}
|
|
|
|
/* Format an object with label and value from a column row */
|
|
static formatGenericObj(obj, row) {
|
|
let label = obj.label ? obj.label : obj.value;
|
|
return label;
|
|
}
|
|
|
|
/* Format a country from a column object */
|
|
static formatCountry(obj, row) {
|
|
let country_code = obj.value;
|
|
let label = obj.label ? obj.label : obj.value;
|
|
return `${label} <img src="/dist/images/blank.gif" class="flag flag-${country_code.toLowerCase()}">`;
|
|
}
|
|
|
|
/* Format an host from a column object */
|
|
static formatHost(obj, row, is_client) {
|
|
let label = "";
|
|
|
|
if (!obj) {
|
|
return label;
|
|
}
|
|
|
|
/* Link */
|
|
let host_key = obj.ip;
|
|
if (row.vlan_id && row.vlan_id.value)
|
|
host_key = host_key + '@' + row.vlan_id.value;
|
|
|
|
/* Label */
|
|
label = obj.label ? obj.label : obj.value;
|
|
if (row.vlan_id && row.vlan_id.label)
|
|
label += `@${row.vlan_id.label}`;
|
|
|
|
const url = NtopUtils.buildURL(`${http_prefix}/lua/host_details.lua`, { host: host_key });
|
|
label = `<a href="${url}">${label}</a>`;
|
|
|
|
/* Country */
|
|
let country_obj = is_client ? row.cli_country : row.srv_country;
|
|
if (!country_obj && row.country) country_obj = row.country;
|
|
if (country_obj && country_obj.value)
|
|
label += ` <img src="${http_prefix}/dist/images/blank.gif" class="flag flag-${country_obj.value.toLowerCase()}" title="${country_obj.title}"></a>`;
|
|
|
|
return label;
|
|
}
|
|
|
|
/* Format a network from a column object */
|
|
static formatNetwork(obj, row) {
|
|
let label = "";
|
|
|
|
if (!obj) {
|
|
return label;
|
|
}
|
|
|
|
/* Link */
|
|
let network_key = obj.value;
|
|
|
|
/* Label */
|
|
label = obj.label ? obj.label : obj.value;
|
|
if (row.vlan_id && row.vlan_id.label)
|
|
label += `@${row.vlan_id.label}`;
|
|
|
|
const url = NtopUtils.buildURL(`${http_prefix}/lua/hosts_stats.lua`, { network: network_key });
|
|
label = `<a href="${url}">${label}</a>`;
|
|
|
|
return label;
|
|
}
|
|
|
|
/* This function converts a mac address to a string*/
|
|
static convertMACAddress(a) {
|
|
return a.toLowerCase().replace(/[^a-f0-9]/g, '');
|
|
}
|
|
/* This function converts an ip to a number equale to the ip but without . or :: in case of ipv6
|
|
* this is needed in case of ordering
|
|
*/
|
|
static convertIPAddress(a) {
|
|
var i, item;
|
|
var m, n, t;
|
|
var x, xa;
|
|
|
|
if (!a) {
|
|
return 0;
|
|
}
|
|
|
|
a = a.replace(/<[\s\S]*?>/g, "");
|
|
//IPv4:Port
|
|
t = a.split(":");
|
|
if (t.length == 2) {
|
|
m = t[0].split(".");
|
|
}
|
|
else {
|
|
m = a.split(".");
|
|
}
|
|
n = a.split(":");
|
|
x = "";
|
|
xa = "";
|
|
|
|
if (m.length == 4) {
|
|
// IPV4
|
|
for (i = 0; i < m.length; i++) {
|
|
item = m[i];
|
|
|
|
if (item.length == 1) {
|
|
x += "00" + item;
|
|
}
|
|
else if (item.length == 2) {
|
|
x += "0" + item;
|
|
}
|
|
else {
|
|
x += item;
|
|
}
|
|
}
|
|
}
|
|
else if (n.length > 0) {
|
|
// IPV6
|
|
var count = 0;
|
|
for (i = 0; i < n.length; i++) {
|
|
item = n[i];
|
|
|
|
if (i > 0) {
|
|
xa += ":";
|
|
}
|
|
|
|
if (item.length === 0) {
|
|
count += 0;
|
|
}
|
|
else if (item.length == 1) {
|
|
xa += "000" + item;
|
|
count += 4;
|
|
}
|
|
else if (item.length == 2) {
|
|
xa += "00" + item;
|
|
count += 4;
|
|
}
|
|
else if (item.length == 3) {
|
|
xa += "0" + item;
|
|
count += 4;
|
|
}
|
|
else {
|
|
xa += item;
|
|
count += 4;
|
|
}
|
|
}
|
|
|
|
// Padding the ::
|
|
n = xa.split(":");
|
|
var paddDone = 0;
|
|
|
|
for (i = 0; i < n.length; i++) {
|
|
item = n[i];
|
|
|
|
if (item.length === 0 && paddDone === 0) {
|
|
for (var padding = 0; padding < (32 - count); padding++) {
|
|
x += "0";
|
|
paddDone = 1;
|
|
}
|
|
}
|
|
else {
|
|
x += item;
|
|
}
|
|
}
|
|
}
|
|
|
|
return x;
|
|
}
|
|
|
|
/* Format an AS from a column object */
|
|
static formatASN(obj, row) {
|
|
let label = "";
|
|
|
|
if (!obj) {
|
|
return label;
|
|
}
|
|
|
|
/* Link */
|
|
let asn_key = obj.value;
|
|
|
|
/* Label */
|
|
label = obj.label ? obj.label : obj.value;
|
|
|
|
const url = NtopUtils.buildURL(`${http_prefix}/lua/hosts_stats.lua`, { asn: asn_key });
|
|
label = `<a href="${url}">${label}</a>`;
|
|
|
|
return label;
|
|
}
|
|
|
|
static createProgressBar(percentage) {
|
|
const pct = Math.min(100, Math.max(0, Math.floor(percentage)));
|
|
const color = 'var(--ntop-orange, #FF8F00)';
|
|
return `<div class="d-flex align-items-center gap-2" style="min-width:0">
|
|
<div style="flex:1;height: 8px;background:var(--border-color,#dee2e6);border-radius:100px;overflow:hidden;">
|
|
<div style="width:${pct}%;height:100%;background:${color};border-radius:100px;transition:width .3s ease;"></div>
|
|
</div>
|
|
<span style="font-size:0.75rem;font-weight:600;color:var(--ntop-muted-text-color,#37474F);white-space:nowrap;min-width:2.5rem;text-align:right;">${pct}%</span>
|
|
</div>`;
|
|
}
|
|
|
|
/**
|
|
* Rounds an array of raw percentage values to integers that always sum to exactly 100,
|
|
* using the Largest Remainder Method: each value is floored first, then the missing
|
|
* points are awarded one by one to the entries with the largest fractional part.
|
|
*
|
|
* @param {number[]} values - Raw percentage values
|
|
* @returns {number[]} Integer percentages in the same order, summing to exactly 100
|
|
*/
|
|
static largestRemainderRound(values) {
|
|
const floored = values.map(Math.floor);
|
|
// How many integer points are still missing to reach exactly 100
|
|
const remainder = 100 - floored.reduce((a, b) => a + b, 0);
|
|
// pair each value with its fractional part
|
|
values.map((v, i) => ({ i, f: v - floored[i] }))
|
|
// sort descending by fractional part
|
|
.sort((a, b) => b.f - a.f)
|
|
// take only as many as needed
|
|
.slice(0, remainder)
|
|
// bump each winner by 1
|
|
.forEach(({ i }) => floored[i]++);
|
|
return floored;
|
|
}
|
|
|
|
static createBreakdown(percentage_1, percentage_2, label_1, label_2) {
|
|
if (percentage_1 == 0 && percentage_2 == 0) {
|
|
return `<div style="height:8px;background:var(--border-subtle,#e9ecef);border-radius:100px;"
|
|
data-bs-toggle="tooltip" data-bs-placement="top" title="No data available"></div>`;
|
|
}
|
|
|
|
let bars = '';
|
|
if (percentage_1 > 0) {
|
|
bars += `<div style="width:${percentage_1}%;height:100%;background:var(--ntop-orange,#FF8F00);border-radius:${percentage_2 > 0 ? '100px 0 0 100px' : '100px'};transition:width .3s ease;"
|
|
data-bs-toggle="tooltip" data-bs-placement="top" title="${label_1}: ${Math.floor(percentage_1 + 0.5)}%"></div>`;
|
|
}
|
|
if (percentage_2 > 0) {
|
|
bars += `<div style="width:${percentage_2}%;height:100%;background:#0d9488;border-radius:${percentage_1 > 0 ? '0 100px 100px 0' : '100px'};transition:width .3s ease;"
|
|
data-bs-toggle="tooltip" data-bs-placement="top" title="${label_2}: ${Math.floor(percentage_2 + 0.5)}%"></div>`;
|
|
}
|
|
|
|
const legend_1 = percentage_1 > 0
|
|
? `<span style="display:inline-flex;align-items:center;gap:3px;font-size:0.7rem;color:var(--ntop-muted-text-color,#37474F);">
|
|
<span style="width:6px;height:6px;border-radius:50%;background:var(--ntop-orange,#FF8F00);flex-shrink:0;"></span>${label_1} ${Math.floor(percentage_1 + 0.5)}%</span>` : '';
|
|
const legend_2 = percentage_2 > 0
|
|
? `<span style="display:inline-flex;align-items:center;gap:3px;font-size:0.7rem;color:var(--ntop-muted-text-color,#37474F);">
|
|
<span style="width:6px;height:6px;border-radius:50%;background:#0d9488;flex-shrink:0;"></span>${label_2} ${Math.floor(percentage_2 + 0.5)}%</span>` : '';
|
|
|
|
return `<div style="display:flex;flex-direction:column;gap:3px;min-width:0;">
|
|
<div style="height:8px;background:var(--border-color,#dee2e6);border-radius:100px;overflow:hidden;display:flex;">${bars}</div>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap;">${legend_1}${legend_2}</div>
|
|
</div>`;
|
|
}
|
|
|
|
static createBreakdown_multi_elem(percentages, labels, colors = null) {
|
|
const defaultColors = [
|
|
'var(--ntop-orange, #FF8F00)',
|
|
'#0d9488',
|
|
'#3b82f6',
|
|
'#a855f7',
|
|
'#ef4444',
|
|
'#f59e0b',
|
|
];
|
|
|
|
// Normalize inputs: filter out zero-percentage entries
|
|
const items = percentages
|
|
.map((pct, i) => ({
|
|
pct,
|
|
label: labels[i],
|
|
color: (colors?.[i]) ?? defaultColors[i % defaultColors.length],
|
|
}))
|
|
.filter(item => item.pct > 0);
|
|
|
|
if (items.length === 0) {
|
|
return `<div style="height:8px;background:var(--border-subtle,#e9ecef);border-radius:100px;"
|
|
data-bs-toggle="tooltip" data-bs-placement="top" title="No data available"></div>`;
|
|
}
|
|
|
|
const bars = items.map((item, i) => {
|
|
const isFirst = i === 0;
|
|
const isLast = i === items.length - 1;
|
|
const radius = isFirst && isLast ? '100px'
|
|
: isFirst ? '100px 0 0 100px'
|
|
: isLast ? '0 100px 100px 0'
|
|
: '0';
|
|
|
|
return `<div style="width:${item.pct}%;height:100%;background:${item.color};border-radius:${radius};transition:width .3s ease;"
|
|
data-bs-toggle="tooltip" data-bs-placement="top" title="${item.label}: ${Math.floor(item.pct)}%"></div>`;
|
|
}).join('');
|
|
|
|
const legend = items.map(item =>
|
|
`<span style="display:inline-flex;align-items:center;gap:3px;font-size:0.7rem;color:var(--ntop-muted-text-color,#37474F);">
|
|
<span style="width:6px;height:6px;border-radius:50%;background:${item.color};flex-shrink:0;"></span>${item.label} ${Math.floor(item.pct)}%</span>`
|
|
).join('');
|
|
|
|
return `<div style="display:flex;flex-direction:column;gap:3px;min-width:0;">
|
|
<div style="height:8px;background:var(--border-color,#dee2e6);border-radius:100px;overflow:hidden;display:flex;">${bars}</div>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap;">${legend}</div>
|
|
</div>`;
|
|
}
|
|
|
|
/* Return the number of rows available in a table */
|
|
static getNumTableRows() {
|
|
return [10, 20, 50, 100];
|
|
}
|
|
|
|
static loadBSTooltips() {
|
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
|
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
|
})
|
|
}
|
|
|
|
static disposeBSTooltips() {
|
|
document.querySelectorAll('.tooltip').forEach(tooltip => {
|
|
const tooltipInstance = bootstrap.Tooltip.getInstance(tooltip);
|
|
if (tooltipInstance) {
|
|
tooltipInstance.dispose();
|
|
}
|
|
tooltip.remove();
|
|
});
|
|
}
|
|
|
|
static reloadBSTooltips() {
|
|
this.disposeBSTooltips();
|
|
this.loadBSTooltips();
|
|
}
|
|
}
|
|
|
|
$(function () {
|
|
// if there are inputs with 'pattern' data attribute
|
|
// then initialize them
|
|
NtopUtils.initDataPatterns();
|
|
});
|
|
|