Ported menu, footer and topbar to vuejs (#10480)
Some checks are pending
Build / build (push) Waiting to run
CIFuzz / Fuzzing (address) (push) Waiting to run
CodeQL / Analyze (push) Waiting to run
CodeQL / Analyze-1 (push) Waiting to run

* Ported menu, footer and topbar to vuejs

* Update dist
This commit is contained in:
GabrieleDeri 2026-06-22 18:19:15 +02:00 committed by GitHub
parent c15b731d73
commit 6e6bb7287c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 5859 additions and 2443 deletions

View file

@ -70,5 +70,4 @@ import * as d3v7 from "d3v7";
window.d3v7 = {
...d3v7
};
};

View file

@ -0,0 +1,874 @@
--
-- (C) 2013-26 - ntop.org
--
require "os"
require "lua_utils"
local ts_utils = require("ts_utils_core")
local template = require "template_utils"
local stats_utils = require("stats_utils")
local page_utils = require "page_utils"
local have_nedge = ntop.isnEdge()
local info = ntop.getInfo(true)
local is_admin = isAdministrator()
local maxSpeed
local ifid = interface.getId()
local _ifstats = interface.getStats()
local http_prefix = ntop.getHttpPrefix()
local random_csrf = ntop.getRandomCSRFValue()
local is_pcap_dump_iface = interface.isPcapDumpInterface()
if not is_pcap_dump_iface and not have_nedge then
-- if the speed in not custom we try to read the speed from the interface
-- and, as a final resort, we use 1Gbps
if tonumber(_ifstats.speed) ~= nil then
maxSpeed = tonumber(_ifstats.speed) * 1e6
else
maxSpeed = 1000000000 -- 1 Gbit
end
end -- closes is_pcap_dump_iface == false
if not info.oem then
-- print ([[
-- <hr>
-- <footer id="n-footer">
-- <div class="container-fluid">
-- <div class="row mt-2">
-- <div class="col-12 col-md-4 pl-md-0 text-center text-md-start">
-- <small>
-- <a href="https://www.ntop.org/products/traffic-analysis/ntop/" target="_blank">
-- ]] .. getNtopngRelease(info) ..[[
-- </a>
-- </small>
-- </div>
-- <div class="col-12 col-md-4 text-center">
-- <small>]].. ntop.getInfo()["copyright"] ..[[</small>
-- ]])
-- print [[
-- </div>
-- <div class="col-12 col-md-4 text-center text-md-end pr-md-0">
-- <small>
-- <i class="fas fa-clock" title="]] print(i18n("about.server_time")) print[["></i> <div class="d-inline-block" id='network-clock'></div> ]] if(info.tzname ~= nil) then print(info.tzname) end print(" | ") print(i18n("about.uptime")) print[[: <div class="d-inline-block" id='network-uptime'></div>
-- </small>
-- </div>
-- </div>
-- </div>
-- </footer>
-- ]]
-- else -- info.oem
-- print[[<div class="col-12 text-end">
-- <small>
-- <i class="fas fa-clock"></i> <div class="d-inline-block" id='network-clock' title="]] print(i18n("about.server_time")) print[["></div> | ]] print(i18n("about.uptime")) print[[: <div class="d-inline-block" id='network-uptime'></div>
-- </small>
-- </div>]]
end
local traffic_peity_width = "96"
if ts_utils.getDriverName() == "influxdb" then
local msg = ntop.getCache("ntopng.cache.influxdb.last_error")
if not isEmptyString(msg) then
print([[
<script type="text/javascript">
$("#influxdb-error-msg-text").html("]] .. (msg:gsub('"', '\\"')) .. [[");
$("#influxdb-error-msg").show();
</script>
]])
end
end
-- Dismiss Notification Code and Toggle Dark theme
print([[
<script type='text/javascript'>
$(document).ready(function() {
$(`.notification button.dismiss`).click(function() {
const $toast = $(this).parents('.notification');
const id = $toast.data("toastId");
ToastUtils.dismissToast(id, "]] .. random_csrf ..
[[", (data) =>{
if (data.success) $toast.toast('hide');
});
});
$(`.toggle-dark-theme`).click(function() {
const request = $.post(`]] .. http_prefix ..
[[/lua/update_prefs.lua`, {
action: 'toggle_theme', toggle_dark_theme: ]] ..
tostring(not page_utils.is_dark_mode_enabled()) .. [[,
csrf: "]] .. random_csrf .. [["
});
request.done(function(res) {
if (res.success) location.reload();
});
});
});
</script>
]])
-- Restart product code
if (is_admin and ntop.isPackage() and not ntop.isWindows()) then
print(template.gen("modal_confirm_dialog.html", {
dialog = {
id = 'restart-modal',
action = 'restartService()',
title = i18n("restart.restart_product", {product = info.product}),
message = i18n("restart.confirm", {product = info.product}),
custom_alert_class = 'alert alert-danger',
confirm = i18n('restart.restart'),
confirm_button = 'btn-danger'
}
}))
print [[
<script type="text/javascript">
const restartCSRF = ']]
print(random_csrf)
print [[';
const restartService = function() {
$.ajax({
type: 'POST',
url: ']]
print(http_prefix)
print [[/lua/admin/service_restart.lua',
data: {
csrf: restartCSRF
},
success: function(rsp) {
alert("]]
print(i18n("restart.restarting", {product = info.product}))
print [[");
$('#restart-modal').modal('hide');
}
});
}
$('.restart-service').click(() => {
$('#restart-modal').modal('show');
});
</script>
]]
end
-- render switchable system view
print([[
<script type="text/javascript">
$(document).ready(function() {
// ignore scientific numbers input
$(`.ignore-scientific`).keypress(function(e) {
if (e.which != 8 && e.which != 0 && e.which < 48 || e.which > 57) {
e.preventDefault();
}
})
ToastUtils.initToasts();
});
const toggleSystemInterface = ($form = null) => {
if($form != null) {
$form.submit();
}
else {
console.error("An error has occurred when switching interface!");
}
}
]])
print([[
</script>
]])
print [[
<script type="text/javascript">
]]
print [[
var is_historical = false;
let updatingChart_uploads = [
$("#n-navbar .network-load-chart-upload").show().peity("line", { width: ]]
print(traffic_peity_width)
print [[, max: null }),
$(".mobile-menu-stats .network-load-chart-upload").show().peity("line", { width: ]]
print(traffic_peity_width)
print [[, max: null }),
];
let updatingChart_downloads = [
$("#n-navbar .network-load-chart-download").show().peity("line", { width: ]]
print(traffic_peity_width)
print [[, max: null, fill: "lightgreen"}),
$(".mobile-menu-stats .network-load-chart-download").show().peity("line", { width: ]]
print(traffic_peity_width)
print [[, max: null, fill: "lightgreen"})
];
let updatingChart_totals = [
$("#n-navbar .network-load-chart-total").show().peity("line", { width: ]]
print(traffic_peity_width)
print [[, max: null}),
$(".mobile-menu-stats .network-load-chart-total").show().peity("line", { width: ]]
print(traffic_peity_width)
print [[, max: null})
];
$(`.copy-http-url`).on('click', function() {
let sampleTextarea = document.createElement("textarea");
document.body.appendChild(sampleTextarea);
sampleTextarea.value = $(this).attr('data-to-copy');
sampleTextarea.select(); //select textarea content
document.execCommand("copy");
document.body.removeChild(sampleTextarea);
})
let isFooterRefreshInProcess = false;
const footerRefresh = function() {
if (isFooterRefreshInProcess == true) { return; }
isFooterRefreshInProcess = true;
$.ajax({
type: 'GET',
url: ']]
print(http_prefix)
print [[/lua/rest/v2/get/interface/data.lua',
data: { ifid: ]]
print(tostring(ifid))
print [[, type: 'summary' },
success: function(content) {
isFooterRefreshInProcess = false;
if(content["rc_str"] != "OK") {
return;
}
const rsp = content["rsp"];
try {
ntopng_events_manager.emit_custom_event(ntopng_custom_events.GET_INTERFACE_DATA, rsp);
let bps_upload = rsp.throughput.upload * 8;
let bps_download = rsp.throughput.download * 8;
/* Upload chart */
let values = updatingChart_uploads[0].text().split(",")
values.shift();
values.push(bps_upload.toString());
updatingChart_uploads[0].text(values.join(",")).change();
updatingChart_uploads[1].text(values.join(",")).change();
$('.chart-upload-text:visible').html(NtopUtils.bitsToSize(bps_upload, 1000));
/* Download chart */
values = updatingChart_downloads[0].text().split(",")
values.shift();
values.push((-bps_download).toString());
updatingChart_downloads[0].text(values.join(",")).change();
updatingChart_downloads[1].text(values.join(",")).change();
$('.chart-download-text:visible').html(NtopUtils.bitsToSize(bps_download, 1000));
/* Disaggregate interface chart */
values = updatingChart_totals[0].text().split(",")
values.shift();
values.push(rsp.throughput_bps * 8);
updatingChart_totals[0].text(values.join(",")).change();
updatingChart_totals[1].text(values.join(",")).change();
]]
if (is_pcap_dump_iface == false) and (not have_nedge) then
print [[
const v = Math.round(Math.min((rsp.throughput_bps * 8 *100)/]]
print(string.format("%u", maxSpeed))
print [[, 100));
$('.network-load').html(v+"%");
]]
end
-- systemInterfaceEnabled is defined inside menu.lua
print [[
//$('#network-clock').html(`${rsp.localtime}`);
//$('#network-uptime').html(`${rsp.uptime}`);
let msg = `<div class='m-2'><div class='d-flex flex-wrap navbar-main-badges'>`;
if(rsp.out_of_maintenance) {
msg += "<a href='https://www.ntop.org/support/faq/how-can-i-renew-maintenance-for-commercial-products/' target='_blank'><span class='badge bg-warning'>]]
print(i18n("about.maintenance_expired", {product = info["product"]}))
print [[ <i class=\"fas fa-external-link-alt\"></i></span></a> ";
}
if(rsp.degraded_performance) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/system_interfaces_stats.lua?page=internals&tab=periodic_activities&periodic_script_issue=any_issue'>"
msg += "<span class='badge bg-warning'><i class='fas fa-exclamation-triangle data-bs-toggle='tooltip' data-bs-placement='bottom' title=']]
print(i18n("internals.degraded_performance"))
print [['></i></span></a>";
}
if ((rsp.engaged_alerts > 0 || rsp.alerted_flows > 0) && ]]
print(ternary(hasAllowedNetworksSet(), "false", "true"))
print [[) {
var error_color = "#B94A48";
if(rsp.engaged_alerts > 0) {
let alerts_badge = "bg-info";
if(rsp.engaged_alerts_error > 0) alerts_badge = "bg-danger";
else if(rsp.engaged_alerts_warning > 0) alerts_badge = "bg-warning";
msg += "<a href=']]
print(http_prefix)
print [[/lua/alert_stats.lua?ifid=]]
print(tostring(ifid))
print [[&status=engaged'>"
msg += "<span class=\"badge " + alerts_badge + "\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=']] print(i18n('graphs.engaged_alerts')) print[['><i class=\"fas fa-exclamation-triangle\"></i> "+NtopUtils.formatValue(rsp.engaged_alerts, 1)+"</span></a>";
}
if(rsp.alerted_flows_warning > 0 && !(systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/flows_stats.lua?status=warning'>"
msg += "<span class=\"badge bg-warning\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=']]
print(i18n("flow_details.alerted_flows"))
print [['>"+NtopUtils.formatValue(rsp.alerted_flows_warning, 1)+ " <i class=\"fas fa-stream\"></i> ]]
print [[ <i class=\"fas fa-exclamation-triangle\"></i></span></a>";
}
if(rsp.alerted_flows_error > 0 && !(systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/flows_stats.lua?status=error'>"
msg += "<span class=\"badge bg-danger\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=']]
print(i18n("flow_details.dangerous_flows"))
print [['>"+NtopUtils.formatValue(rsp.alerted_flows_error, 1)+ " <i class=\"fas fa-stream\"></i> ]]
print [[ <i class=\"fas fa-exclamation-triangle\"></i></span></a>";
}
if(rsp.active_discovery_active === true) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/discover.lua'>"
msg += "<span class=\"badge bg-warning\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=']]
print(i18n("prefs.network_discovery_running"))
print [['><i class=\"fas fa-project-diagram\"></i></span></a>";
}
}
if((rsp.engaged_alerts > 0 || rsp.alerted_flows > 0) && $("#alerts-id").is(":visible") == false) {
$("#alerts-id").show();
}
if(rsp.ts_alerts && rsp.ts_alerts.influxdb && (!systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/monitor/influxdb_monitor.lua?ifid=]]
print(tostring(ifid))
print [[&page=alerts#tab-table-engaged-alerts'>"
msg += "<span class=\"badge bg-danger\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=']] print(i18n('alerts_dashboard.influxdb_error')) print[['><i class=\"fas fa-database\"></i></span></a>";
}
var alarm_threshold_low = 60; /* 60% */
var alarm_threshold_high = 90; /* 90% */
var alert = 0;
if(rsp.num_local_hosts > 0 && (!systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/hosts_stats.lua?mode=local'>";
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]]
print(i18n("local_hosts"))
print [[\" class=\"badge bg-success\">";
msg += NtopUtils.formatValue(rsp.num_local_hosts, 1);
if(rsp.num_local_rcvd_only_hosts > 0) msg += " ("+NtopUtils.formatValue(rsp.num_local_rcvd_only_hosts, 1)+")";
msg += " <i class=\"fas fa-laptop\" aria-hidden=\"true\"></i></span></a>";
}
const num_remote_hosts = rsp.num_hosts - rsp.num_local_hosts;
if(num_remote_hosts > 0 && (!systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/hosts_stats.lua?mode=remote'>";
var remote_hosts_label = "]]
print(i18n("remote_hosts"))
print [[";
if (rsp.hosts_pctg < alarm_threshold_low && !systemInterfaceEnabled) {
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + remote_hosts_label +"\" class=\"badge bg-secondary\">";
} else if (rsp.hosts_pctg < alarm_threshold_high && !systemInterfaceEnabled) {
alert = 1;
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + remote_hosts_label +"\" class=\"badge bg-warning\">";
} else {
alert = 1;
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + remote_hosts_label +"\" class=\"badge bg-danger\">";
}
msg += NtopUtils.formatValue(num_remote_hosts, 1);
num_remote_rcvd_only_hosts = rsp.num_rcvd_only_hosts - rsp.num_local_rcvd_only_hosts;
if(num_remote_rcvd_only_hosts > 0) msg += " ("+NtopUtils.formatValue(num_remote_rcvd_only_hosts, 1)+")";
msg += " <i class=\"fas fa-laptop\" aria-hidden=\"true\"></i></span></a>";
}
if(rsp.num_devices > 0 && (!systemInterfaceEnabled)) {
var macs_label = "]]
print(i18n("mac_stats.layer_2_source_devices", {device_type = ""}))
print [[";
msg += "<a href=']]
print(http_prefix)
print [[/lua/macs_stats.lua?devices_mode=source_macs_only'>";
if (rsp.macs_pctg < alarm_threshold_low) {
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + macs_label +"\" class=\"badge bg-secondary\">";
} else if(rsp.macs_pctg < alarm_threshold_high) {
alert = 1;
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + macs_label +"\" class=\"badge bg-warning\">";
} else {
alert = 1;
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + macs_label +"\" class=\"badge bg-danger\">";
}
msg += NtopUtils.formatValue(rsp.num_devices, 1)+" <i class=\"fas fa-ethernet\"></i></span></a>";
}
if(rsp.num_flows > 0 && (!systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/flows_stats.lua'>";
const flows_label = "]]
print(i18n("live_flows"))
print [["
if (rsp.flows_pctg < alarm_threshold_low) {
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + flows_label +"\" class=\"badge bg-secondary\">";
} else if(rsp.flows_pctg < alarm_threshold_high) {
alert = 1;
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + flows_label +"\" class=\"badge bg-warning\">";
} else {
alert = 1;
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + flows_label +"\" class=\"badge bg-danger\">";
}
msg += NtopUtils.formatValue(rsp.num_flows, 1)+" <i class=\"fas fa-stream\"></i> </span> </a>";
if (rsp.db && rsp.db.flow_export_drops > 0) {
const export_pctg = rsp.db.flow_export_drops / (rsp.db.flow_export_count + rsp.db.flow_export_drops + 1);
if (export_pctg > ]]
print(stats_utils.UPPER_BOUND_INFO_EXPORTS)
print [[) {
const badge_class = (export_pctg <= ]]
print(stats_utils.UPPER_BOUND_WARNING_EXPORTS)
print [[) ? 'warning' : 'danger';
msg += "<a href=']]
print(http_prefix)
print [[/lua/if_stats.lua'><span class=\"badge bg-"+badge_class+"\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]] print(i18n('flow_export_drops')) print[[\"><i class=\"fas fa-exclamation-triangle\" style=\"color: #FFFFFF;\"></i> "+NtopUtils.formatValue(rsp.db.flow_export_drops, 1)+" DB Export drop";
if(rsp.db.flow_export_drops > 1) msg += "s";
msg += "</span></a>";
}
}
if (rsp.dropped_zmq_msg > 0) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/if_stats.lua'><span class=\"badge bg-warning\"><i class=\"fas fa-tint\" style=\"color: #FFFFFF;\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]] print(i18n('dropping_zmq_msg')) print[[\"></i> "+NtopUtils.formatValue(rsp.dropped_zmq_msg, 1)+" ZMQ Drops</span></a>";
}
if (rsp.dropped_flows > 0) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/if_stats.lua'><span class=\"badge bg-warning\"><i class=\"fas fa-tint\" style=\"color: #FFFFFF;\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]] print(i18n('dropping_flows')) print[[\"></i> "+NtopUtils.formatValue(rsp.dropped_flows, 1)+" Flows Drops</span></a>";
}
}
if ((rsp.num_live_captures != undefined) && (rsp.num_live_captures > 0) && (!systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/live_capture_stats.lua'>";
msg += "<span class=\"badge bg-primary\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]] print(i18n('live_capture.active_live_captures')) print[[\">";
msg += NtopUtils.addCommas(rsp.num_live_captures)+" <i class=\"fas fa-download fa-lg\"></i></span></a>";
}
if (rsp.traffic_recording != undefined && (!systemInterfaceEnabled)) {
var status_label="primary";
var status_title="]]
print(i18n("traffic_recording.recording"))
print [[";
if (rsp.traffic_recording != "recording") {
status_label = "danger";
status_title = "]]
print(i18n("traffic_recording.failure"))
print [[";
}
msg += "<a href=']]
print(http_prefix)
print [[/lua/if_stats.lua?ifid=]]
print(tostring(ifid))
print [[&page=traffic_recording&tab=status'>";
msg += "<span class=\"badge bg-"+status_label+"\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=\""+NtopUtils.addCommas(status_title)+"\">";
msg += "<i class=\"fas fa-hdd fa-lg\"></i></span></a>";
}
if (rsp.traffic_extraction != undefined && (!systemInterfaceEnabled)) {
var status_title="]]
print(i18n("traffic_recording.traffic_extraction_jobs"))
print [[";
var status_label = "secondary";
if (rsp.traffic_extraction == "ready") status_label="primary";
msg += "<a href=']]
print(http_prefix)
print [[/lua/if_stats.lua?ifid=]]
print(tostring(ifid))
print [[&page=traffic_recording&tab=jobs'>";
msg += "<span class=\"badge bg-"+status_label+"\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=\""+NtopUtils.addCommas(status_title)+"\">";
msg += rsp.traffic_extraction_num_tasks+" <i class=\"fas fa-tasks fa-lg\"></i></span></a>";
}
if (rsp.vs_in_progress != undefined && rsp.vs_in_progress > 0 && (!systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/vulnerability_scan.lua'>";
msg += "<span class=\"badge bg-primary\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]] print(i18n('vulnerability_scan.vulnerability_scan_in_progress')) print[[\">";
msg += NtopUtils.addCommas(rsp.vs_in_progress) +" <i class=\"fas fa-satellite-dish\"></i></span></a>";
}
if(rsp.is_loading == true) {
msg += "<blink><span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]] print(i18n('loading')) print[[\" class=\"badge bg-primary\"><i class=\"fas fa-spinner\"></i> ]]
print(i18n("loading"))
print [[</span></blink>";
}
]]
if ntop.isOffline() then
print [[
msg += "<a href=\"#\" style=\"cursor: default\"><span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]]
print(i18n("offline"))
print [[\" class=\"badge bg-secondary\"><i class=\"fas fa-plane\"></i></span></a>";
]]
end
print [[
msg += '</div></div>';
// append the message inside the network-load element
const $msg = $(msg);
// resize element's font size to fit better
if ($msg.width() > $msg.css('max-width')) {
$msg.find('span').css('font-size', '0.748rem');
}
$('.network-load').html($msg);
NtopUtils.reloadBSTooltips();
} catch(e) {
console.warn(e);
/* alert("JSON Error (session expired?): logging out"); window.location.replace("]]
print(http_prefix)
print [[/lua/logout.lua"); */
}
},
error: function() {
isFooterRefreshInProcess = false;
}
});
}
$(document).ajaxError(function(err, response, ajaxSettings, thrownError) {
if((response.status == 403) && (response.responseText == "Login Required"))
window.location.href = "]]
print(http_prefix .. "/lua/login.lua")
print [[";
});
footerRefresh(); /* call immediately to give the UI a more responsive look */
setInterval(footerRefresh, 7000); /* re-schedule every [interface-rate] seconds */
//Automatically open dropdown-menu
$(document).ready(function(){
$('ul.nav li.dropdown').hover(function() {
$(this).find('.dropdown-menu').stop(true, true).delay(150).fadeIn(100);
}, function() {
$(this).find('.dropdown-menu').stop(true, true).delay(150).fadeOut(100);
});
$('.collapse')
.on('shown.bs.collapse', function(){
$(this).parent().find(".fa-caret-down").removeClass("fa-caret-down").addClass("fa-caret-up");
})
.on('hidden.bs.collapse', function(){
$(this).parent().find(".fa-caret-up").removeClass("fa-caret-up").addClass("fa-caret-down");
});
});
]]
-- This code rewrites the current page state after a POST request to avoid Document Expired errors
if not table.empty(_POST) then
print [[
if ((typeof(history) === "object")
&& (typeof(history).replaceState === "function")
&& (typeof(window.location.href) === "string"))
history.replaceState(history.state, "", window.location.href);
]]
end
print [[
// hide the possibly shown alerts icon in the header
]]
if not _ifstats.isView then
print("$('#alerts-li').hide();")
else
print("$('#alerts-li').show();")
end
print([[
</script>
]])
-- ######################################
if have_nedge then
print [[<form id="powerOffForm" method="post">
<input name="csrf" value="]]
print(random_csrf)
print [[" type="hidden" />
<input name="poweroff" value="" type="hidden" />
</form>
<form id="rebootForm" method="post">
<input name="csrf" value="]]
print(random_csrf)
print [[" type="hidden" />
<input name="reboot" value="" type="hidden" />
</form>]]
print(template.gen("modal_confirm_dialog.html", {
dialog = {
id = "poweroff_dialog",
action = "$('#powerOffForm').submit()",
title = i18n("nedge.power_off"),
message = i18n("nedge.power_off_confirm"),
confirm = i18n("nedge.power_off")
}
}))
print(template.gen("modal_confirm_dialog.html", {
dialog = {
id = "reboot_dialog",
action = "$('#rebootForm').submit()",
title = i18n("nedge.reboot"),
message = i18n("nedge.reboot_corfirm"),
confirm = i18n("nedge.reboot")
}
}))
end
print(template.gen("modal_confirm_dialog.html", {
dialog = {
id = "ext_link_dialog",
title = i18n("external_link"),
message = i18n("show_alerts.confirm_external_link"),
confirm = i18n("redirect")
}
}))
print [[
<script type="text/javascript">
$(document).ready(function(){
$(document).on('click','a.ntopng-external-link',function(event){
event.preventDefault();
const url=$(this)[0].href;
$("#url_ext_link_dialog").html(url+"<br/>]]
print(i18n("are_you_sure"))
print [[");
$("#btn-confirm-action_ext_link_dialog").attr('href',url);
$('#ext_link_dialog').modal('show');
});
});
</script>
]]
-- Updates submenu
if hasSoftwareUpdatesSupport() then
-- Updates check
print [[
<script type='text/javascript'>
$('#updates-info-li').html(']]
print(i18n("updates.checking"))
print [[');
$('#updates-install-li').hide();
const updates_csrf = ']]
print(random_csrf)
print [[';
/* Updates status */
var updatesStatus = '';
var updatesLastCheck = 0;
/* Install latest update */
var installUpdate = function() {
if (confirm(']]
print(i18n("updates.install_confirm"))
if info["pro.license_days_left"] ~= nil and info["pro.license_days_left"] <=
0 then
-- License is valid, however maintenance is expired: warning the user
print(" " .. i18n("updates.maintenance_expired"))
end
print [[')) {
$.ajax({
type: 'POST',
url: ']]
print(http_prefix)
print [[/lua/install_update.lua',
data: {
csrf: updates_csrf
},
success: function(rsp) {
$('#updates-info-li').html(']]
print(i18n("updates.installing"))
print [[')
$('#updates-info-li').attr('title', '');
$('#updates-install-li').hide();
$('#admin-badge').hide();
updatesStatus = 'installing';
}
});
}
}
/* Check for new updates */
var checkForUpdates = function() {
$.ajax({
type: 'POST',
url: ']]
print(http_prefix)
print [[/lua/check_update.lua',
data: {
csrf: updates_csrf,
search: 'true'
},
success: function(rsp) {
$('#updates-info-li').html(']]
print(i18n("updates.checking"))
print [[');
$('#updates-info-li').attr('title', '');
$('#updates-install-li').hide();
$('#admin-badge').hide();
updatesStatus = 'checking';
}
});
}
/* Update the menu with the current updates status */
var updatesRefresh = function() {
const now = Date.now()/1000;
let check_time_sec = 10;
if (updatesStatus == 'installing' || updatesStatus == 'checking') {
/* Go ahead (frequent update) */
} else if (updatesStatus == 'update-avail' || updatesStatus == 'upgrade-failure') {
return; /* no need to check again */
} else { /* updatesStatus == 'not-avail' || updatesStatus == 'update-failure' || updatesStatus == <other errors> || updatesStatus == '' */
check_time_sec = 300;
}
if (now < updatesLastCheck + check_time_sec) return; /* check with low freq */
updatesLastCheck = now;
$.ajax({
type: 'GET',
url: ']]
print(http_prefix)
print [[/lua/check_update.lua',
data: {},
success: function(rsp) {
if(rsp && rsp.status) {
updatesStatus = rsp.status;
if (rsp.status == 'installing') {
$('#updates-info-li').html(']]
print(i18n("updates.installing"))
print [[')
$('#updates-info-li').attr('title', '');
$('#updates-install-li').hide();
$('#admin-badge').hide();
} else if (rsp.status == 'checking') {
$('#updates-info-li').html(']]
print(i18n("updates.checking"))
print [[');
$('#updates-info-li').attr('title', '');
$('#updates-install-li').hide();
$('#admin-badge').hide();
} else if (rsp.status == 'update-avail' || rsp.status == 'upgrade-failure') {
$('#updates-info-li').html('<span class="badge bg-pill bg-danger">]]
print(i18n("updates.available"))
print [[</span> ]]
print(info["product"])
print [[ ' + rsp.version);
$('#updates-info-li').attr('title', '');
var icon = '<i class="fas fa-download"></i>';
$('#updates-install-li').attr('title', '');
if (rsp.status !== 'update-avail') {
icon = '<i class="fas fa-exclamation-triangle text-danger"></i>';
$('#updates-install-li').attr('title', "]]
print(i18n("updates.update_failure_message"))
print [[: " + rsp.status);
}
$('#updates-install-li').html(icon + " ]]
print(i18n("updates.install"))
print [[");
$('#updates-install-li').show();
$('#updates-install-li').off("click");
$('#updates-install-li').click(installUpdate);
if (rsp.status !== 'update-avail') $('#admin-badge').html('!');
else $('#admin-badge').html('1');
$('#admin-badge').show();
} else /* (rsp.status == 'not-avail' || rsp.status == 'update-failure' || rsp.status == <other errors>) */ {
var icon = '';
$('#updates-info-li').attr('title', '');
if (rsp.status !== 'not-avail') {
icon = '<i class="fas fa-exclamation-triangle text-danger"></i> ';
$('#updates-info-li').attr('title', "]]
print(i18n("updates.update_failure_message"))
print [[: " + rsp.status);
}
$('#updates-info-li').html(icon + ']]
print(i18n("updates.no_updates"))
print [[');
$('#updates-install-li').html("<i class='fas fa-sync'></i> ]]
print(i18n("updates.check"))
print [[");
$('#updates-install-li').show();
$('#updates-install-li').off("click");
$('#updates-install-li').click(checkForUpdates);
$('#admin-badge').hide();
}
}
}
});
}
setInterval(updatesRefresh, 1000);
</script>
]]
end
-- close wrapper
print [[
</main>
</div>
</body>
</html> ]]

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
#n-sidebar {
position: fixed;
width: var(--sidebar-width);
width: var(--sb-rail-w);
top: 0;
left: 0;
bottom: 0;

View file

@ -1 +1 @@
import "./chart_colors";
// chart_colors (legacy footer injector) removed — footer now handled by app-shell.vue

View file

@ -1,6 +1,6 @@
:root {
font-weight: 500;
--sidebar-width: 4.5rem;
--sidebar-width: 0px; /* AppShell Vue sidebar: overridden by --sb-rail-w */
--footer-height: 4rem;
--padding-md-four: 1.5rem;
--scrollable-dropdown-height: 20rem;

316
http_src/vue/app-footer.vue Normal file
View file

@ -0,0 +1,316 @@
<!--
(C) 2013-26 - ntop.org
AppFooter orchestration component.
Handles: software updates polling, ext_link_dialog, nEdge power/reboot modals,
ajaxError -> login redirect, history.replaceState on POST.
No visible UI of its own except modals.
-->
<template>
<!-- External link confirmation modal -->
<div class="modal fade" id="ext_link_dialog" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ _i18n("external_link") }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<span id="url_ext_link_dialog"></span><br/>{{ _i18n("are_you_sure") }}
</div>
<div class="modal-footer">
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">{{ _i18n("cancel") }}</button>
<a id="btn-confirm-action_ext_link_dialog" class="btn btn-primary btn-sm"
href="#" target="_blank" rel="noopener" data-bs-dismiss="modal">
{{ _i18n("redirect") }}
</a>
</div>
</div>
</div>
</div>
<!-- nEdge poweroff/reboot modals -->
<template v-if="ctx.is_nedge && ctx.is_admin">
<form id="powerOffForm" method="post">
<input name="csrf" :value="ctx.csrf" type="hidden" />
<input name="poweroff" value="" type="hidden" />
</form>
<form id="rebootForm" method="post">
<input name="csrf" :value="ctx.csrf" type="hidden" />
<input name="reboot" value="" type="hidden" />
</form>
<div class="modal fade" id="poweroff_dialog" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ _i18n("nedge.power_off") }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">{{ _i18n("nedge.power_off_confirm") }}</div>
<div class="modal-footer">
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">{{ _i18n("cancel") }}</button>
<button class="btn btn-danger btn-sm" @click="submitForm('powerOffForm')">
{{ _i18n("nedge.power_off") }}
</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="reboot_dialog" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ _i18n("nedge.reboot") }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">{{ _i18n("nedge.reboot_corfirm") }}</div>
<div class="modal-footer">
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">{{ _i18n("cancel") }}</button>
<button class="btn btn-primary btn-sm" @click="submitForm('rebootForm')">
{{ _i18n("nedge.reboot") }}
</button>
</div>
</div>
</div>
</div>
</template>
</template>
<script setup>
import { computed, onMounted, onBeforeUnmount } from "vue";
import { ntopng_utility } from "../services/context/ntopng_globals_services.js";
const props = defineProps({ context: Object });
const ctx = computed(() => props.context || {});
function _i18n(key) { return i18n(key) || key; }
// Updates
const UPDATES_POLL_SLOW = 300; // seconds when idle
const UPDATES_POLL_FAST = 10; // seconds when checking/installing
let updatesStatus = "";
let updatesLastCheck = 0;
let updatesTimer = null;
async function updatesRefresh() {
if (!ctx.value.has_updates_support) return;
const now = Date.now() / 1000;
const checkInterval = (updatesStatus === "installing" || updatesStatus === "checking")
? UPDATES_POLL_FAST : UPDATES_POLL_SLOW;
if (updatesStatus === "update-avail" || updatesStatus === "upgrade-failure") return;
if (now < updatesLastCheck + checkInterval) return;
updatesLastCheck = now;
try {
const data = await ntopng_utility.http_request(
`${http_prefix}/lua/check_update.lua`
);
if (!data || !data.status) return;
updatesStatus = data.status;
applyUpdatesStatus(data);
} catch (_) {}
}
function applyUpdatesStatus(data) {
const infoEl = document.getElementById("updates-info-li");
const installEl = document.getElementById("updates-install-li");
const badgeEl = document.getElementById("admin-badge");
if (!infoEl) return;
if (data.status === "installing") {
infoEl.textContent = _i18n("updates.installing");
installEl?.style && (installEl.style.display = "none");
badgeEl?.style && (badgeEl.style.display = "none");
} else if (data.status === "checking") {
infoEl.textContent = _i18n("updates.checking");
installEl?.style && (installEl.style.display = "none");
badgeEl?.style && (badgeEl.style.display = "none");
} else if (data.status === "update-avail" || data.status === "upgrade-failure") {
infoEl.innerHTML = `<span class="badge bg-danger">${_i18n("updates.available")}</span> ${data.version || ""}`;
if (installEl) {
installEl.style.display = "";
const icon = data.status === "update-avail"
? '<i class="fas fa-download me-1"></i>'
: '<i class="fas fa-exclamation-triangle text-danger me-1"></i>';
installEl.innerHTML = `<a class="dropdown-item" href="#">${icon}${_i18n("updates.install")}</a>`;
installEl.querySelector("a")?.addEventListener("click", e => {
e.preventDefault();
doInstallUpdate();
});
}
if (badgeEl) badgeEl.style.display = "";
// Show in major-release alert bar
const alertEl = document.getElementById("major-release-alert");
const textEl = document.getElementById("ntopng_update_available");
if (alertEl && textEl) {
textEl.textContent = `${_i18n("updates.available")}: ${data.version || ""}`;
alertEl.style.display = "";
}
} else {
infoEl.textContent = _i18n("updates.no_updates") + ".";
installEl?.style && (installEl.style.display = "none");
badgeEl?.style && (badgeEl.style.display = "none");
}
}
async function doInstallUpdate() {
let msg = _i18n("updates.install_confirm");
if (!confirm(msg)) return;
try {
await ntopng_utility.http_request(
`${http_prefix}/lua/install_update.lua`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ csrf: ctx.value.csrf || window.__CSRF_DATATABLE__ }),
}
);
const infoEl = document.getElementById("updates-info-li");
const installEl = document.getElementById("updates-install-li");
const badgeEl = document.getElementById("admin-badge");
if (infoEl) infoEl.textContent = _i18n("updates.installing");
installEl?.style && (installEl.style.display = "none");
badgeEl?.style && (badgeEl.style.display = "none");
updatesStatus = "installing";
} catch (_) {}
}
function startUpdatesCheck() {
const infoEl = document.getElementById("updates-info-li");
const installEl = document.getElementById("updates-install-li");
if (infoEl) infoEl.textContent = _i18n("updates.checking");
if (installEl) installEl.style.display = "none";
doManualCheck();
}
async function doManualCheck() {
try {
await ntopng_utility.http_request(
`${http_prefix}/lua/check_update.lua`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
csrf: ctx.value.csrf || window.__CSRF_DATATABLE__,
search: "true",
}),
}
);
updatesStatus = "checking";
updatesLastCheck = 0; // force a refresh on next tick
} catch (_) {}
}
// ext_link_dialog intercept .ntopng-external-link clicks
function onExternalLinkClick(e) {
const anchor = e.target.closest("a.ntopng-external-link");
if (!anchor) return;
e.preventDefault();
const url = anchor.href;
const el = document.getElementById("url_ext_link_dialog");
const btn = document.getElementById("btn-confirm-action_ext_link_dialog");
if (el) el.textContent = url;
if (btn) btn.href = url;
const modalEl = document.getElementById("ext_link_dialog");
if (modalEl && window.bootstrap) {
bootstrap.Modal.getOrCreateInstance(modalEl).show();
}
}
// Toast dismiss, from ToastUtils
function onToastDismiss(e) {
const btn = e.target.closest(".notification button.dismiss");
if (!btn) return;
const toast = btn.closest(".notification");
if (!toast) return;
const id = toast.dataset.toastId;
if (typeof ToastUtils !== "undefined" && id) {
ToastUtils.dismissToast(id, ctx.value.csrf || window.__CSRF_DATATABLE__, data => {
if (data?.success) {
const bsToast = bootstrap.Toast.getInstance(toast);
bsToast?.hide();
}
});
}
}
// ---------------------------------------------------------------
// 403 ajaxError -> redirect to login
function onAjaxError(_evt, xhr) {
if (xhr?.status === 403 && xhr?.responseText === "Login Required") {
window.location.href = `${http_prefix}/lua/login.lua`;
}
}
// ---------------------------------------------------------------
// nEdge form submit helper
function submitForm(id) {
document.getElementById(id)?.submit();
}
// ---------------------------------------------------------------
// history.replaceState (avoid Document Expired on POST pages)
function fixHistoryState() {
if (history?.replaceState && window.location.href) {
history.replaceState(history.state, "", window.location.href);
}
}
function wireUpdatesInstallButton() {
const installEl = document.getElementById("updates-install-li");
if (installEl) {
installEl.addEventListener("click", e => {
e.preventDefault();
startUpdatesCheck();
});
}
}
onMounted(async () => {
document.addEventListener("click", onExternalLinkClick);
document.addEventListener("click", onToastDismiss);
document.addEventListener("ajaxError", onAjaxError);
// jQuery ajaxError fallback
if (window.$) {
$(document).ajaxError((err, response) => {
if (response?.status === 403 && response?.responseText === "Login Required") {
window.location.href = `${http_prefix}/lua/login.lua`;
}
});
}
fixHistoryState();
wireUpdatesInstallButton();
if (ctx.value.has_updates_support) {
// initial check
updatesLastCheck = 0;
await updatesRefresh();
// poll every 10 seconds; the function internally throttles to slow/fast intervals
updatesTimer = setInterval(updatesRefresh, UPDATES_POLL_FAST * 1000);
}
});
onBeforeUnmount(() => {
document.removeEventListener("click", onExternalLinkClick);
document.removeEventListener("click", onToastDismiss);
document.removeEventListener("ajaxError", onAjaxError);
if (updatesTimer) clearInterval(updatesTimer);
});
</script>

1967
http_src/vue/app-shell.vue Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,30 +3,32 @@
-->
<template>
<!-- Search container (expandable input + button + spinner) -->
<div ref="searchContainer" :class="[
'ms-auto expandable-search align-items-center position-relative d-flex',
{ expanded: expanded }
]">
<!-- Search wrapper: position:relative so dropdown is anchored here -->
<div class="search-box-wrapper" @mouseenter="onSearchEnter" @mouseleave="onSearchLeave">
<!-- Search container (expandable input + button + spinner) -->
<div ref="searchContainer" :class="[
'expandable-search align-items-center position-relative d-flex',
{ expanded: expanded }
]">
<!-- Search input -->
<input name="search" type="text" ref="searchInput" class="form-control rounded-pill ps-4-5" autocomplete="off"
autocorrect="off" :placeholder="_i18n('search')">
<!-- Search input -->
<input name="search" type="text" ref="searchInput" class="form-control rounded-pill ps-4-5" autocomplete="off"
autocorrect="off" :placeholder="_i18n('search')">
<!-- Loading spinner shown during async search -->
<Spinner :show="loading" size="1rem" class="spinner-inside">
</Spinner>
<!-- Loading spinner shown during async search -->
<Spinner :show="loading" size="1rem" class="spinner-inside">
</Spinner>
<!-- Search icon button -->
<button class="btn btn-link search-btn" @click="expanded = true">
<!-- Search icon button -->
<button class="btn btn-link search-btn">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
</div>
<i class="fa-solid fa-magnifying-glass"></i>
</button>
<!-- Autocomplete dropdown list -->
<ul ref="searchList" class="autocomplete-dropdown dropdown-menu dropdown-menu-right d-none" role="listbox">
</ul>
</div>
<!-- Autocomplete dropdown list -->
<ul ref="searchList" class="autocomplete-dropdown dropdown-menu dropdown-menu-right d-none" role="listbox">
</ul>
</template>
<script setup>
@ -46,6 +48,7 @@ const props = defineProps({
const expanded = ref(false);
let searchLeaveTimer = null;
/* ======================================================
* Reactive state & constants
@ -336,6 +339,20 @@ const handleKeys = function (e) {
* Lifecycle
* ====================================================== */
function onSearchEnter() {
clearTimeout(searchLeaveTimer);
expanded.value = true;
}
function onSearchLeave() {
searchLeaveTimer = setTimeout(() => {
if (!searchInput.value?.value) {
expanded.value = false;
}
searchList.value?.classList.add("d-none");
}, 250);
}
const outsideClickHandler = function (e) {
const isOutside =
!searchContainer.value?.contains(e.target) &&
@ -365,6 +382,12 @@ onBeforeUnmount(() => {
});
</script>
<style>
.search-box-wrapper {
position: relative;
display: inline-flex;
align-items: center;
}
.ps-4-5 {
padding-left: 2rem !important;
}
@ -378,22 +401,29 @@ onBeforeUnmount(() => {
.autocomplete-dropdown {
position: absolute;
top: 100%;
top: calc(100% + 4px);
right: 0;
display: inline-block;
width: auto;
min-width: 100%;
min-width: 240px;
max-width: min(400px, 40vw);
white-space: nowrap;
z-index: 10;
z-index: 1060;
max-height: 60vh;
scrollbar-gutter: stable;
overflow-y: auto;
overflow-x: hidden;
}
.autocomplete-dropdown .dropdown-item {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.expandable-search {
width: 40px;
/* collapsed */
transition: width 0.3s ease;
width: 36px;
transition: width 0.25s ease;
overflow: hidden;
}
@ -408,14 +438,14 @@ onBeforeUnmount(() => {
.expandable-search .search-btn {
position: absolute;
right: 8px;
right: 0;
color: var(--icon-color);
z-index: 2;
padding: 0.375rem 0.5rem;
}
.expandable-search.expanded {
width: 250px;
/* expanded */
width: 220px;
}
.expandable-search.expanded input {

View file

@ -195,22 +195,29 @@ import { default as MultiPieChart } from "./charts/multi-pie-chart.vue";
import { default as PieChart } from "./charts/pie-chart.vue";
import { default as LineChart } from "./charts/line-chart.vue";
import { default as PeityChart } from "./charts/peity.vue";
import { default as Chatbot } from "./chatbot.vue";
import { default as PageChatbot } from "./page-chatbot.vue";
import { default as FlowChatbotSidebar } from "./flow-chatbot-sidebar.vue";
// app shell
import { default as AppShell } from "./app-shell.vue";
import { default as AppFooter } from "./app-footer.vue";
let ntopVue = {
Chatbot: Chatbot,
PageChatbot: PageChatbot,
FlowChatbotSidebar: FlowChatbotSidebar,
// app shell
AppShell: AppShell,
AppFooter: AppFooter,
// graphs
MultiPieChart: MultiPieChart,
PieChart: PieChart,
PeityChart: PeityChart,
LineChart: LineChart,
// pages
PageAlertStats: PageAlertStats,
PageFlowHistorical: PageFlowHistorical,
@ -405,5 +412,6 @@ let ntopVue = {
ModalSNMPInterfacesAutoRole: ModalSNMPInterfacesAutoRole,
Vue: Vue,
createApp: (options) => Vue.createApp(options).use(window.vuetify),
};
window.ntopVue = ntopVue;

@ -1 +1 @@
Subproject commit 97c7f17dc15382297b0aacd85c7dbeaf42dad37f
Subproject commit cbe5b89116c197109b1a797dc194e7509e32c0e4

View file

@ -0,0 +1,774 @@
{
"sections": [
{
"key": "dashboard",
"i18n": "index_page.dashboard",
"icon": "fas fa-tachometer-alt",
"entries": [
{
"key": "traffic_dashboard",
"i18n": "dashboard.traffic_dashboard",
"icon": "fas fa-chart-area",
"url": "/lua/index.lua"
},
{
"key": "assets_dashboard",
"i18n": "assets",
"icon": "fas fa-boxes",
"url": "/lua/pro/assets_dashboard.lua"
},
{
"key": "gateways_users",
"i18n": "dashboard.gateways_users",
"icon": "fas fa-users",
"url": "/lua/pro/nedge/dashboard.lua"
},
{
"key": "traffic_report",
"i18n": "report.traffic_report",
"icon": "fas fa-file-alt",
"url": "/lua/pro/report.lua"
},
{
"key": "hr_chart",
"i18n": "hr_chart.title",
"icon": "fas fa-clock",
"url": "/lua/pro/hr_chart.lua"
}
]
},
{
"key": "monitoring",
"i18n": "monitoring",
"icon": "fas fa-eye",
"entries": [
{
"key": "active_monitoring",
"i18n": "active_monitoring_stats.active_monitoring",
"icon": "fas fa-heartbeat",
"url": "/lua/active_monitoring.lua"
},
{
"key": "network_discovery",
"i18n": "discover.network_discovery",
"icon": "fas fa-search",
"url": "/lua/discover.lua"
},
{
"key": "infrastructure_dashboard",
"i18n": "infrastructure_dashboard.infrastructure_dashboard",
"icon": "fas fa-project-diagram",
"url": "/lua/pro/enterprise/infrastructure_config.lua"
},
{
"key": "snmp_monitoring",
"i18n": "prefs.snmp",
"icon": "fas fa-network-wired",
"url": "/lua/pro/enterprise/snmpdevices_stats.lua"
},
{
"key": "vulnerability_scan",
"i18n": "scan_hosts",
"icon": "fas fa-shield-alt",
"url": "/lua/vulnerability_scan.lua"
}
]
},
{
"key": "alerts",
"i18n": "details.alerts",
"icon": "fas fa-exclamation-triangle",
"entries": [
{
"key": "alerts_list",
"i18n": "details.alerts_list",
"icon": "fas fa-list",
"url": "/lua/alert_stats.lua"
},
{
"key": "alerts_graph",
"i18n": "details.alerts_graph",
"icon": "fas fa-chart-bar",
"url": "/lua/pro/alerts_graph.lua"
},
{
"key": "alerts_geomap",
"i18n": "details.alerts_geomap",
"icon": "fas fa-map-marked-alt",
"url": "/lua/pro/alerts_geomap.lua"
},
{
"key": "alerts_analysis",
"i18n": "details.alerts_analysis",
"icon": "fas fa-microscope",
"url": "/lua/pro/enterprise/alerts_analysis.lua"
},
{
"key": "divider",
"i18n": "menu_group.notifications"
},
{
"key": "notifications",
"i18n": "endpoint_notifications.notifications",
"icon": "fas fa-bell",
"url": "/lua/admin/endpoint_notifications_list.lua"
}
]
},
{
"key": "flows",
"i18n": "flows",
"icon": "fas fa-stream",
"entries": [
{
"key": "active_flows",
"i18n": "active_flows",
"icon": "fas fa-water",
"url": "/lua/flows_stats.lua"
},
{
"key": "db_explorer",
"i18n": "historical_flows",
"icon": "fas fa-history",
"url": "/lua/pro/db_search.lua"
},
{
"key": "server_ports",
"i18n": "server_ports.server_ports",
"icon": "fas fa-plug",
"url": "/lua/server_ports.lua"
},
{
"key": "divider",
"i18n": "menu_group.routing"
},
{
"key": "bgp_looking_glass",
"i18n": "bgp_looking_glass",
"icon": "fas fa-route",
"url": "/lua/bgp_looking_glass.lua"
}
]
},
{
"key": "nanalyst",
"i18n": "nanalyst",
"icon": "fas fa-robot",
"entries": [
{
"key": "nanalyst",
"i18n": "nanalyst",
"icon": "fas fa-brain",
"url": "/lua/pro/nanalyst.lua"
},
{
"key": "ai_policy",
"i18n": "ai_policy",
"icon": "fas fa-file-contract",
"url": "/lua/pro/ai_policy.lua"
},
{
"key": "analyst_pipeline",
"i18n": "analyst_pipeline",
"icon": "fas fa-sitemap",
"url": "/lua/pro/analyst_pipeline.lua"
},
{
"key": "divider",
"i18n": "menu_group.stats"
},
{
"key": "ai_usage_stats",
"i18n": "ai_usage_stats",
"icon": "fas fa-chart-pie",
"url": "/lua/pro/ai_stats.lua"
},
{
"key": "ai_audit",
"i18n": "ai_audit",
"icon": "fas fa-clipboard-list",
"url": "/lua/pro/ai_audit.lua"
}
]
},
{
"key": "views",
"i18n": "views",
"icon": "fas fa-bars",
"entries": [
{
"key": "nedge_flows",
"i18n": "flows",
"icon": "fas fa-stream",
"url": "/lua/flows_stats.lua"
},
{
"key": "nedge_hosts",
"i18n": "hosts",
"icon": "fas fa-laptop",
"url": "/lua/hosts_stats.lua"
},
{
"key": "nedge_devices",
"i18n": "mac_stats.devices",
"icon": "fas fa-hdd",
"url": "/lua/macs_stats.lua?devices_mode=source_macs_only"
},
{
"key": "nedge_db_explorer",
"i18n": "historical_flows",
"icon": "fas fa-history",
"url": "/lua/pro/db_search.lua"
},
{
"key": "nedge_users",
"i18n": "users",
"icon": "fas fa-users",
"url": "/lua/pool_stats.lua"
},
{
"key": "nedge_vlans",
"i18n": "vlan_stats.vlan_list",
"icon": "fas fa-network-wired",
"url": "/lua/vlan_stats.lua"
},
{
"key": "nedge_geo_map",
"i18n": "geo_map",
"icon": "fas fa-map",
"url": "/lua/hosts_geomap.lua"
},
{
"key": "divider",
"i18n": "menu_group.asn_mode"
},
{
"key": "hosts_asn_mode",
"i18n": "hosts",
"icon": "fas fa-laptop",
"url": "/lua/hosts_stats.lua"
},
{
"key": "active_flows_asn_mode",
"i18n": "active_flows",
"icon": "fas fa-water",
"url": "/lua/flows_stats.lua"
},
{
"key": "db_explorer_asn_mode",
"i18n": "historical_flows",
"icon": "fas fa-history",
"url": "/lua/pro/db_search.lua"
},
{
"key": "server_ports_asn_mode",
"i18n": "server_ports.server_ports",
"icon": "fas fa-plug",
"url": "/lua/server_ports.lua"
},
{
"key": "divider",
"i18n": "menu_group.routing"
},
{
"key": "bgp_looking_glass_asn",
"i18n": "bgp_looking_glass",
"icon": "fas fa-route",
"url": "/lua/bgp_looking_glass.lua"
}
]
},
{
"key": "hosts",
"i18n": "hosts",
"icon": "fas fa-laptop",
"entries": [
{
"key": "hosts",
"i18n": "hosts",
"icon": "fas fa-laptop",
"url": "/lua/hosts_stats.lua"
},
{
"key": "devices",
"i18n": "layer_2",
"icon": "fas fa-hdd",
"url": "/lua/macs_stats.lua"
},
{
"key": "divider",
"i18n": "menu_group.inventory"
},
{
"key": "assets",
"i18n": "assets",
"icon": "fas fa-boxes",
"url": "/lua/pro/assets.lua"
}
]
},
{
"key": "collection",
"i18n": "flow_devices.exporters",
"icon": "fas fa-sitemap",
"entries": [
{
"key": "sflow_exporters",
"i18n": "flows_page.sflow_devices",
"icon": "fas fa-server",
"url": "/lua/pro/enterprise/sflowdevices_stats.lua"
},
{
"key": "nprobe",
"i18n": "flow_devices.nprobe_instances",
"icon": "fas fa-satellite-dish",
"url": "/lua/pro/enterprise/nprobe.lua"
},
{
"key": "exporters_interfaces_stats",
"i18n": "flow_devices.exporters_interfaces_stats",
"icon": "fas fa-chart-line",
"url": "/lua/pro/exporters_interfaces_stats.lua"
},
{
"key": "observation_points",
"i18n": "flow_devices.observation_points",
"icon": "fas fa-eye",
"url": "/lua/pro/enterprise/observation_points.lua"
}
]
},
{
"key": "maps",
"i18n": "maps",
"icon": "fas fa-map",
"entries": [
{
"key": "analysis_map",
"i18n": "analysis",
"icon": "fas fa-project-diagram",
"url": "/lua/pro/enterprise/network_maps.lua?map=service_map"
},
{
"key": "geo_map",
"i18n": "geo_map.geo_map",
"icon": "fas fa-globe",
"url": "/lua/hosts_geomap.lua"
},
{
"key": "hosts_map",
"i18n": "flows_page.hosts",
"icon": "fas fa-map-marked-alt",
"url": "/lua/pro/enterprise/hosts_map.lua"
}
]
},
{
"key": "if_stats",
"i18n": "interface",
"icon": "fas fa-ethernet",
"entries": [
{
"key": "interface",
"i18n": "interface_details",
"icon": "fas fa-info-circle",
"url": "/lua/if_stats.lua"
},
{
"key": "divider",
"i18n": "menu_group.segments"
},
{
"key": "networks",
"i18n": "networks",
"icon": "fas fa-network-wired",
"url": "/lua/network_stats.lua"
},
{
"key": "host_pools",
"i18n": "host_pools.host_pools",
"icon": "fas fa-layer-group",
"url": "/lua/pool_stats.lua"
},
{
"key": "autonomous_systems",
"i18n": "as_stats.autonomous_systems",
"icon": "fas fa-globe",
"url": "/lua/as_stats.lua"
},
{
"key": "countries",
"i18n": "countries",
"icon": "fas fa-flag",
"url": "/lua/country_stats.lua"
},
{
"key": "vlans",
"i18n": "vlan_stats.vlans",
"icon": "fas fa-tags",
"url": "/lua/vlan_stats.lua"
},
{
"key": "divider",
"i18n": "menu_group.containers"
},
{
"key": "pods",
"i18n": "pods",
"icon": "fab fa-docker",
"url": "/lua/pods_stats.lua"
},
{
"key": "containers",
"i18n": "containers",
"icon": "fas fa-cube",
"url": "/lua/containers_stats.lua"
}
]
},
{
"key": "health",
"i18n": "health",
"icon": "fas fa-heartbeat",
"entries": [
{
"key": "system_status",
"i18n": "system_status",
"icon": "fas fa-server",
"url": "/lua/system_stats.lua"
},
{
"key": "interfaces_status",
"i18n": "system_interfaces_status",
"icon": "fas fa-ethernet",
"url": "/lua/system_interfaces_stats.lua"
},
{
"key": "divider",
"i18n": "menu_group.storage"
},
{
"key": "influxdb_status",
"i18n": "InfluxDB",
"icon": "fas fa-database",
"url": "/lua/monitor/influxdb_monitor.lua"
},
{
"key": "redis_status",
"i18n": "Redis",
"icon": "fas fa-memory",
"url": "/lua/monitor/redis_monitor.lua"
},
{
"key": "clickhouse_status",
"i18n": "ClickHouse",
"icon": "fas fa-table",
"url": "/lua/pro/enterprise/monitor/clickhouse_monitor.lua"
}
]
},
{
"key": "pollers",
"i18n": "pollers",
"icon": "fas fa-satellite-dish",
"entries": [
{
"key": "assets_snmp",
"i18n": "assets_snmp",
"icon": "fas fa-boxes",
"url": "/lua/pro/assets.lua"
},
{
"key": "snmp",
"i18n": "prefs.snmp",
"icon": "fas fa-network-wired",
"url": "/lua/pro/enterprise/snmpdevices_stats.lua"
},
{
"key": "active_monitoring_system",
"i18n": "active_monitoring_stats.active_monitoring",
"icon": "fas fa-heartbeat",
"url": "/lua/active_monitoring.lua"
},
{
"key": "infrastructure_dashboard_system",
"i18n": "infrastructure_dashboard.infrastructure_dashboard",
"icon": "fas fa-project-diagram",
"url": "/lua/pro/enterprise/infrastructure_config.lua"
}
]
},
{
"key": "notifications",
"i18n": "endpoint_notifications.notifications",
"icon": "fas fa-bell",
"url": "/lua/admin/endpoint_notifications_list.lua"
},
{
"key": "policies",
"i18n": "policies",
"icon": "fas fa-shield-alt",
"entries": [
{
"key": "divider",
"i18n": "menu_group.access"
},
{
"key": "access_control_list",
"i18n": "access_control_list",
"icon": "fas fa-list-ul",
"url": "/lua/pro/admin/access_control_list.lua"
},
{
"key": "device_protocols",
"i18n": "device_protocols.device_protocols",
"icon": "fas fa-sliders-h",
"url": "/lua/admin/edit_device_protocols.lua"
},
{
"key": "device_exclusions",
"i18n": "edit_check.device_exclusion",
"icon": "fas fa-ban",
"url": "/lua/pro/admin/edit_device_exclusions.lua"
},
{
"key": "network_config",
"i18n": "checks.network_configuration",
"icon": "fas fa-network-wired",
"url": "/lua/admin/network_configuration.lua"
},
{
"key": "traffic_rules",
"i18n": "if_stats_config.traffic_rules",
"icon": "fas fa-traffic-light",
"url": "/lua/pro/traffic_rules.lua"
},
{
"key": "divider",
"i18n": "menu_group.checks"
},
{
"key": "scripts_config",
"i18n": "about.checks",
"icon": "fas fa-check-double",
"url_dynamic": "scripts_config_url"
},
{
"key": "alert_exclusions",
"i18n": "edit_check.exclusion_list",
"icon": "fas fa-filter",
"url": "/lua/pro/admin/edit_alert_exclusions.lua?subdir=host"
},
{
"key": "divider",
"i18n": "menu_group.profiles"
},
{
"key": "profiles",
"i18n": "traffic_profiles.traffic_profiles",
"icon": "fas fa-tags",
"url": "/lua/pro/admin/edit_profiles.lua"
}
]
},
{
"key": "admin",
"i18n": "settings",
"icon": "fas fa-cog",
"entries": [
{
"key": "divider",
"i18n": "menu_group.users"
},
{
"key": "nedge_users",
"i18n": "nedge.users",
"icon": "fas fa-users",
"url": "/lua/pro/nedge/admin/nf_list_users.lua"
},
{
"key": "manage_users",
"i18n": "manage_users.manage_users",
"icon": "fas fa-user-cog",
"url": "/lua/admin/users.lua"
},
{
"key": "divider",
"i18n": "menu_group.configuration"
},
{
"key": "preferences",
"i18n": "prefs.preferences",
"icon": "fas fa-sliders-h",
"url": "/lua/admin/prefs.lua"
},
{
"key": "tags",
"i18n": "tags_page.tags",
"icon": "fas fa-tag",
"url": "/lua/tags.lua"
},
{
"key": "category_lists",
"i18n": "category_lists.category_lists",
"icon": "fas fa-list",
"url": "/lua/admin/blacklists.lua?enabled_status=enabled"
},
{
"key": "manage_configurations",
"i18n": "manage_configurations.manage_configurations",
"icon": "fas fa-file-export",
"url": "/lua/admin/manage_configurations.lua"
},
{
"key": "divider",
"i18n": "menu_group.customization"
},
{
"key": "categories",
"i18n": "custom_categories.apps_and_categories",
"icon": "fas fa-th-large",
"url": "/lua/admin/edit_categories.lua"
}
]
},
{
"key": "dev",
"i18n": "developer",
"icon": "fas fa-code",
"entries": [
{
"key": "rest_api",
"i18n": "swagger_api",
"icon": "fas fa-plug",
"url": "/lua/swagger.lua"
},
{
"key": "analyze_pcap",
"i18n": "upload_pcap",
"icon": "fas fa-file-upload",
"url": "/lua/upload_pcap.lua"
},
{
"key": "manage_data",
"i18n": "manage_data.manage_data",
"icon": "fas fa-database",
"url": "/lua/manage_data.lua"
},
{
"key": "divider",
"i18n": "menu_group.internals"
},
{
"key": "checks_dev",
"i18n": "about.checks",
"icon": "fas fa-check-square",
"url": "/lua/checks_overview.lua"
},
{
"key": "alert_definitions",
"i18n": "about.alert_defines",
"icon": "fas fa-exclamation-circle",
"url": "/lua/defs_overview.lua"
},
{
"key": "ts_definitions",
"i18n": "about.ts_definitions",
"icon": "fas fa-chart-line",
"url": "/lua/ts_overview.lua"
},
{
"key": "directories",
"i18n": "about.directories",
"icon": "fas fa-folder-open",
"url": "/lua/directories.lua"
},
{
"key": "divider",
"i18n": "menu_group.documentation"
},
{
"key": "api",
"i18n": "about.api_reference",
"icon": "fas fa-book-open",
"url": "https://www.ntop.org/guides/ntopng/api/",
"is_external": true
}
]
},
{
"key": "system_stats",
"i18n": "system",
"icon": "fas fa-server",
"entries": []
},
{
"key": "about",
"i18n": "help",
"icon": "fas fa-info-circle",
"entries": [
{
"key": "about",
"i18n": "about.about",
"icon": "fas fa-info",
"url": "/lua/about.lua"
},
{
"key": "license",
"i18n": "license_page.license",
"icon": "fas fa-id-card",
"url": "/lua/license.lua"
},
{
"key": "limits",
"i18n": "limits_page.limits",
"icon": "fas fa-tachometer-alt",
"url": "/lua/limits.lua"
},
{
"key": "divider",
"i18n": "menu_group.community"
},
{
"key": "blog",
"i18n": "about.ntop_blog",
"icon": "fas fa-rss",
"url": "http://blog.ntop.org/",
"is_external": true
},
{
"key": "telegram",
"i18n": "about.telegram",
"icon": "fab fa-telegram-plane",
"url": "https://t.me/ntop_community",
"is_external": true
},
{
"key": "manual",
"i18n": "about.user_manual",
"icon": "fas fa-book",
"url": "https://www.ntop.org/guides/ntopng/",
"is_external": true
},
{
"key": "divider",
"i18n": "menu_group.support"
},
{
"key": "report_issue",
"i18n": "about.report_issue",
"icon": "fas fa-bug",
"url": "https://github.com/ntop/ntopng/issues",
"is_external": true
},
{
"key": "suggest_feature",
"i18n": "about.suggest_feature",
"icon": "fas fa-lightbulb",
"url": "https://www.ntop.org/support/need-help-2/contact-us/",
"is_external": true
}
]
}
]
}

View file

@ -46,16 +46,17 @@
"stylelint": "^14.5.0"
},
"dependencies": {
"@cyclonedx/cyclonedx-npm": "^5.0.0",
"@fortawesome/fontawesome-free": "^6.5.1",
"@popperjs/core": "^2.11.2",
"@rollup/plugin-inject": "^5.0.5",
"@vitejs/plugin-vue": "^5.2.4",
"@vue/compiler-sfc": "^3.5.35",
"@vueform/slider": "^2.1.10",
"@yaireo/tagify": "^4.35.3",
"apexcharts": "^3.33.1",
"bootstrap": "^5.3.0",
"bootstrap-icons": "^1.13.1",
"d3": "^3.5.17",
"d3-array": "^3.1.1",
"d3-chord": "^3.0.1",
"d3-collection": "^1.0.7",
@ -88,7 +89,7 @@
"uuid": "^11.1.0",
"vis-network": "^9.1.6",
"vite": "^6.3.5",
"vue": "3.2.37",
"vue": "^3.5.35",
"world-atlas": "^2.0.2"
}
}

View file

@ -1109,6 +1109,9 @@ local lang = {
["component_versions"] = "Component Versions",
["startup_line"] = "Startup Line",
["suggest_feature"] = "Suggest a Feature",
["user_manual"] = "User Manual",
["ts_definitions"] = "Time Series",
["api_reference"] = "API Reference",
["system"] = "System",
["system_id"] = "System Id",
["system_info"] = "System Information",
@ -9982,6 +9985,26 @@ local lang = {
["abort_edit"] = "Abort edit of a Widget?",
},
},
["menu_group"] = {
["notifications"] = "Notifications",
["routing"] = "Routing",
["stats"] = "Stats",
["asn_mode"] = "ASN Mode",
["inventory"] = "Inventory",
["segments"] = "Segments",
["containers"] = "Containers",
["storage"] = "Storage",
["access"] = "Access",
["checks"] = "Checks",
["profiles"] = "Profiles",
["users"] = "Users",
["configuration"] = "Configuration",
["customization"] = "Customization",
["internals"] = "Internals",
["documentation"] = "Documentation",
["community"] = "Community",
["support"] = "Support",
},
}
return lang

View file

@ -2438,7 +2438,7 @@ if isEmptyString(page) or page == "overview" then
}
},
};
const vue = ntopVue.Vue.createApp(vue_options);
const vue = ntopVue.createApp(vue_options);
const vue_app = vue.mount("#vue-modals");
return vue_app;
}

View file

@ -1,874 +1,60 @@
--
-- (C) 2013-26 - ntop.org
--
require "os"
-- Thin shell: mounts AppFooter Vue component, then closes the page.
-- All footer logic (updates, ext_link_dialog, nEdge modals, 403 handler)
-- lives in http_src/vue/app-footer.vue.
--
local dirs = ntop.getDirs()
package.path = dirs.installdir .. "/scripts/lua/modules/?.lua;" .. package.path
package.path = dirs.installdir .. "/scripts/lua/modules/toasts/?.lua;" .. package.path
require "lua_utils"
local ts_utils = require("ts_utils_core")
local template_utils = require "template_utils"
local json = require "dkjson"
local ts_utils = require "ts_utils_core"
local template = require "template_utils"
local stats_utils = require("stats_utils")
local page_utils = require "page_utils"
local have_nedge = ntop.isnEdge()
local info = ntop.getInfo(true)
local is_admin = isAdministrator()
local maxSpeed
local ifid = interface.getId()
local _ifstats = interface.getStats()
local is_admin = isAdministrator()
local have_nedge = ntop.isnEdge()
local info = ntop.getInfo(true)
local http_prefix = ntop.getHttpPrefix()
local random_csrf = ntop.getRandomCSRFValue()
local is_pcap_dump_iface = interface.isPcapDumpInterface()
if not is_pcap_dump_iface and not have_nedge then
-- if the speed in not custom we try to read the speed from the interface
-- and, as a final resort, we use 1Gbps
if tonumber(_ifstats.speed) ~= nil then
maxSpeed = tonumber(_ifstats.speed) * 1e6
else
maxSpeed = 1000000000 -- 1 Gbit
end
end -- closes is_pcap_dump_iface == false
if not info.oem then
-- print ([[
-- <hr>
-- <footer id="n-footer">
-- <div class="container-fluid">
-- <div class="row mt-2">
-- <div class="col-12 col-md-4 pl-md-0 text-center text-md-start">
-- <small>
-- <a href="https://www.ntop.org/products/traffic-analysis/ntop/" target="_blank">
-- ]] .. getNtopngRelease(info) ..[[
-- </a>
-- </small>
-- </div>
-- <div class="col-12 col-md-4 text-center">
-- <small>]].. ntop.getInfo()["copyright"] ..[[</small>
-- ]])
-- print [[
-- </div>
-- <div class="col-12 col-md-4 text-center text-md-end pr-md-0">
-- <small>
-- <i class="fas fa-clock" title="]] print(i18n("about.server_time")) print[["></i> <div class="d-inline-block" id='network-clock'></div> ]] if(info.tzname ~= nil) then print(info.tzname) end print(" | ") print(i18n("about.uptime")) print[[: <div class="d-inline-block" id='network-uptime'></div>
-- </small>
-- </div>
-- </div>
-- </div>
-- </footer>
-- ]]
-- else -- info.oem
-- print[[<div class="col-12 text-end">
-- <small>
-- <i class="fas fa-clock"></i> <div class="d-inline-block" id='network-clock' title="]] print(i18n("about.server_time")) print[["></div> | ]] print(i18n("about.uptime")) print[[: <div class="d-inline-block" id='network-uptime'></div>
-- </small>
-- </div>]]
end
local traffic_peity_width = "96"
-- Show InfluxDB error immediately if one is cached
if ts_utils.getDriverName() == "influxdb" then
local msg = ntop.getCache("ntopng.cache.influxdb.last_error")
if not isEmptyString(msg) then
print([[
<script type="text/javascript">
$("#influxdb-error-msg-text").html("]] .. (msg:gsub('"', '\\"')) .. [[");
$("#influxdb-error-msg").show();
</script>
]])
end
local msg = ntop.getCache("ntopng.cache.influxdb.last_error")
if not isEmptyString(msg) then
msg = msg:gsub('"', '\\"')
print([[<script type="text/javascript">
document.getElementById("influxdb-error-msg-text").innerHTML = "]] .. msg .. [[";
document.getElementById("influxdb-error-msg").style.display = "";
</script>]])
end
end
-- Dismiss Notification Code and Toggle Dark theme
print([[
<script type='text/javascript'>
$(document).ready(function() {
$(`.notification button.dismiss`).click(function() {
const $toast = $(this).parents('.notification');
const id = $toast.data("toastId");
ToastUtils.dismissToast(id, "]] .. random_csrf ..
[[", (data) =>{
if (data.success) $toast.toast('hide');
});
});
$(`.toggle-dark-theme`).click(function() {
const request = $.post(`]] .. http_prefix ..
[[/lua/update_prefs.lua`, {
action: 'toggle_theme', toggle_dark_theme: ]] ..
tostring(not page_utils.is_dark_mode_enabled()) .. [[,
csrf: "]] .. random_csrf .. [["
});
request.done(function(res) {
if (res.success) location.reload();
});
});
});
</script>
]])
-- Restart product code
if (is_admin and ntop.isPackage() and not ntop.isWindows()) then
print(template.gen("modal_confirm_dialog.html", {
dialog = {
id = 'restart-modal',
action = 'restartService()',
title = i18n("restart.restart_product", {product = info.product}),
message = i18n("restart.confirm", {product = info.product}),
custom_alert_class = 'alert alert-danger',
confirm = i18n('restart.restart'),
confirm_button = 'btn-danger'
}
}))
print [[
<script type="text/javascript">
const restartCSRF = ']]
print(random_csrf)
print [[';
const restartService = function() {
$.ajax({
type: 'POST',
url: ']]
print(http_prefix)
print [[/lua/admin/service_restart.lua',
data: {
csrf: restartCSRF
},
success: function(rsp) {
alert("]]
print(i18n("restart.restarting", {product = info.product}))
print [[");
$('#restart-modal').modal('hide');
}
});
}
$('.restart-service').click(() => {
$('#restart-modal').modal('show');
});
</script>
]]
end
-- render switchable system view
print([[
<script type="text/javascript">
$(document).ready(function() {
// ignore scientific numbers input
$(`.ignore-scientific`).keypress(function(e) {
if (e.which != 8 && e.which != 0 && e.which < 48 || e.which > 57) {
e.preventDefault();
}
})
ToastUtils.initToasts();
});
const toggleSystemInterface = ($form = null) => {
if($form != null) {
$form.submit();
}
else {
console.error("An error has occurred when switching interface!");
}
}
]])
print([[
</script>
]])
print [[
<script type="text/javascript">
]]
print [[
var is_historical = false;
let updatingChart_uploads = [
$("#n-navbar .network-load-chart-upload").show().peity("line", { width: ]]
print(traffic_peity_width)
print [[, max: null }),
$(".mobile-menu-stats .network-load-chart-upload").show().peity("line", { width: ]]
print(traffic_peity_width)
print [[, max: null }),
];
let updatingChart_downloads = [
$("#n-navbar .network-load-chart-download").show().peity("line", { width: ]]
print(traffic_peity_width)
print [[, max: null, fill: "lightgreen"}),
$(".mobile-menu-stats .network-load-chart-download").show().peity("line", { width: ]]
print(traffic_peity_width)
print [[, max: null, fill: "lightgreen"})
];
let updatingChart_totals = [
$("#n-navbar .network-load-chart-total").show().peity("line", { width: ]]
print(traffic_peity_width)
print [[, max: null}),
$(".mobile-menu-stats .network-load-chart-total").show().peity("line", { width: ]]
print(traffic_peity_width)
print [[, max: null})
];
$(`.copy-http-url`).on('click', function() {
let sampleTextarea = document.createElement("textarea");
document.body.appendChild(sampleTextarea);
sampleTextarea.value = $(this).attr('data-to-copy');
sampleTextarea.select(); //select textarea content
document.execCommand("copy");
document.body.removeChild(sampleTextarea);
-- AppFooter boot context
local footer_context = json.encode({
csrf = random_csrf,
http_prefix = http_prefix,
ifid = tostring(interface.getId()),
is_admin = is_admin,
is_nedge = have_nedge,
has_updates_support = (hasSoftwareUpdatesSupport() == true),
is_package = (ntop.isPackage() == true),
is_windows = ntop.isWindows(),
product = info.product or "",
maintenance_expired = (info["pro.license_days_left"] ~= nil and
tonumber(info["pro.license_days_left"]) <= 0),
})
let isFooterRefreshInProcess = false;
const footerRefresh = function() {
if (isFooterRefreshInProcess == true) { return; }
isFooterRefreshInProcess = true;
$.ajax({
type: 'GET',
url: ']]
print(http_prefix)
print [[/lua/rest/v2/get/interface/data.lua',
data: { ifid: ]]
print(tostring(ifid))
print [[, type: 'summary' },
success: function(content) {
isFooterRefreshInProcess = false;
if(content["rc_str"] != "OK") {
return;
}
const rsp = content["rsp"];
try {
ntopng_events_manager.emit_custom_event(ntopng_custom_events.GET_INTERFACE_DATA, rsp);
let bps_upload = rsp.throughput.upload * 8;
let bps_download = rsp.throughput.download * 8;
/* Upload chart */
let values = updatingChart_uploads[0].text().split(",")
values.shift();
values.push(bps_upload.toString());
updatingChart_uploads[0].text(values.join(",")).change();
updatingChart_uploads[1].text(values.join(",")).change();
$('.chart-upload-text:visible').html(NtopUtils.bitsToSize(bps_upload, 1000));
/* Download chart */
values = updatingChart_downloads[0].text().split(",")
values.shift();
values.push((-bps_download).toString());
updatingChart_downloads[0].text(values.join(",")).change();
updatingChart_downloads[1].text(values.join(",")).change();
$('.chart-download-text:visible').html(NtopUtils.bitsToSize(bps_download, 1000));
/* Disaggregate interface chart */
values = updatingChart_totals[0].text().split(",")
values.shift();
values.push(rsp.throughput_bps * 8);
updatingChart_totals[0].text(values.join(",")).change();
updatingChart_totals[1].text(values.join(",")).change();
]]
if (is_pcap_dump_iface == false) and (not have_nedge) then
print [[
const v = Math.round(Math.min((rsp.throughput_bps * 8 *100)/]]
print(string.format("%u", maxSpeed))
print [[, 100));
$('.network-load').html(v+"%");
]]
end
-- systemInterfaceEnabled is defined inside menu.lua
print [[
//$('#network-clock').html(`${rsp.localtime}`);
//$('#network-uptime').html(`${rsp.uptime}`);
let msg = `<div class='m-2'><div class='d-flex flex-wrap navbar-main-badges'>`;
if(rsp.out_of_maintenance) {
msg += "<a href='https://www.ntop.org/support/faq/how-can-i-renew-maintenance-for-commercial-products/' target='_blank'><span class='badge bg-warning'>]]
print(i18n("about.maintenance_expired", {product = info["product"]}))
print [[ <i class=\"fas fa-external-link-alt\"></i></span></a> ";
}
if(rsp.degraded_performance) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/system_interfaces_stats.lua?page=internals&tab=periodic_activities&periodic_script_issue=any_issue'>"
msg += "<span class='badge bg-warning'><i class='fas fa-exclamation-triangle data-bs-toggle='tooltip' data-bs-placement='bottom' title=']]
print(i18n("internals.degraded_performance"))
print [['></i></span></a>";
}
if ((rsp.engaged_alerts > 0 || rsp.alerted_flows > 0) && ]]
print(ternary(hasAllowedNetworksSet(), "false", "true"))
print [[) {
var error_color = "#B94A48";
if(rsp.engaged_alerts > 0) {
let alerts_badge = "bg-info";
if(rsp.engaged_alerts_error > 0) alerts_badge = "bg-danger";
else if(rsp.engaged_alerts_warning > 0) alerts_badge = "bg-warning";
msg += "<a href=']]
print(http_prefix)
print [[/lua/alert_stats.lua?ifid=]]
print(tostring(ifid))
print [[&status=engaged'>"
msg += "<span class=\"badge " + alerts_badge + "\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=']] print(i18n('graphs.engaged_alerts')) print[['><i class=\"fas fa-exclamation-triangle\"></i> "+NtopUtils.formatValue(rsp.engaged_alerts, 1)+"</span></a>";
}
if(rsp.alerted_flows_warning > 0 && !(systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/flows_stats.lua?status=warning'>"
msg += "<span class=\"badge bg-warning\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=']]
print(i18n("flow_details.alerted_flows"))
print [['>"+NtopUtils.formatValue(rsp.alerted_flows_warning, 1)+ " <i class=\"fas fa-stream\"></i> ]]
print [[ <i class=\"fas fa-exclamation-triangle\"></i></span></a>";
}
if(rsp.alerted_flows_error > 0 && !(systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/flows_stats.lua?status=error'>"
msg += "<span class=\"badge bg-danger\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=']]
print(i18n("flow_details.dangerous_flows"))
print [['>"+NtopUtils.formatValue(rsp.alerted_flows_error, 1)+ " <i class=\"fas fa-stream\"></i> ]]
print [[ <i class=\"fas fa-exclamation-triangle\"></i></span></a>";
}
if(rsp.active_discovery_active === true) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/discover.lua'>"
msg += "<span class=\"badge bg-warning\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=']]
print(i18n("prefs.network_discovery_running"))
print [['><i class=\"fas fa-project-diagram\"></i></span></a>";
}
}
if((rsp.engaged_alerts > 0 || rsp.alerted_flows > 0) && $("#alerts-id").is(":visible") == false) {
$("#alerts-id").show();
}
if(rsp.ts_alerts && rsp.ts_alerts.influxdb && (!systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/monitor/influxdb_monitor.lua?ifid=]]
print(tostring(ifid))
print [[&page=alerts#tab-table-engaged-alerts'>"
msg += "<span class=\"badge bg-danger\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=']] print(i18n('alerts_dashboard.influxdb_error')) print[['><i class=\"fas fa-database\"></i></span></a>";
}
var alarm_threshold_low = 60; /* 60% */
var alarm_threshold_high = 90; /* 90% */
var alert = 0;
if(rsp.num_local_hosts > 0 && (!systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/hosts_stats.lua?mode=local'>";
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]]
print(i18n("local_hosts"))
print [[\" class=\"badge bg-success\">";
msg += NtopUtils.formatValue(rsp.num_local_hosts, 1);
if(rsp.num_local_rcvd_only_hosts > 0) msg += " ("+NtopUtils.formatValue(rsp.num_local_rcvd_only_hosts, 1)+")";
msg += " <i class=\"fas fa-laptop\" aria-hidden=\"true\"></i></span></a>";
}
const num_remote_hosts = rsp.num_hosts - rsp.num_local_hosts;
if(num_remote_hosts > 0 && (!systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/hosts_stats.lua?mode=remote'>";
var remote_hosts_label = "]]
print(i18n("remote_hosts"))
print [[";
if (rsp.hosts_pctg < alarm_threshold_low && !systemInterfaceEnabled) {
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + remote_hosts_label +"\" class=\"badge bg-secondary\">";
} else if (rsp.hosts_pctg < alarm_threshold_high && !systemInterfaceEnabled) {
alert = 1;
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + remote_hosts_label +"\" class=\"badge bg-warning\">";
} else {
alert = 1;
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + remote_hosts_label +"\" class=\"badge bg-danger\">";
}
msg += NtopUtils.formatValue(num_remote_hosts, 1);
num_remote_rcvd_only_hosts = rsp.num_rcvd_only_hosts - rsp.num_local_rcvd_only_hosts;
if(num_remote_rcvd_only_hosts > 0) msg += " ("+NtopUtils.formatValue(num_remote_rcvd_only_hosts, 1)+")";
msg += " <i class=\"fas fa-laptop\" aria-hidden=\"true\"></i></span></a>";
}
if(rsp.num_devices > 0 && (!systemInterfaceEnabled)) {
var macs_label = "]]
print(i18n("mac_stats.layer_2_source_devices", {device_type = ""}))
print [[";
msg += "<a href=']]
print(http_prefix)
print [[/lua/macs_stats.lua?devices_mode=source_macs_only'>";
if (rsp.macs_pctg < alarm_threshold_low) {
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + macs_label +"\" class=\"badge bg-secondary\">";
} else if(rsp.macs_pctg < alarm_threshold_high) {
alert = 1;
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + macs_label +"\" class=\"badge bg-warning\">";
} else {
alert = 1;
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + macs_label +"\" class=\"badge bg-danger\">";
}
msg += NtopUtils.formatValue(rsp.num_devices, 1)+" <i class=\"fas fa-ethernet\"></i></span></a>";
}
if(rsp.num_flows > 0 && (!systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/flows_stats.lua'>";
const flows_label = "]]
print(i18n("live_flows"))
print [["
if (rsp.flows_pctg < alarm_threshold_low) {
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + flows_label +"\" class=\"badge bg-secondary\">";
} else if(rsp.flows_pctg < alarm_threshold_high) {
alert = 1;
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + flows_label +"\" class=\"badge bg-warning\">";
} else {
alert = 1;
msg += "<span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"" + flows_label +"\" class=\"badge bg-danger\">";
}
msg += NtopUtils.formatValue(rsp.num_flows, 1)+" <i class=\"fas fa-stream\"></i> </span> </a>";
if (rsp.db && rsp.db.flow_export_drops > 0) {
const export_pctg = rsp.db.flow_export_drops / (rsp.db.flow_export_count + rsp.db.flow_export_drops + 1);
if (export_pctg > ]]
print(stats_utils.UPPER_BOUND_INFO_EXPORTS)
print [[) {
const badge_class = (export_pctg <= ]]
print(stats_utils.UPPER_BOUND_WARNING_EXPORTS)
print [[) ? 'warning' : 'danger';
msg += "<a href=']]
print(http_prefix)
print [[/lua/if_stats.lua'><span class=\"badge bg-"+badge_class+"\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]] print(i18n('flow_export_drops')) print[[\"><i class=\"fas fa-exclamation-triangle\" style=\"color: #FFFFFF;\"></i> "+NtopUtils.formatValue(rsp.db.flow_export_drops, 1)+" DB Export drop";
if(rsp.db.flow_export_drops > 1) msg += "s";
msg += "</span></a>";
}
}
if (rsp.dropped_zmq_msg > 0) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/if_stats.lua'><span class=\"badge bg-warning\"><i class=\"fas fa-tint\" style=\"color: #FFFFFF;\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]] print(i18n('dropping_zmq_msg')) print[[\"></i> "+NtopUtils.formatValue(rsp.dropped_zmq_msg, 1)+" ZMQ Drops</span></a>";
}
if (rsp.dropped_flows > 0) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/if_stats.lua'><span class=\"badge bg-warning\"><i class=\"fas fa-tint\" style=\"color: #FFFFFF;\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]] print(i18n('dropping_flows')) print[[\"></i> "+NtopUtils.formatValue(rsp.dropped_flows, 1)+" Flows Drops</span></a>";
}
}
if ((rsp.num_live_captures != undefined) && (rsp.num_live_captures > 0) && (!systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/live_capture_stats.lua'>";
msg += "<span class=\"badge bg-primary\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]] print(i18n('live_capture.active_live_captures')) print[[\">";
msg += NtopUtils.addCommas(rsp.num_live_captures)+" <i class=\"fas fa-download fa-lg\"></i></span></a>";
}
if (rsp.traffic_recording != undefined && (!systemInterfaceEnabled)) {
var status_label="primary";
var status_title="]]
print(i18n("traffic_recording.recording"))
print [[";
if (rsp.traffic_recording != "recording") {
status_label = "danger";
status_title = "]]
print(i18n("traffic_recording.failure"))
print [[";
}
msg += "<a href=']]
print(http_prefix)
print [[/lua/if_stats.lua?ifid=]]
print(tostring(ifid))
print [[&page=traffic_recording&tab=status'>";
msg += "<span class=\"badge bg-"+status_label+"\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=\""+NtopUtils.addCommas(status_title)+"\">";
msg += "<i class=\"fas fa-hdd fa-lg\"></i></span></a>";
}
if (rsp.traffic_extraction != undefined && (!systemInterfaceEnabled)) {
var status_title="]]
print(i18n("traffic_recording.traffic_extraction_jobs"))
print [[";
var status_label = "secondary";
if (rsp.traffic_extraction == "ready") status_label="primary";
msg += "<a href=']]
print(http_prefix)
print [[/lua/if_stats.lua?ifid=]]
print(tostring(ifid))
print [[&page=traffic_recording&tab=jobs'>";
msg += "<span class=\"badge bg-"+status_label+"\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=\""+NtopUtils.addCommas(status_title)+"\">";
msg += rsp.traffic_extraction_num_tasks+" <i class=\"fas fa-tasks fa-lg\"></i></span></a>";
}
if (rsp.vs_in_progress != undefined && rsp.vs_in_progress > 0 && (!systemInterfaceEnabled)) {
msg += "<a href=']]
print(http_prefix)
print [[/lua/vulnerability_scan.lua'>";
msg += "<span class=\"badge bg-primary\" data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]] print(i18n('vulnerability_scan.vulnerability_scan_in_progress')) print[[\">";
msg += NtopUtils.addCommas(rsp.vs_in_progress) +" <i class=\"fas fa-satellite-dish\"></i></span></a>";
}
if(rsp.is_loading == true) {
msg += "<blink><span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]] print(i18n('loading')) print[[\" class=\"badge bg-primary\"><i class=\"fas fa-spinner\"></i> ]]
print(i18n("loading"))
print [[</span></blink>";
}
]]
if ntop.isOffline() then
print [[
msg += "<a href=\"#\" style=\"cursor: default\"><span data-bs-toggle='tooltip' data-bs-placement='bottom' title=\"]]
print(i18n("offline"))
print [[\" class=\"badge bg-secondary\"><i class=\"fas fa-plane\"></i></span></a>";
]]
end
print [[
msg += '</div></div>';
// append the message inside the network-load element
const $msg = $(msg);
// resize element's font size to fit better
if ($msg.width() > $msg.css('max-width')) {
$msg.find('span').css('font-size', '0.748rem');
}
$('.network-load').html($msg);
NtopUtils.reloadBSTooltips();
} catch(e) {
console.warn(e);
/* alert("JSON Error (session expired?): logging out"); window.location.replace("]]
print(http_prefix)
print [[/lua/logout.lua"); */
}
},
error: function() {
isFooterRefreshInProcess = false;
}
});
}
$(document).ajaxError(function(err, response, ajaxSettings, thrownError) {
if((response.status == 403) && (response.responseText == "Login Required"))
window.location.href = "]]
print(http_prefix .. "/lua/login.lua")
print [[";
});
footerRefresh(); /* call immediately to give the UI a more responsive look */
setInterval(footerRefresh, 7000); /* re-schedule every [interface-rate] seconds */
//Automatically open dropdown-menu
$(document).ready(function(){
$('ul.nav li.dropdown').hover(function() {
$(this).find('.dropdown-menu').stop(true, true).delay(150).fadeIn(100);
}, function() {
$(this).find('.dropdown-menu').stop(true, true).delay(150).fadeOut(100);
});
$('.collapse')
.on('shown.bs.collapse', function(){
$(this).parent().find(".fa-caret-down").removeClass("fa-caret-down").addClass("fa-caret-up");
})
.on('hidden.bs.collapse', function(){
$(this).parent().find(".fa-caret-up").removeClass("fa-caret-up").addClass("fa-caret-down");
});
});
]]
-- This code rewrites the current page state after a POST request to avoid Document Expired errors
if not table.empty(_POST) then
print [[
if ((typeof(history) === "object")
&& (typeof(history).replaceState === "function")
&& (typeof(window.location.href) === "string"))
history.replaceState(history.state, "", window.location.href);
]]
end
print [[
// hide the possibly shown alerts icon in the header
]]
if not _ifstats.isView then
print("$('#alerts-li').hide();")
else
print("$('#alerts-li').show();")
end
print([[
</script>
]])
-- ######################################
if have_nedge then
print [[<form id="powerOffForm" method="post">
<input name="csrf" value="]]
print(random_csrf)
print [[" type="hidden" />
<input name="poweroff" value="" type="hidden" />
</form>
<form id="rebootForm" method="post">
<input name="csrf" value="]]
print(random_csrf)
print [[" type="hidden" />
<input name="reboot" value="" type="hidden" />
</form>]]
print(template.gen("modal_confirm_dialog.html", {
dialog = {
id = "poweroff_dialog",
action = "$('#powerOffForm').submit()",
title = i18n("nedge.power_off"),
message = i18n("nedge.power_off_confirm"),
confirm = i18n("nedge.power_off")
}
}))
print(template.gen("modal_confirm_dialog.html", {
dialog = {
id = "reboot_dialog",
action = "$('#rebootForm').submit()",
title = i18n("nedge.reboot"),
message = i18n("nedge.reboot_corfirm"),
confirm = i18n("nedge.reboot")
}
}))
end
print(template.gen("modal_confirm_dialog.html", {
dialog = {
id = "ext_link_dialog",
title = i18n("external_link"),
message = i18n("show_alerts.confirm_external_link"),
confirm = i18n("redirect")
}
}))
print [[
<script type="text/javascript">
$(document).ready(function(){
$(document).on('click','a.ntopng-external-link',function(event){
event.preventDefault();
const url=$(this)[0].href;
$("#url_ext_link_dialog").html(url+"<br/>]]
print(i18n("are_you_sure"))
print [[");
$("#btn-confirm-action_ext_link_dialog").attr('href',url);
$('#ext_link_dialog').modal('show');
});
});
</script>
]]
-- Updates submenu
if hasSoftwareUpdatesSupport() then
-- Updates check
print [[
<script type='text/javascript'>
$('#updates-info-li').html(']]
print(i18n("updates.checking"))
print [[');
$('#updates-install-li').hide();
const updates_csrf = ']]
print(random_csrf)
print [[';
/* Updates status */
var updatesStatus = '';
var updatesLastCheck = 0;
/* Install latest update */
var installUpdate = function() {
if (confirm(']]
print(i18n("updates.install_confirm"))
if info["pro.license_days_left"] ~= nil and info["pro.license_days_left"] <=
0 then
-- License is valid, however maintenance is expired: warning the user
print(" " .. i18n("updates.maintenance_expired"))
end
print [[')) {
$.ajax({
type: 'POST',
url: ']]
print(http_prefix)
print [[/lua/install_update.lua',
data: {
csrf: updates_csrf
},
success: function(rsp) {
$('#updates-info-li').html(']]
print(i18n("updates.installing"))
print [[')
$('#updates-info-li').attr('title', '');
$('#updates-install-li').hide();
$('#admin-badge').hide();
updatesStatus = 'installing';
}
});
}
}
/* Check for new updates */
var checkForUpdates = function() {
$.ajax({
type: 'POST',
url: ']]
print(http_prefix)
print [[/lua/check_update.lua',
data: {
csrf: updates_csrf,
search: 'true'
},
success: function(rsp) {
$('#updates-info-li').html(']]
print(i18n("updates.checking"))
print [[');
$('#updates-info-li').attr('title', '');
$('#updates-install-li').hide();
$('#admin-badge').hide();
updatesStatus = 'checking';
}
});
}
/* Update the menu with the current updates status */
var updatesRefresh = function() {
const now = Date.now()/1000;
let check_time_sec = 10;
if (updatesStatus == 'installing' || updatesStatus == 'checking') {
/* Go ahead (frequent update) */
} else if (updatesStatus == 'update-avail' || updatesStatus == 'upgrade-failure') {
return; /* no need to check again */
} else { /* updatesStatus == 'not-avail' || updatesStatus == 'update-failure' || updatesStatus == <other errors> || updatesStatus == '' */
check_time_sec = 300;
}
if (now < updatesLastCheck + check_time_sec) return; /* check with low freq */
updatesLastCheck = now;
$.ajax({
type: 'GET',
url: ']]
print(http_prefix)
print [[/lua/check_update.lua',
data: {},
success: function(rsp) {
if(rsp && rsp.status) {
updatesStatus = rsp.status;
if (rsp.status == 'installing') {
$('#updates-info-li').html(']]
print(i18n("updates.installing"))
print [[')
$('#updates-info-li').attr('title', '');
$('#updates-install-li').hide();
$('#admin-badge').hide();
} else if (rsp.status == 'checking') {
$('#updates-info-li').html(']]
print(i18n("updates.checking"))
print [[');
$('#updates-info-li').attr('title', '');
$('#updates-install-li').hide();
$('#admin-badge').hide();
} else if (rsp.status == 'update-avail' || rsp.status == 'upgrade-failure') {
$('#updates-info-li').html('<span class="badge bg-pill bg-danger">]]
print(i18n("updates.available"))
print [[</span> ]]
print(info["product"])
print [[ ' + rsp.version);
$('#updates-info-li').attr('title', '');
var icon = '<i class="fas fa-download"></i>';
$('#updates-install-li').attr('title', '');
if (rsp.status !== 'update-avail') {
icon = '<i class="fas fa-exclamation-triangle text-danger"></i>';
$('#updates-install-li').attr('title', "]]
print(i18n("updates.update_failure_message"))
print [[: " + rsp.status);
}
$('#updates-install-li').html(icon + " ]]
print(i18n("updates.install"))
print [[");
$('#updates-install-li').show();
$('#updates-install-li').off("click");
$('#updates-install-li').click(installUpdate);
if (rsp.status !== 'update-avail') $('#admin-badge').html('!');
else $('#admin-badge').html('1');
$('#admin-badge').show();
} else /* (rsp.status == 'not-avail' || rsp.status == 'update-failure' || rsp.status == <other errors>) */ {
var icon = '';
$('#updates-info-li').attr('title', '');
if (rsp.status !== 'not-avail') {
icon = '<i class="fas fa-exclamation-triangle text-danger"></i> ';
$('#updates-info-li').attr('title', "]]
print(i18n("updates.update_failure_message"))
print [[: " + rsp.status);
}
$('#updates-info-li').html(icon + ']]
print(i18n("updates.no_updates"))
print [[');
$('#updates-install-li').html("<i class='fas fa-sync'></i> ]]
print(i18n("updates.check"))
print [[");
$('#updates-install-li').show();
$('#updates-install-li').off("click");
$('#updates-install-li').click(checkForUpdates);
$('#admin-badge').hide();
}
}
}
});
}
setInterval(updatesRefresh, 1000);
</script>
]]
end
-- close wrapper
print [[
</main>
</div>
</body>
</html> ]]
template_utils.render("pages/vue_page.template", {
vue_page_name = "AppFooter",
page_context = footer_context,
})
print("</main>")
print("</div>")
print("</body>")
print("</html>")

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,284 @@
--
-- (C) 2013-26 - ntop.org
--
--
-- Exports two functions:
-- get_flags() -> flat table of named boolean flags used by entry_hidden checks
-- get_dynamic_entries(section_key, flags, page_utils, http_prefix)
-- -> array of extra {key,label,url,is_divider} rows injected by
-- page_utils.scripts_menu, nedge, appliance, etc.
--
local ntopng_menu_visibility = {}
function ntopng_menu_visibility.get_flags()
local page_utils = require "page_utils"
local auth = require "auth"
local behavior_utils = require "behavior_utils"
local vs_utils = require "vs_utils"
local prefs = ntop.getPrefs()
local info = ntop.getInfo(true)
local ifid_stats = interface.getStats()
local current_ifid = interface.getId()
local is_admin = isAdministrator()
local http_prefix = ntop.getHttpPrefix()
local is_system_interface = toboolean(page_utils.is_system_view())
local system_ifid = getSystemInterfaceId()
local is_nedge = ntop.isnEdge()
local is_nedge_enterprise = ntop.isnEdgeEnterprise()
local is_routing_mode = is_nedge and ntop.isRoutingMode() or false
local is_enterprise = ntop.isEnterprise()
local is_enterprise_m = ntop.isEnterpriseM()
local is_enterprise_l = ntop.isEnterpriseL()
local is_enterprise_xl = ntop.isEnterpriseXL()
local is_pro = ntop.isPro()
local is_appliance = ntop.isAppliance()
local is_windows = ntop.isWindows()
local is_oem = (info.oem == true)
local has_ch_support = hasClickHouseSupport()
local is_influxdb = (ntop.getPref("ntopng.prefs.timeseries_driver") == "influxdb")
local asset_inventory = assetsInventoryEnabled()
local is_geoip = ntop.hasGeoIP()
local limit_resources = ntop.limitResourcesUsage()
local has_dump_cache = ntop.hasDumpCache()
local has_local_auth = (ntop.getPref("ntopng.prefs.local.auth_enabled") ~= "0")
local has_nanalyst = page_utils.has_nanalyst and page_utils.has_nanalyst()
local has_developer_menu = (ntop.getPref("ntopng.prefs.menu_entries.developer") ~= "0")
local has_help_menu = (ntop.getPref("ntopng.prefs.menu_entries.help") ~= "0")
local is_asn_mode = isASNModeEnabled()
local is_pcap_dump = interface.isPcapDumpInterface()
local is_db_view = interface.isDatabaseViewInterface()
local is_viewed_interface = interface.isViewed()
local is_zmq_interface = interface.isZMQInterface()
local is_loopback = interface.isLoopback()
local is_discoverable = interface.isDiscoverableInterface()
local has_vlans = interface.hasVLANs()
local is_viewed = (ifid_stats.isViewed == true)
local is_db_type = (ifid_stats["type"] == "db")
local has_hr_flows = (ifid_stats["has_hr_flows"] == true)
local has_macs = (ifid_stats.has_macs == true)
local has_seen_pods = (ifid_stats.has_seen_pods == true)
local has_seen_containers = (ifid_stats.has_seen_containers == true)
local flowDevices = (interface.getFlowDevices and interface.getFlowDevices()) or {}
local sFlowDevices = interface.getSFlowDevices() or {}
local obsInfo = interface.getObsPointsInfo() or {}
local num_obs_points = obsInfo.numObsPoints or 0
local infrastructure_view = false
infrastructure_view, _ = isInfrastructureView()
-- capability shortcuts
local cap_alerts = auth.has_capability(auth.capabilities.alerts)
local cap_checks = auth.has_capability(auth.capabilities.checks)
local cap_hist_flows = auth.has_capability(auth.capabilities.historical_flows)
local cap_developer = auth.has_capability(auth.capabilities.developer)
-- computed compound flags
local has_exporters = (ifid_stats.type == "zmq") or (ifid_stats.type == "custom") or
(is_pro and (table.len(flowDevices) > 0))
local service_map_available, _ = behavior_utils.mapsAvailable()
local vs_available = vs_utils.is_available()
local has_bgp_server = not (isEmptyString(ntop.getPref("ntopng.prefs.bgp_server.ip_address")) or
isEmptyString(ntop.getPref("ntopng.prefs.bgp_server.port")))
local devices_exclusion_enabled = false
local checks_config = require("checks").getConfigset()["config"]
if checks_config and checks_config["interface"] and
checks_config["interface"]["device_connection_disconnection"] and
checks_config["interface"]["device_connection_disconnection"]["min"]["enabled"] then
devices_exclusion_enabled = true
end
local is_system_ifid = (tonumber(system_ifid) == tonumber(current_ifid))
local is_allowed_sys_iface = isAllowedSystemInterface()
return {
-- interface state
is_pcap_dump = is_pcap_dump,
is_system_interface = is_system_interface,
no_system_interface = not is_system_interface,
is_db_view_interface = is_db_view,
is_viewed = is_viewed,
is_viewed_interface = is_viewed_interface,
is_zmq_interface = is_zmq_interface,
is_loopback_interface = is_loopback,
no_discoverable_interface = not is_discoverable,
is_asn_mode_enabled = is_asn_mode,
no_asn_mode = not is_asn_mode,
infrastructure_view = infrastructure_view,
is_db_type = is_db_type,
no_vlans = not has_vlans,
no_macs = not has_macs,
no_pods = not has_seen_pods,
no_containers = not has_seen_containers,
no_obs_points = (num_obs_points == 0),
no_sflow_devices = (table.len(sFlowDevices) == 0),
no_exporters = not has_exporters,
no_hr_flows = not has_hr_flows,
no_bgp_server = not has_bgp_server,
no_service_map = not service_map_available,
no_vs_utils = not vs_available,
-- user/admin
is_admin = is_admin,
no_admin = not is_admin,
no_local_auth_or_local_user = (not _SESSION["localuser"] and not has_local_auth),
-- features / capabilities
alerts_disabled = not prefs.are_alerts_enabled,
no_alerts_cap = not cap_alerts,
no_checks_cap = not cap_checks,
no_developer_cap = not cap_developer,
-- license / edition
is_pro = is_pro,
no_pro = not is_pro,
is_nedge = is_nedge,
is_nedge_enterprise = is_nedge_enterprise,
is_routing_mode = is_routing_mode,
is_oem = is_oem,
is_windows = is_windows,
is_appliance = is_appliance,
is_allowed_sys_iface = is_allowed_sys_iface,
limit_resource_usage = limit_resources,
-- enterprise tiers
no_enterprise = not is_enterprise,
no_enterprise_m = not is_enterprise_m,
no_enterprise_l = not is_enterprise_l,
no_enterprise_xl = not is_enterprise_xl,
no_enterprise_l_or_nedge = (not is_enterprise_l and not is_nedge_enterprise),
no_enterprise_m_or_nedge = (not is_enterprise_m and not is_nedge_enterprise),
no_enterprise_m_no_windows = (not is_enterprise_m or is_windows),
-- compound conditions
no_ch_support = not has_ch_support,
no_influxdb = not is_influxdb,
no_asset_inventory = not asset_inventory,
no_geoip = not is_geoip,
no_dump_cache = not has_dump_cache,
no_nanalyst = not has_nanalyst,
no_developer_menu = (not has_developer_menu) and is_enterprise_m,
no_help_menu = (not has_help_menu) and is_enterprise_m,
no_devices_exclusion = not devices_exclusion_enabled,
is_system_ifid = is_system_ifid,
pro_forced_community = (info["pro.forced_community"] == true),
-- complex compound
no_enterprise_with_ch = not (is_enterprise and has_ch_support),
no_enterprise_or_nedge_with_ch_hist = (not is_enterprise and not is_nedge_enterprise) or
not cap_hist_flows or not has_ch_support,
}
end
-- get_dynamic_entries: returns plugin/nedge/appliance-injected menu rows
-- for sections that can be extended at runtime.
--
-- Each returned entry is already resolved: {key, label, url, is_divider}
-- Hidden entries are simply omitted.
function ntopng_menu_visibility.get_dynamic_entries(section_key, flags, page_utils, http_prefix)
local entries = {}
-- health
if section_key == "health" then
if page_utils.scripts_menu then
for k, entry in pairsByField(page_utils.scripts_menu, "sort_order", rev) do
if entry.menu_entry.section == page_utils.menu_sections.health.key then
local label_raw = i18n(entry.menu_entry.i18n or entry.menu_entry.key)
entries[#entries + 1] = {
key = entry.menu_entry.key,
label = (type(label_raw) == "string") and label_raw or entry.menu_entry.key,
url = http_prefix .. entry.url,
is_divider = false,
}
end
end
end
-- pollers
elseif section_key == "pollers" then
if page_utils.scripts_menu then
for k, entry in pairsByField(page_utils.scripts_menu, "sort_order", rev) do
if entry.menu_entry.section == page_utils.menu_sections.pollers.key then
local label_raw = i18n(entry.menu_entry.i18n or entry.menu_entry.key)
entries[#entries + 1] = {
key = entry.menu_entry.key,
label = (type(label_raw) == "string") and label_raw or entry.menu_entry.key,
url = http_prefix .. entry.url,
is_divider = false,
}
end
end
end
-- system_stats: nedge / appliance entries
elseif section_key == "system_stats" then
-- script-injected (skip pollers, those live under pollers section)
if page_utils.scripts_menu then
for k, entry in pairsByField(page_utils.scripts_menu, "sort_order", rev) do
if entry.menu_entry.section ~= "pollers" then
local label_raw = i18n(entry.menu_entry.i18n or entry.menu_entry.key)
entries[#entries + 1] = {
key = entry.menu_entry.key,
label = (type(label_raw) == "string") and label_raw or entry.menu_entry.key,
url = http_prefix .. entry.url,
is_divider = false,
}
end
end
end
-- nedge entries
if flags.is_nedge then
if #entries > 0 and not flags.no_admin then
entries[#entries + 1] = { key = "divider", is_divider = true }
end
local nedge_rows = {
{ key = "system_setup", i18n_key = "nedge.system_setup", url = "/lua/system_setup_ui/interfaces.lua", hidden = flags.no_admin },
{ key = "dhcp_static_leases", i18n_key = "nedge.dhcp_static_leases", url = "/lua/pro/nedge/admin/dhcp_leases.lua", hidden = flags.no_admin or not flags.is_routing_mode },
{ key = "dhcp_active_leases", i18n_key = "nedge.dhcp_active_leases", url = "/lua/pro/nedge/admin/dhcp_active_leases.lua", hidden = flags.no_admin or not flags.is_routing_mode },
{ key = "port_forwarding", i18n_key = "nedge.port_forwarding", url = "/lua/pro/nedge/admin/port_forwarding.lua", hidden = flags.no_admin or not flags.is_routing_mode },
{ key = "rules_config", i18n_key = "nedge.rules_config", url = "/lua/pro/nedge/admin/rules_config.lua", hidden = flags.no_admin or not flags.is_routing_mode },
{ key = "forwarders_config", i18n_key = "nedge.forwarders_config", url = "/lua/pro/nedge/admin/forwarders_config.lua", hidden = flags.no_admin or not flags.is_routing_mode },
}
for _, r in ipairs(nedge_rows) do
if not r.hidden then
local label_raw = i18n(r.i18n_key)
entries[#entries + 1] = {
key = r.key,
label = (type(label_raw) == "string") and label_raw or r.key,
url = http_prefix .. r.url,
is_divider = false,
}
end
end
end
-- appliance entries
if flags.is_appliance then
if #entries > 0 and not flags.no_admin then
entries[#entries + 1] = { key = "divider", is_divider = true }
end
if not flags.no_admin then
local label_raw = i18n("nedge.system_setup")
entries[#entries + 1] = {
key = "system_setup",
label = (type(label_raw) == "string") and label_raw or "system_setup",
url = http_prefix .. "/lua/system_setup_ui/mode.lua",
is_divider = false,
}
end
end
end
return entries
end
return ntopng_menu_visibility

View file

@ -929,7 +929,7 @@ end
-- NOTE: this function must be called after page_utils.print_header_and_set_active_menu_entry
function page_utils.print_page_title(title, mini_title)
local help_link = page_utils.menu_entries[active_entry].help_link or ""
local help_link = (page_utils.menu_entries[active_entry] or {}).help_link or ""
print("<header class='mb-3 d-flex align-items-center'>")
print("<h2 class='d-inline-block'>" .. title .. "</h2>")
if (not isEmptyString(help_link)) then
@ -958,7 +958,8 @@ function page_utils.print_header_and_set_active_menu_entry(entry, i18n_params, a
active_sub_entry = entry.subkey
end
page_utils.print_header(alt_title or i18n(entry.i18n_title, i18n_params) or entry.i18n_title)
local _title_key = entry.i18n_title
page_utils.print_header(alt_title or (_title_key and i18n(_title_key, i18n_params)) or _title_key or entry.key or "")
end
-- #################################
@ -1063,15 +1064,14 @@ function page_utils.print_header(title, addLoginJS)
print(http_prefix)
print [[/dist/white-mode.css?]]
print(static_file_epoch)
print [[" rel="stylesheet">]]
if (dark_mode) then
print [[<link href="]]
print(http_prefix)
print [[/dist/dark-mode.css?]]
print(static_file_epoch)
print [[" rel="stylesheet">]]
end
print [[" rel="stylesheet">
<link id="dark-mode-css" href="]]
print(http_prefix)
print [[/dist/dark-mode.css?]]
print(static_file_epoch)
print [["]]
if not dark_mode then print [[ disabled]] end
print [[ rel="stylesheet">]]
if (ntop.isPro() or ntop.isnEdge()) and ntop.exists(dirs.installdir .. "/httpdocs/img/custom_favicon.ico") then
favicon_path = http_prefix .. "/img/custom_favicon.ico"
@ -1241,8 +1241,8 @@ end
-- #################################
function page_utils.get_navbar_context(title, base_url, items_table, label_url, back_url)
local help_link = page_utils.menu_entries[active_entry].help_link or nil
local icon = page_utils.menu_sections[active_section].icon or ""
local help_link = (page_utils.menu_entries[active_entry] or {}).help_link or nil
local icon = (page_utils.menu_sections[active_section] or {}).icon or ""
local navbar = {
main_icon = icon,
@ -1259,8 +1259,8 @@ end
-- #################################
function page_utils.get_new_navbar_context(title, base_url, items_table, label_url, back_url)
local help_link = page_utils.menu_entries[active_entry].help_link or nil
local icon = page_utils.menu_sections[active_section].icon or ""
local help_link = (page_utils.menu_entries[active_entry] or {}).help_link or nil
local icon = (page_utils.menu_sections[active_section] or {}).icon or ""
local navbar = {
main_title = {
@ -1278,8 +1278,8 @@ end
-- #################################
function page_utils.print_navbar(title, base_url, items_table, label_url, back_url, end_items)
local help_link = page_utils.menu_entries[active_entry].help_link or nil
local icon = page_utils.menu_sections[active_section].icon or ""
local help_link = (page_utils.menu_entries[active_entry] or {}).help_link or nil
local icon = (page_utils.menu_sections[active_section] or {}).icon or ""
local context = {
navbar = {
@ -1295,272 +1295,6 @@ function page_utils.print_navbar(title, base_url, items_table, label_url, back_u
print(template_utils.gen("pages/components/page-navbar.template", context))
end
-- #################################
local menubar_structure = {}
function page_utils.init_menubar()
menubar_structure = {}
end
-- #################################
function page_utils.add_menubar_section(section)
menubar_structure[#menubar_structure + 1] = section
end
-- #################################
local function print_submenu(section, container_list_name)
local active_subpage = page_utils.get_active_entry()
local active_subsubpage = page_utils.get_active_sub_entry()
local section_key = section.section.key
local section_id = section_key .. "-submenu"
local list_name = section_id .. "-list"
print [[<div data-parent='#]]
print(container_list_name)
print [[' class='collapse side-collapse' id=']]
print(section_id)
print [['>
<ul class='nav flex-column' id=']]
print(list_name)
print [['>
]]
for _, section_entry in ipairs(section.entries) do
if not section_entry.hidden then
if section_entry.custom then
print(section_entry.custom)
elseif section_entry.entry.key == "divider" then
print [[
<li class="dropdown-divider"></li>
]]
else
local external_link = false
local section_has_submenu = section_entry.entries
local active = false
if section_entry.entry and (section_entry.entry.key == active_subpage and
(not section_entry.entry.subkey or section_entry.entry.subkey == active_subsubpage)) then
active = true
end
if section_entry.url:starts("http") then
-- Absolute (external) url
external_link = true
end
print [[
<li class=']]
if active then
print("active")
end
print [['>
<a class="]]
if section_has_submenu then
print [[submenu ]]
end
if active then
print [[active ]]
end
if external_link == true then
print [[ntopng-external-link ]]
end
print [["]]
if section_has_submenu then
print [[ data-bs-toggle="collapse" ]]
end
print [[href="]]
if section_has_submenu then
local submenu_section_key = section_entry.section.key
local submenu_section_id = submenu_section_key .. "-submenu"
print("#" .. submenu_section_id)
else
if external_link then
print(section_entry.url)
else
print(ntop.getHttpPrefix() .. section_entry.url)
end
end
print [["]]
if section_entry.entry.is_modal then
print(' data-bs-toggle="modal"')
end
if external_link then
-- Open in a new page
print(' target="_blank"')
end
print [[>]]
print(i18n(section_entry.entry.i18n_title) or section_entry.entry.i18n_title)
if section_entry.suffix then
print(section_entry.suffix)
end
if external_link then
print(" <i class='fas fa-external-link-alt'></i>")
end
print [[</a>]]
if section_has_submenu then
print_submenu(section_entry, list_name)
end
print [[</li>
]]
end
end
end
print [[
</ul>
</div>
]]
end
-- #################################
local function print_section(section, list_name)
local active_page = page_utils.get_active_section()
local show_section = false
local section_has_submenu = (section.entries and #section.entries > 0)
if (not section.hidden) then
if not section_has_submenu then
show_section = true
else
-- The section is shown only if at least one of its sub entries
-- is shown
for _, section_entry in pairs(section.entries or {}) do
if not section_entry.hidden then
show_section = true
break
end
end
end
end
if show_section then
local section_key = section.section.key
local section_id = section_key .. "-submenu"
print [[
<li class="nav-item ]]
print(active_page == section_key and 'active' or '')
print [[">
<a class="]]
if section_has_submenu then
print [[submenu ]]
end
if active_page == section_key then
print [[active ]]
end
print [[" ]]
if section_has_submenu then
print [[ data-bs-toggle="collapse" ]]
end
print [[href="]]
if section.url then
print(ntop.getHttpPrefix() .. section.url)
else
print("#" .. section_id)
end
print [[">
<span class="]]
print(section.section.icon)
print [["></span> ]]
print(i18n(section.section.i18n_title))
print [[
</a>]]
if section_has_submenu then
print_submenu(section, list_name)
end
end
end
-- #################################
function page_utils.print_menubar()
local is_system_interface = page_utils.is_system_view()
local logo_path = nil
local list_name = "sidebar"
if (ntop.isPro() or ntop.isnEdge()) and ntop.exists(dirs.installdir .. "/httpdocs/img/custom_logo.png") then
logo_path = ntop.getHttpPrefix() .. "/img/custom_logo.png"
end
local navbar_style = _POST["toggle_theme"] or ntop.getPref("ntopng.user." .. _SESSION["user"] .. ".theme")
if ((navbar_style == nil) or (navbar_style == "white") or (navbar_style == "")) then
navbar_style = "default"
end
if (navbar_style == "default") then
navbar_style = "dark"
end
print('<nav id="n-sidebar" class="bg-' .. navbar_style .. ' py-0 px-2">')
print([[
<div class="mobile-menu-button d-flex">
<form class='w-100' method='post' action="/?">
<input hidden name='switch_interface' value='1' />
<input hidden name='csrf' value=']] .. ntop.getRandomCSRFValue() .. [[' />
]] .. template_utils.gen("pages/components/ifaces-select.template", {
ifaces = page_utils.get_interface_list(),
is_sys_iface = is_system_interface
}) .. [[
</form>
<button data-bs-toggle="]] .. list_name .. [[" class='ms-5 my-auto btn btn-close'></button>
</div>
<div class="mobile-menu-stats">
]])
-- the network load will not be displayed in system view mode
if not is_system_interface then
print([[<div class="network-load mb-2 w-100"></div>]])
end
print([[
<div class="info-stats w-100">]] .. page_utils.generate_info_stats() .. [[</div>
</div>
]])
print("<h3 class='muted'><a href='" .. ntop.getHttpPrefix() .. "/'>")
if (logo_path ~= nil) then
print("<img class=\"logo-brand\" height=\"52px\" src=\"" .. logo_path .. "\" alt='Custom Logo' />")
else
print(addLogoSvg())
end
print([[
</a>
</h3>
<ul class="nav-side mb-4" id=']] .. list_name .. [['>
]])
for _, section in ipairs(menubar_structure) do
print_section(section, list_name)
end
print([[
</ul>
</nav>
]])
end
-- ##############################################
function page_utils.get_interface_list()

View file

@ -121,6 +121,7 @@ local function get_schema_info(schema_name)
info.series_meta[series_id] = {
label = smeta.label or series_id,
invert_direction = smeta.invert_direction or false,
color = smeta.color,
}
end
end
@ -224,6 +225,9 @@ if queries and type(queries) == "table" then
if smeta then
serie.label = smeta.label
serie.invert_direction = smeta.invert_direction
if smeta.color then
serie.color = smeta.color
end
end
end
end